4. 令牌生命周期管理:签发、验证、刷新、吊销
令牌这东西,说白了就是一把临时钥匙。钥匙从铸造、使用、换新到销毁,每一步都有坑。我在好几个项目里都见过,明明授权模型设计得不错,偏偏在令牌管理这环节漏了风,最后被安全审计追着改。
这一章我们就把令牌的四个阶段——签发、验证、刷新、吊销——逐个拆开。你想想看,每个阶段都要回答一个关键问题:谁可以拿令牌?谁可以用令牌?令牌过期了怎么办?令牌被盗了怎么废掉?
4.1 令牌签发:第一次握手
签发是令牌生命周期的起点。说白了,就是认证成功之后,服务端给客户端发一张“通行证”。我个人习惯把签发过程分成三个步骤:
- 身份确认:用户名密码、OAuth授权码、SSO断言……你得先确认来的人是谁。
- 权限组装:根据用户角色、资源访问策略,把允许的权限拼成一个claims集合。
- 令牌生成:用私钥或对称密钥签名,加上过期时间、jti(唯一ID)、issuer等信息。
我曾经在签名密钥管理上吃过亏。项目初期图省事,把签名密钥硬编码在配置文件里。后来代码泄露,攻击者直接用密钥签发任意角色的令牌,整个权限体系形同虚设。记住:密钥必须定期轮转,且只能存在HSM或密钥管理服务里。
签发的关键参数,我列个表格方便你对照:
| 参数 | 含义 | 示例值 | 避坑重点 |
|---|---|---|---|
| sub | 用户标识 | user_123456 | 不要包含敏感信息 |
| aud | 受众 | api_gateway | 必须校验,防止令牌被其他服务冒用 |
| exp | 过期时间 | 1700000000 | 不要设成永久,建议15分钟到2小时 |
| iat | 签发时间 | 1699996400 | 严格校验iat不能是未来时间 |
| jti | 令牌唯一ID | guid_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 令牌吊销:收回来时的钥匙
吊销是最容易被忽视的阶段。用户修改密码了、手机丢了、账户被黑了——你需要立即让当前所有有效的令牌失效。
吊销方案主要有三种,我按推荐度排个序:
- 黑名单(Deny List):在Redis或数据库里维护一个“已吊销令牌ID”列表。每次验证时查一下令牌的jti是否在黑名单里。优点是实时性好;缺点是需要额外存储,而且访问令牌验证会增加一次查询。
- 短有效期 + 刷新令牌撤销:访问令牌有效期设得很短(比如5分钟),只记录刷新令牌的吊销状态。访问令牌即使泄露,也很快过期;刷新令牌一旦吊销,客户端就无法续期。适合对实时性要求不那么极端的场景。
- 版本号方案(增量版本):给每个用户维护一个令牌版本号。令牌签发时把版本号写进的claim里。用户每次操作(如修改密码)时递增版本号。验证时如果令牌中的版本号小于当前版本号,直接拒绝。
我个人的习惯是:访问令牌用短有效期方案,刷新令牌用黑名单方案。两个方案互补,既不牺牲性能,又能保证吊销的实时性。
吊销的最佳实践清单
- 吊销操作必须记录审计日志(谁、什么时候、吊销了哪个令牌)
- 批量吊销:当用户被禁用时,需要吊销该用户的所有令牌
- 黑名单条目不要永久保留,可以根据exp时间设置TTL自动清理
- 刷新令牌的吊销比访问令牌的吊销更重要——控制了续期就控制了未来
4.5 实战中的几个决策点
说了这么多理论,不如聊聊真实项目里怎么选。我总结了三组决策点,每次设计令牌服务时我都会再过一遍。
| 决策问题 | 方案A | 方案B | 个人推荐 |
|---|---|---|---|
| 访问令牌有效期 | 短期(5分钟) | 长期(24小时) | 15分钟,配合刷新令牌 |
| 刷新令牌存储 | 数据库持久化 | Redis内存存储 | Redis,加持久化备份 |
| 吊销检查方式 | 每次验证查黑名单 | 黑名单+本地缓存 | 本地缓存(TTL 1分钟内),减少DB压力 |
| 刷新令牌轮转 | 单次轮转 | 令牌族轮转 | 令牌族,避免并发冲突 |
我的一个小习惯: 在签发令牌时,把客户的IP归属地(城市级别)塞进access 的额外claim里。验证时如果检测到IP归属地突然跨省,可以在网关层触发二次验证。虽然不能完全防住攻击,但能提高一些安全水位。当然,前提是你的用户分布没有频繁出差的情况。
令牌生命周期管理,说到底就是一套围绕时间的权限控制策略。签发决定谁来,验证决定谁在,刷新决定谁续,吊销决定谁走。这四个阶段形成一个闭环——任何一环断了,整个令牌服务就不安全或者不可用了。