4. 令牌生命周期管理:签发、验证、刷新、吊销

令牌这东西,说白了就是一把临时钥匙。钥匙从铸造、使用、换新到销毁,每一步都有坑。我在好几个项目里都见过,明明授权模型设计得不错,偏偏在令牌管理这环节漏了风,最后被安全审计追着改。

这一章我们就把令牌的四个阶段——签发、验证、刷新、吊销——逐个拆开。你想想看,每个阶段都要回答一个关键问题:谁可以拿令牌?谁可以用令牌?令牌过期了怎么办?令牌被盗了怎么废掉?

4.1 令牌签发:第一次握手

签发是令牌生命周期的起点。说白了,就是认证成功之后,服务端给客户端发一张“通行证”。我个人习惯把签发过程分成三个步骤:

  1. 身份确认:用户名密码、OAuth授权码、SSO断言……你得先确认来的人是谁。
  2. 权限组装:根据用户角色、资源访问策略,把允许的权限拼成一个claims集合。
  3. 令牌生成:用私钥或对称密钥签名,加上过期时间、jti(唯一ID)、issuer等信息。

我曾经在签名密钥管理上吃过亏。项目初期图省事,把签名密钥硬编码在配置文件里。后来代码泄露,攻击者直接用密钥签发任意角色的令牌,整个权限体系形同虚设。记住:密钥必须定期轮转,且只能存在HSM或密钥管理服务里。

签发的关键参数,我列个表格方便你对照:

参数含义示例值避坑重点
sub用户标识user_123456不要包含敏感信息
aud受众api_gateway必须校验,防止令牌被其他服务冒用
exp过期时间1700000000不要设成永久,建议15分钟到2小时
iat签发时间1699996400严格校验iat不能是未来时间
jti令牌唯一IDguid_xxxx吊销时用,必须全局唯一

4.2 令牌验证:每道门都要查票

验证阶段往往被低估。很多人觉得“验签通过就完事儿了”,其实远远不够。嗯,这里要注意:验证至少包含三层。

三层验证流程

  • 签名验证:用公钥检查令牌有没有被篡改。这是底线。
  • 声明校验:exp是否过期?aud是否匹配本服务?iss是否是信任的颁发者?
  • 吊销状态检查:令牌是否已被吊销。这一步很多人漏掉,导致“已吊销的令牌依然能访问”。

我建议每个API网关都做一次完整的令牌验证,微服务内部可以用信任传递减少重复验签。但有一个例外——跨信任边界的调用必须重新验证。

性能优化小技巧: 在网关层验证后,可以把验证结果(用户ID、角色列表)放在请求头里传给下游服务。下游服务直接解析头信息,不用再调密钥服务。但前提是必须保证网关和下游之间的通信是可信网络(比如通过mTLS)。

4.3 令牌刷新:无感续期

令牌过期了怎么办?总不能让人家每隔15分钟就重新登录一次吧。所以有了刷新令牌(Refresh )。

刷新机制的核心思路是:短命的访问令牌 + 长命的刷新令牌。访问令牌过期后,客户端用刷新令牌去令牌服务换新的访问令牌,整个过程对用户无感。

我在项目中遇到过一种典型错误:把刷新令牌的有效期设得比访问令牌还短。你想想看,这样刷新令牌还有什么意义?标准做法是:

  • 访问令牌:15分钟 ~ 2小时
  • 刷新令牌:7天 ~ 30天
  • 刷新令牌仅用于令牌端点,不能用于业务API

刷新流程最好用轮转模式:每次刷新时,旧刷新令牌立即失效,同时返回一个新的刷新令牌。这样就算刷新令牌泄露,攻击者也只能用一次。

// 简化的刷新令牌轮转示例(伪代码)
POST //refresh
{
  "refresh_": "old_rt_value",
  "client_id": "my_app"
}

// 响应
{
  "access_": "new_at_value",
  "refresh_": "new_rt_value",  // 新令牌
  "expires_in": 900
}

// 服务端同时将 old_rt_value 标记为已使用
// 任何重复使用 old_rt_value 的请求都会被拒绝

警惕刷新令牌的并发问题。如果客户端同时发出两个刷新请求,而你用状态检查的方式去禁用旧令牌,就可能导致第二个请求失败。我曾经踩过这个坑,后来改用了“刷新令牌族(Refresh Family)”方案:一个族内的所有刷新令牌都是等价的,只要族内有一个被派发过新的,整个族的旧令牌全部作废。这样可以避免并发冲突。

4.4 令牌吊销:收回来时的钥匙

吊销是最容易被忽视的阶段。用户修改密码了、手机丢了、账户被黑了——你需要立即让当前所有有效的令牌失效。

吊销方案主要有三种,我按推荐度排个序:

  1. 黑名单(Deny List):在Redis或数据库里维护一个“已吊销令牌ID”列表。每次验证时查一下令牌的jti是否在黑名单里。优点是实时性好;缺点是需要额外存储,而且访问令牌验证会增加一次查询。
  2. 短有效期 + 刷新令牌撤销:访问令牌有效期设得很短(比如5分钟),只记录刷新令牌的吊销状态。访问令牌即使泄露,也很快过期;刷新令牌一旦吊销,客户端就无法续期。适合对实时性要求不那么极端的场景。
  3. 版本号方案(增量版本):给每个用户维护一个令牌版本号。令牌签发时把版本号写进的claim里。用户每次操作(如修改密码)时递增版本号。验证时如果令牌中的版本号小于当前版本号,直接拒绝。

我个人的习惯是:访问令牌用短有效期方案,刷新令牌用黑名单方案。两个方案互补,既不牺牲性能,又能保证吊销的实时性。

吊销的最佳实践清单

  • 吊销操作必须记录审计日志(谁、什么时候、吊销了哪个令牌)
  • 批量吊销:当用户被禁用时,需要吊销该用户的所有令牌
  • 黑名单条目不要永久保留,可以根据exp时间设置TTL自动清理
  • 刷新令牌的吊销比访问令牌的吊销更重要——控制了续期就控制了未来
令牌生命周期管理流程 签发 身份确认→权限组装→签名 验证 签名·声明·吊销状态 刷新 短命AT+长寿RT+轮转 吊销 黑名单/版本号/短过期 虚线:令牌过期/吊销后重新签发 形成完整的生命周期闭环

4.5 实战中的几个决策点

说了这么多理论,不如聊聊真实项目里怎么选。我总结了三组决策点,每次设计令牌服务时我都会再过一遍。

决策问题方案A方案B个人推荐
访问令牌有效期短期(5分钟)长期(24小时)15分钟,配合刷新令牌
刷新令牌存储数据库持久化Redis内存存储Redis,加持久化备份
吊销检查方式每次验证查黑名单黑名单+本地缓存本地缓存(TTL 1分钟内),减少DB压力
刷新令牌轮转单次轮转令牌族轮转令牌族,避免并发冲突

我的一个小习惯: 在签发令牌时,把客户的IP归属地(城市级别)塞进access 的额外claim里。验证时如果检测到IP归属地突然跨省,可以在网关层触发二次验证。虽然不能完全防住攻击,但能提高一些安全水位。当然,前提是你的用户分布没有频繁出差的情况。

令牌生命周期管理,说到底就是一套围绕时间的权限控制策略。签发决定谁来,验证决定谁在,刷新决定谁续,吊销决定谁走。这四个阶段形成一个闭环——任何一环断了,整个令牌服务就不安全或者不可用了。