六、Golang gRPC 认证

93 阅读3分钟

gRPC 可与各种身份验证机制配合使用,因此可以安全的使用 gRPC 与其他系统通信。 gRPC 也提供了一些简单的认证API,当创建通道或者调用,让你提供所需必要认证信息作为 Credentials。

TLS 相关的知识可以参考链接 www.cloudflare.com/zh-cn/learn…

image.png

gRPC 基于 TLS 认证

开发环境:
golang 1.20.1
protoc libprotoc 3.20.3
目录结构

├── com
│   └── example
│       ├── auth
│       │   ├── client.go
│       │   └── server.go
│       ├── data
│       │   ├── data.go
│       │   └── x509
│       │       ├── ca_cert.pem
│       │       ├── ca_key.pem
│       │       ├── client_ca_cert.pem
│       │       ├── client_ca_key.pem
│       │       ├── client_cert.pem
│       │       ├── client_key.pem
│       │       ├── create.sh
│       │       ├── openssl.cnf
│       │       ├── server_cert.pem
│       │       └── server_key.pem
│       └── echo
│           ├── echo.go
│           ├── echo.pb.go
│           ├── echo.proto
│           └── echo_grpc.pb.go
├── go.mod
└── go.sum

data 目录可以通过 github.com/grpc/grpc-g… 获取,x509 目录夹可以通过create.shopenssl.cnf 生成。
整理代码如下:
echo.proto 增加一个认证的方法。

syntax = "proto3";

package echo;

option go_package = "com/example/echo";

service EchoService{
  rpc UnaryEcho(EchoRequest) returns(EchoResponse){}
  rpc ServerStreamingEcho(EchoRequest) returns(stream EchoResponse){}
  rpc ClientStreamingEcho(stream EchoRequest) returns(EchoResponse){}
  rpc BidirectionalStreamingEcho(stream EchoRequest) returns(stream EchoResponse){}
  // 增加
  rpc UnaryEchoWithAuth(EchoRequest) returns(EchoResponse){}
}

message EchoRequest{
  string message = 1;
}

message EchoResponse{
  string message = 1;
}

echo.go 服务方法

type EchoService struct {
 EchoServiceServer
}

func (s *EchoService) UnaryEchoWithAuth(context.Context, *EchoRequest) (*EchoResponse, error) {
 return &EchoResponse{
  Message: "hello with auth",
 }, nil
}

服务端 server.go

package main

func main() {
 // TLS 认证
 cert, err := tls.LoadX509KeyPair(data.Path("x509/server_cert.pem"), data.Path("x509/server_key.pem"))
 if err != nil {
  log.Fatalf("failed to load key pair: %s", err)
 }
 // 实例化grpc Server,并开启 TLS 认证
 server := grpc.NewServer(grpc.Creds(credentials.NewServerTLSFromCert(&cert)))
 // 注册 service
 echo.RegisterEchoServiceServer(server, &echo.EchoService{})
 listen, err := net.Listen("tcp", ":9091")
 if err != nil {
  log.Print(err.Error())
 }
 log.Print("listen on port:9091 >>>>")
 // 启动服务
 if er := server.Serve(listen); er != nil {
  log.Print(er.Error())
 }
}

客户端 client.go

package main

import (
 "context"
 "example.com/com/example/data"
 "example.com/com/example/echo"
 "google.golang.org/grpc"
 "google.golang.org/grpc/credentials"
 "log"
 "time"
)

func main() {
// 认证
 creds, err := credentials.NewClientTLSFromFile(data.Path("x509/ca_cert.pem"), "x.test.example.com")
 if err != nil {
  log.Fatalf("failed to load credentials: %v", err)
 }
 // 连接增加认证
 conn, err := grpc.Dial("127.0.0.1:9091", grpc.WithTransportCredentials(creds))
 if err != nil {
  panic(err)
 }
 defer conn.Close()
 client := echo.NewEchoServiceClient(conn)
 // 调用服务
 callUnaryEchoWithAuth(client)
}

