浅谈JWT

前言

SpringBoot集成SpringSecurity(一) 入门一文中我们曾经提到做会话管理控制的有两种方式(如果对其不了解的话,建议你去上一篇去看一下):

  1. 基于session方式;
  2. 基于token方式;

在之前的文章中(security系列)我们采用的会话管理方式皆为session的方式,而在本文中,我们将采用token的方式,顺便我们可以将两种方式的优劣势做一个对比。

本文代码已上传至GitHub

https://github.com/wanglongsxr/springsecurity.git

基于session方式的身份验证

在这里插入图片描述

  1. 用户向服务器发送用户名和密码进行身份验证;

  2. 验证服务器后,相关数据(如用户角色,登录时间等)将保存在当前会话中;

  3. 服务器向用户返回session_id;

  4. session信息都会写入到用户的Cookie;

  5. 用户的每个后续请求都将通过在Cookie中取出session_id传给服务器;

  6. 服务器收到session_id并对比之前保存的数据,确认用户的身份。

这种模式最大的问题是,没有分布式架构,无法支持横向扩展。如果使用一个服务器,该模式完全没有问题。

但是,如果它是服务器群集或面向服务的跨域体系结构的话,基于session的认证会出现一个问题,每个应用服务都需要在session中存储用户身份信息,通过负载均衡将本地的请求分配到另一个应用服务需要将session信息带过去,否则会重新认证。

这个时候,通常的做法有下面几种:

session方式的身份验证.jpg

  1. Session复制:多台应用服务器之间同步session,使session保持一致,对外透明。
  2. Session黏贴:当用户访问集群中某台服务器后,强制指定后续所有请求均落到此机器上。
  3. Session集中存储:将Session存入分布式缓存中,所有服务器应用实例统一从分布式缓存中存取Session。(我们项目采用的就是此方式)

总体来讲,基于session认证的认证方式,可以更好的在服务端对会话进行控制,且安全性较高。但是,session机 制方式基于cookie,在复杂多样的移动客户端上不能有效的使用,并且无法跨域,另外随着系统的扩展需提高 session的复制、黏贴及存储的容错性。

基于token方式的身份验证

服务端不用存储认证数据,易维护扩展性强, 客户端可以把token 存在任意地方,并且可 以实现web和app统一认证机制。

其缺点也很明显,token由于自包含信息,因此一般数据量较大,而且每次请求 都需要传递,因此比较占带宽。另外,token的签名验签操作也会给cpu带来额外的处理负担。

基于token的认证方式.jpg

引入JWT

什么是JWT

在上文中我们介绍了session与token两种方式的区别,那么token方式怎么实现呢?

可以这么说,JWT是token实现方式的一种,另外的有:自定义token,oauth2等…

JWT是一种认证协议,JWT提供了一种用于发布接入令牌(Access Token),并对发布的签名接入令牌进行验证的方法。 其中令牌(Token)本身包含了一系列声明,应用程序可以根据这些声明限制用户对资源的访问。

通俗点来讲:服务器认证以后,会根据用户的相关信息生成一个 JSON 对象,发回给用户,然后用户与服务端通信的时候,服务器通过这个JSON对象去认定用户身份

一个jwt的实际案例;

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

它由3部分组成,分别为:

  1. header(头)
  2. payload(有效载荷)
  3. signature(签名)

三者之间使用“.”链接,格式如下:

header.claims.signature

将它们写成一行如下:

1590067117(1).jpg

很多人肯定很好奇为什么一大堆看似无规律字母包含了用户的信息,这是因为为了安全的在url中使用,所有部分都base64 URL-safe进行编码处理。

Header头部分

头部分简单声明了类型(JWT)以及产生签名所使用的算法。

{
  "alg" : "AES256",
  "typ" : "JWT"
}
payload有效载荷

此部分时用于存储用户的详细信息。

有些情况下,我们很可能要在一个服务器上实现认证,然后访问另一台服务器上的资源;或者,通过单独的接口来生成token,token被保存在应用程序客户端(比如浏览器)使用。

JWT指定七个默认字段供选择: 1. iss:发行人 2. exp:到期时 3. sub:主题 4. aud:用户 5. nbf:在此之前不可用 6. iat:发布时间 7. jti:JWT ID用于标识该JWT

除以上默认字段外,我们还可以自定义私有字段

一个简单的声明(payload)的例子:

{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true
}

注意: 编码的一个特点:编码和解码的整个过程是可逆的。得知编码方式后,整个 jwt 串便是明文了,所以一定不能够携带敏感数据如密码等信息的。

Signature签名

混淆Header和payload,保证上边两部分信息不被篡改。

