很多网站初期通常会用 Session 方式实现登录用户的用户鉴权,也就是在用户登录成功后,将这个用户的具体信息写在服务端的 Session 缓存中,并分配一个 session_id 保存在用户的 Cookie 中。该用户的每次请求时候都会带上这个 ID,通过 ID 可以获取到登录时写入服务端 Session 缓存中的记录。
500
这种方式的好处在于信息都在服务端储存,对客户端不暴露任何用户敏感的数据信息,并且每个登录用户都有共享的缓存空间(Session Cache)。
但是随着流量的增长,这个设计也暴露出很大的问题——用户中心的身份鉴权在大流量下很不稳定。因为用户中心需要维护的 Session Cache 空间很大,并且被各个业务频繁访问,那么缓存一旦出现故障,就会导致所有的子系统无法确认用户身份,进而无法正常对外服务。
这主要是由于 Session Cache 和各个子系统的耦合极高,全站的请求都会对这个缓存至少访问一次,这就导致缓存的内容长度和响应速度,直接决定了全站的 QPS 上限,让整个系统的隔离性很差,各子系统间极易相互影响。
那么,如何降低用户中心与各个子系统间的耦合度,提高系统的性能呢?我们一起来看看。

JWT 登陆和 token 校验

常见方式是采用签名加密的 token,这是登录的一个行业标准,即 JWT(JSON Web Token):
500
上图就是 JWT 的登陆流程,用户登录后会将用户信息放到一个加密签名的 token 中,每次请求都把这个串放到 header 或 cookie 内带到服务端,服务端直接将这个 token 解开即可直接获取到用户的信息,无需和用户中心做任何交互请求。
这个 token 内部包含过期时间,快过期的 token 会在客户端自动和服务端通讯更换,这种方式可以大幅提高截取客户端 token 并伪造用户身份的难度。
同时,服务端也可以和用户中心解耦,业务服务端直接解析请求带来的 token 即可获取用户信息,无需每次请求都去用户中心获取。而 token 的刷新可以完全由 App 客户端主动请求用户中心来完成,而不再需要业务服务端业务请求用户中心去更换。
通过 token 方式,用户中心压力最大的接口可以下线了,每个业务的服务端只要解开 token 验证其合法性,就可以拿到用户信息。不过这种方式也有缺点,就是用户如果被拉黑,客户端最快也要在 token 过期后才能退出登陆,这让我们的管理存在一定的延迟。
如果我们希望对用户进行实时管理,可以把新生成的 token 在服务端暂存一份,每次用户请求就和缓存中的 token 对比一下,但这样很影响性能,极少数公司会这么做。同时,为了提高 JWT 系统的安全性,token 一般会设置较短的过期时间,通常是十五分钟左右,过期后客户端会自动更换 token。

token 的更换和离线

那么如何对 JWT 的 token 进行更换和离线验签呢?
具体的服务端换签很简单,只要客户端检测到当前的 token 快过期了,就主动请求用户中心更换 token 接口,重新生成一个离当前还有十五分钟超时的 token。
但是期间如果超过十五分钟还没换到,就会导致客户端登录失败。为了减少这类问题,同时保证客户端长时间离线仍能正常工作,行业内普遍使用双 token 方式,具体你可以看看后面的流程图:
500
可以看到,这个方案里有两种 token:一种是 refresh_token,用于更换 access_token,有效期是 30 天;另一种是 access_token,用于保存当前用户信息和权限信息,每隔 15 分钟更换一次。如果请求用户中心失败,并且 App 处于离线状态,只要检测到本地 refresh_token 没有过期,系统仍可以继续工作,直到 refresh_token 过期为止,然后提示用户重新登陆。这样即使用户中心坏掉了,业务也能正常运转一段时间。

安全建议

最后我再啰嗦几句,除了上述代码中的注释外,在使用 JWT 方案的时候还有一些关键的注意事项,这里分享给你。
第一,通讯过程必须使用 HTTPS 协议,这样才可以降低被拦截的可能。
第二,要注意限制 token 的更换次数,并定期刷新 token,比如用户的 access_token 每天只能更换 50 次,超过了就要求用户重新登陆,同时 token 每隔 15 分钟更换一次。这样可以降低 token 被盗取后给用户带来的影响。
第三,Web 用户的 token 保存在 cookie 中时,建议加上 httponly、SameSite=Strict 限制,以防止 cookie 被一些特殊脚本偷走。

总结

为了降低用户中心的流量压力,同时让各个子系统与用户中心脱耦,我们采用信任“签名”的 token,把用户信息加密发放到客户端,让客户端本地拥有这些信息。而子系统只需通过签名算法对 token 进行验证,就能获取到用户信息。
这种方式的核心是把用户信息放在服务端外做传递和维护,以此解决用户中心的流量性能瓶颈。此外,通过定期更换 token,用户中心还拥有一定的用户控制能力,也加大了破解难度,可谓一举多得。
其实,还有很多类似的设计简化系统压力,比如文件 crc32 校验签名可以帮我们确认文件在传输过程中是否损坏;通过 Bloom Filter 可以确认某个 key 是否存在于某个数据集合文件中等等,这些都可以大大提高系统的工作效率,减少系统的交互压力。这些技巧在硬件能力腾飞的阶段,仍旧适用。

