微服务之gRPC认证模式

miloyang
0 评论
/ /
903 阅读
/
7298 字
04 2023-09

上一篇介绍了 gRPC入门 ,但未使用任何的安全校验模式,也就是任意一个客户端只要知道了服务端的ip和proto文件,则可以连接上去,这是一个不安全的访问方式。接下来介绍两种常用的RPC认证方式。

SSL/TLS认证方式

啥是SSL/TLS?

TLS是SSL的后续版本,都叫传输层的安全协议。作用于互联网两台计算机之间通讯身份验证和加密的一种协议。通过X509证书的数字文档,将网站和公司的实体信息绑定到加密秘钥中来进行工作。
每一对秘钥都有一个私钥和公钥,是非对称加密。私钥是独有的,只是存放于服务器端,公钥可以进行公开,任何人都可以进行请求获取,简单的解释如下图: liuchengtu图示

  • 客户端向服务器端索要公钥
  • 使用公钥加密信息,发送到服务端
  • 服务端接收后,使用私钥进行加密
  • 服务端使用公钥和私钥加密数据给客户端
  • 客户端使用公钥进行解密
    可以看出,SSL/TLS协议提供的安全通道有着机密性、完整性、可靠性等特征。

    生成自己的安全证书

    首先得安装OpenSSL,从官网下载,或者 第三方网站 ,建议从第三方网站下载,因为官网下载还需要编译等等,第三方网站下载直接安装完配置环境变量即可。
openssl genrsa -out server.key 2048 # 生成私钥

openssl req -new -x509 -key server.key -out server.crt -days 36500 # 生成证书,全部回车即可,可以不填

openssl req -new -key server.key -out server.csr # 生成csr文件,用户提交给证书颁发机构CA对证书签名,全部回车即可

openssl req -new -nodes -key private.key -out test.csr -days 36500 -subj "/C=cn/OU=myorg/O=mycomp/CN=myname" -config ./openssl.cfg -extensions v3_req # 生成自己的私钥,需要有cfg文件

openssl x509 -req -days 36500 -in test.csr -out public.pem -CA server.crt -CAkey server.key -CAcreateserial -extfile ./openssl.cfg -extensions v3_req # 生成自己的公钥文件,需要有cfg文件

其中openssl.cfg文件,在刚刚安装的openssl中的bin路径下,有个openssl.cfg文件,做如下修改:

1:复制openssl.cfg文件到项目所有目录,就刚刚生成证书的路径下
2:找到[ CA_defalut ],打开 copy_extensions = copy 就是把前面的# 去掉
3: 找到[ req ],打开req_extensions = v3_req 
4: 找到 [v3_req ],添加 subjectAltName = @alt_names
5: 添加新的标签 [ alt_names ]和标签字段
DSN.1 = *.boxiaoyang.club #表示为当前域名的证书
# DSN.2 = *.boxiaoyang.* # 可以设置多个
# DSN.3 = * # 所有的都可以访问,不安全

这一套组合拳下来,有七个文件生成 private.key/public.pem/server.crt/server.csr/server.key/server.srl/test.csr

key:服务器上的私钥文件,用户对发送给客户端数据的加密,以及从客户端接收到数据的解密。
csr:证书签名请求文件,用户提交给证书颁发机构CA对证书签名。
crt:由证书颁发机构CA签名后的证书,或者是开发者自签名的证书,包含证书持有者的信息,持有者的公钥以及签署者的签名信息
pem:基于Base64编码的证书格式,扩展名包括pem/crt/cer等

但是我们用到的,其实就是private.key和public.pem。

gRPC使用TLS进行认证

服务器端

    // 配置好证书文件
    creds, _ := credentials.NewServerTLSFromFile("E:\\workspaces\\goland\\grpc-study\\key\\public.pem", "E:\\workspaces\\goland\\grpc-study\\key\\private.key")

    listen, err := net.Listen("tcp", ":8080")
    if err != nil {
        log.Fatal(err)
    }
    //创建grpc
    grpcServer := grpc.NewServer(grpc.Creds(creds)) //创建gRPC服务,并把验证加入进来

客户端

    creds, _ := credentials.NewClientTLSFromFile("E:\\workspaces\\goland\\grpc-study\\key\\public.pem", "www.boxiaoyang.club")
    var opts []grpc.DialOption
    opts = append(opts, grpc.WithTransportCredentials(creds))