如果尝试使用Bas64对解码后的token进行修改,签名信息就会失效。一般使用一个私钥(secret)通过特定算法对Header和payload进行混淆产生签名信息,所以只有原始的token才能于签名信息匹配。

混淆公式为:HMACSHA256(base64UrlEncode(header) + “.” + base64UrlEncode(payload),secret)

secret特别重要,相当于jwt对象是个锁,而secret就是个钥匙,所以,永远不要把私钥信息放在客户端(比如浏览器)。

在计算出签名哈希后,JWT头,有效载荷和签名哈希的三个部分组合成一个字符串,每个部分用"."分隔,就构成整个JWT对象。

JWT的使用方式

客户端收到服务器返回的 JWT,可以储存在 Cookie 里面,也可以储存在 localStorage。

此后,客户端每次与服务器通信,都要带上这个 JWT。你可以把它放在 Cookie 里面自动发送,但是这样不能跨域,所以更好的做法是放在 HTTP 请求的头信息Authorization字段里面。

Authorization: Bearer <token>

另一种做法是,跨域的时候,JWT 就放在 POST 请求的数据体里面。

JWT工作流程

jwt工作流程.jpg

步骤的标注已经很清楚了,在这里我们详细说一下第四点跟第五点这里

在携带jwt请求的时候,过滤器会拦截所有的请求,而在过滤器这里要做的内容很多,如下:

  1. 首先先去放行不需要jwt的页面
  2. 其次判断request请求是否携带jwt,否则返回登录页;
  3. 然后判断jwt是否过期,否则返回登录页;
  4. 验证token是否合法,是否存在,用户是否存在;
  5. token的续签问题(后面着重讲述)

JWT的利弊

优点

  1. 无状态;

    其实怎么理解这个无状态呢?token包含了我们身份验证的所有信息,从而在服务端无需生成session_id,也就是说服务端无需维护状态。对于分布式系统来讲(多台服务器组成的集群),等同于无需同步各个服务器之间的状态。

  2. 有效避免了CSRF 攻击;

    避免了非法分子通过cookie或者session未过期的时间段登录系统

  3. 不依赖认证服务即可完成授权,降低服务器查询数据库的次数;

    token校验是通过算法来完成校验,避免了数据库查询次数

  4. 无需储存在服务器,降低内存开销,支持跨域访问;

  5. 适合移动端应用。

    解决移动端无cookie的场景

缺点

  1. 令牌较长,占存储空间比较大;
  2. Token不能撤销,不能作废;
  3. 不应存储敏感信息

JWT的使用场景

  1. 一次性的验证

    比如GitHub账号之后,会收到一封激活邮箱。注意,当我们点开连接的适合,并不需要登录GitHub。

    这类场景的特点即为:

    • 时效性(有效时间内,比如4小时之内);
    • 不可篡改性;
  2. 服务器集群或者分布式系统

    无需维护状态,保证用户信息在各服务器之间的通用性;

  3. 服务器与服务器之间的认证。

    比如oauth2框架内部的认证体系。

对JWT引发的思考

JWT 不是万能的,在实际使用中, JWT 也会带来诸多问题,这些问题也着实令人头疼:

注销登录等场景一系列问题

类似的场景有:

  1. 退出登录;
  2. 修改密码;
  3. 服务端修改了某个用户具有的权限或者角色;
  4. 用户的账号被删除/暂停;
  5. 用户由管理员注销;

而产生这一系列问题原因是token的无法撤销以及无法更改

服务端没有存储起来,所以即使客户端删除了 jwt,但是该 jwt 还是在有效期内,只不过处于一个游离状态。如果后端不增加其他逻辑的话,它在失效之前都是有效的

那么该如何解决这个问题呢?

  1. 引入第三方存储,管理 jwt 的状态

    以 jwt 为 key,实现去 redis 一类的缓存中间件中去校实现。如果需要让某个 token 失效,就直接从 redis 中删除这个 token 即可。但是,这样会导致每次使用 token 发送请求都要先从 DB 中查询 token 是否存在的步骤,而且违背了 JWT 的无状态原则。

  2. 黑名单机制

    和上面的方式类似,使用内存数据库比如 redis 维护一个黑名单,如果想让某个 token 失效的话就直接将这个 token 加入到 黑名单 即可,token过期删除即可。然后,每次使用 token 进行请求的话都会先判断这个 token 是否存在于黑名单中。

  3. 修改密钥 (Secret)

    不建议这种,原因如下:

    • 如果系统是分布式系统,一旦修改密钥,就意味着你需要在多个服务器同步密钥,这样与session的实现无异;
    • 如果用户同时在两个浏览器打开系统,或者在手机端也打开了系统,如果它从一个地方将账号退出,那么其他地方都要重新进行登录,这是不可取的。
  4. 保持令牌的有效期限短并经常轮换

    不建议,用户体验效果极差。需要用户经常登录。

  5. 对于修改密码后 token 还有效问题的解决方案:

    使用用户的密码的哈希值对 token 进行签名。因此,如果密码更改,则任何先前的令牌将自动无法验证。

