目前web开发前后端已经算非常的普及了。前后端分离要求我们对用户会话状态要进行一个无状态处理。我们都知道通常管理用户会话是session。用户每次从服务器认证成功后,服务器会发送一个sessionid给用户,session是保存在服务端 的,服务器通过session辨别用户,然后做权限认证等。那如何才知道用户的session是哪个?这时候cookie就出场了,浏览器第一次与服务器建立连接的时候,服务器会生成一个sessionid返回浏览器,浏览器把这个sessionid存储到cookie当中,以后每次发起请求都会在请求头cookie中带上这个sessionid信息,所以服务器就是根据这个sessionid作为索引获取到具体session。

痛点

上面的场景会有一个痛点。对于前后端分离来说。比如前端都是部署在一台服务器的nginx上,后端部署在另一台服务器的web容器上。甚至 前端不能直接访问后端,中间还加了一层代理层。

大概如下所示:

20210824223916

也就是说前后端分离在应用解耦后增加了部署的复杂性。通常用户一次请求就要转发多次。如果用session 每次携带sessionid 到服务器,服务器还要查询用户信息。同时如果用户很多。这些信息存储在服务器内存中,给服务器增加负担。还有就是CSRF(跨站伪造请求攻击)攻击,session是基于cookie进行用户识别的, cookie如果被截获,用户就会很容易受到跨站请求伪造的攻击。还有就是sessionid就是一个特征值,表达的信息不够丰富。不容易扩展。而且如果你后端应用是多节点部署,那么就需要实现session共享机制(基于session复制)。不方便集群应用。

什么是JWT

JSON Web Token (JWT)是一个开放标准(RFC 7519),它定义了一种紧凑的、自包含的方式,用于作为JSON对象在各方之间安全地传输信息。该信息可以被验证和信任,因为它是数字签名的。

所以JSON WEB TOKEN(以下称JWT)可以解决上面的问题。JWT还是一种token。token 是服务器颁发给客户端的。就像户籍管理部门给你发的身份证一样。你拿着这个证件就能去其他部门办事。其他部门验证你这个身份证是否过期,是否真假。不用每次都让户籍来认可。同时token 天然防止CSRF攻击。而且JWT可以携带一些不敏感的用户信息。这样服务器不用每次都去查询用户信息。开箱即用,方便服务器处理鉴权逻辑。

是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519)。该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。

JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。

JWT的特点

  • 简洁(Compact): 可以通过URL,POST参数或者在HTTP header发送,因为数据量小,传输速度也很快。
  • 自包含(Self-contained):负载中包含了所有用户所需要的信息,避免了多次查询数据库或缓存。

JWT消息结构

JWT有3个组成部分,分别是:

  • 头部(header) 声明类型以及加密算法 如 {“alg”:”HS256”,”typ”:”JWT”} 用Base64进行了处理
  • 载荷(payload) 携带一些用户身份信息,用户id,颁发机构,颁发时间,过期时间等。用Base64进行了处理。这一段其实是明文,所以一定不要放敏感信息。
  • 签证(signature) 签名信息,使用了自定义的一个密钥然后加密后的结果,目的就是为了保证签名的信息没有被别人改过,这个一般是让服务器验证的。

从上面的例子可以看出来JWT的规则是这样的 <header>.<payload>.<signature>三部分通过”.”进行拼接 。JWT.io提供解析的方法 我们可以拿上面那个token去玩一玩

20210824224350

所以JWT不是简单的token,比session+cookie机制更加丰富。应用场景更加丰富。

JWT优点

  1. 可扩展性好 应用程序分布式部署的情况下,session需要做多机数据共享,通常可以存在数据库或者redis里面。而jwt不需要。
  2. 无状态 jwt不在服务端存储任何状态。RESTful API的原则之一是无状态,发出请求时,总会返回带有参数的响应,不会产生附加影响。用户的认证状态引入这种附加影响,这破坏了这一原则。另外jwt的载荷中可以存储一些常用信息,用于交换信息,有效地使用 JWT,可以降低服务器查询数据库的次数。

JWT缺点

