什么是 JWT?

论坛 期权论坛 金融     
yiski   2022-7-6 07:34   3519   5
什么是 JWT?
分享到 :
0 人收藏

5 个回复

倒序浏览
2#
s2niu  1级新秀 | 2022-7-6 07:35:13 发帖IP地址来自 云南
JWT 基本概念详解这篇文章中,我介绍了:

  • 什么是 JWT?
  • JWT 由哪些部分组成?
  • 如何基于 JWT 进行身份验证?
  • JWT 如何防止 Token 被篡改?
  • 如何加强 JWT 的安全性?
这篇文章,我们一起探讨一下 JWT 身份认证的优缺点以及常见问题的解决办法。
JWT 的优势

相比于 Session 认证的方式来说,使用 JWT 进行身份认证主要有下面 4 个优势。
无状态

JWT 自身包含了身份验证所需要的所有信息,因此,我们的服务器不需要存储 Session 信息。这显然增加了系统的可用性和伸缩性,大大减轻了服务端的压力。
不过,也正是由于 JWT 的无状态,也导致了它最大的缺点:不可控!
就比如说,我们想要在 JWT 有效期内废弃一个 JWT 或者更改它的权限的话,并不会立即生效,通常需要等到有效期过后才可以。再比如说,当用户 Logout 的话,JWT 也还有效。除非,我们在后端增加额外的处理逻辑比如将失效的 JWT 存储起来,后端先验证 JWT 是否有效再进行处理。具体的解决办法,我们会在后面的内容中详细介绍到,这里只是简单提一下。
有效避免了 CSRF 攻击

CSRF(Cross Site Request Forgery) 一般被翻译为 跨站请求伪造,属于网络攻击领域范围。相比于 SQL 脚本注入、XSS 等安全攻击方式,CSRF 的知名度并没有它们高。但是,它的确是我们开发系统时必须要考虑的安全隐患。就连业内技术标杆 Google 的产品 Gmail 也曾在 2007 年的时候爆出过 CSRF 漏洞,这给 Gmail 的用户造成了很大的损失。
那么究竟什么是跨站请求伪造呢? 简单来说就是用你的身份去做一些不好的事情(发送一些对你不友好的请求比如恶意转账)。
举个简单的例子:小壮登录了某网上银行,他来到了网上银行的帖子区,看到一个帖子下面有一个链接写着“科学理财,年盈利率过万”,小壮好奇的点开了这个链接,结果发现自己的账户少了 10000 元。这是这么回事呢?原来黑客在链接中藏了一个请求,这个请求直接利用小壮的身份给银行发送了一个转账请求,也就是通过你的 Cookie 向银行发出请求。
<a src="http://www.mybank.com/Transfer?bankId=11&money=10000">科学理财,年盈利率过万</a>
CSRF 攻击需要依赖 Cookie ,Session 认证中 Cookie 中的 SessionID 是由浏览器发送到服务端的,只要发出请求,Cookie 就会被携带。借助这个特性,即使黑客无法获取你的 SessionID,只要让你误点攻击链接,就可以达到攻击效果。
另外,并不是必须点击链接才可以达到攻击效果,很多时候,只要你打开了某个页面,CSRF 攻击就会发生。
<img src="http://www.mybank.com/Transfer?bankId=11&money=10000" />
那为什么 JWT 不会存在这种问题呢?
一般情况下我们使用 JWT 的话,在我们登录成功获得 JWT 之后,一般会选择存放在 localStorage 中。前端的每一个请求后续都会附带上这个 JWT,整个过程压根不会涉及到 Cookie。因此,即使你点击了非法链接发送了请求到服务端,这个非法请求也是不会携带 JWT 的,所以这个请求将是非法的。
总结来说就一句话:使用 JWT 进行身份验证不需要依赖 Cookie ,因此可以避免 CSRF 攻击。
不过,这样也会存在 XSS 攻击的风险。为了避免 XSS 攻击,你可以选择将 JWT 存储在标记为httpOnly 的 Cookie 中。但是,这样又导致了你必须自己提供 CSRF 保护,因此,实际项目中我们通常也不会这么做。
常见的避免 XSS 攻击的方式是过滤掉请求中存在 XSS 攻击风险的可疑字符串。
在 Spring 项目中,我们一般是通过创建 XSS 过滤器来实现的。
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class XSSFilter implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response,
      FilterChain chain) throws IOException, ServletException {
        XSSRequestWrapper wrappedRequest =
          new XSSRequestWrapper((HttpServletRequest) request);
        chain.doFilter(wrappedRequest, response);
    }

    // other methods
}
适合移动端应用

使用 Session 进行身份认证的话,需要保存一份信息在服务器端,而且这种方式会依赖到 Cookie(需要 Cookie 保存 SessionId),所以不适合移动端。
但是,使用 JWT 进行身份认证就不会存在这种问题,因为只要 JWT 可以被客户端存储就能够使用,而且 JWT 还可以跨语言使用。
单点登录友好

使用 Session 进行身份认证的话,实现单点登录,需要我们把用户的 Session 信息保存在一台电脑上,并且还会遇到常见的 Cookie 跨域的问题。但是,使用 JWT 进行认证的话, JWT 被保存在客户端,不会存在这些问题。
JWT 身份认证常见问题及解决办法

注销登录等场景下 JWT 还有效

