grpc认证需分层建设:tls仅保障传输安全而不验证调用者身份,per-rpc认证(如jwt token)提供身份凭证,服务端拦截器才是权限控制落地的关键;跳过任一层均可能导致绕过。

gRPC 认证不是“开了 TLS 就安全了”,必须分层建设:传输层加密(TLS)是底线,Per-RPC 认证(如 Token)是身份凭证,而服务端拦截器才是权限落地的关键环节。跳过任一层,都可能被绕过。
为什么 NewServerTLSFromFile 不能替代服务端身份校验
TLS 只保证“连接的是对的服务器”和“数据没被窃听”,它不验证“调用者是谁”。客户端只要拿到服务端证书(比如开发环境直接复制 server.crt),就能建立连接并发起任意 RPC 调用——哪怕它根本没登录。
- 常见错误现象:
rpc error: code = Unauthenticated desc = transport: authentication handshake failed是 TLS 层失败;而code = PermissionDenied或静默返回空数据,才是业务层认证缺失的典型表现 - 生产环境必须禁用自签名证书直传:把
server.crt放进客户端代码等于把锁芯图纸公开,应改用私有 CA 签发 + 客户端信任根证书(ca.crt) -
NewServerTLSFromFile加载的是单向证书;如需双向认证(mTLS),服务端得用credentials.NewServerTLSFromCert并配合客户端提供证书,且服务端需在拦截器中调用peer.FromContext(ctx).AuthInfo.(credentials.TLSInfo).State.VerifiedChains检查链有效性
如何正确实现 PerRPCCredentials 并避免 Token 泄露
Token 必须随每次 RPC 请求携带,但不能硬编码在客户端实例里,也不能明文塞进日志。核心是让凭证动态、可刷新、作用域可控。
- 不要这样写:
token := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."—— Token 泄露即永久失陷 - 应该封装成结构体,实现
GetRequestMetadata方法,在每次调用前从安全存储(如内存缓存 + TTL)读取,并检查过期:func (a *auth) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) { t, err := a.tokenProvider.Get() if err != nil { return nil, err } return map[string]string{"authorization": "Bearer " + t}, nil } - 务必设置
RequireTransportSecurity():否则grpc.Dial在非 TLS 连接下仍会发送 Token,导致明文泄露 - HTTP/2 的
authorizationheader 是标准传递方式,gRPC 服务端拦截器可通过metadata.FromIncomingContext(ctx)提取,无需解析 raw HTTP headers
服务端拦截器中解析 JWT 的三个关键检查点
拿到 Token 后,不能只做 ParseToken 就放行。JWT 验证必须覆盖签名、时效、受众三要素,否则攻击者可篡改 payload 或复用过期 Token。
- 必须校验
aud(Audience):Token 应明确指定仅用于当前服务,例如claims.Audience == "user-service",防止一个服务的 Token 被盗用到订单服务 - 必须校验
exp和nbf:用jwt.WithValidator或手动比对time.Now().Before(claims.ExpiresAt.Time),注意时钟漂移问题(建议允许 ±1 分钟误差) - 密钥不能硬编码:
HS256的 secret 必须从环境变量或 KMS 获取;若用RS256,公钥应从可信源(如 JWKS endpoint)动态加载,而非本地文件 - 示例中常见的坑:
jwt.ParseWithClaims(tokenString, &CustomClaims{}, func(t *jwt.Token) (interface{}, error) { return []byte("hardcoded-secret"), nil })—— 这种写法让所有服务共用同一密钥,一处泄露全盘崩溃
RBAC 权限检查必须在拦截器中完成,而非业务 handler 内
权限逻辑如果散落在每个 GetUser、CreateOrder 的 handler 里,极易遗漏或写错。统一拦截器能强制执行、集中审计、便于灰度开关。
- 拦截器获取用户角色后,应查预定义策略表(如
map[string][]string{"admin": {"user.*", "order.*"}}),而不是用if role == "admin"硬判断 - 路径匹配要支持通配符:
"user.get"匹配UserService/GetUser,"user.*"匹配全部用户接口,避免漏掉新添加的 RPC 方法 - 拒绝时必须返回标准 gRPC 错误码:
status.Error(codes.PermissionDenied, "insufficient scope"),而非codes.Unauthenticated或自定义字符串,否则客户端重试逻辑会混乱 - 特别注意流式 RPC(
stream):Unary 拦截器不生效,必须用StreamInterceptor单独处理,且首次消息才校验权限,后续消息不再重复校验(避免性能损耗)
最常被忽略的点是:TLS 和 Per-RPC 认证是正交的,不是二选一。即使用了 mTLS,也仍需在拦截器里提取证书中的 SAN 字段做服务身份校验(比如确认调用方是 payment-service 而非伪造的 attacker-service),否则内部网络一旦被突破,横向移动毫无阻碍。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