1. 安全性

由于jwt的payload是使用base64编码的,并没有加密,因此jwt中不能存储敏感数据。而session的信息是存在服务端的,相对来说更安全。

2. 性能

jwt太长。由于是无状态使用JWT,所有的数据都被放到JWT里,如果还要进行一些数据交换,那载荷会更大,经过编码之后导致jwt非常长,cookie的限制大小一般是4k,cookie很可能放不下,所以jwt一般放在local storage里面。并且用户在系统中的每一次http请求都会把jwt携带在Header里面,http请求的Header可能比Body还要大。而sessionId只是很短的一个字符串,因此使用jwt的http请求比使用session的开销大得多。

3. 一次性

无状态是jwt的特点,但也导致了这个问题,jwt是一次性的。想修改里面的内容,就必须签发一个新的jwt。

(1)无法废弃 通过上面jwt的验证机制可以看出来,一旦签发一个jwt,在到期之前就会始终有效,无法中途废弃。例如你在payload中存储了一些信息,当信息需要更新时,则重新签发一个JWT,但是由于旧的JWT还没过期,拿着这个旧的JWT依旧可以登录,那登录后服务端从JWT中拿到的信息就是过时的。为了解决这个问题,我们就需要在服务端部署额外的逻辑,例如设置一个黑名单,一旦签发了新的jwt,那么旧的就加入黑名单(比如存到redis里面),避免被再次使用。

(2)续签 如果你使用jwt做会话管理,传统的cookie续签方案一般都是框架自带的,session有效期30分钟,30分钟内如果有访问,有效期被刷新至30分钟。一样的道理,要改变jwt的有效时间,就要签发新的jwt。最简单的一种方式是每次请求刷新jwt,即每个http请求都返回一个新的jwt。这个方法不仅暴力不优雅,而且每次请求都要做jwt的加密解密,会带来性能问题。另一种方法是在redis中单独为每个jwt设置过期时间,每次访问时刷新jwt的过期时间。

可以看出想要破解jwt一次性的特性,就需要在服务端存储jwt的状态。但是引入 redis 之后,就把无状态的jwt硬生生变成了有状态了,违背了jwt的初衷。而且这个方案和session都差不多了。

适合使用JWT的场景

  • 有效期短
  • 只希望被使用一次

比如,用户注册后发一封邮件让其激活账户,通常邮件中需要有一个链接,这个链接需要具备以下的特性:能够标识用户,该链接具有时效性(通常只允许几小时之内激活),不能被篡改以激活其他可能的账户,一次性的。这种场景就适合使用jwt。

而由于jwt具有一次性的特性。单点登录和会话管理非常不适合用jwt,如果在服务端部署额外的逻辑存储jwt的状态,那还不如使用session。基于session有很多成熟的框架可以开箱即用,但是用jwt还要自己实现逻辑。

token 的续签问题

token 有效期一般都建议设置的不太长,那么 token 过期后如何认证,如何实现动态刷新 token,避免用户经常需要重新登录?

我们先来看看在 Session 认证中一般的做法:假如 session 的有效期30分钟,如果 30 分钟内用户有访问,就把 session 有效期被延长30分钟

  1. 类似于 Session 认证中的做法:这种方案满足于大部分场景。假设服务端给的 token 有效期设置为30分钟,服务端每次进行校验时,如果发现 token 的有效期马上快过期了,服务端就重新生成 token 给客户端。客户端每次请求都检查新旧token,如果不一致,则更新本地的token。这种做法的问题是仅仅在快过期的时候请求才会更新 token ,对客户端不是很友好。
  2. 每次请求都返回新 token :这种方案的的思路很简单,但是,很明显,开销会比较大。
  3. token 有效期设置到半夜 :这种方案是一种折衷的方案,保证了大部分用户白天可以正常登录,适用于对安全性要求不高的系统。
  4. 用户登录返回两个 token :第一个是 acessToken ,它的过期时间 token 本身的过期时间比如半个小时,另外一个是 refreshToken 它的过期时间更长一点比如为1天。客户端登录后,将 accessToken和refreshToken 保存在本地,每次访问将 accessToken 传给服务端。服务端校验 accessToken 的有效性,如果过期的话,就将 refreshToken 传给服务端。如果有效,服务端就生成新的 accessToken 给客户端。否则,客户端就重新登录即可。
    该方案的不足是:
    • 需要客户端来配合;
    • 用户注销的时候需要同时保证两个 token 都无效;
    • 重新请求获取 token 的过程中会有短暂 token 不可用的情况(可以通过在客户端设置定时器,当accessToken 快过期的时候,提前去通过 refreshToken 获取新的accessToken)。

