记录一下笔者关于状态的一些相关认知。
在计算机领域,状态[1]指的是一个系统被设计用来记住之前的事件或用户交互,那么就称之为有状态的系统,系统记录的信息则就是状态。注意这里的重点不是说记录信息,而是记住之前的。
举个例子:你去楼下的便利店买东西,95元,你给了店家100块,但是店家暂时没零钱,这时候你说了,算了,你先记着吧,下次买东西少收我5块就是了,店家说好嘞。然后第二天你又去买东西了,要求店家减免你5块钱,店家说凭什么啊。。。然后你说昨天你没零钱找我5块钱,忘了?噢噢噢,对对,不好意思啊,忘了。店家满脸歉意的说到。
在上述的例子中:店家他没有找你5块钱这件事就是状态。你没有忘记,但是店家暂时忘记了。由此可以得出状态的前提条件是发生在双方通信交互时,双方维持的一些之前的交互信息。在计算机领域,双方通信时,这种交互风格就叫做客户端-服务器,也就是我们通常说的C/S。
还有一个B/S,其实也是属于C/S,只是它的C是特定的Browser这种客户端。
客户端-服务器 风格[2]其背后的原则是分离关注点,使得客户端和服务器分别只关注各自领域的问题。通过此举可以简化服务器的实现(分离一部分逻辑到客户端),使得服务器可以独立部署维护,从而改善系统的可伸缩性。
无状态[3]并不是说我们彻底不要状态了,而仅仅只是说在双方通信时:从客户端到服务器的每次请求都必须包含理解该请求所必须的所有信息,不能利用服务器存储会话的上下文信息,会话状态全部保存在客户端。重点在于把状态的维护从服务端转移到客户端来。这样做可以改善可见性(监视系统不必为了确定一个请求的全部性质而去查看请求之外的其他请求);改善可靠性(减轻了从局部故障中恢复的任务量);改善可伸缩性(服务端不必在多个请求直接保存状态,从而允许服务器迅速释放资源)。但是无状态也有相应的缺点,由于服务器不能保持会话状态数据,则会造成在每一次请求中发送大量重复的数据,可能会降低网络性能。
在计算机通信方面,绝大部分的协议都是无状态的,比如IP协议,UDP协议,HTTP协议。拿上面的例子来说,比如店家写个欠条,按上手印,交给你。那么这时候对于店家来说就不必再维护状态了,而全部转移到了你的手中,下次你只需拿着欠条找店家即可,或者你朋友代你拿着欠条也可以。店家自己的手印在,是不可抵赖不认账的。
TCP协议[4]是有状态的协议,通信双方事先需要实现建立连接,维持通信的状态。还是上面的例子,店家见到了你,你俩都需要记得欠你5块钱这件事才行,一方忘了(对方再也记不起来了),那就再也要不回来了。TCP中的RST标记就是这样的,客户端拿着一个SYN要求和服务器建立连接,由于种种原因,服务器不记得了,认为这个SYN无效,然后就对客户端说:滚蛋,我不认识你。。。
OAuth2 OIDC JWT是认证和授权相关方面的协议。有人说了,状态与认证和授权[5]有什么关系啊?众所周知,HTTP协议是无状态的协议,OAuth2和OIDC则都是基于HTTP的协议。但是认证和授权都是有状态的行为,也就是会产生状态出来,OIDC会产生认证的结果(id_token
),授权会得到授权的结果(access_token
),然后拿着这些*_token
来维持后续的交互的状态。那么这时候问题来了,谁来维护这些状态?
在OAuth2协议中,access_token
对于客户端来说是一个黑盒的字符串。那么为何是一个黑盒的字符串?在回答这个问题前有件事需要先搞清楚,access_token
的客户端是谁?有人说了,废话,当然是第三方client啊!非也非也,并不是,第三方client是access_token
的持有者,但是并不是它的客户端。access_token
是授权服务器颁发的,受保护的资源服务器才是access_token
的客户端。第三方client只是受保护的资源的服务器的客户端。受保护的资源服务器拿着第三方client传递过来的access_token
去授权服务器检查是否有效。在这种交互模式下,第三方client和受保护的资源服务器都完全不必关心access_token
的内容是什么,统统交给授权服务器即可。所以答案就来了:黑盒字符串足以,授权服务器维护access_token
的状态。
在OIDC协议中,id_token
对于客户端来说,一个黑盒字符串就远远不够了,想一下认证的目的是什么?告诉客户端你是谁!这时候你给客户端一个黑盒的字符串能有个屁用。。。所以这时候就用上了JWT这个数据格式来告诉客户端当然通过认证的用户是谁。那么此时,状态则仅仅位于持有id_token
的客户端了。认证服务器认证完了(id_token
会包含有效时间范围和用户的id,也可以包含用户的名字、头像等信息),后续也就不必再维护任何状态了。
回看一下OAuth2有没有什么可以改进的地方?有!就是授权服务器维护access_token
的状态,这会使得授权服务器压力大增。那么这时候怎么办?把压力转移出去呗,在OAuth2协议发布之后诞生了JWT这种数据格式,得益于这个格式的特性,用它来保存access_token
的状态就再合适不过了。这时受保护的资源服务器在收到access_token
之后,就可以解析出来其中的信息,独立的完成验证,不必再去授权服务器检查了。
不过这时又有了新的问题,id_token
和access_token
不再去认证服务器和授权服务器去检查,这时候我要作废*_token
怎么办?解决办法有两个:
*_token
的有效时间,比如30分钟,结合自动刷新机制,降低作废的必要性。*_token
的客户端可以通过检查这个列表来阻止提前作废的*_token
。有人说了,这不是还是需要每次都调用检查,又回到了老路子上。其实并不是的,这次是只是作废检查,客户端可以根据自己的业务需要来缓存,比如你的qps是100,那么即使你一秒刷新一次这个作废列表,也可以节省99次的检查;其次,作废列表的量级会小很多(结合*_token
中的有效时间范围,那些本来已经过期的还可以进一步的剔除);再次,这个作废检查可以完全的独立于认证服务器和授权服务器,因为它无需关心认证和授权内部的细节,只是一个作废列表罢了,逻辑非常简单通用。分析了一下状态相关的一些概念和问题和对应的解决方案。
本文首发于:https://linianhui.github.io/talk/stateless