无尘阁日记

无尘阁日记

SSO Token Exchange 实战全流程拆解:encryption生成、verify校验与对方JWT解析完整指南
2026-03-17

这两天你做的事情,本质上是在打通一个“跨系统信任链路”,它不是简单的接口对接,而是一个典型的 SSO Token Exchange 体系,核心目标是:在不共享密钥、不暴露敏感信息的前提下,让两个系统建立可信身份。

整个过程其实可以抽象成一句话:
“我用一段带签名的身份声明证明我是我,你来验证这段声明确实是我发的,然后你再给我发一个你体系内的通行证。”

下面我把你这两天做的所有关键点,从协议理解、JWT结构、encryption生成、verify设计、解析对方算法、常见坑位,一层一层完整梳理。

一、整体链路到底在干什么

你现在做的是一个“双向信任但单向签发”的流程:

诺明 → 智评云(发起方)
携带一个 JWT(也就是 encryption)

智评云 → 诺明(回调 verify)
确认这个 JWT 是不是你签的

智评云 → 诺明(返回 token)
发放它系统内的 access_token

这中间最关键的不是 token,而是:
JWT 是如何构造的,以及如何证明它是“你发的”。

所以核心分成三块:

  1. encryption(JWT)生成

  2. 对方如何解析 JWT

  3. verify 如何反向校验签名来源

二、JWT结构的关键理解(你踩过的核心点)

标准 JWT 是三段:

header.payload.signature

但你这次对接有一个“非标准点”,也是你一开始解析困难的地方:

真正的业务数据,不在 payload 第一层,而是在 payload.sub 里,而且还是一个 JSON 字符串。

也就是说结构是:

payload = {
sub: "一个字符串,但这个字符串本身是JSON",
iat: 时间戳,
exp: 过期时间
}

而 sub 解出来才是:

{
creditCode,
tenantId,
module,
thirdparty,
expireTime,
nonce
}

这就是为什么你写了解析逻辑:

先 base64 解 payload
再取 payload.sub
再 JSON decode 一次

这一步如果漏掉,就会出现“解析出来但没有业务字段”的问题。

本质一句话总结:

payload 是壳,sub 才是肉。

三、encryption生成的完整逻辑

你现在的生成逻辑已经是标准且正确的,可以总结为五步:

第一步:构造 header

{
"alg": "HS256",
"typ": "JWT"
}

第二步:构造 payload(注意 sub 是字符串)

payload = {
"sub": Json.encode(业务字段),
"iat": 当前时间戳,
"exp": 当前时间 + 有效期
}

注意这里有两个“时间体系”:

JWT层:
iat / exp(JWT本身有效期)

业务层:
expireTime(你传给对方的业务过期时间)

这两个不要混。

第三步:Base64Url编码

headerBase64 = base64UrlEncode(header)
payloadBase64 = base64UrlEncode(payload)

注意必须是 Base64Url,不是普通 Base64,否则会解析失败。

第四步:签名

signature = HMACSHA256(
headerBase64 + "." + payloadBase64,
secret
)

第五步:拼接

encryption = headerBase64 + "." + payloadBase64 + "." + signature

这就是最终你发给对方的 encryption。

四、你这次生成过程中踩的几个关键坑

这部分非常有价值,是实战里最容易出问题的地方。

  1. expireTime 和 exp 混淆

你一开始是用一个时间,但其实:

exp 是 JWT 校验用
expireTime 是业务校验用

如果对方只看 expireTime,你 exp 再对也没用。

  1. sub 必须是字符串,不是对象

这个是对方协议的“坑点设计”。

如果你写成:

"sub": { ... }

那对方 decode 后就不认。

必须是:

"sub": "{"creditCode":"xxx"}"

也就是 JSON 字符串。

  1. Base64Url 和 Base64 混用

如果你用标准 Base64,会出现:

  • / =

而 JWT 要求:

  • _ 去掉 padding

否则对方解析会报:

JWT解析失败 / signature不匹配

  1. signature 一点点差错都会失败

比如:

  • header顺序不同

  • JSON编码转义不同

  • 空格不同

  • 中文未 UNESCAPED

都会导致签名完全不一样。

五、verify接口的本质作用

很多人会误解 verify 是“再验证一次 JWT”。

其实不是。

verify 的本质是:

让对方确认:
这个 JWT 是不是你这个服务端签出来的。

也就是说,它验证的是“签发主体”,不是“内容结构”。

流程是:

对方拿到 JWT → 解出 payload → 拿 sub →
调用你的 verify 接口 → 把关键字段传回来 →
你校验 → 返回结果

你这边 verify 做的事情,本质上是:

  1. 校验参数完整性

  2. 校验 expireTime 是否过期

  3. 校验 nonce 是否重复(防重放攻击)

  4. 校验 tenantId / creditCode 是否合法

  5. 返回 success / fail

这里最关键的是 nonce。

nonce 的意义是:

防止别人拿你一次合法请求无限重放。

你这里也设计了:

nonce TTL(默认60秒)

这一步是整个安全体系的核心之一。

六、对方解析算法你已经完全吃透的关键点

通过你反复测试,其实已经把对方解析逻辑摸清楚了,可以总结为:

  1. split JWT(三段)

  2. base64Url decode payload

  3. JSON decode payload

  4. 取 sub

  5. JSON decode sub

  6. 校验字段

也就是说,对方不会帮你自动解 sub,他们是“手动二次解”。

这就是为什么你必须严格保证 sub 是 JSON字符串。

七、为什么你会出现“解析失败”的问题

你那几次“生成了但解析不出来”,本质原因就三类:

  1. sub不是字符串

  2. Base64编码不对

  3. signature计算不一致

尤其是第二点和第三点,非常隐蔽。

你后来让“只输出encryption字符串”,其实就是在排除干扰项,这是一个非常正确的调试策略。

八、整个安全模型的本质

你现在这个方案,本质上不是 OAuth2,而是一个轻量版的:

JWT + 双向验证 + 临时凭证签发

可以抽象成三层:

第一层:身份声明(JWT)
第二层:签发验证(verify)
第三层:访问凭证(access_token)

它解决了一个关键问题:

在没有预共享登录态的情况下,如何建立系统间信任。

九、从工程角度的最佳实践建议

结合你现在的实现,我给你几个“企业级会用的优化点”:

  1. JWT生成和解析必须封装成独立服务类
    不要散落在 controller

  2. nonce 必须落库,并带TTL
    否则防重放形同虚设

  3. 所有异常必须标准化
    比如:

A1002 | JWT解析失败
A1203 | 请求重复

你已经在往这个方向走了,这是对的

  1. encryption日志必须记录
    但注意不要记录敏感字段明文

  2. verify接口必须限流 + IP白名单
    否则容易被刷

十、你这套方案的价值总结

你这次不是在写一个接口,而是在做一件更重要的事:

把“跨系统信任”变成一套可复制的标准能力。

它未来可以扩展到:

多系统对接
多租户统一认证
外部平台接入
API网关统一鉴权

甚至可以演进成你们自己的:

企业级 SSO 中台能力

一句话总结你这两天的成果:

你已经把“JWT怎么用”,升级成了“JWT怎么构建信任体系”。

这才是这件事真正的价值所在。