点击上方蓝字关注我们

起源
在传统意义的互联网概念中,Web只用于浏览展示内容,几乎没有 server 的概念。而随着业务兴起,用户需求不断攀升,传统的 C/S 架构逐渐被 B/S 架构取代,从而 Web 端也承担了更多的交互工作,逐渐成为了现在所熟知的 Web App。在B/S架构中,前后端的交互通道主要通过HTTP请求来维持,而由于 HTTP 协议的无状态特性,使得很多场景无法得到满足,例如社交、购物网站的用户系统等。从而需要衍生技术维持客户端与服务端之间的用户登录状态,也就是本文需要展开讲解的两项主流技术概念:JWT(Json Web Token)以及 Session 。
01
技术方案
首先我们需要弄明白,HTTP 是无状态协议( Stateless ),这就使得后端无法根据这个 HTTP 请求得到更多的认证信息,后端也无法判断到底是什么用户发送的请求。例如我们使用一个电商网站时,会不断地往购物车中加入商品,并且希望在整个购物的过程中都能记录添加的商品,这意味着全程都要记录整个购物状态,而记录的方式就是利用会话。在本文中我们会介绍两种不同的用户认证方式。
基于 Session 的认证
Session 即会话,这种方案的具体做法是每次用户登陆成功后,会分发给浏览器一个随机的 Session ID,并且把这个 Session ID和与之对应的用户信息存储在服务端,并且通过设置 response headers 中的 Set-Cookie 字段赋值来设置浏览器的cookie ,利用浏览器cookie的特性,每次发起的 HTTP 请求都会自动带上浏览器的cookie ,从而使其成为“有状态的” HTTP 请求。

这种方案有以下几个点可以被挑战:
1、如 cookie 被禁用了怎么办?
2、如何预防跨站请求伪造(Cross-site request forgery)?
3、所有状态都被保存在单服务器中,后端是集群怎么办?
cookie 若被禁用,我们当然可以选用浏览器其他的存储方案。当然 cookie 本身可以被后端所设置,使用了其他方案例如 localstorage ,将会增加前端的工作量,改造成本是极高的。
跨站请求伪造(简称 CSRF ),通俗来讲就是利用了 HTTP 请求特性,在其他网站的 HTML 元素中填写了其他网站的请求路由,请求发出后就会自动携带该浏览器中所存储的未被清空的 cookie 达到目的。当然,预防的措施除了替换 cookie 的存储方案,针对于该请求做一定的鉴权处理,或者放置其他校验标识符也是解决方案,也就是从请求本身的业务层面解决问题。
这也是该方案最为头疼的一点,也就是后端状态的共享与高可用性。在单服务器条件下所有状态存储没有任何问题。但是,一旦业务量增加,巨石型的应用大概率需要进行服务拆分,甚至使用微服务的架构进行集群化部署。在这种情况下,如果要确保各服务之间的状态一致性,就必须通过诸如 Redis 这样的公共服务来实现状态共享,但这也意味着这个服务将会非常重要,一旦 crash 将会导致整个 Web 服务不可用,所以本身需要做高可用支持(状态服务本身集群化),而集群化的场景下又会引发状态同步等一系列的问题。我们为了维持 HTTP 请求的状态引入了 Session 技术,但却使我们的整体架构变得更加复杂了。

基于 Json Web Tokens 的认证
Json Web Tokens(简称JWT)方案不同于 Session ,它倡导不在服务端维持状态,只签发一个服务端所能识别的签名供给前端存储(一般使用 localstorage ),而每次 HTTP 请求的 header 中都带有该签名,服务端就能根据此字段值识别其特征。而这段被哈希过后的签名,其中包含的可以是用户的ID、名称、签名过期时间、权限等基础信息,服务端的各服务只需要使用相同的密钥解析该 Token ,就可以识别出这些信息并且确认该请求是否过期。

当然,JWT 方案看上去美好,同样也存在着一些问题:
1、第三方通过抓包截获了包含在 header 中的 token 值是不是就可以在过期时间内为所欲为了?
2、服务端没有维持状态,是否就无法主动踢出一些请求?
1、首先第一个问题,为了防止 token 被滥用,一般这个有效时间会被设置地很短,从而让危害最小化。那么会引申出一个问题,每次 token 只在登陆时签发,那么过短的有效时间就意味着需要用户频繁地重新登录来保持状态,这毫无疑问会极大地降低用户体验。而我们希望一定程度上保证 token 本身的安全性(维持极短的有效期),又希望浏览器能无感保持用户状态,所以在 JWT 方案中还提供了 refresh token 的机制来做权衡。
极短的有效时间虽然提升了安全性,但是牺牲了可用性。在首次签发 token 时,我们会同时签发两种 token ,一种是用于普通请求认证的 access_token ,该类 token 的特征是有效时间非常短,在该 token 过期后,请求也会无法被正常响应。这时候前端代码中需要实现一个拦截器,在检测到 access token 过期的报错后,会把该请求暂存在队列中,并使用第二类 token 也就是 refresh token(该 token 过期时间一般会比较长,)去请求重新签发最新的 token ,待请求返回后我们会存储最新的token到浏览器的缓存中,并重新发送队列中的所有请求,整个过程对于用户来说是无感的,也一定程度地保证了 token 的安全性。当然,上述操作只能让黑客的攻击成本更高,而达不到完全预防的效果。
简单总结下来,refresh token 存在的意义在于前端页面探测到 access token 过期后利用 refresh token 自动去申请新的token而无需用户重新登录,即保证了用户体验,否则只保存一个时效过短的 access token 如果过期,那么只能通过重新登录的方式申请新 token 。

2、 token 本身是无状态的,且服务器也不会保存其状态。本质上而言,服务端并不了解有多少用户登陆了系统,只关心它们所携带的签名是否合法。这就带来了一个问题,服务端无法主动踢出已登陆的用户,只能等待 token 过期。比如视频、聊天类网站的场景,通常不允许同一账号在不同终端同时登录,若强制登录则另一个账号会被踢出,而由于 JWT 无法在服务端维持状态,也就不能完成主动踢出用户的操作,无法满足其业务需求。
02
JWT 过渡方案
通过上一段落对于两种状态维持方案的分析后,使用 Session 还是 JWT 想必大家都有了一定的认知,主要还是需要结合实际业务场景进行选型。若对用户状态没有很强烈的操控需求,那么 JWT 无疑是更加轻量级、简单的解决方案。那么如果在选用了 JWT 的情况下,遇到业务转型,需要针对于用户状态进行限制、主动终止会话等操作,难道需要把一整套的JWT方案切换为 Session 吗?这显然代价过大,实际上我们也可以把现有的 JWT 状态维持在Redis中达到过渡效果,在 token 中加入类似 Session 方案中的 Session ID ,再将用户ID、Session ID 放置到 Redis 中,每当用户请求到达后便去比对 map 中所存储的 Session 是否已经存在,达到限制相同用户登陆效果。当然,该方案同样也会遇到状态服务(这里所指代的就是 Redis )的高可用特性,本质上而言也会遇到和 Session 同样的架构臃肿、性能羸弱问题。
03
总结
全文比对了两种状态维持方案后,相较之下没有绝对利弊,需要结合业务本身来选择技术架构,而使用 webstorage 存储相应信息比起 cookie 是更加稳妥的浏览器存储方式。当然,无论是哪种浏览器存储方案,都是可以被 JS 代码直接读取的,做好 XSS 的预防也是开发者需要注意的点。
作者:钟灵
简介:云趣科技全栈工程师
出品:云趣科技


云趣 ,等你关注




