本文最后更新于:10 个月前
前言
示例代码已经上传到github:点击跳转 gRPC官方文档:点击跳转 在前面的章节中,我们介绍了两种可全局认证的方法:
而在实际需求中,常常会对某些模块的 RPC 方法做特殊认证或校验,而gRPC也专门提供了这类特殊认证的接口。
一、概述 gRPC为每个gRPC方法调用提供了Token认证支持,可以基于用户传入的Token判断用户是否登陆、以及权限等,实现Token认证的前提是,需要定义一个结构体,并实现credentials.PerRPCCredentials
接口。
1、credentials.PerRPCCredentials 接口 类型定义:
1 2 3 4 5 6 type PerRPCCredentials interface { GetRequestMetadata(ctx context.Context, uri ...string ) (map [string ]string , error ) RequireTransportSecurity() bool }
在 gRPC 中默认定义了 PerRPCCredentials
,是 gRPC 默认提供用于自定义认证的接口,它的作用是将所需的安全认证信息添加到每个 RPC 方法的上下文中。其包含 2 个方法:
GetRequestMetadata
:获取当前请求认证所需的元数据(metadata),以 map 的形式返回本次调用的授权信息,ctx 是用来控制超时的
RequireTransportSecurity
:是否需要基于 TLS 认证进行安全传输,如果返回 true 则说明该 Credentials 需要在一个有 TLS 认证的安全连接上传输,如果当前连接并没有使用 TLS 则会报错:
1 transport: cannot send secure credentials on an insecure connection
2、实现流程
在发出请求之前,gRPC 会将 Credentials(认证凭证)存放在 metadata(元数据)中进行传递。
在真正发起调用之前,gRPC 会通过 GetRequestMetadata函数,将用户定义的 Credentials(认证凭证)提取出来,并添加到 metadata(元数据)中,随着请求一起传递到服务端。
然后服务端从 metadata 中取出 Credentials 进行有效性校验。
二、实现自定义身份验证 具体分为以下两步:
1)客户端请求时带上 Credentials;
2)服务端取出 Credentials,并验证有效性,一般配合拦截器使用(这里我们使用两种方法,拦截器以及RPC方法)。
1、目录结构 1 2 3 4 5 6 7 8 9 10 11 12 13 go -grpc-example ├─client │ ├─token_client │ │ └──client.go ├─pkg │ ├─token │ │ └──token.go ├─proto │ ├─token │ │ └──token.proto └─server ├─token_server │ └──server.go
2、编写IDL 在 proto/token 文件夹下的 token.proto 文件中,写入如下内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 syntax = "proto3" ; option go_package = "./proto/token;token" ;package tokenservice; message TokenValidateParam { string token = 1 ; int32 uid = 2 ; } message Request { string name = 1 ; } message Response { int32 uid = 1 ; string name = 2 ; } service TokenService { rpc Token(Request) returns (Response); }
在Makefile文件中写入:
1 2 token: protoc --go_out=. --go -grpc_out=. ./proto/token
用make token
指令生成Go代码:
1 2 ➜ make token protoc --go_out=. --go -grpc_out=. ./proto/token
3、编写基础模板和空定义 我们先把基础的模板和空定义写出来在进行完善
1)server.go 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 const Address = "127.0.0.1:8888" type TokenService struct { token.UnimplementedTokenServiceServer }func main () { listen, err := net.Listen("tcp" , Address) if err != nil { fmt.Println("start error:" , err) return } var opts []grpc.ServerOption server := grpc.NewServer(opts...) token.RegisterTokenServiceServer(server, &TokenService{}) fmt.Println("服务启动成功...." ) server.Serve(listen) }
2)client.go 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 const Address = "127.0.0.1:8888" func main () { var opts []grpc.DialOption conn, err := grpc.Dial(Address, opts...) if err != nil { fmt.Println("grpc.Dial error:" , err) return } defer conn.Close() client := token.NewTokenServiceClient(conn) token, err := client.Token(context.Background(), &token.Request{Name: "linzy" }) if err != nil { fmt.Println("client.Token error:" , err) return } fmt.Println("return result:" , token) }
4、实现PerRPCCredentials 接口 我们在 pkg/token 目录里的 token.go 文件内实现PerRPCCredentials 接口的方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 const IsTLS = false type TokenAuth struct { token.TokenValidateParam }func (x *TokenAuth) GetRequestMetadata(ctx context.Context, uri ...string ) (map [string ]string , error ) { return map [string ]string { "uid" : strconv.FormatInt(int64 (x.GetUid()), 10 ), "token" : x.GetToken(), }, nil }func (x *TokenAuth) RequireTransportSecurity() bool { return IsTLS }
5、实现认证功能 我们已经实现了客户端请求时带上 Credentials 凭证,后面就需要实现服务端的功能,在获取授权信息并校验有效性。
1)实现拦截器认证 在 pkg/Interceptor
目录下的 Interceptor.go 文件内写入以下内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 func ServerInterceptorCheckToken () grpc.UnaryServerInterceptor { return func (ctx context.Context, req interface {}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface {}, err error ) { _, err = CheckToken(ctx) if err != nil { fmt.Println("Interceptor 拦截器内token认证失败\n" ) return nil , err } fmt.Println("Interceptor 拦截器内token认证成功\n" ) return handler(ctx, req) } }func CheckToken (ctx context.Context) (*token.Response, error ) { md, b := metadata.FromIncomingContext(ctx) if !b { return nil , status.Error(codes.InvalidArgument, "token信息不存在" ) } var token, uid string tokenInfo, ok := md["token" ] if !ok { return nil , status.Error(codes.InvalidArgument, "token不存在" ) } token = tokenInfo[0 ] uidTmp, ok := md["uid" ] if !ok { return nil , status.Error(codes.InvalidArgument, "uid不存在" ) } uid = uidTmp[0 ] sum := md5.Sum([]byte (uid)) md5Str := fmt.Sprintf("%x" , sum) if md5Str != token { fmt.Println("md5Str:" , md5Str) fmt.Println("uid:" , uid) fmt.Println("token:" , token) return nil , status.Error(codes.InvalidArgument, "token验证失败" ) } return nil , nil }
gPRC 传输的时候把授权信息存放在 metada 的,所以需要先获取 metadata。通过metadata.FromIncomingContext
可以从 ctx 中取出本次调用的 metadata,然后再从 md 中取出授权信息并校验即可。
在server.go文件内添加拦截器:
1 opts = append (opts, grpc.UnaryInterceptor(Interceptor.ServerInterceptorCheckToken()))
2)实现RPC方法认证 实现了校验有效性我们就需要在 server.go
服务端实现Token RPC的方法进行授权认证:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 type TokenService struct { token.UnimplementedTokenServiceServer tokenAuth.TokenAuth }func (u TokenService) Token(ctx context.Context, r *token.Request) (*token.Response, error ) { _, err := Interceptor.CheckToken(ctx) if err != nil { fmt.Println("Token RPC方法内token认证失败\n" ) return nil , err } fmt.Printf("%v Token RPC方法内token认证成功\n" , r.GetName()) return &token.Response{Name: r.GetName()}, nil }
同样的在client.go
文件内输入token信息,并调用grpc.WithPerRPCCredentials
:
1 2 3 4 5 6 7 8 auth := tokenAuth.TokenAuth{ token.TokenValidateParam{ Token: "81dc9bdb52d04dc20036dbd8313ed055" , Uid: 1234 , }, } opts = append (opts, grpc.WithPerRPCCredentials(&auth))
6、启动 & 请求 输入一个正确的token:
1 2 3 4 5 6 7 8 9 10 11 12 # 启动服务端 $ go run server.go API server listening at: 127.0 .0 .1 :52505 服务启动成功.... Interceptor 拦截器内token认证成功 linzy Token RPC方法内token认证成功 # 启动客户端 $ go run client.go API server listening at: 127.0 .0 .1 :52545 return result: name:"linzy"
修改token信息为:
1 2 3 4 5 6 7 auth := tokenAuth.TokenAuth{ token.TokenValidateParam{ Token: "81dc9bdb52d0ed0585" , Uid: 1234 , }, }
测试一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 # 启动服务端 $ go run server.go API server listening at: 127.0 .0 .1 :52505 服务启动成功.... md5Str: 81 dc9bdb52d04dc20036dbd8313ed055 uid: 1234 token: 81 dc9bdb52d0ed0585 Interceptor 拦截器内token认证失败 # 启动客户端 $ go run client.go API server listening at: 127.0 .0 .1 :52857 client.Token error : rpc error : code = InvalidArgument desc = token验证失败
7、实现RequireTransportSecurity()方法 身份认证功能已经完成,但是我们gRPC通信还是明文传输,对于如此重要的信息肯定要建立安全连接,所以要实现 RequireTransportSecurity 方法。
方法实现很简单,我们只需要建立安全连接的时候,返回一个true就行,使用我们之前的证书进行TLS连接即可。
具体可以看我的上一篇《通过TLS建立安全连接》
server.go添加以下内容:
1 2 3 4 5 6 7 8 9 if tokenAuth.IsTLS { c, err := credentials.NewServerTLSFromFile("./conf/server_side_TLS/server.pem" , "./conf/server_side_TLS/server.key" ) if err != nil { log.Fatalf("credentials.NewServerTLSFromFile err: %v" , err) } opts = append (opts, grpc.Creds(c)) }
client.go添加以下内容:
1 2 3 4 5 6 7 8 9 10 11 12 if tokenAuth.IsTLS { c, err := credentials.NewClientTLSFromFile("./conf/server_side_TLS/server.pem" , "go-grpc-example" ) if err != nil { log.Fatalf("credentials.NewClientTLSFromFile err: %v" , err) } opts = append (opts, grpc.WithTransportCredentials(c)) } else { opts = append (opts, grpc.WithInsecure()) }
我们只需要修改token.go文件内的IsTLS
变量就可以实现是否使用安全链接(TLS)。
启动 & 请求之后我们抓个包看一下是否已经建立安全链接了了。
三、小结 1)实现credentials.PerRPCCredentials
接口就可以把数据当做 gRPC 中的 Credential 在添加到 metadata 中,跟着请求一起传递到服务端; 2)服务端从 ctx 中解析 metadata,然后从 metadata 中获取 授权信息并进行验证; 3)可以借助 Interceptor 实现全局身份验证。