func callUnaryEchoWithAuth(client echo.EchoServiceClient) {
 ctx, cancel := context.WithTimeout(context.Background(), time.Second)
 defer cancel()
 resp, err := client.UnaryEchoWithAuth(ctx, &echo.EchoRequest{
  Message: "hello",
 })
 if err != nil {
  log.Print(err.Error())
  return
 }
 log.Print("UnaryEchoWithAuth :", resp.Message)
}

gRPC 基于 Token 认证

参考微信公众号获取access_token的方式 (链接),通过一个示例来展示 Token 认证的使用。

image.png

开发环境:
golang 1.20.1
protoc libprotoc 3.20.3
目录结构

│   └── example
│       ├── auth
│       │   ├── client.go
│       │   └── server.go
│       ├── data
│       │   ├── data.go
│       │   └── x509
│       │       ├── ca_cert.pem
│       │       ├── ca_key.pem
│       │       ├── client_ca_cert.pem
│       │       ├── client_ca_key.pem
│       │       ├── client_cert.pem
│       │       ├── client_key.pem
│       │       ├── create.sh
│       │       ├── openssl.cnf
│       │       ├── server_cert.pem
│       │       └── server_key.pem
│       └── echo
│           ├── echo.go
│           ├── echo.pb.go
│           ├── echo.proto
│           ├── echo_grpc.pb.go
│           ├── login.go
│           └── utils.go
├── go.mod
└── go.sum

login.go

package echo

import (
 "context"
 "crypto/md5"
 "fmt"
 "google.golang.org/grpc/codes"
 "google.golang.org/grpc/status"
 "time"
)

type LoginService struct {
 LoginServiceServer
}

var (
 Appid  = "test-id"
 Secret = "abcdefgh"
 Salt   = "abc123"
)

func (s *LoginService) GetToken(ctx context.Context, req *GetTokenRequest) (*GetTokenResponse, error) {
 if req.Appid == Appid && req.Secret == Secret {
  expire := time.Now().Unix() + 1800
  return &GetTokenResponse{
   Token:  MD5(req.Appid + "::" + req.Secret + "::" + Salt),
   Expire: expire,
  }, nil
 }
 return nil, status.Error(codes.InvalidArgument, "appid or secret wrong!")
}

func MD5(str string) string {
 data := []byte(str)
 has := md5.Sum(data)
 md5str := fmt.Sprintf("%x", has)
 return md5str
}

utils.go

package echo

import (
 "context"
 "google.golang.org/grpc/metadata"
)

var (
 // 无需认证的接口
 publicAPIMap = map[string]bool{
  "/echo.LoginService/GetToken": true,
 }
)

func IsPublic(method string) bool {
 return publicAPIMap[method]
}

func GetAccessToken(ctx context.Context) string {
 md, ok := metadata.FromIncomingContext(ctx)
 if !ok {
  return ""
 }
 if v, ok2 := md["access-token"]; ok2 {
  if len(v) == 1 {
   return v[0]
  }
 }
 return ""
}

client.go

package main

import (
 "context"
 "example.com/com/example/data"
 "example.com/com/example/echo"
 "google.golang.org/grpc"
 "google.golang.org/grpc/credentials"
 "google.golang.org/grpc/metadata"
 "google.golang.org/grpc/status"
 "log"
 "time"
)

const (
 Appid  = "test-id"
 Secret = "abcdefgh"
)

var (
 accessToken = ""
)

func main() {
 creds, err := credentials.NewClientTLSFromFile(data.Path("x509/ca_cert.pem"), "x.test.example.com")
 if err != nil {
  log.Fatalf("failed to load credentials: %v", err)
 }
 conn, err := grpc.Dial("127.0.0.1:9091", grpc.WithTransportCredentials(creds), grpc.WithChainUnaryInterceptor(TokenMetadataInterceptor))
 if err != nil {
  panic(err)
 }
 defer conn.Close()
 // 通过appid, secret 获取 accessToken
 loginClient := echo.NewLoginServiceClient(conn)
 accessToken = callGetToken(loginClient)
 if len(accessToken) == 0 {
  log.Fatalf("获取access token 失败")
 }
 log.Println("accessToken=", accessToken)
 client := echo.NewEchoServiceClient(conn)
 // 调用远程方法
 callUnaryEchoWithAuth(client)
}