思考题

用户如果更换了昵称,如何快速更换 token 中保存的用户昵称呢?
客户端可以缓存修改后的昵称,直到更换了access token再清除缓存,类似弹幕本地先展示发送让用户自己认为发送成功了。
access_token由于安全问题设置过期的时间非常短,但是refresh_token有效时间非常长,如果refresh_token被泄漏掉,是不是能一直刷新access_token呢。。
作者回复: 你好,7S,很高兴收到你的思考,关于这里有一些特殊的小技巧,如请求时带上一些客户端特征,如:请求更换access_token时,带上的refresh_token的请求 同时 需要特殊的签名,存储在本地的token不用明文保存,与服务端通讯时用特殊协议加密等~
老师您好,请教下,如果只是用户中心出现故障,导致客户端更换 access_token失败,APP没有离线,但是refresh_token没有过期。这个时候会怎么处理?客户端不会提示用户重新登陆,依旧拿着旧的 access_token请求其他业务接口,其他业务接口由于token过期返回登录超时?
作者回复: 你好,DZ,这时候可以做客户端自行签名功能,比如在token后面追一个特殊的串,是由客户端结合本地refresh token制作的,然后再请求服务端的时候会特殊处理。另外,即使access token过期一些服务接受的情况下也可以允许过期一小时,这些都需要业务根据自己的场景定制,以前我们直播期间所有token是不判断过期的,只有进入直播前检测一下
把用户信息放在服务器外做传递和维护,子系统通过签名算法对token进行验证,是否会存在子系统可以拿到签名的密钥,从而可以自行签发token的能力,会不安全。
作者回复: 你好,zhou,token更换这个可以用户中心提供接口,但是触发更换生成token后,如何让客户端本地token同步更新是个问题,毕竟很多token并不是保存在cookie内,并且很多数据不是在同域名下,如果是多个网站联合sso刷新token会很麻烦
在修改后昵称直接颁发新token给客户端,或者让access_token过期用户重新用获取新token
作者回复: 你好,等这个回答好久了,这里也补充一句,这个方式有个漏洞,如何预防入侵代码恶意刷用户的token
更新昵称一般是用户自己发起的,更新昵称的同时,业务后台重新生成token就可以了。
作者回复: 你好,frag007,很高兴收到你的留言,这是个最简单的实现方式~唯一缺点就是多客户端的情况下同步有些问题~
我的理解是,token中应该只存放和session生命周期同步的操作。比如:用户Id和权限。这两个东西,在用户session的生命周期内一般来说是不会变的。翻译一下,token代表着:你是谁,你能做什么。能做到这两个事情就够了。而不应该去单独关注用户的扩展信息。 至于昵称,我觉得应该单独放缓存中。通过用户ID获取。因为昵称当前token下修改还好说,如果跨token呢?比如web端修改了昵称web端端token可以立马换一个新的,移动端怎么办呢?所以我认为,昵称,头像,这种会修改的信息不应该放到token体里。
作者回复: 你好,alien,确实如此,而很多业务为了方便,token有额外一段在结尾放附加消息
关于用户中心里,服务器端对access_token和refresh_token的管理,有没详细的设计?
作者回复: 你好,特修斯之船,由于这个篇幅过长不太好详细回复,详细的设计可以查一下oauth2.0协议的一些资料可能会对你有帮助!
用户如果被拉黑,客户端最快也要在 token 过期后才能退出登陆,这让我们的管理存在一定的延迟。
作者回复: 你好,Geek_00乐,很高兴你的心得分享,同时补充一句~子系统不会每次都问询用户中心~导致了这个问题
token的方式是怎么处理多终端登录以及“踢下线”类似的功能的呢?
作者回复: 你好,sky,很高兴收到你的留言,踢下线可以用网关黑名单方式,每个业务网关会解开token,发现redis中有这个uid以及低于指定版本,就会要求更换
如果我来做快速更换昵称的功能,两种方式, a.在用户修改昵称后,内存中加入个用户标识,解析token后读取该标识,有则返回特定code,让客户端重新拿token。甚至可以不用客户端参与,返回301重定向到获取新token的路由。 b. token里面不存用户信息,只存用户ID,需要用户信息的时候从缓存读。
作者回复: 所以使用token方式来签名发给客户端,客户端请求其他子系统的时候,会带上它,子系统只要验证这个token的签名就不需要再去用户中心问一句。所以token使用后,用户中心不会被其他子系统频繁请求,但是也导致token发出去没法再次更改,即使我们用户中心给他拉黑了,其他子系统只认印章,不会过来问问。同时为了方便token内会保存当前用户一些基础信息,减少其他系统过来询问的次数,这导致,用户更新头像,token没更换,是不会同步更新的第一个很暴力,但是很有趣~ 第二个方式也很有趣,同时补一个技巧我们可以通过设定固定网址 user/用户uid/heaer.jpg方式直接获取用户头像,这样也不用考虑更新问题了