1. 认识JWT

1.1 什么是JWT

Token(JWT)是一个开放的行业标准(RFC 7519),它定义了一种简介的、自包含的协议格式,用于在通信双方传递 json 对象,传递的信息经过数字签名可以被验证和信任。JWT 可以使用 HMAC 算法或使用 RSA 的公钥/私钥对来签名,防止被篡改。

  1. 优点:
  2. 1. jwt 基于 json,非常方便解析。
  3. 2. 可以在令牌中自定义丰富的内容,易扩展。
  4. 3. 通过非对称加密算法及数字签名技术,JWT防止篡改,安全性高。
  5. 4. 资源服务使用JWT可不依赖认证服务即可完成授权。
  6. 缺点:
  7. 1. JWT令牌较长,占存储空间比较大。

1.2 JWT构成

1.2.1 头部

JWT 由三部分构成:头部、负载、签证。JWT 的头部承载两部分信息,声明类型以及加密算法。

// 头部使用Base64加密 eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9

  1. <a name="J3Q0Y"></a>
  2. ### 1.2.2 负载
  3. 负载,就是存放有效信息的地方。这个名字像是特指飞机上承载的货品,这些有效信息包含三个部分:
  4. - 标准中注册的声明(建议但不强制使用)
  5. - **iss**: jwt 签发者
  6. - **sub**: jwt 所面向的用户
  7. - **aud**: 接收 jwt 的一方
  8. - **exp**: jwt 的过期时间,这个过期时间必须要大于签发时间
  9. - **nbf**: 定义在什么时间之前,该 jwt 都是不可用的.
  10. - **iat**: jwt 的签发时间
  11. - **jti**: jwt 的唯一身份标识,主要用来作为一次性 token,从而回避重放攻击。
  12. - 公共的声明:公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密.
  13. - 私有的声明:私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为 base64 是对称解密的,意味着该部分信息可以归类为明文信息。
  14. ```java
  15. {
  16. "sub": "1234567890",
  17. "name": "John Doe",
  18. "admin": true
  19. }
  20. // 加密
  21. eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9

1.2.3 签证

jwt 的第三部分是一个签证信息,这个签证信息由三部分组成:

  • header (base64后的)
  • payload (base64后的)
  • secret

这个部分需要 base64 加密后的 header 和 base64加密后的 payload 使用.连接组成的字符串,然后通过 header 中声明的加密方式进行加盐 secret 组合加密,然后就构成了jwt的第三部分。

注意:secret 是保存在服务器端的,jwt 的签发生成也是在服务器端的,secret 就是用来进行 jwt 的签发和 jwt 的验证,所以,它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个 secret, 那就意味着客户端是可以自我签发 jwt 了。

1.3 JWT应用

一般是在请求头里加入 Authorization,并加上Bearer 标注:

  1. fetch('api/user/1', {
  2. headers: {
  3. 'Authorization': 'Bearer ' + token
  4. }
  5. })

服务端会验证 token,如果验证通过就会返回相应的资源。整个流程就是这样的:
JWT - 图1

2. 认识JJWT

2.1 什么是JJWT

JJWT 是一个提供端到端的 JWT 创建和验证的 Java 库。永远免费和开源(Apache License,版本2.0),JJWT 很容易使用和理解。它被设计成一个以建筑为中心的流畅界面,隐藏了它的大部分复杂性。规范官网:https://jwt.io/

2.2 引入依赖

  1. <!-- jwt 依赖配置-->
  2. <dependency>
  3. <groupId>io.jsonwebtoken</groupId>
  4. <artifactId>jjwt</artifactId>
  5. <version>0.9.1</version>
  6. </dependency>

2.3 快速入门

1、引入依赖。

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <project xmlns="http://maven.apache.org/POM/4.0.0"
  3. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  4. xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  5. <parent>
  6. <artifactId>springsecurity-demo</artifactId>
  7. <groupId>com.xuwei</groupId>
  8. <version>1.0-SNAPSHOT</version>
  9. </parent>
  10. <modelVersion>4.0.0</modelVersion>
  11. <artifactId>springboot-jjwt</artifactId>
  12. <properties>
  13. <maven.compiler.source>11</maven.compiler.source>
  14. <maven.compiler.target>11</maven.compiler.target>
  15. </properties>
  16. <dependencies>
  17. <!-- jwt 依赖配置-->
  18. <dependency>
  19. <groupId>io.jsonwebtoken</groupId>
  20. <artifactId>jjwt</artifactId>
  21. </dependency>
  22. <dependency>
  23. <groupId>org.springframework.boot</groupId>
  24. <artifactId>spring-boot-starter-web</artifactId>
  25. </dependency>
  26. <dependency>
  27. <groupId>org.springframework.boot</groupId>
  28. <artifactId>spring-boot-starter-test</artifactId>
  29. <scope>test</scope>
  30. <exclusions>
  31. <exclusion>
  32. <groupId>org.junit.vintage</groupId>
  33. <artifactId>junit-vintage-engine</artifactId>
  34. </exclusion>
  35. </exclusions>
  36. </dependency>
  37. </dependencies>
  38. </project>

2、自定义 JWT 测试类。

  1. @SpringBootTest
  2. public class JjwtTest {
  3. /**
  4. * 创建Token
  5. */
  6. @Test
  7. public void testCreateToken() {
  8. System.out.println("获取token");
  9. // 创建一个JwtBuilder对象
  10. JwtBuilder jwtBuilder = Jwts.builder()
  11. // 声明的标识{"jti":"888"}
  12. .setId("888")
  13. // 主体,用户{"sub":"Rose"}
  14. .setSubject("Rose")
  15. // 创建日期{"ita":"xxxxxx"}
  16. .setIssuedAt(new Date())
  17. // 签名手段,参数1:算法,参数2:盐
  18. .signWith(SignatureAlgorithm.HS256, "xxxx");
  19. // 获取jwt的token
  20. String token = jwtBuilder.compact();
  21. System.out.println(token);
  22. // 三部分的base64解密
  23. String[] tokenSplit = token.split("\\.");
  24. // {"alg":"HS256"}
  25. System.out.println(Base64Codec.BASE64.decodeToString(tokenSplit[0]));
  26. // {"jti":"888","sub":"Rose","iat":1646360468
  27. System.out.println(Base64Codec.BASE64.decodeToString(tokenSplit[1]));
  28. // 无法解密
  29. System.out.println(Base64Codec.BASE64.decodeToString(tokenSplit[2]));
  30. }
  31. /**
  32. * token过期校验
  33. */
  34. @Test
  35. public void testCreateTokenHasExp() {
  36. long now = System.currentTimeMillis();
  37. // 设置过期时间,这里是1分钟后的时间长整型
  38. long exp = now + 60 * 1000;
  39. JwtBuilder jwtBuilder = Jwts.builder()
  40. //声明的标识{"jti":"888"}
  41. .setId("888")
  42. //主体,用户{"sub":"Rose"}
  43. .setSubject("Rose")
  44. //创建日期{"ita":"xxxxxx"}
  45. .setIssuedAt(new Date())
  46. //签名手段,参数1:算法,参数2:盐
  47. .signWith(SignatureAlgorithm.HS256, "xxxx")
  48. // 设置过期时间
  49. .setExpiration(new Date(exp));
  50. String token = jwtBuilder.compact();
  51. System.out.println(token);
  52. }
  53. /**
  54. * 自定义Claims,在负载里存储更多的信息可以自定义
  55. */
  56. @Test
  57. public void testCreateTokenByClaims() {
  58. long now = System.currentTimeMillis();
  59. // 设置过期时间,这里是1分钟后的时间长整型
  60. long exp = now + 60 * 1000;
  61. JwtBuilder jwtBuilder = Jwts.builder()
  62. //声明的标识{"jti":"888"}
  63. .setId("888")
  64. //主体,用户{"sub":"Rose"}
  65. .setSubject("Rose")
  66. //创建日期{"ita":"xxxxxx"}
  67. .setIssuedAt(new Date())
  68. //签名手段,参数1:算法,参数2:盐
  69. .signWith(SignatureAlgorithm.HS256, "xxxx")
  70. // 设置过期时间
  71. .setExpiration(new Date(exp))
  72. // 直接传入map也可以 .addClaims(map)
  73. .claim("roles", "admin")
  74. .claim("logo", "shsd.jpg");
  75. String token = jwtBuilder.compact();
  76. System.out.println(token);
  77. }
  78. /**
  79. * 解析token
  80. */
  81. @Test
  82. public void testParseToken() {
  83. String token = "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiJSb3NlIiwiaWF0IjoxNjQ2MzYxMjIwLCJleHAiOjE2NDYzNjEyODAsInJvbGVzIjoiYWRtaW4iLCJsb2dvIjoic2hzZC5qcGcifQ.41yPcSw0Lp_AQ0b3nXGh513IUP8qs7pc_UJgym4olZ8";
  84. // 解析token获取负载中的声明对象
  85. Claims claims = Jwts.parser()
  86. .setSigningKey("xxxx")
  87. .parseClaimsJws(token)
  88. .getBody();
  89. // 打印声明的属性
  90. System.out.println("id:"+claims.getId()); // 888
  91. System.out.println("subject:"+claims.getSubject()); // Rose
  92. System.out.println("issuedAt:"+claims.getIssuedAt()); // Fri Mar 04 10:22:04 CST 2022
  93. DateFormat sf =new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
  94. System.out.println("签发时间:"+sf.format(claims.getIssuedAt()));
  95. System.out.println("过期时间:"+sf.format(claims.getExpiration()));
  96. System.out.println("当前时间:"+sf.format(new Date()));
  97. System.out.println("roles:"+claims.get("roles"));
  98. System.out.println("logo:"+claims.get("logo"));
  99. }
  100. }