与之类似的具体相关场景有:

  • 退出登录;
  • 修改密码;
  • 服务端修改了某个用户具有的权限或者角色;
  • 用户的帐户被封禁/删除;
  • 用户被服务端强制注销;
  • 用户被踢下线;
  • ......
这个问题不存在于 Session 认证方式中,因为在 Session 认证方式中,遇到这种情况的话服务端删除对应的 Session 记录即可。但是,使用 JWT 认证的方式就不好解决了。我们也说过了,JWT 一旦派发出去,如果后端不增加其他逻辑的话,它在失效之前都是有效的。
那我们如何解决这个问题呢?查阅了很多资料,我简单总结了下面 4 种方案:
1、将 JWT 存入内存数据库
将 JWT 存入 DB 中,Redis 内存数据库在这里是不错的选择。如果需要让某个 JWT 失效就直接从 Redis 中删除这个 JWT 即可。但是,这样会导致每次使用 JWT 发送请求都要先从 DB 中查询 JWT 是否存在的步骤,而且违背了 JWT 的无状态原则。
2、黑名单机制
和上面的方式类似,使用内存数据库比如 Redis 维护一个黑名单,如果想让某个 JWT 失效的话就直接将这个 JWT 加入到 黑名单 即可。然后,每次使用 JWT 进行请求的话都会先判断这个 JWT 是否存在于黑名单中。
前两种方案的核心在于将有效的 JWT 存储起来或者将指定的 JWT 拉入黑名单。
虽然这两种方案都违背了 JWT 的无状态原则,但是一般实际项目中我们通常还是会使用这两种方案。
3、修改密钥 (Secret) :
我们为每个用户都创建一个专属密钥,如果我们想让某个 JWT 失效,我们直接修改对应用户的密钥即可。但是,这样相比于前两种引入内存数据库带来了危害更大:

  • 如果服务是分布式的,则每次发出新的 JWT 时都必须在多台机器同步密钥。为此,你需要将密钥存储在数据库或其他外部服务中,这样和 Session 认证就没太大区别了。
  • 如果用户同时在两个浏览器打开系统,或者在手机端也打开了系统,如果它从一个地方将账号退出,那么其他地方都要重新进行登录,这是不可取的。
4、保持令牌的有效期限短并经常轮换
很简单的一种方式。但是,会导致用户登录状态不会被持久记录,而且需要用户经常登录。
另外,对于修改密码后 JWT 还有效问题的解决还是比较容易的。说一种我觉得比较好的方式:使用用户的密码的哈希值对 JWT 进行签名。因此,如果密码更改,则任何先前的令牌将自动无法验证。
JWT 的续签问题

JWT 有效期一般都建议设置的不太长,那么 JWT 过期后如何认证,如何实现动态刷新 JWT,避免用户经常需要重新登录?
我们先来看看在 Session 认证中一般的做法:假如 Session 的有效期 30 分钟,如果 30 分钟内用户有访问,就把 Session 有效期延长 30 分钟。
JWT 认证的话,我们应该如何解决续签问题呢?查阅了很多资料,我简单总结了下面 4 种方案:
1、类似于 Session 认证中的做法
这种方案满足于大部分场景。假设服务端给的 JWT 有效期设置为 30 分钟,服务端每次进行校验时,如果发现 JWT 的有效期马上快过期了,服务端就重新生成 JWT 给客户端。客户端每次请求都检查新旧 JWT,如果不一致,则更新本地的 JWT。这种做法的问题是仅仅在快过期的时候请求才会更新 JWT ,对客户端不是很友好。
2、每次请求都返回新 JWT
这种方案的的思路很简单,但是,开销会比较大,尤其是在服务端要存储维护 JWT 的情况下。
3、JWT 有效期设置到半夜
这种方案是一种折衷的方案,保证了大部分用户白天可以正常登录,适用于对安全性要求不高的系统。
4、用户登录返回两个 JWT
第一个是 accessJWT ,它的过期时间 JWT 本身的过期时间比如半个小时,另外一个是 refreshJWT 它的过期时间更长一点比如为 1 天。客户端登录后,将 accessJWT 和 refreshJWT 保存在本地,每次访问将 accessJWT 传给服务端。服务端校验 accessJWT 的有效性,如果过期的话,就将 refreshJWT 传给服务端。如果有效,服务端就生成新的 accessJWT 给客户端。否则,客户端就重新登录即可。
这种方案的不足是:

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

JWT 其中一个很重要的优势是无状态,但实际上,我们想要在实际项目中合理使用 JWT 的话,也还是需要保存 JWT 信息。
JWT 也不是银弹,也有很多缺陷,具体是选择 JWT 还是 Session 方案还是要看项目的具体需求。万万不可尬吹 JWT,而看不起其他身份认证方案。
另外,不用 JWT 直接使用普通的 Token(随机生成,不包含具体的信息) 结合 Redis 来做身份认证也是可以的。我在 「优质开源项目推荐」第 8 期推荐过的 Sa-Token 这个项目是一个比较完善的 基于 JWT 的身份认证解决方案,支持自动续签、踢人下线、账号封禁、同端互斥登录等功能,感兴趣的朋友可以看看。


参考


  • JWT 超详细分析:https://learnku.com/articles/17883
  • How to log out when using JWT:https://medium.com/devgorilla/how-to-log-out-when-using-jwt-a8c7823e8a6
  • CSRF protection with JSON Web JWTs:https://medium.com/@agungsantoso/csrf-protection-with-json-web-JWTs-83e0f2fcbcc
  • Invalidating JSON Web JWTs:https://stackoverflow.com/questions/21978658/invalidating-json-web-JWTs
