栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

分享一套较为完善的JWT令牌续签策略,长短令牌三验证,有无状态混用

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

分享一套较为完善的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算法的验证;刷新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中被删而无法通过第三种验证。
同样由于一次性机制,即使长令牌泄露并被黑客使用,也会被合法客户端及时发现——因为下一次自动续签时,合法客户端的长令牌就会因为一次性机制失效了,继而要求用户重新登录。也方便服务端根据登录、续签时的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,或者加盐加密后的用户密码。简言之,一个有状态token就应该有一个有状态token的样子(无意义字符串的样子),拿自带无状态属性的jwt token当有状态token用,太过旁门左道,有损一个清晰简明系统设计的尊严。

  2. 是不是可以随机生成一个字符串作为有状态token,每次续签或登陆返回给客户端三个token?
    答:可以,功能上没问题,三个token的设计本来就是现在这套设计的雏形。因为考虑到refresh token(JWT)和有状态token永远绑定在一起使用,而且后者原本只是比对字符串使用,用啥机制生成都行,完全不妨碍它们合并成一个。一来如无必要勿增实体,俩token能完成的工作就不要搞成三个,对和客户端的通信约定来说也更简洁一点;二来如果在客户端存个意义不明很像jsessionid的玩意儿,也太容易让黑客猜到这是啥和干啥用的了,万一被他们找到啥方法做坏事呢?而你搞个自定义策略生成这个有状态token,首先转换策略就是千人千面难以猜测,其次整个有状态token的存在对客户端都不可见了,安全性肯定能好很多。

  3. 长短令牌的必要性,是不是能合成一个,并使用在服务端判断是否临近过期的机制来实现自动续签?
    答:长短令牌的策略就我搜到的资料来看其实已经挺主流了,老生常谈一下就是短令牌使用频繁且重复使用,泄露风险非常高,所以过期时间越短泄露后的危害就越小。一个长令牌打天下的策略,泄露风险一样高,危害(过期时间)还不能缩小(因为这个令牌过期时间是用户重登陆的时间),那么安全性就很痛苦了。合理推断一下长短令牌的策略最初应该就是在“token时效越短越安全”和“用户重登陆的间隔太短体验不好”的矛盾下诞生的。

  4. 既然最终结果由第三种验证(有状态token的比对)来决定,那么第二种验证策略,即在续签请求中首先以JWT规则验证长令牌是否合法的步骤,是不是可以去掉?
    答:不要去掉,虽然最终结果由第三种验证决定,但这是一个在性能优化方面非常具有性价比的环节。这道验证的功能类似布隆过滤器,先在本机以计算JWT合法性的方式将请求过滤一遍,排除掉那些连JWT token的合法性都没做到的请求,以此来节约访问远端缓存服务时的资源开销。

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/943319.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号