JWT里的双TOKEN

频繁刷新这种服务器的损耗根本不是问题,你完全可以拿出过期时间判断一下,然后只在临近过期时才刷新token

短令牌使用频繁且重复使用,泄露风险非常高,所以过期时间越短泄露后的危害就越小。一个长令牌打天下的策略,泄露风险一样高,危害(过期时间)还不能缩小,因为这个令牌过期时间是用户重登陆的时间,间隔越短用户体验越差。所以长短令牌策略

的优越性就体现在这里,可以很好解决“token时效

越短越安全”和“用户重登陆的间隔越短体验越差”之间的矛盾。

摘自拙作《“长短令牌三验证”的JWT令牌续签策略》

全文如下:

“长短令牌三验证”的JWT续签、管理策略

前言:最近研究JWT的续签机制,发现虽然JWT已经在业界广泛应用,但续签机制的探讨还是处于一种百家争鸣的状态(有些策略甚至能看出连JWT的基本规范都没学扎实)。所以不才在吸收了一圈各路大神分享的续签策略后加上一些个人的思考,提出这套名叫“长短令牌三验证”的解决策略,希望能为互联网开发生态的完善做出一点自己微薄的贡献。

令牌使用策略概述

顾名思义,本机制下所使用的令牌分为长短两种:长令牌即过期时间较长的refresh_token

,专门用于token的续签,同时刷新两种token;短令牌即过期时间较短的access_token,进行常规业务请求时使用。

流程概述:

  1. 登陆
  2. 用户登录成功后,同时获得长短两种令牌,之后长令牌存在客户端暂不使用,仅使用短令牌用于常规业务请求。
  3. 服务端在接收到每个携带短令牌的业务请求时,按照JWT的规则进行用户的身份验证,并从payload中获取用户身份信息,此即“三验证”中的第一种验证。
  4. 上述操作中,客户端如果发现自己的请求因短令牌过期被拒,则使用长令牌(refresh_token)向专用于续签令牌的接口发起请求,申请一对拥有全新过期时间的长短令牌。
  5. 服务端在接收到续签请求时,会进行两步验证:一是根据JWT的算法规则验证长令牌的合法性,此即“三验证”中的第二种验证;如果通过,则将该token按照某一规则进行转变,转变为一个有状态的token,用于比对服务端所存储的token数据,也即“三验证”中的第三种验证。(该步骤是本策略的核心关键,后面会展开详说。)
  6. 以上的两步验证均通过时,才会将此次请求正常返回。后续处理和登陆成功时一致,即生成并返回一对拥有全新过期时间的长短令牌,客户端也同样,拿短令牌访问常规业务接口,用长令牌续签。
  7. 上述操作中如果因为长令牌过期而请求被拒,则回到1步骤重新登陆。

补充:

  1. 短令牌的过期时间为“自动续签”的最短时间,小于该时间则令牌有效,大于该时间则客户端自动使用长令牌进行令牌续签,以上操作对用户无感;
  2. 长令牌的过期时间为“用户重新登陆”的最短时间,在该时间内只要有任何访问服务的操作,则客户端均会在需要时进行自动续签,用户无感。一旦超过该时间才会需要用户重新登陆。
  3. 常规的业务请求使用短token,验证时只用秘钥和jwt算法
  1. 验证合法性;刷新token的请求使用长token,进行两步验证:一是jwt算法验证,二是转换成有状态的token后进行字符串匹配验证

长令牌续签时的两道验证机制

使用短令牌进行身份验证的过程非常基础,这里略过,只说续签: 续签服务的第一道JWT验证机制好理解,和业务请求时的JWT身份验证基本一致,即按照JWT的算法规则验证该token是否合法。

秘钥可以和短令牌一致也可以不一致,个人建议设计成不一致,多少能提升一些安全性。不过如果你的续签接口是和其他业务接口混用的,设计成一致的也无伤大雅,以免同一个服务还要搞两套不同秘钥的JWT验证策,没必要增加这种复杂度。

重点是接下来要说的第三种验证,即“有状态”的token验证。

如何“转变”为有状态token

首先要介绍的是在第三种验证中提及的“转变”为有状态token。本质上是一个你自定义的规则,即要求你自定义一个方式,将格式为JWT的refresh_token转变为一个仅作为匹配一致性使用的、无意义的字符串,该转变只需要保证结果唯一,无需可逆。你可以对整个refresh_token进行一次md5计算(长度32),也可以直接把jwt token的签名部分截取使用(长度43)。开发阶段可以先使用后者,方便一眼看出对应关系。本文中把这个由长令牌转换来的字符串称为“有状态token”,以区别于运用jwt规则验证的无状态token

第三种验证(有状态token验证)机制流程