后记

自荐一个非常不错的 Java 教程类开源项目:JavaGuide 。这是我在大三开始准备秋招面试的时候创建的,已经维护了 5 年多了,目前这个项目在 Github 上收到了 120k+ 的 star。



并且,这个项目还推出了一个PDF版本:完结撒花!JavaGuide面试突击版来啦!

《Java 面试进阶指北》是一份教你如何更高效地准备面试的小册,涵盖常见八股文(系统设计、常见框架、分布式、高并发 ......)、优质面经等内容。



原创不易,欢迎点赞分享,欢迎关注 @JavaGuide,我会持续分享原创干货~
3#
1jj70  1级新秀 | 2022-7-6 07:36:02 发帖IP地址来自 广东阳江
此前学习JWT时从油管中找到的视频,应该是相关视频中解析的最清楚的,不过是纯英文。视频通过比较传统的session + cookie方式,弄清楚JWT是怎么回事,以及和session方式比较的优缺点。
4#
MH08  2级吧友 | 2022-7-6 07:36:26 发帖IP地址来自 北京西城
怎么觉得治标不治本。
不用redis、不用db保存session,实际上是分散保存在各个客户端浏览器的cookie。
到期了硬重新登录。
要做改进,就又涉及集中保存,或复杂的广播、更新机制。
5#
2ac6k  1级新秀 | 2022-7-6 07:36:43 发帖IP地址来自 北京
JWT是什么?

JSON Web Token (or JWT)只是一个包含某种意义数据的JSON串。它最重要的特性就是,为了确认它是否有效,我们只需要看JWT本身的内容,而不需要借助于第三方服务或者在多个请求之间将其保存在内存中-这是因为它本身携带了信息验证码MAC(Message Authentication Code)。
一个JWT包含3个部分:头部Header,数据Payload,签名Signature。让我们逐个来了解一下,先从Payload开始吧。
JWT Payload看起来是怎样的呢?

Payload只是一个普通的Javascript 对象。对于payload的内容,JWT是没有任何限制的,但必须注意的是,JWT是没有加密的。因此,任何放在token里面的信息,如果被截获了,对任何人别人是可读的。因此,我们不应该在Payload里面存放任何黑客可以利用的用户信息。
JWT Header – 为什么是必须的?

Payload的内容在接收者端是通过签名(Signature)来校验的。不过存在多种类型的签名,因此,接收者需要知道使用的是哪种类型的签名。
这种关于token本身的元数据信息存放在另外的Javascript对象里面,并随着Payload一起发送给客户。这个独立的对象就是一个JSON对象,叫JWT Header,它也是普通的Javascript对象,在这里面我们可以看到签名类型信息,比如RS256。
JWT signatures – 如何被使用来完成认证的?

JWT的最后一部分是签名,它也叫信息验证码MAC。签名只能由拥有Payload、Header和密钥的角色生成。
那签名是如何完成认证功能的呢,且看:

  • 用户向认证服务器提交用户名和密码,认证服务器也可以和应用服务器部署在一起,但往往是独立的居多;
  • 认证服务器校验用户名和密码组合,然后创建一个JWT token,token的Payload里面包含用户的身份信息,以及过期时间戳;
  • 认证服务器使用密钥对Header和Payload进行签名,然后发送给客户浏览器;
  • 浏览器获取到经过签名的JWT token,然后在之后的每个HTTP请求中附带着发送给应用服务器。经过签名的JWT就像一个临时的用户凭证,代替了用户名和密码组合,之后都是JWT token和应用服务器打交道了;
  • 应用服务器检查JWT签名,确认Payload确实是由密钥拥有者签过名的;
  • Payload身份信息代表了某个用户;
  • 只有认证服务器拥有私钥,并且认证服务器只把token发给提供了正确密码的用户;
  • 因此应用服务器可以认为这个token是由认证服务器颁发的也是安全的,因为该用户具有了正确的密码;
  • 应用服务器继续完成HTTP请求,并认为这些请求确实属于这个用户;
这样的话,黑客假扮合法用户的办法要么是盗到了用户名和密码组合,要么盗到了认证服务器上的签名私钥。
签名的确是JWT的关键部分!签名使得无状态的服务器只需要通过查看HTTP请求中的JWT token就能保证HTTP请求是来自某个用户,而不需要每次请求时都发送密码。
JWT的目标是让服务器无状态?

实际上,JWT真正的好处是让认证服务器和校验JWT token的应用服务器可以完全分开,而让服务器无状态化只是它的一个副作用罢了。这意味着应用服务器只需要最简单的认证逻辑-校验JWT!我们可以将整个应用集群的登录/注册委托给一个单独的认证服务器。这也意味着应用服务器更简单更安全,因为更多的认证功能集中部署在认证服务器,可以被跨应用使用。
JSON Web Token看起来是怎样的呢?

我们可以看到,这个JWT包含3部分,是由“.”号分开的

  • JWT Header:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

  • JWT Payload:
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9

  • JWT Signature:
TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQBase64 vs Base64Url

但是我们在JWT看到的并不是Base64,实际上是Base64Url,它和Base64类似,但有一些字符不一样,因此我们可以将JWT作为URL的参数在请求行中进行传递。
那个“=”在URL栏中会显示为“%3D”,会显得混乱,这也解释了在我们把JWT拼接到URL发送时,需要Base64Url的原因。我们看一下Payload部分:
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9我们使用在线解码器来解析它,就得到了一个JSON对象,因此,我们可以得到这样的结论:JWT Header和Payload的内容是普通的javascript对象,转换成JSON并进行Base64Url编码,以“.”号隔开。
基于JWT的用户会话管理: 主题和期限

