公司的登录模块也从Session切换到JWT挺长一段时间了,抽时间来总结一下遇到的问题以及解决方案.
为什么用JWT?
在JWT之前,公司是利用传统的Session来实现登录状态的保持,分布式下则利用Redis实现Session共享集中管理,共享集中管理会带来登录强依赖Redis,然而公司的Redis确实不太稳定,导致一蹦或者网络抖动就会使得用户受到影响,另外一点就是用户的Session存在redis中没有设置超时时间,其格式为 UUID - 用户信息JSON串,这个是大坑,导致Redis等我接手时已经膨胀到了30G之多,因此在大家强烈要求下JWT的改造就此开始.
之所以选择JWT,因为其不需要服务端存储,也就是可以直接下掉Redis,另外其playload可以存储不少关键信息并且小巧接入成本极小.
会有哪些问题?
Token中存哪些必要东西?
Jwt Token的playload实际上只是base64编码了一层,其被解码后可以查看到具体的文本信息,因此不能存私密性的东西.建议存储 id,username,expire,version等一些不是很关键的数据.至于version有什么用,后面吊销的时候在讨论.
Token何时下发?
-
登录下发新token,原token实际上并没有失效,保证多端登录没问题.
-
Token剩余有效时长大于可续期时长(根据业务平台自己定)时重新下发.举个栗子针对WEB端Token24小时有效,当有效时间小于12小时刷新Token,也就是当用户连续12小时没操作网站才会被退出.
-
修改或者重置密码时下发新Token,并吊销之前的Token
Token如何吊销?
JWT不需要在服务端存储,因此吊销是个大问题,无法吊销的话就会出现用户密码被盗,即使用户修改了密码,其他人也并不会立即失效,这点在安全性很高的地方几乎是不允许的情况.因此吊销是必要的.
吊销方案有存储黑名单Token,个人觉得不是很好,一长串的东西扔哪都不合适啊,因此想着存储用户的version在Token中,当用户修改或重置密码后期版本自增,那么当请求到来时与Token中version进行对比,不一致则直接拒绝访问,实现了吊销.
缺点也很明显每次请求到来都需要去DB或者缓存取出用户的版本,然后与Token中的version进行一次判断,这个看业务容忍度来取舍了.个人建议放Redis的中,即使1000w数据内存占用也是非常少的,而且对于大多数业务来说这个并不需要强依赖,Redis挂了则根据异常以及Token的到期时间*选择直接放行还是拒绝.
Token签名秘钥如何替换
Token为了保证其可靠性,因此必须有签名串并且还需要HTTPS防止中间人攻击,因此秘钥更换也是必须的.
所采取的方案是用一个定长为2的secret[2]数组来保存秘钥,秘钥是存储在配置中,下发时使用secret[0],验签时也从secret[0]开始验签,验签失败则使用secret[1]验签,当然为了加快替换流程,secret[1]验签成功后需要重新下发secret[0]签名的Token,在运行一段时间后则可以把secret[0]替换为secret[1],重新增加新秘钥放入secret[0]中.因为秘钥都是写在配置中的,因此每次只需要重新发下配置即可.
如何与现有结构兼容?
兼容实际上判断有没有下发Token,没有则使用原本Session的验证,在验证成功后下发Token,保证下次请求可以使用Token验证,那么这样跑一段时间则能保证绝大部分活跃用户切换到了Token流程.
旧Redis如何清理?
旧Redis中存储着大量key为UUID并且没有失效时间的字符串,清理只能扫描所有的key,然后判断是不是UUID的格式,判断是否有失效时间,没有则删除.那么会有以下问题
列出所有Key
-
KEYS 生产上禁止使用的命令,其复杂度是O(N)也就是全遍历,会带来性能问题.
-
SCAN迭代模式, SCAN每次返回一定量的key集合,并且返回下次迭代的游标,是可以在生产环境上使用的命令,因此最佳选择.
清理操作
清理脚本就很简单的扫描出key,判断是否为UUID格式,然后利用TTL命令判断是否设置过期时间,没设置则删除.注意该清理要在Token替换了大部分Session之后进行,保证对当前使用Session的用户无太大影响