有状态和无状态的接口:REST 的无状态约束,以及 JWT 撞上的撤销难题
先把两件经常被混在一起的事分开:连接持不持久,和服务器要不要在两次请求之间记得你,是完全不相关的两层东西。HTTP/1.1 的 keep-alive、HTTP/2 的多路复用,都是让一条 TCP 连接能被反复用于发送多个请求——连接本身是”持久”的,这跟这条连接上跑的每个请求是不是无状态的,没有任何关系。一条长连接上完全可以跑满无状态请求,服务器照样不用记得上一个请求说了什么。
无状态:一句话定义 + 一个例子
无状态说的是:每个请求必须自己带全所有需要的信息,服务器不能靠”记得你之前说过什么”来处理这个请求。这是 Roy Fielding 定义 REST 时提出的约束之一。
最常见的例子是 token 认证:
1 | curl https://api.example.com/orders/123 \ |
这一个请求里,token 已经包含了”你是谁、有什么权限”这些全部信息。服务器验证一下签名就知道该不该放行,不需要提前知道你是谁、也不需要记得你上一次请求做了什么——换一台服务器来处理这个请求,结果完全一样,这正是无状态换来的好处:随便加机器、随便让负载均衡器把请求转发到任意一台,都不影响结果。
有状态:一句话定义 + 一个例子
有状态说的是:服务器在多个请求之间,记住了跟你相关的东西,下一个请求得依赖这份记忆才能被正确处理。
最常见的例子是老式的 session 登录:
1 | # 第一次请求:登录 |
第二个请求单独拿出来看,完全看不出是谁发的——必须依赖服务器内存里存的那条记录才能处理。这就是有状态:处理请求这件事,脱离了服务器自己记的东西就做不成。
两者的代价
无状态换来的是可以随便扩展、出故障恢复简单,代价是每个请求都要带更多信息(不能省成一个短短的 session_id)。有状态换来的是请求可以很轻量、服务器全程记着你是谁,代价是这个 session 存在哪台机器上,后续请求就得想办法找回那台机器(常说的”会话粘性”),那台机器一旦挂了,状态跟着丢,除非专门做了跨机器的状态复制。WebSocket 长连接、gRPC 双向流也是有状态的例子——连接本身的存在,就是一种记忆。
JWT 想绕开服务器状态,结果撞上了撤销这堵墙
Authorization: Bearer <token> 这种做法,本质是把”你是谁、你有什么权限”这份状态从服务器搬到了客户端自己随身携带的 token 里——服务器不用为了认证这件事在自己这边存东西,验证 token 签名就知道信息可信,这正是无状态约束想要的效果。
问题出在撤销上。一个已经签发出去的 JWT,在它自己声明的过期时间到之前,天生就是有效的——这个”有效期”是编码在 token 内容里的,服务器验证的时候只看签名对不对、有没有过期,不会去查一个额外的地方确认”这个 token 还作数吗”。用户登出、改密码、账号被封,这些场景都要求权限立刻失效,但已经签发出去的 JWT 手上的服务器根本不知道该不该继续认它。
于是能想到的解法只剩下往服务器这边加一份记录——一个”已撤销 token”的黑名单,每次验证 JWT 之前先去查一下这个黑名单。这里的讽刺之处在于:一旦你要维护这份黑名单,你验证一个”无状态”的 JWT 就必须先去查一个”有状态”的存储,这跟你当初为了避免维护服务器端状态才选 JWT 的初衷,正好是反着来的。走到这一步,”无状态”这个标签基本上名存实亡。
工程上常见的折中有三种:直接维护一份撤销黑名单,每次验证都查一遍(最直接,但重新引入了状态和一次额外的存储查询);把过期时间设得很短(比如几分钟),配合一个长期有效的 refresh token 去换新的短期 token,撤销的最大延迟被压缩到一个 token 的有效期之内;黑名单里只存 token 的 jti(JWT ID)而不是整个 token 内容,省存储空间。三种折中没有一种是真正意义上”完全无状态”的,都是在无状态的理想和撤销的现实需求之间找一个能接受的平衡点。
该选哪个:看要不要跨请求记住东西
无状态和有状态不是”哪个更先进”的排位,真正要问的问题是:这个场景需不需要服务器在多个请求之间记住点什么。一次性的、独立的 API 调用(查询、提交表单)天然适合无状态;一段持续的交互过程(实时协作、消息推送、大文件的分片上传会话)本身就需要连续性,硬拆成无状态反而更复杂,有状态的连接或者 session 才是更直接的做法。JWT 的撤销困境说明的是另一件事:**”无状态”经常不是免费的**,牵扯到”立刻失效”这种依赖服务器记忆的需求时,宣称无状态往往只是把状态挪了个地方藏起来,没有真的消失。