之前有提到,JWT的Payload理论上可以存放任何内容,不一定是用户身份信息,只不过使用JWT作为认证是最常用的方式。Payload还有一些特定的属性来支持:

  • 用户身份
  • 会话过期
这里是Payload的几个最常用的标准属性:
· iss 代表生成token的实体,一般就是认证服务器
· iat 创建JWT的时间戳(in seconds since Epoch)
· sub 包含用户的身份信息
· exp token的过期时间戳
我们把这叫做Bearer Token,意思是:应用服务器确认这个token的持有者是具有由sub属性表示的ID的用户,因此可以放行
签名Signature

对于JWT,签名方式有很多种,这里我们主要了解HS256和RS256
HS256 JWT数字签名 – 它是如何工作的?

和很多签名方法一样,HS256 数字签名基于一种特殊的函数:加密哈希函数。
什么是哈希函数(Hashing function)?

哈希函数是一种特殊的函数:它在数字签名中有很多实际的使用案例。现在我们将谈论它四个有趣的属性,然后看看这些属性如何使得我们可以生成可校验的签名。这里我们将使用的哈希函数是:SHA-256。

  • 哈希函数属性 1 – 不可逆性
这就意味着我们把Header和Payload作用于这个函数后,没有人可以从函数输出的信息中取回Header和Payload的原始值。
使用在线的哈希计算器,我们可以看到SHA-256的一个输出值如下:
3f306b76e92c8a8fbae88a3ef1c0f9b0a81fe3a953fa9320d5d0281b059887c3同时,哈希并不是加密,加密在定义中是可逆的,我们总是需要从加密后的信息中得到原始信息。

  • 哈希函数属性 2 – 可重复生成
另外一个需要知道的是,哈希函数是可重复生成信息的,也就是如果我们输入同样的Header和Payload信息,每次得到的结果是完全一样的。这就意味着,给定输入组合和哈希输出值,我们总是可以校验该输出值(比如签名signature)的正确性,因为我们可以重新计算(我们有输入值的情况下)。

  • 哈希函数属性 3 – 没有冲突
还有一个属性是,如果我们提供不同的输入值,总是得到不同的唯一的输出值。这就意味着我们将哈希函数作用于某个Payload和Header之后,总是得到相同的结果,其它输入值组合不会得到和这一样的结果,因此,哈希函数的不同输出值就代表了输入值的不同。

  • 哈希函数属性 4 – 不可预测性
哈希函数的最后一个属性就是不可预测性,给定一个输出值,无法通过各种手段猜测到输入值。假设我们尝试从上面的输出值中找到生成它的Payload,我们只能猜测输入值然后对比输出值看看是否匹配。
哈希函数是怎样完成数字签名的呢?黑客是否可以拿着Header和Payload,而不管Signature呢?任何人都可以使用SHA-256哈希函数生成一个输出,然后附加到JWT的signature部分,对吧?
怎样使用哈希函数生成签名?

这是正确的,任何人都可以使用哈希函数,然后输入Header和Payload来生成结果。但HS256签名不止这样,我们拿到Header、Payload外,还要加上一个密码,将这三个输入值一起哈希。输出结果是一个SHA-256 HMAC或者基于哈希的MAC。如果需要重复生成,则需要同时拥有Header、Payload和密码才可以。这也意味着,哈希函数的输出结果是一个数字签名,因为输出结果就表示了Payload是由拥有密码的角色生成并加签了的,没有其它方式可以生成这样的输出值了。
将哈希结果附加到消息上,是为了让接收者可以验证。哈希结果叫HMAC:Hash-Based Message Authentication Code,是数字签名的一种形式。这就是我们在JWT中所做的,JWT的第三部分是由Header、Payload通过SHA-256函数生成,并使用Base64Url进行编码。
如何校验JWT签名?

当我们的服务接收到HS256签名的JWT时,我们需要使用同样的密码才能校验并确认token里面的Payload是否有效。为了验证签名,我们只需要将JWT Header和Payload以及密码通过哈希函数生成结果。如果是使用HS256函数,JWT的接收者需要拿到和发送者一样的密码值。如果我们得到的哈希结果和JWT第三部分的签名值是一致的,则说明有效,可以确认发送者确实和接收者拥有相同的密码值。
而数字签名和HMAC又是如何工作的呢?
为什么需要其它的签名类型呢?

以上解释了JWT签名是如何应用于认证的,HS256只是一种具体的签名类型。其它的签名类型中,最常用的是:RS256。
有什么区别呢?我们介绍HS256只是为了更容易理解MAC码的概念, 你可能也会发现它在一些生产环境的应用中被使用。但是一般来说,使用RS256签名方式会更好,下一节我们将看到,RS256相对于HS256来说有诸多优势。
HS256签名方式的劣势

如果输入的密码相对弱的话,HS256可能会被暴力破解,基于密钥的技术都有这个问题。更甚的是,HS256要求JWT的生产者和消费者都预先拥有相同的密码。

  • 不切实际的密码分发
这意味着我们在修改密码后,需要把它分发并安装到所有需要它的网络节点。这不仅不方便,而且容易出错,还涉及到服务器间的协调和暂停服务问题。如果服务器是由另外的团队维护,比如第三方组织,这种方式就更不可行了。

  • Token的创建和校验没有分离