就这两步配置,其实gRPC就是很简单的配置,如果感觉到复杂了,那就有问题了,假设我们是其他网站来访问,则会报错:

2023/09/04 12:43:26 rpc error: code = Unavailable desc = connection error: desc = "transport: authentication handshake failed: tls: failed to verify certificate: x509: certificate is valid for *.boxiaoyang.
club, not www.baidu.club"

使用Token认证

其实gRPC提供了我们的一个接口,这个接口中有两个方法,我们需要实现它,接口定义如下:

// PerRPCCredentials defines the common interface for the credentials which need to
// attach security information to every RPC (e.g., oauth2).
type PerRPCCredentials interface {
    // GetRequestMetadata gets the current request metadata, refreshing tokens
    // if required. This should be called by the transport layer on each
    // request, and the data should be populated in headers or other
    // context. If a status code is returned, it will be used as the status for
    // the RPC (restricted to an allowable set of codes as defined by gRFC
    // A54). uri is the URI of the entry point for the request.  When supported
    // by the underlying implementation, ctx can be used for timeout and
    // cancellation. Additionally, RequestInfo data will be available via ctx
    // to this call.  TODO(zhaoq): Define the set of the qualified keys instead
    // of leaving it as an arbitrary string.
    GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error)
    // RequireTransportSecurity indicates whether the credentials requires
    // transport security.
    RequireTransportSecurity() bool
}
  • GetRequestMetadata 获取元数据信息,通过客户端提供的key、value对,ctx用户控制超时和取消,uri请求入口处的uri
  • RequireTransportSecurity 使用几乎TLS认证进行安全传输,如果是true则必须加上TLS验证,false则不用。如果要和上面结合,则改为true即可

    客户端携带约定的token信息

type ClientTokenAuth struct {
}
// map就是与服务端约定的token信息
func (c ClientTokenAuth) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
    return map[string]string{
        "APPID":  "milo", 
        "APPKEY": "123456",
    }, nil
}

func (c ClientTokenAuth) RequireTransportSecurity() bool {
    return false
}

服务端验证是否携带有约定的信息

// SayHello 直接在业务中处理,当然实际项目中,应该是在中间件中处理,业务传输层的校验
func (s *server) SayHello(ctx context.Context, req *pb.HelloRequest) (*pb.HelloResponse, error) {
    md, ok := metadata.FromIncomingContext(ctx)
    if !ok {
        return nil, errors.New("not token")
    }
    var appId string
    var appKey string
    if v, ok := md["appid"]; ok {
        appId = v[0]
    }

    if v, ok := md["appkey"]; ok {
        appKey = v[0]
    }

    if appId != "milo" {
        return nil, errors.New("appid verify")
    }
    if appKey != "123456" {
        return nil, errors.New("appKey verify")
    }

    return &pb.HelloResponse{
        ResponseMsg: "hello " + req.RequestName,
    }, nil
}

值得注意的是,客户端中map的key,是不区分大小写的,但是在服务端校验从map中拿的时候,必须要小写,这样根据有一致性和规范性。
我们看FromIncomingContext的源码:

func FromIncomingContext(ctx context.Context) (MD, bool) {
    md, ok := ctx.Value(mdIncomingKey{}).(MD)
    if !ok {
        return nil, false
    }
    out := make(MD, len(md))
    for k, v := range md {
        // We need to manually convert all keys to lower case, because MD is a
        // map, and there's no guarantee that the MD attached to the context is
        // created using our helper functions.
        key := strings.ToLower(k)
        out[key] = copyOf(v)
    }
    return out, true
}

总结

gRPC将各种认证方式浓缩到一个凭证上,可以单独使用一种凭证,如TLS或者Token,也可多种凭证组合,gRPC提供统一的API验证机制,使研发人员使用方便。 还有一些校验方式如:

  • OAuth2认证,客户端获取访问令牌,传递给gRPC服务器进行访问控制,和第三方登录类似。
  • 自定义认证,那就是每个公司都不一样的方式,根据项目选择特定的需求定义化的一种验证方式。
  • 其他等等

    文献

    grpc系列思路,主要借鉴与 狂神说 以及 go语言高级编程
人未眠
工作数十年
脚步未曾歇,学习未曾停
乍回首
路程虽丰富,知识未记录
   借此博客,与之共进步