跳转至

凭证体系与 Token 鉴权机制

除了网络传输层的 TLS 加密,分布式系统中更细粒度的控制来自于用户与租户级别的身份凭证(Credentials)与授权(Authorization)


1. gRPC 凭证分层模型

gRPC 将安全体系清晰划分为两个独立层级:

graph TD
    subgraph ChannelCreds ["1. 通道凭证 (Channel Credentials)"]
        CC1["TLS / mTLS / Insecure"]
        CC2["保障 TCP 连接级别的保密性与节点身份合法性"]
    end

    subgraph CallCreds ["2. 调用凭证 (Call Credentials)"]
        CA1["OAuth2 Token / JWT / API Key"]
        CA2["伴随每个 RPC 独立传递,用于标识操作者身份"]
    end

    subgraph Composite ["复合凭证 (Composite Credentials)"]
        Comp["结合 ChannelCreds + CallCreds<br/>(例如:在 TLS 安全信道上安全传递 Bearer Token)"]
    end

    ChannelCreds --> Composite
    CallCreds --> Composite

[!CAUTION] 安全防护铁律PerRPCCredentials 默认强制要求 RequireTransportSecurity() == true。如果底层不是 TLS 加密信道,gRPC 会直接拒绝发送 Token,防止核心密码凭据在公网明文泄漏。


2. 客户端:实现自定义 PerRPCCredentials

在 Go 中,只需实现 credentials.PerRPCCredentials 接口:

package main

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

// 1. 定义 Token 凭据对象
type TokenAuth struct {
    Token string
}

// 2. 实现 GetRequestMetadata: 在发起 RPC 时自动注入 Metadata
func (t TokenAuth) GetRequestMetadata(ctx context.Context, in ...string) (map[string]string, error) {
    return map[string]string{
        "authorization": "Bearer " + t.Token,
    }, nil
}

// 3. 声明是否强制要求 TLS 加密
func (t TokenAuth) RequireTransportSecurity() bool {
    return true // 生产环境必须返回 true
}

func main() {
    // 加载 TLS 凭据
    tlsCreds, _ := credentials.NewClientTLSFromFile("ca-cert.pem", "localhost")

    // 组合 TLS 与 Token 认证
    conn, err := grpc.Dial(
        "localhost:50051",
        grpc.WithTransportCredentials(tlsCreds),
        grpc.WithPerRPCCredentials(TokenAuth{Token: "my-jwt-secret-token"}),
    )
    if err != nil {
        panic(err)
    }
    defer conn.Close()
}

3. 服务端:基于拦截器的 JWT 鉴权与 RBAC 权限控制

服务端通过全局 Unary 拦截器拦截所有调用,统一解析 JWT 并执行基于角色的访问控制(RBAC):

package main

import (
    "context"
    "strings"

    "google.golang.org/grpc"
    "google.golang.org/grpc/codes"
    "google.golang.org/grpc/metadata"
    "google.golang.org/grpc/status"
)

// 定义基于方法的白名单或权限表
var methodPermissions = map[string]string{
    "/order.v1.OrderService/GetOrder":    "user",
    "/order.v1.OrderService/DeleteOrder": "admin",
}

func AuthInterceptor(
    ctx context.Context,
    req any,
    info *grpc.UnaryServerInfo,
    handler grpc.UnaryHandler,
) (any, error) {
    // 1. 提取请求头中的 Metadata
    md, ok := metadata.FromIncomingContext(ctx)
    if !ok {
        return nil, status.Errorf(codes.Unauthenticated, "缺少元数据头部")
    }

    authHeaders := md.Get("authorization")
    if len(authHeaders) == 0 {
        return nil, status.Errorf(codes.Unauthenticated, "缺少 authorization Token")
    }

    rawToken := authHeaders[0]
    if !strings.HasPrefix(rawToken, "Bearer ") {
        return nil, status.Errorf(codes.Unauthenticated, "Token 格式非法,必须为 Bearer 格式")
    }
    tokenString := strings.TrimPrefix(rawToken, "Bearer ")

    // 2. 校验 JWT 并解析出 Claims
    claims, err := parseAndVerifyJWT(tokenString)
    if err != nil {
        return nil, status.Errorf(codes.Unauthenticated, "Token 验证失效: %v", err)
    }

    // 3. RBAC 细粒度权限校验
    requiredRole, exists := methodPermissions[info.FullMethod]
    if exists && claims.Role != requiredRole && claims.Role != "super_admin" {
        return nil, status.Errorf(codes.PermissionDenied, "权限不足: 接口需要 [%s] 角色", requiredRole)
    }

    // 4. 将解析出的身份上下文绑定到 Context,传递给业务 Handler
    newCtx := context.WithValue(ctx, "user_id", claims.UserID)
    return handler(newCtx, req)
}

生产治理建议

  • 公网边界网关鉴权:通常由 API Gateway(如 Envoy、APISIX)完成外部 JWT 解析与验签,向内部微服务转发时转换为带有数字签名的内部用户 Header,从而减轻内部各微服务重复验签的 CPU 开销。
  • 服务间相互调用授权(mTLS + SPIFFE/SPIRE):微服务与微服务之间的身份准入依靠 mTLS 证书中的 SPIFFE ID,而不用传用户密码,实现真正解耦。