创建和校验JWT的能力没有区分开,使用HS256时,网络的任何人都可以创建和校验token,因为他们都有密码。这就意味着密码可能会从更多的地方丢失或者受攻击,因为密码到处分发,而并不是每个应用都具有一样的安全保护机制。
弥补这问题的一个方法是,创建一个共享的密码给每一种类型的应用。不过,我们马上要学习新的签名方式,这个签名方式解决了以上所有的问题,并且目前所有基于JWT的方案都默认使用的,那就是RS256。
RS256 JWT签名

使用RS256我们同样需要生成一个MAC,其目的仍然是创建一个数字签名来证明一个JWT的有效性。只是在这种签名方式中就,我们将创建token和校验token的能力分开,只有认证服务器具备创建的能力,而应用服务器,具备校验的能力。
这样,我们需要创建两个密钥而不是一个:

  • 仍然需要一个私钥,不过这次它只能被认证服务器拥有,只用来签名JWT。
  • 私钥只能用来签名JWT,不能用来校验它。
  • 第二个密钥叫做公钥(public key),是应用服务器使用来校验JWT。
  • 公钥可以用来校验JWT,但不能用来给JWT签名。
  • 公钥一般不需要严密保管,因为即便黑客拿到了,也无法使用它来伪造签名。
RSA加密技术介绍

RS256使用一种特殊的密钥,叫RSA密钥。RSA是一种加解密密钥,使用一个密钥进行加密,然后用另外一个密钥解密
来看一下RSA公钥是怎样的:
—–BEGIN PUBLIC KEY—–
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDdlatRjRjogo3WojgGHFHYLugdUWAY9iR3fy4arWNA1KoS8kVw33cJibXr8bvwUAUparCwlvdbH6dvEOfou0/gCFQsHUfQrSDv+MuSUMAe8jzKE4qW+jK+xQU9a03GUnKHkkle+Q0pX/g6jXZ7r1/xAK5Do2kQ+X5xK9cipRgEKwIDAQAB—–END PUBLIC KEY—–
这个公钥是公开发布的,因此黑客根本不需要猜测,他本来就可以拥有它。
但这里还有一个RSA私钥:
—–BEGIN RSA PRIVATE KEY—–
MIICWwIBAAKBgQDdlatRjRjogo3WojgGHFHYLugdUWAY9iR3fy4arWNA1KoS8kVw33cJibXr8bvwUAUparCwlvdbH6dvEOfou0/gCFQsHUfQrSDv+MuSUMAe8jzKE4qW+jK+xQU9a03GUnKHkkle+Q0pX/g6jXZ7r1/xAK5Do2kQ+X5xK9cipRgEKwIDAQABAoGAD+onAtVye4ic7VR7V50DF9bOnwRwNXrARcDhq9LWNRrRGElESYYTQ6EbatXS3MCyjjX2eMhu/aF5YhXBwkppwxg+EOmXeh+MzL7Zh284OuPbkglAaGhV9bb6/5CpuGb1esyPbYW+Ty2PC0GSZfIXkXs76jXAu9TOBvD0ybc2YlkCQQDywg2R/7t3Q2OE2+yo382CLJdrlSLVROWKwb4tb2PjhY4XAwV8d1vy0RenxTB+K5Mu57uVSTHtrMK0GAtFr833AkEA6avx20OHo61Yela/4k5kQDtjEf1N0LfI+BcWZtxsS3jDM3i1Hp0KSu5rsCPb8acJo5RO26gGVrfAsDcIXKC+bQJAZZ2XIpsitLyPpuiMOvBbzPavd4gY6Z8KWrfYzJoI/Q9FuBo6rKwl4BFoToD7WIUS+hpkagwWiz+6zLoX1dbOZwJACmH5fSSjAkLRi54PKJ8TFUeOP15h9sQzydI8zJU+upvDEKZsZc/UhT/SySDOxQ4G/523Y0sz/OZtSWcol/UMgQJALesy++GdvoIDLfJX5GBQpuFgFenRiRDabxrE9MNUZ2aPFaFp+DyAe+b4nDwuJaW2LURbr8AEZga7oQj0uYxcYw==—–END RSA PRIVATE KEY—–
黑客没有任何办法猜测私钥。而且,这两个密钥是相关的,一个密钥加密的内容只能由另外的密钥来解密。那我们又如何用它们生成签名呢?
为什么不用RSA加密Payload就完了?

现在尝试着使用RSA来生成一个数字签名:
我们使用Header和Payload,然后使用私钥对其进行RSA加密,最后返回JWT。
接收者拿到JWT后,使用公钥解密,然后检查解密后的值。如果解密过程顺利并且其输出是一个JSON值,往往就意味着该JWT就是认证服务器创建并加密了的。
相比哈希函数,RSA加密过程比较慢。对于数据比较大的Payload来说,可能会是个问题。
那HS256签名方式在实际中又是如何使用RSA的呢?
接收者是怎样检查RS256签名的?

接收者将:

  • 取出Header和Payload,然后使用SHA-256进行哈希。
  • 使用公钥解密数字签名,得到签名的哈希值。
  • 接收者将解密签名得到的哈希值和刚使用Header和Payload参与计算的哈希值进行比较。如果两个哈希值相等,则证明JWT确实是由认证服务器创建的。