token续签

对于传统的session方案,session 有效期 30 分钟,30 分钟内如果有访问,session 有效期被刷新至 30 分钟;

而jwt自身的设计(token的无法撤销以及无法更改)感觉天然不支持续签;即使他支持exp这个属性,但是token一旦生成,就无法更改。

那么如何解决呢?有如下几个方案:

  1. 每次请求刷新 jwt;

    不推荐,这也太暴力了,会带来性能问题。

  2. 只要快要过期的时候刷新 jwt;

    只在最后的几分钟返回给客户端一个新的 jwt,但是这样做就带点运气成分,如果用户最后几分钟没有操作,导致jwt未刷新,进而重新登录;

  3. 使用 redis 记录独立的过期时间;

    在 redis 中单独会为每个 jwt 设置了过期时间,并规定死一个刷新时间点,每次访问时,先判断距离过期时间还有多久,与规定好的时间点做比较,如果小于规定好的时间点,便刷新 jwt 的过期时间,若 jwt 不存在与 redis 中则认为过期。

  4. 用户登录返回两个 token

    第一个是 acessToken ,它的过期时间 token 本身的过期时间比如半个小时,另外一个是 refreshToken 它的过期时间更长一点比如为1天。客户端登录后,将 accessToken和refreshToken 保存在本地,每次访问将 accessToken 传给服务端。服务端校验 accessToken 的有效性,如果过期的话,就将 refreshToken 传给服务端。如果有效,服务端就生成新的 accessToken 给客户端。否则,客户端就重新登录即可。该方案的不足是:

    • 需要客户端来配合;
    • 用户注销的时候需要同时保证两个 token 都无效;
    • 重新请求获取 token 的过程中会有短暂 token 不可用的情况(可以通过在客户端设置定时器,当accessToken 快过期的时候,提前去通过 refreshToken 获取新的accessToken)。

针对于JWT的安全性考虑

为了防止用户 JWT 令牌泄露而威胁系统安全,需要注意以下几点或者完善系统功能方向:

  1. JWT 默认是不加密,但也是可以加密的。生成原始 Token 以后,可以用密钥再加密一次。

  2. 为了减少盗用,JWT 不应该使用 HTTP 协议明码传输,要使用 HTTPS 协议传输。

  3. 监控请求频率:如果 JWT 密令被盗取,攻击者或通过某些工具伪造用户身份,高频次的对系统发送请求,以套取用户数据。针对这种情况,可以监控用户在单位时间内的请求次数,当单位时间内的请求次数超出预定阈值值,则判定该用户密令是有问题的。例如 1 秒内连续超过 5 次请求,则视为用户身份非法,服务端终止请求并强制将该用户的 JWT 密令清除,然后回跳到认证中心对用户身份进行验证。

  4. 敏感操作保护:在涉及到诸如新增,修改,删除,上传,下载等敏感性操作时,定期(30分钟,15分钟甚至更短)检查用户身份,如手机验证码,扫描二维码等手段,确认操作者是用户本人。如果身份验证不通过,则终止请求,并要求重新验证用户身份信息。

  5. 对于加密算法,尽量采用rsa 这种非对称加密方式。

    -既然是加密,自然是不希望别人知道我的消息,只有我自己才能解密,所以公钥负责加密,私钥负责解密。这是大多数的使用场景,使用 rsa 来加密。
    - 既然是签名,自然是希望别人不能冒充我发消息,只有我才能发布签名,所以私钥负责签名,公钥负责验证。
    所以,在客户端使用 rsa 算法生成 jwt 串时,是使用私钥来“加密”的,而公钥是公开的,谁都可以解密,内容也无法变更(篡改者无法得知私钥)。

总结

上文中我们详细的总结了jwt的优缺点以及适用场景,同时也简单的对比了传统session的登录方式,至于采用哪种方案,就需要看实际场景了,毕竟这两个方案并不是万能的。

如你已对上文内容了解,又缺少实践操作的话,代码已上传至GitHub。欢迎你的star

另:该实践采用的是使用 redis 记录独立的过期时间的方式实现了token续签的场景(建议有security基础观看)

https://github.com/wanglongsxr/springsecurity.git

Reference

JSON Web Token 入门教程

JSON Web Token - 在Web应用间安全地传递信息

深入理解JWT的使用场景和优劣

JWT 身份认证优缺点分析以及常见问题解决方案
JWT 也不是万能的呀,入坑需谨慎!

SpringSecurity实战-fulilnlin

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值