Token的几个存储方案比较

cookie是大家最熟悉的前端存储方案。大家对它的一些特性很熟悉,比如同源限制,自动携带等等。那这里就主要说它的几个安全方面的配置:

  • http-only:这个参数限定只能通过请求自动携带的方式获取cookie,没办法通过脚本去获取,这样一来就可以有效避免cookie被xss攻击,及时你的页面被不小心注入脚本,也无法获取你的cookie。
  • same-site:这个属性规定,只有请求是从同源页面发起才能获取cookie。这个主要是用来防御csrf的,因为如果只要是同源请求就可以自由携带cookie的话,很容易就遭受csrf攻击,比如在token失效之前打开了攻击者页面,该页面自动向后台发送请求,就会携带cookie。
  • secure:这个参数可以让cookie只会被https请求携带,https应该是是目前最好用的几种防止中间人攻击的方式之一,可以有效防止你的token被窃听。
其他前端存储方案

SessionStorage:sessionStorage 特别的一点在于,即便是相同域名下的两个页面,只要它们不在同一个浏览器窗口中打开,那么它们的 sessionStorage 内容便无法共享;

LocalStorage:在同源的所有标签页和窗口之间共享数据,保存的数据长期存在,下一次访问该网站的时候,网页可以直接读取以前保存的数据。

IndexDB:类似LocalStorage,只不过容量大很多

硬件级别的Token保护:USB Token

将Token存储在独立的硬件上,重要请求需要用户自主填写Token。

USB Key中也可以存储数字证书,这个最常见的应该就是网银的U盾了。

比较

SessionStorage安全性较高,能比较好防御CSRF,但是对XSS无用,且局限较大。

LocalStorage和IndexDB受同源保护,但是一旦被注入脚本也很危险。

USB Key安全性较高,属于物理级别的防御,但是有一定的成本。

Cookie最方便,但是默认状态下最不安全,需要将http-only,same-site,secure等开启才能有较高的安全性。

一些问题

什么时候你应该用JSON Web Token?

下列场景中使用JSON Web Token是很有用的:

  • Authorization (授权) : 这是使用JWT的最常见场景。一旦用户登录,后续每个请求都将包含JWT,允许用户访问该令牌允许的路由、服务和资源。单点登录是现在广泛使用的JWT的一个特性,因为它的开销很小,并且可以轻松地跨域使用。
  • Information Exchange (信息交换) : 对于安全的在各方之间传输信息而言,JSON Web Tokens无疑是一种很好的方式。因为JWT可以被签名,例如,用公钥/私钥对,你可以确定发送人就是它们所说的那个人。另外,由于签名是使用头和有效负载计算的,您还可以验证内容没有被篡改。

JSON Web Tokens是如何工作的?

在认证的时候,当用户用他们的凭证成功登录以后,一个JSON Web Token将会被返回。此后,token就是用户凭证了,你必须非常小心以防止出现安全问题。一般而言,你保存令牌的时候不应该超过你所需要它的时间。

无论何时用户想要访问受保护的路由或者资源的时候,用户代理(通常是浏览器)都应该带上JWT,典型的,通常放在Authorization header中,用Bearer schema。

header应该看起来是这样的:

Authorization: Bearer

服务器上的受保护的路由将会检查Authorization header中的JWT是否有效,如果有效,则用户可以访问受保护的资源。如果JWT包含足够多的必需的数据,那么就可以减少对某些操作的数据库查询的需要,尽管可能并不总是如此。