任何人都可以计算哈希值,但只有认证服务器可以使用RSA私钥对其进行加密。
如何进行密钥分发部署

还记得之前我们说过,用来校验token的公钥可以随意分发,黑客无法使用它来做任何有意义的事情。然而黑客并不是想校验token,他们只是想伪造它们。这就使得我们将公钥放置到受我们自己控制的服务器上成为可能。应用服务器连接到公钥放置的服务器获取公钥,然后定期检查公钥是否有变化。因此,在更新密钥时,应用服务器和认证服务器不需要同时暂停服务。那公钥又如何分发呢?下面是一种可行的格式。

  • JSON Web Key Set Endpoints
有多种发布公钥的格式,但这里有一种较为熟悉:JWKS,全称Json Web Key Set。
如果你好奇这些endpoints 看起来是怎样的,可以看一下这个线上例子live example,下面这个是我们从HTT GET请求得到的回复:
Kid是密钥身份, x5c是某种公钥的表示法。这种格式的优点是其标准化,我们只需要知道endpoint的URL,和一个可以解析JWKS的库,就可以使用公钥来校验JWT了,而不需要安装到自己的服务器。
JWT常常使用在公共网站上,以及社交产品的登录方案中。对于内部系统,它是怎么被使用的呢?
JWT在企业中的应用

JWT同样适用于企业内部,替代经典的存在已知安全隐患的预身份验证设置(Pre-Authentication setup)方式。
预身份验证设置方式中,我们的应用服务器在私有网络的一个代理后面运行,然后从HTTP请求头中获取当前用户信息。代表用户身份的HTTP请求头通常由中心化的登录页面填充,同时中心化的节点也对用户session进行管理(以前是把登录的用户信息存放在session中)。
当session过期后,服务器将阻止对应用的访问,并要求用户重新登录认证。之后,它将所有请求转发到应用服务器并在HTTP请求头添加代表用户身份的信息
传统的session跨域失效问题

理解的cookie与session的交互流程,我们就明白了session失效的原因,比如客户端访问A服务器的时候,生成的jsessionid的之为11111,但是当浏览器去调用B服务器的资源时,会携带这个jsessionid过去,发现B服务器上没有与之对应的session,这时B服务器又会生成一个新的session,并通过set-cookie方法把与该新的session对应的jsessionid(如222)设置到cookie中(自己的后端代码逻辑),这时候浏览器的jsessionid就变为了222,当浏览器再访问A服务器时,发现与222对应的没有session,这时候A服务器又会重新生成新的session
session跨域的解决方案

1、session 复制 每一台服务器上都保持一份相同的session (造成额外的存储开销和网络开销)
2、session 集中存储 :存储在db、 存储在缓存服务器 (redis)
问题是这种设置方式,内网上的任何人都可以假扮成某个用户,只要设置同样的HTTP请求头
对此也有一些解决方案,比如白名单列表,或者某种客户凭证。

  • 更好的预身份验证设置方式
预身份验证设置方式是一个好主意,毕竟这种方式可以使得应用开发者不需要实现认证逻辑,减少开发时间和潜在的安全问题。如果能有预身份验证设置方式的便捷,又没有安全方面的妥协,岂不美哉?如果我们考虑到JWT,则可以轻松做到。我们不像以往那样将用户名放到HTTP请求头,而是将HTTP请求头封装成一个JWT。我们将用户名放到Payload里面,再由认证服务器加签。
应用服务器不再从HTTP请求头获取用户名,而是首先校验JWT:

  • 如果签名是正确的,则用户认证通过,请求可以放行;
  • 否则,应用服务器简单的拒绝请求就好了;
这样的结果就是,即使在私有网络内,我们的认证功能也可以正常工作。我们再也不需要通过HTTP请求头来识别用户了,我们保证了HTTP请求头的有效性并且是由代理生成的,而不是某黑客试图以某个用户身份登录。

  • 总结
通过本文,我们对JWT是什么有了一个全面的了解,以及它是如何在认证中被使用的。JWT只是一个简单的JSON对象,并且易于验证、难于伪造。
此外,JWT并不是一定要用来做认证的,我们可以使用JWT在网络上发送各种数据。
另外一个和安全相关的使用JWT的情况是授权:我们可以在Payload里面放置用户的角色列表,比如只读用户、管理员等等,对用户在应用服务器上的行为进行限制。
6#
optiver  10级大牛  options' trader | 2022-7-6 07:37:07 发帖IP地址来自 北京
文章已收录到我的Github精选,欢迎Star:https://github.com/yehongzhi/learningSummary
起源

需要了解一门技术,首先从为什么产生开始说起是最好的。JWT主要用于用户登录鉴权,所以我们从最传统的session认证开始说起。
session认证

众所周知,http协议本身是无状态的协议,那就意味着当有用户向系统使用账户名称和密码进行用户认证之后,下一次请求还要再一次用户认证才行。因为我们不能通过http协议知道是哪个用户发出的请求,所以如果要知道是哪个用户发出的请求,那就需要在服务器保存一份用户信息(保存至session),然后在认证成功后返回cookie值传递给浏览器,那么用户在下一次请求时就可以带上cookie值,服务器就可以识别是哪个用户发送的请求,是否已认证,是否登录过期等等。这就是传统的session认证方式。
session认证的缺点其实很明显,由于session是保存在服务器里,所以如果分布式部署应用的话,会出现session不能共享的问题,很难扩展。于是乎为了解决session共享的问题,又引入了redis,接着往下看。
token认证