// TokenMetadataInterceptor 增加 token metadata
func TokenMetadataInterceptor(ctx context.Context, method string, req, reply any, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
 // 增加 token metadata
 ctx = metadata.AppendToOutgoingContext(ctx, "access-token", accessToken)
 return invoker(ctx, method, req, reply, cc)
}

func callGetToken(client echo.LoginServiceClient) string {
 ctx, cancel := context.WithTimeout(context.Background(), time.Second)
 defer cancel()
 resp, err := client.GetToken(ctx, &echo.GetTokenRequest{
  Appid:  Appid,
  Secret: Secret,
 })
 if err != nil {
  code := status.Code(err)
  log.Fatal(code.String())
  return ""
 }
 log.Printf("GetToken Token=%v, Expire=%v\n", resp.Token, resp.Expire)
 return resp.Token
}

func callUnaryEchoWithAuth(client echo.EchoServiceClient) {
 ctx, cancel := context.WithTimeout(context.Background(), time.Second)
 defer cancel()
 resp, err := client.UnaryEchoWithAuth(ctx, &echo.EchoRequest{
  Message: "hello",
 })
 if err != nil {
  log.Print(err.Error())
  return
 }
 log.Print("UnaryEchoWithAuth :", resp.Message)
}

server.go

package main

import (
 "context"
 "crypto/tls"
 "example.com/com/example/data"
 "example.com/com/example/echo"
 "google.golang.org/grpc"
 "google.golang.org/grpc/codes"
 "google.golang.org/grpc/credentials"
 "google.golang.org/grpc/status"
 "log"
 "net"
)

func main() {
 // TLS 认证
 cert, err := tls.LoadX509KeyPair(data.Path("x509/server_cert.pem"), data.Path("x509/server_key.pem"))
 if err != nil {
  log.Fatalf("failed to load key pair: %s", err)
 }
 // 实例化grpc Server,并开启 TLS 认证
 server := grpc.NewServer(grpc.Creds(credentials.NewServerTLSFromCert(&cert)), grpc.ChainUnaryInterceptor(TokenInterceptor))
 // 注册 service
 echo.RegisterEchoServiceServer(server, &echo.EchoService{})
 echo.RegisterLoginServiceServer(server, &echo.LoginService{})
 listen, err := net.Listen("tcp", ":9091")
 if err != nil {
  log.Print(err.Error())
 }
 log.Print("listen on port:9091 >>>>")
 // 启动服务
 if er := server.Serve(listen); er != nil {
  log.Print(er.Error())
 }
}

func TokenInterceptor(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp any, err error) {
 log.Println("TokenInterceptor::" + info.FullMethod)
 if echo.IsPublic(info.FullMethod) {
  return handler(ctx, req)
 }
 // 进行 token 校验
 token := echo.GetAccessToken(ctx)
 if len(token) == 0 {
  return nil, status.Errorf(codes.Unauthenticated, err.Error())
 }
 // 这里只是简单的演示认证的使用,实际开发过程可能需要以下的步骤
 // TODO 这里需要判断token是否过期,例如存储到类似 redis 这样的组件
 // TODO 同时这里还需要进行风控判断,token 有可能被泄漏, 也可以在加一个拦截器进行风控判断
 return handler(ctx, req)
}

总结
对 gRPC 认证相关知识的了解,token 的生成可能有 2 种,一种是在客户端生成,另一种是在服务生成。基于 token 的示例展示的 token 在服务端生成再下发到客户端。在真实的业务开发中,还需要了解更多认证相关的知识,token 管理,安全,风控等等。