对 RPC 方法做自定义认证

前言

在前面的章节中,我们介绍了两种(证书算一种)可全局认证的方法:

  1. TLS 证书认证
  2. 基于 CA 的 TLS 证书认证
  3. Unary and Stream interceptor

而在实际需求中,常常会对某些模块的 RPC 方法做特殊认证或校验。今天将会讲解、实现这块的功能点

课前知识

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

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

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

目录结构

新建 simple_token_server/server.go 和 simple_token_client/client.go,目录结构如下:

  1. go-grpc-example
  2. ├── client
  3. ├── simple_client
  4. ├── simple_http_client
  5. ├── simple_token_client
  6. └── stream_client
  7. ├── conf
  8. ├── pkg
  9. ├── proto
  10. ├── server
  11. ├── simple_http_server
  12. ├── simple_server
  13. ├── simple_token_server
  14. └── stream_server
  15. └── vendor

gRPC

Client

  1. package main
  2. import (
  3. "context"
  4. "log"
  5. "google.golang.org/grpc"
  6. "github.com/EDDYCJY/go-grpc-example/pkg/gtls"
  7. pb "github.com/EDDYCJY/go-grpc-example/proto"
  8. )
  9. const PORT = "9004"
  10. type Auth struct {
  11. AppKey string
  12. AppSecret string
  13. }
  14. func (a *Auth) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
  15. return map[string]string{"app_key": a.AppKey, "app_secret": a.AppSecret}, nil
  16. }
  17. func (a *Auth) RequireTransportSecurity() bool {
  18. return true
  19. }
  20. func main() {
  21. tlsClient := gtls.Client{
  22. ServerName: "go-grpc-example",
  23. CertFile: "../../conf/server/server.pem",
  24. }
  25. c, err := tlsClient.GetTLSCredentials()
  26. if err != nil {
  27. log.Fatalf("tlsClient.GetTLSCredentials err: %v", err)
  28. }
  29. auth := Auth{
  30. AppKey: "eddycjy",
  31. AppSecret: "20181005",
  32. }
  33. conn, err := grpc.Dial(":"+PORT, grpc.WithTransportCredentials(c), grpc.WithPerRPCCredentials(&auth))
  34. ...
  35. }

在 Client 端,重点实现 type PerRPCCredentials interface 所需的方法,关注两点即可:

  • struct Auth:GetRequestMetadata、RequireTransportSecurity
  • grpc.WithPerRPCCredentials

Server

  1. package main
  2. import (
  3. "context"
  4. "log"
  5. "net"
  6. "google.golang.org/grpc"
  7. "google.golang.org/grpc/codes"
  8. "google.golang.org/grpc/metadata"
  9. "google.golang.org/grpc/status"
  10. "github.com/EDDYCJY/go-grpc-example/pkg/gtls"
  11. pb "github.com/EDDYCJY/go-grpc-example/proto"
  12. )
  13. type SearchService struct {
  14. auth *Auth
  15. }
  16. func (s *SearchService) Search(ctx context.Context, r *pb.SearchRequest) (*pb.SearchResponse, error) {
  17. if err := s.auth.Check(ctx); err != nil {
  18. return nil, err
  19. }
  20. return &pb.SearchResponse{Response: r.GetRequest() + " Token Server"}, nil
  21. }
  22. const PORT = "9004"
  23. func main() {
  24. ...
  25. }
  26. type Auth struct {
  27. appKey string
  28. appSecret string
  29. }
  30. func (a *Auth) Check(ctx context.Context) error {
  31. md, ok := metadata.FromIncomingContext(ctx)
  32. if !ok {
  33. return status.Errorf(codes.Unauthenticated, "自定义认证 Token 失败")
  34. }
  35. var (
  36. appKey string
  37. appSecret string
  38. )
  39. if value, ok := md["app_key"]; ok {
  40. appKey = value[0]
  41. }
  42. if value, ok := md["app_secret"]; ok {
  43. appSecret = value[0]
  44. }
  45. if appKey != a.GetAppKey() || appSecret != a.GetAppSecret() {
  46. return status.Errorf(codes.Unauthenticated, "自定义认证 Token 无效")
  47. }
  48. return nil
  49. }
  50. func (a *Auth) GetAppKey() string {
  51. return "eddycjy"
  52. }
  53. func (a *Auth) GetAppSecret() string {
  54. return "20181005"
  55. }

在 Server 端就更简单了,实际就是调用 metadata.FromIncomingContext 从上下文中获取 metadata,再在不同的 RPC 方法中进行认证检查

验证

重新启动 server.go 和 client.go,得到以下结果:

  1. $ go run client.go
  2. 2018/10/05 20:59:58 resp: gRPC Token Server

修改 client.go 的值,制造两者不一致,得到无效结果:

  1. $ go run client.go
  2. 2018/10/05 21:00:05 client.Search err: rpc error: code = Unauthenticated desc = invalid token
  3. exit status 1

一个个加太麻烦

我相信你肯定会问一个个加,也太麻烦了吧?有这个想法的你,应当把 type PerRPCCredentials interface 做成一个拦截器(interceptor)

总结

本章节比较简单,主要是针对 RPC 方法的自定义认证进行了介绍,如果是想做全局的,建议是举一反三从拦截器下手哦。

参考

本系列示例代码