如果token是在授权头(Authorization header)中发送的,那么跨源资源共享(CORS)将不会成为问题,因为它不使用cookie。

如何保证token安全性

他只能拿到自己的,坑也只能坑自己,这有什么关系呢?后端要做的防御是即使被绕过前端界面的操作直接发请求,也要保证该做的验证要做了。比如说这个账户是否有权限做某些事情等。

另外要做的,就是做好XSS防御,不要让别人在你的网站上有注入脚本的方法,否则其他用户的token被注入脚本的人拿到了,这才是有极大危险的。

还有就是加https,尽可能提高中间人攻击的成本。

基于Token的身份认证 与 基于服务器的身份认证

1.基于服务器的身份认证

在讨论基于Token的身份认证是如何工作的以及它的好处之前,我们先来看一下以前我们是怎么做的:

HTTP协议是无状态的,也就是说,如果我们已经认证了一个用户,那么他下一次请求的时候,服务器不知道我是谁,我们必须再次认证

传统的做法是将已经认证过的用户信息存储在服务器上,比如Session。用户下次请求的时候带着Session ID,然后服务器以此检查用户是否认证过。

这种基于服务器的身份认证方式存在一些问题:

  • Sessions : 每次用户认证通过以后,服务器需要创建一条记录保存用户信息,通常是在内存中,随着认证通过的用户越来越多,服务器的在这里的开销就会越来越大。
  • Scalability : 由于Session是在内存中的,这就带来一些扩展性的问题。
  • CORS : 当我们想要扩展我们的应用,让我们的数据被多个移动设备使用时,我们必须考虑跨资源共享问题。当使用AJAX调用从另一个域名下获取资源时,我们可能会遇到禁止请求的问题。
  • CSRF : 用户很容易受到CSRF攻击。
2.JWT与Session的差异

相同点是:它们都是存储用户信息;然而,Session是在服务器端的,而JWT是在客户端的。

Session方式存储用户信息的最大问题在于要占用大量服务器内存,增加服务器的开销。

而JWT方式将用户状态分散到了客户端中,可以明显减轻服务端的内存压力。

Session的状态是存储在服务器端,客户端只有session id;而Token的状态是存储在客户端。

20210824224836

3.基于Token的身份认证是如何工作的

基于Token的身份认证是无状态的,服务器或者Session中不会存储任何用户信息。

没有会话信息意味着应用程序可以根据需要扩展和添加更多的机器,而不必担心用户登录的位置。

虽然这一实现可能会有所不同,但其主要流程如下:

用户携带用户名和密码请求访问 -服务器校验用户凭据 -应用提供一个token给客户端 -客户端存储token,并且在随后的每一次请求中都带着它 -服务器校验token并返回数据

注意:

每一次请求都需要token -Token应该放在请求header中 -我们还需要将服务器设置为接受来自所有域的请求,用Access-Control-Allow-Origin: *

20210824224905

4.用Token的好处 - 无状态和可扩展性

Tokens存储在客户端。完全无状态,可扩展。我们的负载均衡器可以将用户传递到任意服务器,因为在任何地方都没有状态或会话信息。 - 安全:Token不是Cookie。(The token, not a cookie.)每次请求的时候Token都会被发送。而且,由于没有Cookie被发送,还有助于防止CSRF攻击。即使在你的实现中将token存储到客户端的Cookie中,这个Cookie也只是一种存储机制,而非身份认证机制。没有基于会话的信息可以操作,因为我们没有会话!

还有一点,token在一段时间以后会过期,这个时候用户需要重新登录。这有助于我们保持安全。还有一个概念叫token撤销,它允许我们根据相同的授权许可使特定的token甚至一组token无效。

5.JWT与OAuth的区别?

OAuth2是一种授权框架,JWT是一种认证协议,无论使用哪种方式切记用HTTPS来保证数据的安全性,OAuth2用在使用第三方账号登录的情况(比如使用weibo, qq, github登录某个app),而JWT是用在前后端分离, 需要简单的对后台API进行保护时使用。

参考

一文了解web无状态会话token技术JWT
五分钟带你了解啥是JWT

评论