本地验签与服务端交换
本页解释 wicket 三个无状态验证包背后的两种机制,以及如何按手里凭据的形态选择。
三个包的共同点:都是业务后端直接调用的普通 Go 包,不经过 wicket 的中间件、不读写任何 store、也不依赖 DI 容器;输入一个移动端或前端拿到的凭据串,输出一份已核实的身份或一个可分类的错误。
两种机制
本地验签(oidcverify):凭据是一个带签名、自包含的 ID Token(JWT)。后端用提供者公布的 JWKS 公钥在本地验证签名,再检查 iss、aud、exp 等 claim。公钥会被缓存,所以绝大多数验证不需要出网。
服务端交换(wechatverify、phoneverify):凭据是一个只有厂商自己能解读的短时不透明串(微信授权 code、号码认证 token)。后端带上自己的应用凭据,把它发给厂商接口,由厂商返回身份。每次验证都是一次网络往返。
对比
| 维度 | 本地验签 oidcverify | 服务端交换 wechatverify / phoneverify |
|---|---|---|
| 凭据形态 | 签名 JWT,自带 claims | 不透明短时串 |
| 每次验证是否出网 | 否;只有首次取 JWKS、遇到未知 kid、缓存到期刷新时出网 | 是,每次一个往返 |
| 典型延迟 | 本地密码学运算,亚毫秒级 | 取决于到厂商的网络往返 |
| 厂商故障的影响 | 公钥已缓存时不受影响 | 厂商不可用即无法验证 |
| 后端需持有的机密 | 无,只需公钥 | 有:应用密钥 / 访问密钥 |
| 时间判定 | 本方:需注入时钟校验 exp / iat / nbf | 厂商侧:凭据是否过期由厂商判断 |
| 配额与限流 | 无 | 受厂商 QPS 与配额约束,“可重试”是常规分支 |
| 同一凭据重复验证 | 可以,结果一致 | 通常不可以,凭据大多一次性,重复交换会失败 |
| 错误风格 | 9 个细分哨兵(签名、过期、签发方、受众、nonce……) | 4 个类别哨兵 + *VendorError 携带厂商原始码 |
怎么选
规则只有一条:看凭据形态,而不是看厂商名字。
- 凭据是签名 JWT(Apple 的
identityToken、Google 登录 SDK 的 ID Token、任何标准 OIDC 提供者的 ID Token)→ 用oidcverify。即使厂商也提供“把 token 发过来我帮你验”的接口,也没有必要用:多一次出网,只会把厂商的故障变成你的故障。 - 凭据是厂商私有的不透明串 → 只能走服务端交换,因为除了厂商没人能读懂它。
两类共同遵循的约定
无论哪种机制,三个包都采用同一套保守姿态:
- 失败一律关闭(fail-closed)。任何无法确认身份的情况都返回错误,不存在“验证失败但仍给个部分身份”。
- 未配置即构造失败。缺少必要配置时,
NewVerifier当场返回错误,而不是等到第一次验证才发现。wechatverify/phoneverify返回ErrUnavailable;oidcverify在签发方或受众为空时返回普通错误。 - 读不懂的响应按“拒绝”处理。服务端交换中遇到无法识别的厂商返回码,归入
ErrInvalidCredential而不是ErrRetryable——把一次读不懂的拒绝当作可重试,可能把它重试成一次意外放行。 - 调用方取消不算可重试(仅
wechatverify/phoneverify)。ctx被取消时,错误满足errors.Is(err, context.Canceled),且不带可重试标记;deadline 到期则同时满足context.DeadlineExceeded与ErrRetryable。oidcverify不保留上下文错误:取 JWKS 时的取消或超时都表现为ErrKeyNotFound。 - 包内不写日志。错误值和配置的字符串形式已对机密脱敏;日志由调用方记录。
对外响应码建议
下表是把验证结果映射为你自己 API 响应的一种常见做法,供参考:
| 结果 | 建议响应 | 理由 |
|---|---|---|
| 成功 | 200,继续业务流程 | — |
ErrInvalidCredential,或 oidcverify 除 ErrKeyNotFound 外的哨兵 | 401 | 用户提交的凭据不成立 |
oidcverify.ErrKeyNotFound | 401 或 503 | 可能是令牌的 kid 确实不存在,也可能是 JWKS 拉取失败;两者无法仅凭哨兵区分,建议告警并结合日志判断 |
ErrRetryable | 503,可带 Retry-After | 暂时无法判定,客户端可稍后重试 |
ErrUnavailable | 503,并告警 | 本方配置或权限问题,应由运维处理 |
ErrMalformedResponse | 502 | 厂商返回了无法理解的内容 |
context.Canceled | 不必响应 | 调用方已放弃 |
移动端登录的典型时序(服务端交换)
text
App ──(厂商 SDK)──> 厂商:用户授权
App <───────────── 厂商:一次性 code / token
App ──(code)──────> 你的后端
你的后端 ──(code + 应用凭据)──> 厂商接口
你的后端 <──(unionid / 手机号)── 厂商接口
App <──(你的会话)── 你的后端:按身份查找或创建用户应用密钥只存在于你的后端(以及 wicket 所在进程),不必下发给 App,也不必分发到每个业务系统。