RPC 系列(八):Go gRPC 的 TLS 认证

前面篇章的 gRPC 都是明文传输的,容易被篡改数据。本篇将介绍如何为 gRPC 添加安全机制,包括 TLS 证书认证和 Token 认证。

TLS 证书认证

什么是 TLS

TLS(Transport Layer Security,安全传输层),是建立在传输层 TCP 协议之上的协议,服务于应用层。它的前身是 SSL(Secure Socket Layer,安全套接字层)。TLS 实现了将应用层的报文进行加密后再交由 TCP 进行传输的功能。

TLS 的作用

TLS 协议主要解决如下三个网络安全问题:数据被窃听(机密性)、数据被篡改(完整性)、身份被冒充(认证性)。

生成私钥

生成 RSA 私钥:

openssl genrsa -out server.key 2048

命令的最后一个参数将指定生成密钥的位数,如果没有指定,默认为 512。

生成 ECC 私钥:

openssl ecparam -genkey -name secp384r1 -out server.key

该命令用于椭圆曲线密钥参数的生成及操作,本文中 ECC 曲线选择的是 secp384r1。

生成公钥

openssl req -new -x509 -sha256 -key server.key -out server.pem -days 3650

openssl req 用于生成自签名证书:-new 指生成证书请求、-sha256 指使用 sha256 加密、-key 指定私钥文件、-x509 指输出证书、-days 3650 为有效期。

此后则输入证书拥有者信息:

Country Name (2 letter code) [AU]:CN
State or Province Name (full name) [Some-State]:XxXx
Locality Name (eg, city) []:XxXx
Organization Name (eg, company) [Internet Widgits Pty Ltd]:XX Co. Ltd
Organizational Unit Name (eg, section) []:Dev
Common Name (e.g. server FQDN or YOUR name) []:go-grpc-example
Email Address []:xxx@xxx.com

服务端构建 TLS 证书并认证

func main() {
    // 监听本地端口
    listener, err := net.Listen(Network, Address)
    if err != nil {
        log.Fatalf("net.Listen err: %v", err)
    }
    // 从输入证书文件和密钥文件为服务端构造TLS凭证
    creds, err := credentials.NewServerTLSFromFile("../pkg/tls/server.pem", "../pkg/tls/server.key")
    if err != nil {
        log.Fatalf("Failed to generate credentials %v", err)
    }
    // 新建gRPC服务器实例,并开启TLS认证
    grpcServer := grpc.NewServer(grpc.Creds(creds))
    // 在gRPC服务器注册我们的服务
    pb.RegisterSimpleServer(grpcServer, &SimpleService{})
    log.Println(Address + " net.Listing with TLS and token...")
    //通过服务器 Serve() 方法监听该端口,阻塞等待直到进程被杀死或者 Stop() 被调用
    err = grpcServer.Serve(listener)
    if err != nil {
        log.Fatalf("grpcServer.Serve err: %v", err)
    }
}

客户端配置 TLS 连接

var grpcClient pb.SimpleClient

func main() {
    //从输入的证书文件中为客户端构造TLS凭证
    creds, err := credentials.NewClientTLSFromFile("../pkg/tls/server.pem", "go-grpc-example")
    if err != nil {
        log.Fatalf("Failed to create TLS credentials %v", err)
    }
    // 连接服务器
    conn, err := grpc.Dial(Address, grpc.WithTransportCredentials(creds))
    if err != nil {
        log.Fatalf("net.Connect err: %v", err)
    }
    defer conn.Close()

    // 建立gRPC连接
    grpcClient = pb.NewSimpleClient(conn)
}

到这里,已经完成 TLS 证书认证了,gRPC 传输不再是明文传输。此外,添加自定义的验证方法能使 gRPC 相对更安全。下面以 Token 认证为例,介绍 gRPC 如何添加自定义验证方法。

Token 认证

客户端发请求时,添加 Token 到上下文 context.Context 中,服务器接收到请求,先从上下文中获取 Token 验证,验证通过才进行下一步处理。

客户端请求添加 Token 到上下文中

gRPC 中默认定义了 PerRPCCredentials,是提供用于自定义认证的接口,它的作用是将所需的安全认证信息添加到每个 RPC 方法的上下文中。其包含 2 个方法:

  • GetRequestMetadata:获取当前请求认证所需的元数据(metadata);
  • RequireTransportSecurity:是否需要基于 TLS 认证进行安全传输。

接口定义如下:

type PerRPCCredentials interface {
    GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error)
    RequireTransportSecurity() bool
}

接下来我们实现这两个方法:

// Token token认证
type Token struct {
    AppID     string
    AppSecret string
}

// GetRequestMetadata 获取当前请求认证所需的元数据(metadata)
func (t *Token) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
    return map[string]string{"app_id": t.AppID, "app_secret": t.AppSecret}, nil
}

