跳到主要内容

gRPC 的认证

gRPC 提供了多种认证机制,以确保客户端和服务器之间的通信是安全的。以下是 gRPC 常用的认证方法:

  • Token 令牌认证(Token-based Authentication)
    • gRPC 支持使用令牌(如 JWT)进行认证。客户端在每次请求时携带令牌,服务器验证令牌的有效性以确定客户端的身份。
    • 这种方法适用于需要无状态认证的场景。
  • SSL/TLS 认证
    • gRPC 支持使用 SSL/TLS 协议来加密通信,确保数据在传输过程中不被窃听或篡改。通过配置服务器和客户端的证书,可以实现双向认证。
    • 服务器端需要配置 SSL 证书和私钥,客户端则需要信任服务器的证书。
  • 元数据认证(Metadata-based Authentication)
    • gRPC 允许在请求的元数据中添加认证信息。客户端可以在请求头中添加自定义的认证字段,服务器在处理请求时读取这些字段进行认证。
    • 这种方法灵活且易于实现,但需要确保元数据的安全性。
  • OAuth2 认证
    • gRPC 可以与 OAuth2 认证机制集成,允许客户端使用 OAuth2 令牌进行认证。客户端在请求中携带 OAuth2 访问令牌,服务器验证令牌以确定客户端的身份。
    • 这种方法适用于需要与第三方身份提供商集成的场景。
  • 自定义认证
    • gRPC 允许开发者实现自定义的认证机制。通过实现 gRPC 的拦截器(Interceptor),可以在请求处理过程中添加自定义的认证逻辑。
    • 这种方法适用于特殊的认证需求,但需要开发者自行处理认证细节

Token 令牌认证

实现原理

gRPC 的 Token 认证通过一元拦截器Unary Interceptor实现,在请求到达具体的服务方法之前进行统一验证。这种方式避免了在每个服务方法中重复编写验证逻辑,提高了代码的可维护性和一致性

实现流程

服务端拦截器

服务端拦截器负责

  • 从 gRPC metadata 中提取 token
  • 验证 token 的有效性
  • 记录请求日志
  • 处理白名单方法
func UnaryAuthInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
startTime := time.Now()

// 提取 metadata
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return nil, status.Errorf(codes.Unauthenticated, "missing metadata")
}

// 检查白名单
if shouldIgnoreGrpcMethod(info.FullMethod) {
resp, handlerErr := handler(ctx, req)
// 白名单方法也记录日志,但不验证 token
logRequest(ctx, md, "", info.FullMethod, startTime, handlerErr)
return resp, handlerErr
}

// 提取并验证 token
tokenValue, err := extractTokenFromMetadata(md)
if err != nil {
return nil, status.Errorf(codes.Unauthenticated, "%v", err)
}

username, err := verifyToken(ctx, tokenValue)
if err != nil {
return nil, status.Errorf(codes.Unauthenticated, "%v", err)
}

// 执行请求处理
resp, handlerErr := handler(ctx, req)

// 记录请求日志
logRequest(ctx, md, username, info.FullMethod, startTime, handlerErr)

return resp, handlerErr
}

Token 提取

从 gRPC metadata 中提取 authorization header,支持 Bearer 前缀:

func extractTokenFromMetadata(md metadata.MD) (string, error) {
authHeaders := md.Get("authorization")
if len(authHeaders) == 0 {
return "", fmt.Errorf("missing authorization header")
}

tokenValue := authHeaders[0]
// 去除 Bearer 前缀
if strings.HasPrefix(tokenValue, "Bearer ") {
tokenValue = strings.TrimPrefix(tokenValue, "Bearer ")
}
tokenValue = strings.TrimSpace(tokenValue)

if tokenValue == "" {
return "", fmt.Errorf("token value is empty")
}

return tokenValue, nil
}

Token 验证

Token 验证逻辑可以根据实际需求实现,例如验证 JWT token:

func verifyToken(ctx context.Context, tokenValue string) (string, error) {
// 解析并验证 JWT token
claims, err := parseJWTToken(tokenValue)
if err != nil {
// 区分 token 过期和 token 无效
if isTokenExpired(err) {
return "", fmt.Errorf("token expired: %v", err)
}
return "", fmt.Errorf("token invalid: %v", err)
}

// 返回用户名或其他标识信息
return claims.Username, nil
}

客户端 Token 传递

客户端通过 metadata.AppendToOutgoingContext 将 token 添加到请求的 metadata 中:

func (c *Client) callService(ctx context.Context, req *pb.Request) (*pb.Response, error) {
ctx, cancel := context.WithTimeout(ctx, time.Second*30)
defer cancel()

// 通过 metadata 传递 token
if c.token != "" {
ctx = metadata.AppendToOutgoingContext(ctx, "authorization", "Bearer "+c.token)
}

resp, err := c.client.SomeMethod(ctx, req)
return resp, err
}

客户端使用示例:

// 创建客户端并设置 token
client := NewClient().
SetEndpoint("localhost:50051").
SetToken("your-jwt-token-here")

// 调用服务方法,token 会自动通过 metadata 传递
resp, err := client.SomeMethod(ctx, &pb.Request{...})

拦截器注册

在创建 gRPC 服务器时注册拦截器:

func main() {
// 创建 gRPC 服务器并注册拦截器
grpcSrv := grpc.NewServer(
grpc.UnaryInterceptor(UnaryAuthInterceptor),
)

// 注册服务
pb.RegisterYourServiceServer(grpcSrv, &yourService{})

// 启动服务器
lis, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatalf("failed to listen: %v", err)
}

if err := grpcSrv.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}

白名单机制

对于不需要 token 验证的方法(如健康检查、内部服务调用等),可以通过白名单机制放行:

var globalIgnoreGrpcMethods = []string{
"/grpc.health.v1.Health/Check", // 健康检查
}

func shouldIgnoreGrpcMethod(fullMethod string) bool {
for _, ignoreMethod := range globalIgnoreGrpcMethods {
if fullMethod == ignoreMethod {
return true
}
}
return false
}

请求日志记录

拦截器可以记录每次请求的详细信息,包括客户端 IP、用户名、状态码、延迟等:

func logRequest(ctx context.Context, md metadata.MD, username, method string, startTime time.Time, handlerErr error) {
endTime := time.Now()
grpcStatus := codes.OK
if handlerErr != nil {
if st, ok := status.FromError(handlerErr); ok {
grpcStatus = st.Code()
} else {
grpcStatus = codes.Unknown
}
}

log.Printf(
"gRPC request - client_ip: %s, username: %s, status: %d, latency: %s, timestamp: %d, method: %s",
getClientIP(ctx, md),
username,
int(grpcStatus),
endTime.Sub(startTime).String(),
endTime.Unix(),
method,
)
}

func getClientIP(ctx context.Context, md metadata.MD) string {
// 优先从 X-Forwarded-For 获取(经过代理的情况)
if forwardedFor := md.Get("x-forwarded-for"); len(forwardedFor) > 0 {
ips := strings.Split(forwardedFor[0], ",")
if len(ips) > 0 {
return strings.TrimSpace(ips[0])
}
}
// 从 X-Real-IP 获取
if realIP := md.Get("x-real-ip"); len(realIP) > 0 {
return realIP[0]
}
// 从 peer 信息获取
if p, ok := peer.FromContext(ctx); ok {
return p.Addr.String()
}
return "unknown"
}