bigywater.tech
gRPC · 26

认证:per-RPC credentials 与 token 刷新

gRPC 的 token 认证是怎么挂到每个调用上的?为什么必须配 TLS?token 要过期了怎么刷新才不出错?并发刷新会不会把令牌服务打爆?这篇用自己实现的 PerRPCCredentials 逐项验证。

9.2% → 0%
令牌过期才刷新时被服务端拒绝的比例;提前 300 ms 刷新后为 0
50 → 1
50 个并发调用遇到过期令牌,不加锁与加锁时取令牌的次数
716 → 9 字节
1000 字节令牌的请求 HEADERS 帧,第 1 个与第 100 个的大小

接口

实测 · 本机执行输出
  RequireTransportSecurity()=true + 明文连接:NewClient 的结果:grpc: the credentials require transport level security (use grpc.WithTransportCredentials() to set)
  RequireTransportSecurity()=false + 明文连接:代理(窃听者)在线上看到 authorization: Bearer SECRET-TOKEN-123
  RequireTransportSecurity()=true + TLS 连接:NewClient 结果 <nil>;调用 成功

PerRPCCredentials 有两个方法:GetRequestMetadata(每个 RPC 调用一次,返回要加的头)和 RequireTransportSecurity(是否必须加密)。用 WithPerRPCCredentials 设为连接级,或用 PerRPCCredentials 调用选项设单次。

必须加密

实测 · 本机执行输出
  100 个调用:GetRequestMetadata 被调用 100 次;请求 HEADERS 帧载荷大小 第 1 个 716 字节、第 2 个 17 字节、第 100 个 9 字节

声明需要加密而连接是明文,NewClient 直接报错;如果声明不需要,令牌会在线上以明文出现。

刷新要提前

实测 · 本机执行输出
  每次都取新令牌                共 141 次调用;取令牌 141 次;被服务端拒绝   0 次(0.0%)
  缓存,过期才刷新               共 141 次调用;取令牌   3 次;被服务端拒绝  13 次(9.2%)
  缓存,提前 300 ms 刷新        共 142 次调用;取令牌   5 次;被服务端拒绝   0 次(0.0%)

令牌过期前就刷新,留出时钟偏差和传输延迟的余量,否则会出现一批“刚好过期”的请求被拒绝。

并发保护

实测 · 本机执行输出
  不加锁                        50 个并发调用,取令牌 50 次
  加锁(只有一个去取,其余等待)            50 个并发调用,取令牌 1 次

GetRequestMetadata 会被并发调用,刷新令牌要有锁,避免过期瞬间所有调用一起去取。

错误和 deadline

实测 · 本机执行输出
  GetRequestMetadata 返回 普通 error                       → 调用 code=Unauthenticated message="transport: per-RPC creds failed due to error: 取令牌失败"
  GetRequestMetadata 返回 status.Error(Unavailable)      → 调用 code=Unavailable message="令牌服务不可用"
  GetRequestMetadata 返回 status.Error(PermissionDenied) → 调用 code=PermissionDenied message="无权"
  取令牌要 500 ms、调用 deadline 200 ms:code=Unauthenticated message="transport: per-RPC creds failed due to error: context deadline exceeded";201.2 ms 后返回

返回 status 错误能控制调用得到的 code;返回普通 error 会变成 Unauthenticated。取令牌的时间算在调用的 deadline 里。

叠加而非覆盖

实测 · 本机执行输出
  只有连接级凭证:服务端看到 authorization = ["Bearer CONN-LEVEL"]
  再加单次调用的凭证:服务端看到 authorization = ["Bearer CONN-LEVEL" "Bearer CALL-LEVEL"]

连接级和单次调用的凭证同时存在时,两个 authorization 都会被发送。

分步演示

认证
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Go 1.27.1 / grpc-go v1.84.0 / protobuf-go v1.36.12 / golang.org/x/net v0.57.0 / protoc 36.2(Homebrew);服务端和客户端都是本机起的 Go 进程(127.0.0.1,端口 9800 以上,不访问任何外部服务);需要看线上字节时用我自己写的 TCP 抓包代理加 golang.org/x/net/http2 的 Framer 解析 HTTP/2 帧;计时用 time.Now 或 perf_counter,数字每次运行会有小幅抖动。令牌与过期校验是我自己写的最小实现(不是 OAuth);“服务端时钟快 100 ms”是人为模拟,用来说明提前刷新的必要性,实际余量要看你的时钟偏差和网络延迟;HEADERS 帧大小来自抓包代理;TLS 部分用内存里生成的 CA
    • 没有验证:官方 oauth / google 凭证包的刷新策略与默认提前量;credentials.PerRPCCredentials 在 xDS / ALTS 下的行为;令牌被服务端拒绝(Unauthenticated)后客户端是否要强制刷新再重试;服务端从 metadata 读令牌后对 authorization 多值的取舍;JWT 校验的性能
    京ICP备2026067502号-1京公网安备11010502063487号