这种方式跟session的方式流程差不多,不同的地方在于保存的是一个token值到redis,token一般是一串随机的字符(比如UUID),value一般是用户ID,并且设置一个过期时间。每次请求服务的时候带上token在请求头,后端接收到token则根据token查一下redis是否存在,如果存在则表示用户已认证,如果token不存在则跳到登录界面让用户重新登录,登录成功后返回一个token值给客户端。
优点是多台服务器都是使用redis来存取token,不存在不共享的问题,所以容易扩展。缺点是每次请求都需要查一下redis,会造成redis的压力,还有增加了请求的耗时,每个已登录的用户都要保存一个token在redis,也会消耗redis的存储空间。
有没有更好的方式呢?接着往下看。
什么是JWT

JWT(全称:Json Web Token)是一个开放标准(RFC 7519),它定义了一种紧凑的、自包含的方式,用于作为JSON对象在各方之间安全地传输信息。该信息可以被验证和信任,因为它是数字签名的。
上面说法比较文绉绉,简单点说就是一种认证机制,让后台知道该请求是来自于受信的客户端。
首先我们先看一个流程图:


流程描述一下:

  • 用户使用账号、密码登录应用,登录的请求发送到Authentication Server。
  • Authentication Server进行用户验证,然后创建JWT字符串返回给客户端。
  • 客户端请求接口时,在请求头带上JWT。
  • Application Server验证JWT合法性,如果合法则继续调用应用接口返回结果。
可以看出与token方式有一些不同的地方,就是不需要依赖redis,用户信息存储在客户端。所以关键在于生成JWT,和解析JWT这两个地方。
JWT的数据结构

JWT一般是这样一个字符串,分为三个部分,以"."隔开:
xxxxx.yyyyy.zzzzz


Header

JWT第一部分是头部分,它是一个描述JWT元数据的Json对象,通常如下所示。
{
    "alg": "HS256",
    "typ": "JWT"
}
alg属性表示签名使用的算法,默认为HMAC SHA256(写为HS256),typ属性表示令牌的类型,JWT令牌统一写为JWT。
最后,使用Base64 URL算法将上述JSON对象转换为字符串保存。
Payload

JWT第二部分是Payload,也是一个Json对象,除了包含需要传递的数据,还有七个默认的字段供选择。
分别是,iss:发行人、exp:到期时间、sub:主题、aud:用户、nbf:在此之前不可用、iat:发布时间、jti:JWT ID用于标识该JWT。
如果自定义字段,可以这样定义:
{
    //默认字段
    "sub":"主题123",
    //自定义字段
    "name":"java技术爱好者",
    "isAdmin":"true",
    "loginTime":"2021-12-05 12:00:03"
}
需要注意的是,默认情况下JWT是未加密的,任何人都可以解读其内容,因此如果一些敏感信息不要存放在此,以防信息泄露。
JSON对象也使用Base64 URL算法转换为字符串保存。
Signature

JWT第三部分是签名。是这样生成的,首先需要指定一个secret,该secret仅仅保存在服务器中,保证不能让其他用户知道。然后使用Header指定的算法对Header和Payload进行计算,然后就得出一个签名哈希。也就是Signature。
那么Application Server如何进行验证呢?可以利用JWT前两段,用同一套哈希算法和同一个secret计算一个签名值,然后把计算出来的签名值和收到的JWT第三段比较,如果相同则认证通过。
JWT的优点


  • json格式的通用性,所以JWT可以跨语言支持,比如Java、JavaScript、PHP、Node等等。
  • 可以利用Payload存储一些非敏感的信息。
  • 便于传输,JWT结构简单,字节占用小。
  • 不需要在服务端保存会话信息,易于应用的扩展。
怎么使用JWT

首先引入Maven依赖。
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>
创建工具类,用于创建jwt字符串和解析jwt。
@Component
public class JwtUtil {

    @Value("${jwt.secretKey}")
    private String secretKey;

    public String createJWT(String id, String subject, long ttlMillis, Map<String, Object> map) throws Exception {
        JwtBuilder builder = Jwts.builder()
                .setSubject(null) // 发行者
                .setId(id)
                .setSubject(subject)
                .setIssuedAt(new Date()) // 发行时间
                .signWith(SignatureAlgorithm.HS256, secretKey) // 签名类型 与 密钥
                .compressWith(CompressionCodecs.DEFLATE);// 对载荷进行压缩
        if (!CollectionUtils.isEmpty(map)) {
            builder.setClaims(map);
        }
        if (ttlMillis > 0) {
            builder.setExpiration(new Date(System.currentTimeMillis() + ttlMillis));
        }
        return builder.compact();
    }


    public Claims parseJWT(String jwtString) {
        return Jwts.parser().setSigningKey(secretKey)
                .parseClaimsJws(jwtString)
                .getBody();
    }
}
接着在application.yml配置文件配置jwt.secretKey。
## 用户生成jwt字符串的secretKey
jwt:
  secretKey: ak47
接着创建一个响应体。
public class BaseResponse {

    private String code;

    private String msg;

    public static BaseResponse success() {
        return new BaseResponse("0", "成功");
    }

    public static BaseResponse fail() {
        return new BaseResponse("1", "失败");
    }
    //构造器、getter、setter方法
}

public class JwtResponse extends BaseResponse {

    private String jwtData;

    public static JwtResponse success(String jwtData) {
        BaseResponse success = BaseResponse.success();
        return new JwtResponse(success.getCode(), success.getMsg(), jwtData);
    }

