参考文献
[2] JWT令牌-CSDN博客 参考日期:2024.05.21
[1] Day12-06. 登录校验-JWT令牌-介绍哔哩哔哩bilibili 参考日期:2024.05.21
JWT 是何
概念
JWT 是 JSON Web Token 的缩写,即 JSON Web 令牌,是一种用于在网络应用程序之间 以 JSON 数据格式安全传输信息 的开放标准(RFC 7519)
JWT 是一种 轻量级 的 Token,定义了一种简洁的、自包含的格式,包含了一些关键信息,比如用户身份、权限等
由于 数字签名 的存在,这些信息是可靠的,在传输过程中 不被篡改或伪造
使用 JWT 的好处:可以方便地在不同的应用程序或服务之间共享用户身份信息,而无需每次都进行身份验证。且因为 JWT 是基于标准的 JSON 格式,易于使用和传输
构成
JWT 的格式是由 RFC 7519 标准规定的三部分组成:
-
头部(Header):包含 Token 类型、加密算法等信息
-
载荷(Payload):包含 了需要传输的信息,分为 自定义信息 和 默认信息,例如用户 ID、角色、权限等
可能称为 “携带” 更能体现 Payload 的字面意义
-
签名(Signature):将 Header 和 Payload 加密 后得到的结果,用于验证 Token 是否被篡改,确保安全性
加入指定秘钥,通过指定签名算法计算
假设一个网站需要验证用户身份并授权用户访问某些受保护的资源,使用 JWT,用户登录后,服务器会生成一个包含用户信息的 JWT,然后将该 Token 返回给客户端
如下所示:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9. eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWQiOiIxMjM0NTY3ODkwIiwicm9sZXMiOlsiYWRtaW4iLCJ1c2VyIl19. SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
不要被这一 “坨” 字符串吓到,这不是我们处理的,甚至无需去看,这是给计算机去处理的
JWT 接收原始 JSON 格式后,会对其根据 Base64 进行编码处理,然后再进行解码
Base64:是一种基于 64 个可打印字符(A-Z、a-z、0-9、+、-)还有一个补位字符 = 来表示二进制数据的编码方式
其中,第 1 行是 Header;第 2-3 行是 Payload;第 3 行是 Signature。三部分之间用英文 . 分隔
Header
从 Header 开始分析,这 “坨” 字符串表示的原始 JSON 格式是:
{"alg": "HS256", "type": "JWT"}
参数含义:
-
alg:表示使用的签名算法,上例中指定为 HMAC-SHA256 算法
-
type:表示 Token 类型,上例中指定为 JWT
关于签名算法可以前往 JWT 官网 查看:
Payload
{ "sub": "1234567890", // ID "name": "John Doe", // 角色 "iat": 1516239022 // 权限 }
所有参数含义:
-
iss:JWT 签发者
-
sub:JWT 所面向的用户
-
aud:接收 JWT 的一方
-
iat:JWT 的签发时间
-
exp:JWT 的过期时间,这个过期时间必须要大于签发时间
-
nbf:在定义时间之前,该 JWT 不可用
-
jti:JWT 唯一身份标识,主要用来作为一次性 Token,从而回避重放攻击
Signature
Signature 的目的就是为了防止 Token 被篡改,确保 Token 的安全性
JWT 会根据依照 Header 中指定的 alg,即 “签名算法” 来合并前面的 Header 和 Payload,并且还会加入 我们个人自定义的密钥,然后再来编码 Signature
这个 Signature 是自动计算出来的,并不是根据 Base64 编码而成
一旦 Signature 中的某个字符被篡改,那整个 JWT 的校验都会失败。正是因为 Signature 的存在,才使得 JWT 非常安全可靠
JWT 如何
常用场景 - 登录校验
JWT 最常用的场景就是登录校验
登录流程:
-
浏览器(前端)发起登录操作,此时访问登录接口(后端)
-
登录校验成功,后端 生成 JWT
-
后端将生成的 JWT 返回给前端
-
前端拿到 JWT 后,会将其存储在浏览器的 localScorage 中,在 后续所有请求 中,每一次请求都会将这个 JWT 存储在请求头中随着请求发送到后端
local scorage 本地存储
-
当前端再次发起请求时,后端会进行 统一拦截,判断这次请求中是否携带 JWT:
统一拦截就是过滤器和拦截器的知识点了,与 JWT 本身无关
-
没有携带 JWT:拒绝前端访问,请求响应失败
-
携带 JWT:校验 JWT 是否有效
-
-
前端请求携带的 JWT 有效,则后端放行,正常处理请求
-
后端主要操作就是 生成和校验 JWT
注意:
-
登录页面的请求不会拦截,因为后端需要根据登录信息生成 JWT
-
后端通过解析 JWT 并验证 Signature,可以获取到 Payload 部分存储的用户的身份和权限,从而判断是否放行
基于 Java 生成和校验 JWT
在项目中引入 JWT 库
想要使用 JWT,首先就要在项目中引入 JWT 的相关依赖
在实际使用中,通常会使用现有的 JWT 库来生成和解析 JWT,常用的 JWT 库有 java-jwt、jjwt
这些库已经实现了 RFC 7519 标准中的格式和规范,因此可以方便地使用这些库来生成和解析 JWT
下面以 jjwt 为例
在项目 pom.xml 中引入 jjwt 依赖:
io.jsonwebtoken jjwt0.9.1 引入依赖后,就可以使用该工具包中的 API 来实现 JWT 的生成和校验
生成 JWT
以参考文献中黑马程序员 - Java Web 开发教程\day12-SpringBootWeb登录认证\代码\tlias-web-management 的项目为例
引入依赖后,打开测试类 TliasWebManagementApplicationTests.java
在测试类中添加测试生成 JWT 的方法:
@Test public void testGenJWT(){ // 通过 Map 集合存储生成 JWT 的载荷 Map
claims = new HashMap<>(); // 自定义信息(一般是存储前端请求携带的登录信息) claims.put("id", 1); // 用户 ID claims.put("name", "Li"); // 用户名 // 调用 Jwts.builder() 生成 JWT,使用链式方法设置生成 JWT 时需要的参数 String jwt = Jwts.builder() // 设置签名算法为 HS256,密钥自定义 .signWith(SignatureAlgorithm.HS256, "LiiiYiAn") .setClaims(claims) // 设置载荷 // 设置 JWT 有效期为 1h(以 ms 为单位) .setExpiration(new Date(System.currentTimeMillis() + 3600 * 1000)) .compact(); // 返回生成 JWT 根据设置信息编码后的字符串 System.out.println("生成的 JWT 为:" + jwt); } 编写完成后,就可以测试运行这个方法了
这里有一个小技巧,当测试方法是独立的,即与整个 Spring 环境无关联,就可以先注释掉测试类上的注解 @SpringBootTest,这样就不会加载整个 Spring 环境,能 加快方法运行速度
运行后输出:
生成的 JWT 为:eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoiTGkiLCJpZCI6MSwiZXhwIjoxNzE2MzEyOTg0fQ.Uhqi2bGY6SBjNVRto6nv-PbpFAg1ZFZi5HLKA4GLirE
此时可以复制这串字符串,然后打开 JWT 官网,将这串字符串放在 Encoded 框中:
此时会解析这串字符串,解析后就能得到 JWT 编码前的原始 JSON 格式信息了
参数解析:
-
alg:签名算法
-
exp:JWT 过期时间
因为 Signature 并不是 Base64 编码,是通过 Header + Payload + 签名算法计算得出的,所以不会解析出来
这是 JWT 的 “看家本领”,怎么能展示出来呢,如果解析出来了那别人岂不是能反推从而破解 JWT,那还谈什么传输信息安全性
也可以直接在网上搜索 Base64 编码解码工具,也能得到 Header 和 Payload 的解码原始 JSON 数据格式,比如 Base64 在线编码解码 | Base64 加密解密 - Base64.us
同样的,Signature 不能解码
复制 Header 部分进行解码:
解析 JWT(校验)
在测试类中添加测试解析 JWT 的方法:
@Test public void testParseJWT(){ // 调用 Jwts.parser() 解析 JWT 中携带的载荷部分 // 使用链式方法设置解析 JWT 时需要的参数 Claims claims = Jwts.parser() // 设置解析时的签名算法密钥,必须与生成 JWT 时设置的密钥一致否则解析失败 .setSigningKey("LiiiYiAn") // 解析生成 JWT 根据设置信息编码后的字符串 .parseClaimsJws("eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoiTGkiLCJpZCI6MSwiZXhwIjoxNzE2MzEyOTg0fQ.Uhqi2bGY6SBjNVRto6nv-PbpFAg1ZFZi5HLKA4GLirE") // 返回解析 JWT 后载荷部分的原始 JSON 数据格式 // 也可以使用 Jwts.getHeader() 返回头部部分 // 不过前面的参数类型要改为 Header .getBody(); System.out.println(claims); }
运行测试:
报错信息:
io.jsonwebtoken.ExpiredJwtException: JWT expired at 2024-05-22T01:36:24Z. Current time: 2024-05-22T10:33:24Z, a difference of 32220847 milliseconds. Allowed clock skew: 0 milliseconds. 过期JwtException:JWT于2024-05-22T01:36:24Z过期。当前时间:2024-05-22T10:33:24Z,相差32220847毫秒。允许的时钟偏移:0毫秒
就是 JWT 过期了,所以无法解码
如果 JWT 解析校验时报错,则说明 JWT 被篡改或失效了,Token 非法
这是因为我使用的是前面生成的 JWT,而当时设置的 JWT 有效时间是 1h,而这次解析 JWT 已经过了 1h,所以 JWT 失效了
此时我们应该重新去运行前面生成 JWT 的方法,然后复制生成的 JWT,替换掉解析 JWT 中的 JWT 字符串:
.parseClaimsJws("eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoiTGkiLCJpZCI6MSwiZXhwIjoxNzE2MzQ5MDQxfQ.4T3cw6vzdjQffK2vBrokxsX7-nPNuKFPol8jGlEVYxk")
再次运行:
解析 JWT 中的载荷部分成功
同时,一旦 JWT 生成了,我们篡改其中的任何一个字符都会解析失败,可以尝试修改后运行
-
-
-
-
-
-
-
-
-
还没有评论,来说两句吧...