上一篇介绍了 gRPC入门 ,但未使用任何的安全校验模式,也就是任意一个客户端只要知道了服务端的ip和proto文件,则可以连接上去,这是一个不安全的访问方式。接下来介绍两种常用的RPC认证方式。
SSL/TLS认证方式
啥是SSL/TLS?
TLS是SSL的后续版本,都叫传输层的安全协议。作用于互联网两台计算机之间通讯身份验证和加密的一种协议。通过X509证书的数字文档,将网站和公司的实体信息绑定到加密秘钥中来进行工作。
每一对秘钥都有一个私钥和公钥,是非对称加密。私钥是独有的,只是存放于服务器端,公钥可以进行公开,任何人都可以进行请求获取,简单的解释如下图:
- 客户端向服务器端索要公钥
- 使用公钥加密信息,发送到服务端
- 服务端接收后,使用私钥进行加密
- 服务端使用公钥和私钥加密数据给客户端
- 客户端使用公钥进行解密
可以看出,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语言高级编程