核心机制:

首先在用户登陆阶段(流程概述的步骤1)做出改动,在生成了长短两个jwt token之外,还需生成一个有状态token,由长令牌按前面所说的规则“转变”而来。之后将长短令牌返回客户端,将有状态token作为键名存在redis中。留待后续的续签服务中使用。 之后在第三种验证时(流程概述的步骤5),只需将请求所携带的长令牌按照你设定的规则转变为为有状态token,再以此为键去redis中查询是否存在对应键即可。如果存在,则表示验证通过,其后的操作则与登录时一致,即生成长短令牌与有状态token,前俩令牌返回客户端,有状态令牌存redis,并在redis中删去此次请求中已使用过的 有状态令牌。

设计思路:

以上操作的意义在于给长令牌赋予了只能一次性使用的特性,可以大大提升系统的安全性,弥补JWT token无法提前失效的弊端。即假如自动续签token的请求被黑客抓包,将其中的长令牌重复使用,虽然能顺利通过第二种验证,但随即就会因为其所对应的“有状态token”已经在redis中被删而无法通过第三种验证。 同样由于一次性机制,即使长令牌泄露并被黑客使用,也会被合法客户端及时发现——因为下一次自动续签时,合法客户端的长令牌就会因为一次性机制失效了,继而要求用户重新登录,使得黑客手中的refresh token

失效。也方便服务端根据登录、续签时的ip地址等记录判断该用户是否存在被盗号的风险,进一步进行账号冻结或提醒操作。 同时,纯JWT机制下,服务端系统原本无能为力的注销用户、拉黑名单等用户状态管理操作也得到了补全。注销时只需在redis中删去对应的有状态token,拉黑名单则在前者基础上进一步限制登陆即可,此时客户端发来的续签请求都会因为无法通过有状态token的验证而被拒。虽然客户端的短token还能在失效前继续可用,但已经利大于弊,采用JWT机制

所失去的用户登录状态管理功能已经能够得到最大限度的挽回。

以下再简述一下进阶用法:

同一用户多设备的情形下登陆状态的管理

基于上述设计,再稍稍扩展一下,也能做好同一用户多设备的情形下登陆状态的管理:比如限制用户同时在线的设备数目、限制用户在同一类设备上仅能同时在线一个等等。

拿其中最为复杂的“限制用户在同一类设备上仅能同时在线一个”举例,在redis中以用户id为键,再维护一套hash(子map)对照表。表的filed

为设备类型,用户登陆时由客户端传来,如果对应filed不存在则直接保存,以设备类型为filed,以有状态token的字符串为值;如果对应filed已存在,则先根据其中存的值,找到对应有状态token的键并删除,再将“设备类型--token”的field--value对存入。此时同类型旧设备上所存的refresh_token即会因为对应有状态token被删而失效,需要用户重新登录。 所以“限制用户同时在线的设备数目”也更简单了,把上面的设计的hash结构改成list结构,用lua脚本

确保list元素上限,超过了则连同有状态token的key一并移除,以此让最旧的设备上的refresh token失效。 并且,如果客户端需要加个“管理我的设备”功能,也能一并实现了。用户可以自行移除自己其他设备上的自动登录状态。

可选策略:

系统设计的关键是合适,而非通用,所以根据不同情形和要求可以基于以上设计产生多种附加或删减的设计:

1. 结合通信加密(进一步加强长令牌的安全性)

登陆的请求本来就应当加密。同时为了避免登陆或续签令牌时的返回值被人抓包,这部分也建议和登陆请求一样加密。原本refresh_token就已经做成一次性了,在续签请求时被抓包也不怕,但是获取到新token并返回时还是有点风险,这部分加上加密策略

就几乎完美的安全了。

这里简述一下通信加密策略的建议,即对称和不对称加密混用的通信加密策略: 每次请求前客户端先生成一个临时秘钥,用该秘钥对称加密消息体(或其中的敏感数据),将该秘钥用不对称加密的公钥加密后,放在请求头中,和加密后的消息体一起发送到服务端。 服务端在获得请求后,从请求头中获取加密后的临时秘钥并用不对称加密的私钥解密得到临时秘钥,然后用临时秘钥对称解密消息体。 返回值如果需要加密,就也和客户端的做法一样,用临时秘钥加密消息体后返回到客户端,临时秘钥也不用带了,因为本来就是客户端发来的,它自己必然还存着一份。

2. 结合ip地址绑定(进一步加强短令牌的安全性)

加强与设备的绑定肯定能让通信更为安全,但是设备唯一id,比如pc的网卡号和iphone的UDID,得由客户端自行获取并填入请求消息体中,可以造假,没有意义。剩下的只有IP地址了,但是如今的互联网环境是设备可能在多个网络间切换,比如wifi信号不好我就切蜂窝网络

