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"
}