gRPC 可与各种身份验证机制配合使用,因此可以安全的使用 gRPC 与其他系统通信。 gRPC 也提供了一些简单的认证API,当创建通道或者调用,让你提供所需必要认证信息作为 Credentials。
TLS 相关的知识可以参考链接 www.cloudflare.com/zh-cn/learn…
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.sh
和 openssl.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 认证的使用。
开发环境:
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 管理,安全,风控等等。