// RequireTransportSecurity 是否需要基于 TLS 认证进行安全传输
func (t *Token) RequireTransportSecurity() bool {
    return true
}

然后在客户端中调用 Dial 时添加自定义验证方法进去:

    //构建Token
    token := auth.Token{
        AppID:     "grpc_token",
        AppSecret: "123456",
    }
    // 连接服务器
    conn, err := grpc.Dial(Address, grpc.WithTransportCredentials(creds), grpc.WithPerRPCCredentials(&token))

服务端验证 Token

首先需要从上下文中获取元数据,然后从元数据中解析 Token 进行验证:

// Check 验证token
func Check(ctx context.Context) error {
    //从上下文中获取元数据
    md, ok := metadata.FromIncomingContext(ctx)
    if !ok {
        return status.Errorf(codes.Unauthenticated, "获取Token失败")
    }
    var (
        appID     string
        appSecret string
    )
    if value, ok := md["app_id"]; ok {
        appID = value[0]
    }
    if value, ok := md["app_secret"]; ok {
        appSecret = value[0]
    }
    if appID != "grpc_token" || appSecret != "123456" {
        return status.Errorf(codes.Unauthenticated, "Token无效: app_id=%s, app_secret=%s", appID, appSecret)
    }
    return nil
}

// Route 实现Route方法
func (s *SimpleService) Route(ctx context.Context, req *pb.SimpleRequest) (*pb.SimpleResponse, error) {
    //检测Token是否有效
    if err := Check(ctx); err != nil {
        return nil, err
    }
    res := pb.SimpleResponse{
        Code:  200,
        Value: "hello " + req.Data,
    }
    return &res, nil
}

服务端添加拦截器

服务端代码中,每个服务的方法都需要添加 Check(ctx) 来验证 Token,这样十分麻烦。gRPC 拦截器能很好地解决这个问题。gRPC 拦截器功能类似中间件,拦截器收到请求后,先进行一些操作,然后才进入服务的代码处理。

func main() {
    // 监听本地端口
    listener, err := net.Listen(Network, Address)
    if err != nil {
        log.Fatalf("net.Listen err: %v", err)
    }
    // 从输入证书文件和密钥文件为服务端构造TLS凭证
    creds, err := credentials.NewServerTLSFromFile("../pkg/tls/server.pem", "../pkg/tls/server.key")
    if err != nil {
        log.Fatalf("Failed to generate credentials %v", err)
    }
    //普通方法:一元拦截器(grpc.UnaryInterceptor)
    var interceptor grpc.UnaryServerInterceptor
    interceptor = func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
        //拦截普通方法请求,验证Token
        err = Check(ctx)
        if err != nil {
            return
        }
        // 继续处理请求
        return handler(ctx, req)
    }
    // 新建gRPC服务器实例,并开启TLS认证和Token认证
    grpcServer := grpc.NewServer(grpc.Creds(creds), grpc.UnaryInterceptor(interceptor))
    // 在gRPC服务器注册我们的服务
    pb.RegisterSimpleServer(grpcServer, &SimpleService{})
    log.Println(Address + " net.Listing with TLS and token...")
    //通过服务器 Serve() 方法监听该端口,阻塞等待直到进程被杀死或者 Stop() 被调用
    err = grpcServer.Serve(listener)
    if err != nil {
        log.Fatalf("grpcServer.Serve err: %v", err)
    }
}

客户端发起请求,当 Token 不正确的时候,会返回:

Call Route err: rpc error: code = Unauthenticated desc = Token无效: app_id=grpc_token, app_secret=12345

总结

本篇介绍如何为 gRPC 添加 TLS 证书认证和自定义认证,从而让 gRPC 更安全。添加 gRPC 拦截器,从而省略在每个方法前添加 Token 检测代码,使代码更简洁。

系列导航


   转载规则


《RPC 系列(八):Go gRPC 的 TLS 认证》 由 吴杭沉 采用 知识共享署名 4.0 国际许可协议 进行许可。
 上一篇
RPC 系列(九):gRPC 的 deadline 语义 RPC 系列(九):gRPC 的 deadline 语义
最近在将应用的 RPC 更换为 gRPC,使用过程中发现报错 rpc error: code = DeadlineExceeded desc = context deadline exceeded。这是啥?原来是某位仁兄设置了环境的超时时间
2023-10-31
下一篇 
RPC 系列(七):Go gRPC 超时设置 RPC 系列(七):Go gRPC 超时设置
gRPC 默认的请求超时时间是很长的。当你没有设置请求超时时间时,所有在运行的请求都占用大量资源且可能运行很长的时间,导致服务资源损耗过高,使得后来的请求响应过慢,甚至会引起整个进程崩溃。 为了避免这种情况,我们的服务应该设置超时时间。当客
2023-10-17
  目录