4、令牌接口设计原则:RESTful令牌接口规范,无状态设计
做接口设计,尤其是令牌相关的接口,最容易踩的坑就是“我觉得这样也行”。
我见过太多项目,一开始图省事,把令牌状态塞进Session,或者搞个全局Map存登录信息。结果呢?等到要水平扩展、要上Kubernetes的时候,全傻眼了。
这一章,我们就聊聊“对”的做法——RESTful风格的令牌接口到底该怎么设计,以及无状态为什么那么重要。
4.1 RESTful 不等于“CRUD + JSON”
很多人把RESTful理解成“用POST增、用DELETE删、返回值写成JSON”。其实不止这些。RESTful的核心是资源导向,配合统一接口,再加上无状态约束。
对于令牌系统来说,资源只有一个:令牌()。
| HTTP方法 | 资源路径 | 语义 | 典型用途 |
|---|---|---|---|
| POST | /tokens | 创建令牌 | 登录/获取访问令牌 |
| GET | /tokens/{id} | 查验令牌 | 验证令牌有效性 |
| DELETE | /tokens/{id} | 撤销令牌 | 登出/踢下线 |
| PATCH | /tokens/{id} | 续期/刷新 | 延长令牌有效期 |
我个人习惯,把令牌视为“一次性的凭证资源”。不是“用户”,而是“令牌”。接口设计盯着令牌本身,逻辑就清晰多了。
设计原则:接口路径只表达资源,动作由HTTP方法表达。不要出现 //check 或 //refresh 这样的“动词路径”。用 GET /tokens/{id} 代替 check,用 POST /tokens/{id}/renew 其实也不推荐——统一用 PATCH 更干净。
4.2 无状态设计——为什么必须做?
说白了,无状态就是:服务端不存任何与客户端会话相关的数据。每次请求都是独立的,服务端只管验证令牌,不管“你之前来过没”。
你想想看,如果我把用户登录状态放在服务端内存里——
- 用户A登录,我往服务器的HashMap里写一条记录
- 负载均衡把下一个请求转发到另一台机器,查不到记录
- 用户A被强制登出
这就是“粘性Session”的噩梦。我早期的项目就吃过这亏,上线当天就被运维骂惨了。
避坑指南:我曾经在某个项目中,把令牌的剩余有效期存在Redis里,理论上也算“无状态”了。但Redis挂了,所有在线用户全部掉线,生产事故。后来我改成令牌自身携带过期时间(JWT的方式),服务端只做验签,不依赖任何中间件。这才是真正的无状态。
4.3 令牌的生命周期与接口映射
令牌从出生到死亡,经历几个阶段。每个阶段对应一个接口操作。下面这张图理清了整个流程。
4.4 接口规范细节——不只是路径
4.4.1 请求头
令牌放哪里?我建议统一放 Authorization: Bearer <>。不推荐放请求体、URL参数或自定义Header。原因很简单:标准兼容性。主流网关、日志工具、监控系统都默认识别 Bearer 模式。
GET /tokens/abc123
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
4.4.2 响应格式
统一用JSON。错误码也标准化,不要一会儿返回 {"code": 401},一会儿又返回 {"error": "unauthorized"}。
// 成功创建令牌
HTTP/1.1 201 Created
{
"": "eyJhbGciOiJIUzI1NiIs...",
"expires_in": 3600,
"_type": "Bearer"
}
// 令牌验证失败
HTTP/1.1 401 Unauthorized
{
"error": "invalid_",
"error_description": "令牌已过期或签名无效"
}
实践建议:我做接口规范时,会强制要求团队使用 error 和 error_description 字段。既方便程序解析,也方便人工排查问题。别把错误信息藏在深层嵌套里。
4.5 无状态设计的代价与补偿
无状态不是银弹。它带来了扩展性,但也付出了代价——令牌撤销困难。
服务端不存状态,你怎么让一个令牌立刻失效?
常用的补偿手段:
- 黑名单机制:在服务端维护一个短期的“已撤销令牌列表”,请求过来先查黑名单。缺点是多了一次IO。
- 短有效期 + 刷新令牌:访问令牌15分钟过期。即使泄露,危害时间可控。配合刷新令牌(Refresh )实现持续登录。
- 版本号校验:在令牌中嵌入用户的“令牌版本号”。用户改密码或踢下线时,递增版本号。签名验证时附带检查版本一致性。
我个人倾向“短有效期 + 刷新令牌”策略。你在项目中可以这样设计接口:
推荐组合方式:
- 访问令牌(Access ):有效期5~15分钟,JWT格式,包含用户ID、角色、过期时间
- 刷新令牌(Refresh ):有效期7~30天,服务端存储(如Redis),用于获取新的访问令牌
- 接口路径:
POST /tokens(获取)、PATCH /tokens/{id}(刷新)、DELETE /tokens/{id}(撤销)
你想想看,如果每次请求都要去数据库查一次版本号,那无状态的优势就没了。所以权衡下来,短时效 + 刷新令牌是最实用的无状态方案。
4.6 常见的“伪无状态”设计
我审查过不少团队的设计,经常看到以下几种“伪无状态”:
- 令牌里塞了太多东西:把用户权限列表、部门信息全塞进JWT的Payload里。令牌膨胀到几KB,每次请求都在传输。无状态是做到了,但性能全丢在网络上。
- 用令牌ID查询Redis:虽然接口是无状态写法,但每个请求都要走一次Redis查“令牌是否有效”。本质上是把Session从本地内存搬到了分布式缓存——还是状态依赖。
- Signature依赖用户信息:JWT的签名密钥跟用户绑死。每次验签都要先查到用户的密钥。绕了一圈,又回到状态依赖。
经验教训:我曾经帮客户重构一套API。他们用了JWT,但每次验签都要查数据库获取用户的“专属密钥”。名义上无状态,实际上RPS一上去就挂了。我们改成统一密钥 + 令牌内嵌用户信息后,QPS从200直接飙到2000。
4.7 小结
令牌接口设计,说白了就是两件事:资源建模和无状态落地。
资源建模让你接口清晰可维护,无状态设计让你系统的扩展性上一个台阶。别贪图一时的便利,把状态藏在角落里。后续的每一次扩容、每一次升级,都会替当初的“图省事”买单。