,出门换个地儿也就换了wifi,以此导致ip地址可能频繁变化。所以ip的绑定要做也只能绑短令牌access_token(将ip地址加入到payload中),一旦网络切换,短令牌失效而长令牌继续有效,就需要走令牌续签的接口获取新的ip地址下的新短令牌。同时由于我们上面有状态token的机制,refresh_token的验证已经非常安全了,所以短令牌加上ip绑定后也“全上加全”。 当然更安全的代价是服务端资源的更大开销,首先是每次常规的业务请求,在jwt的合法性验证之后,还要拿出客户端的ip地址对比一下和该token的payload中保存的ip地址是否一致;其次是每次切换网络,都要走一遍续签接口,重新生成长短令牌对并更新缓存中的有状态token。这开销和安全性之间的平衡,就由读者自行取舍了。 当然纯PC客户端或者内网办公系统要这么玩还是非常适合的。前者ip地址不会频繁变化,后者并发量也不会有多高。(不过这两种情形,是不是连采用JWT的必要性都有待商榷了?)

Q&A,系统设计思路

  1. 为什么要做“转变为有状态token”这一步骤 ,是不是可以直接存长令牌的JWT token? 答:可以是可以,功能也能走通。但是JWT token更长更占空间都是其次,更重要的是我们系统设计中应该遵循“文要对题”、“形意合一”的原则。JWT token本身的性质就是无状态token,用重新计算签名来验证它的合法性才是它的天然的、正经的用法。作为一个有状态token,既然它被当做比对字符串来使用,那它在形式上就应该是个无意义的字符串,类似jsessionid
  1. ,或者加盐加密后的用户密码。简言之,一个有状态token就应该有一个有状态token的样子(无意义字符串的样子),拿自带无状态属性的jwt token当有状态token用,太过旁门左道,违背一个系统应当清晰简明的原则。
  2. 是不是可以随机生成一个字符串作为有状态token,每次续签或登陆返回给客户端三个token? 答:可以,功能上没问题,三个token的设计本来就是现在这套设计的雏形。因为考虑到refresh token(JWT)和有状态token永远绑定在一起使用,而且后者原本只是比对字符串使用,用啥机制生成都行,完全不妨碍它们合并成一个。一来如无必要勿增实体,俩token能完成的工作就不要搞成三个,越简洁的设计越不容易出错;二来如果在客户端存个意义不明很像jsessionid的玩意儿,也增加了被黑客反推和利用第三种验证机制的风险。而你搞个自定义策略生成这个有状态token,首先转换策略就是千人千面难以猜测,其次在客户端层面完全隐藏了第三道验证的存在,安全性方面可以提升很多。
  3. 长短令牌的必要性,是不是能合成一个,并使用在服务端判断是否临近过期的机制来实现自动续签? 答:长短令牌的策略就我搜到的资料来看其实已经挺主流了,这里老生常谈一下:就是短令牌使用频繁且重复使用,泄露风险非常高,所以过期时间越短泄露后的危害就越小。一个长令牌打天下的策略,泄露风险一样高,危害(过期时间)还不能缩小,因为这个令牌过期时间是用户重登陆的时间,间隔越短用户体验越差。所以长短令牌策略的优越性就体现在这里,可以很好解决“token时效越短越安全”和“用户重登陆的间隔越短体验越差”之间的矛盾。
  4. 既然最终结果由第三种验证(有状态token的比对)来决定,那么第二种验证策略,即在续签请求中首先以JWT规则验证长令牌是否合法的步骤,是不是可以去掉? 答:不要去掉,虽然最终结果由第三种验证决定,但第二种验证是一个在性能优化方面非常具有性价比的环节。这道验证的存在类似布隆过滤器,先以低代价的本机运算方式将请求过滤一遍,排除掉那些连JWT token的合法性都没做到的请求,之后再进行远程访问,从而有效节约网络通信和远程缓存服务器的开销。

特征总结

  1. 短token业务用,长token续签用。普通业务时短token用jwt规则验一次,续签时长token不光用jwt规则验一次,还要转变成有状态token再比对服务端的token缓存再验一次。
  2. 有状态token使用一次后即刷新替换,以此保证其安全性。通过一次性机制有效防止非法客户端使用refresh_token进行“无限续杯”。同时也可以此获得在服务端管理用户在线、登出的功能,弥补jwt令牌只能“坐待过期”的不足。

原文地址

https://www.zhihu.com/question/506320859

“长短令牌三验证”的JWT令牌续签策略

 

 
 
posted @ 2023-02-24 10:55  甜菜波波  阅读(1367)  评论(2编辑  收藏  举报