    public static JwtResponse fail(String jwtData) {
        BaseResponse fail = BaseResponse.fail();
        return new JwtResponse(fail.getCode(), fail.getMsg(), jwtData);
    }
    //构造器、getter、setter方法
}
接着创建一个UserController:
@RestController
@RequestMapping("/user")
public class UserController {

    @Resource
    private UserService userService;

    @RequestMapping(value = "/login", method = RequestMethod.POST)
    public JwtResponse login(@RequestParam(name = "userName") String userName,
                             @RequestParam(name = "passWord") String passWord){
        String jwt = "";
        try {
            jwt = userService.login(userName, passWord);
            return JwtResponse.success(jwt);
        } catch (Exception e) {
            e.printStackTrace();
            return JwtResponse.fail(jwt);
        }
    }
}
还有UserService:
@Service
public class UserServiceImpl implements UserService {

    @Resource
    private JwtUtil jwtUtil;

    @Resource
    private UserMapper userMapper;

    @Override
    public String login(String userName, String passWord) throws Exception {
        //登录验证
        User user = userMapper.findByUserNameAndPassword(userName, passWord);
        if (user == null) {
            return null;
        }
        //如果能查出,则表示账号密码正确,生成jwt返回
        String uuid = UUID.randomUUID().toString().replace("-", "");
        HashMap<String, Object> map = new HashMap<>();
        map.put("name", user.getName());
        map.put("age", user.getAge());
        return jwtUtil.createJWT(uuid, "login subject", 0L, map);
    }
}
还有UserMapper.xml:
@Mapper
public interface UserMapper {
    User findByUserNameAndPassword(@Param("userName") String userName, @Param("passWord") String passWord);

}

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="io.github.yehongzhi.jwtdemo.mapper.UserMapper">
    <select id="findByUserNameAndPassword" resultType="io.github.yehongzhi.jwtdemo.model.User">
        select * from user where user_name = #{userName} and pass_word = #{passWord}
    </select>
</mapper>
user表结构如下:


启动项目,然后用POSTMAN请求login接口。


返回的jwt字符串如下:
eyJhbGciOiJIUzI1NiIsInppcCI6IkRFRiJ9.eNqqVspLzE1VslJ6OnHFsxnzX67coKSjlJgOFDEzqAUAAAD__w.qib2DrjRKcFnY77Cuh_b1zSzXfISOpCA-g8PlAZCWoU
接着我们写一个接口接收这个jwt,并做验证。
@RestController
@RequestMapping("/jwt")
public class TestController {

    @Resource
    private JwtUtil jwtUtil;

    @RequestMapping("/test")
    public Map<String, Object> test(@RequestParam("jwt") String jwt) {
        //这个步骤可以使用自定义注解+AOP编程做解析jwt的逻辑,这里为了简便就直接写在controller里
        Claims claims = jwtUtil.parseJWT(jwt);
        String name = claims.get("name", String.class);
        String age = claims.get("age", String.class);
        HashMap<String, Object> map = new HashMap<>();
        map.put("name", name);
        map.put("age", age);
        map.put("code", "0");
        map.put("msg", "请求成功");
        return map;
    }
}


像这样能正常解析成功的话,就表示该用户登录未过期,并且已认证成功,所以可以正常调用服务。那么有人会问了,这个jwt字符串能不能被伪造呢?
除非你知道secretKey,否则是不能伪造的。比如客户端随便猜一个secretKey的值,然后伪造一个jwt:
eyJhbGciOiJIUzI1NiIsInppcCI6IkRFRiJ9.eNqqVspLzE1VslJ6OnHFsxnzX67coKSjlJgOFDEzqAUAAAD__w.bHr9p3-t2qR4R50vifRVyaYYImm2viZqiTlDdZHmF5Y
然后传进去解析,会报以下错误:


还记得原理吧,是根据前面两部分(Header、Payload)加上secretKey使用Header指定的哈希算法计算出第三部分(Signature),所以可以看出最关键就是secretKey。secretKey只有服务端自己知道,所以客户端不知道secretKey的值是伪造不了jwt字符串的。
总结

最后讲讲JWT的缺点,任何技术都不是完美的,所以我们得用辩证思维去看待任何一项技术。

  • 安全性没法保证,所以jwt里不能存储敏感数据。因为jwt的payload并没有加密,只是用Base64编码而已。
  • 无法中途废弃。因为一旦签发了一个jwt,在到期之前始终都是有效的,如果用户信息发生更新了,只能等旧的jwt过期后重新签发新的jwt。
  • 续签问题。当签发的jwt保存在客户端,客户端一直在操作页面,按道理应该一直为客户端续长有效时间,否则当jwt有效期到了就会导致用户需要重新登录。那么怎么为jwt续签呢?最简单粗暴就是每次签发新的jwt,但是由于过于暴力,会影响性能。如果要优雅一点,又要引入Redis解决,但是这又把无状态的jwt硬生生变成了有状态的,违背了初衷。
所以印证了那句话,没有最好的技术,只有适合的技术。感谢大家的阅读,希望看完之后能对你有所收获。
觉得有用就点个赞吧,你的点赞是我创作的最大动力~
我是一个努力让大家记住的程序员。我们下期再见!!!
能力有限,如果有什么错误或者不当之处,请大家批评指正,一起学习交流!
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

积分:
帖子:
精华:
期权论坛 期权论坛
发布
内容

下载期权论坛手机APP