公司的登录模块也从Session切换到JWT挺长一段时间了,抽时间来总结一下遇到的问题以及解决方案.
在JWT之前,公司是利用传统的Session来实现登录状态的保持,分布式下则利用Redis实现Session共享集中管理,共享集中管理会带来登录强依赖Redis,然而公司的Redis确实不太稳定,导致一蹦或者网络抖动就会使得用户受到影响,另外一点就是用户的Session存在redis中没有设置超时时间,其格式为 UUID - 用户信息JSON串,这个是大坑,导致Redis等我接手时已经膨胀到了30G之多,因此在大家强烈要求下JWT的改造就此开始. 之所以选择JWT,因为其不需要服务端存储,也就是可以直接下掉Redis,另外其playload可以存储不少关键信息并且小巧接入成本极小.
Jwt Token的playload实际上只是base64编码了一层,其被解码后可以查看到具体的文本信息,因此不能存私密性的东西.建议存储 id,username,expire,version等一些不是很关键的数据.至于version有什么用,后面吊销的时候在讨论.
JWT不需要在服务端存储,因此吊销是个大问题,无法吊销的话就会出现用户密码被盗,即使用户修改了密码,其他人也并不会立即失效,这点在安全性很高的地方几乎是不允许的情况.因此吊销是必要的. 吊销方案有存储黑名单Token,个人觉得不是很好,一长串的东西扔哪都不合适啊,因此想着存储用户的version在Token中,当用户修改或重置密码后期版本自增,那么当请求到来时与Token中version进行对比,不一致则直接拒绝访问,实现了吊销. 缺点也很明显每次请求到来都需要去DB或者缓存取出用户的版本,然后与Token中的version进行一次判断,这个看业务容忍度来取舍了.个人建议放Redis的中,即使1000w数据内存占用也是非常少的,而且对于大多数业务来说这个并不需要强依赖,Redis挂了则根据异常以及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中存储着大量key为UUID并且没有失效时间的字符串,清理只能扫描所有的key,然后判断是不是UUID的格式,判断是否有失效时间,没有则删除.那么会有以下问题
清理脚本就很简单的扫描出key,判断是否为UUID格式,然后利用TTL命令判断是否设置过期时间,没设置则删除.注意该清理要在Token替换了大部分Session之后进行,保证对当前使用Session的用户无太大影响.