Skip to content

JWT介绍

本文主要介绍JWT,是Spring Security系列第四篇文章,前三篇文章如下:

1. 什么是JWT

1.1 介绍

JWT全称JSON Web Tokens,是以json为基础格式的,经过签名后的可信凭证。 参考网站:https://jwt.io/

JWT由三部分组成,使用.分隔:

  • Header (头部): 包含令牌的类型和使用的哈希算法。
  • Payload (负载): 包含声明(例如用户 ID、过期时间等)。
  • Signature (签名): 用于验证令牌的完整性和真实性。

1.2 生成JWT的过程

生成JWT的过程如下:

  1. 首先创建头部,确定令牌的类型和使用的哈希算法
json
{
  "alg": "HS256",
  "typ": "JWT"
}
  1. 然后创建负载,包含一些声明,例如用户ID、过期时间等。
json
{
  "sub": "1234567890",
  "name": "John Doe",
  "iat": 1516239022
}

在JWT中,预定义了一些声明名称,它们具有特殊含义:

  • iss (Issuer): 签发者的标识。它标识了 JWT 的签发方。例如,可以是一个公司或应用程序的名称。
  • sub (Subject): 主题的标识。它标识了 JWT 所面向的用户或实体。例如,可以是一个用户的 ID。
  • aud (Audience): 接收者的标识。它标识了 JWT 的预期接收方。例如,可以是一个或多个应用程序的名称。
  • exp (Expiration Time): 过期时间。它定义了 JWT 的过期时间,超过该时间后,JWT 将失效。这是一个重要的安全措施,用于限制 JWT 的有效期限。该值是一个 Unix 时间戳(自 1970 年 1 月 1 日 00:00:00 UTC 以来的秒数)。
  • nbf (Not Before): 生效时间。它定义了 JWT 的生效时间,在该时间之前,JWT 不应被接受处理。该值也是一个 Unix 时间戳。
  • iat (Issued At): 签发时间。它定义了 JWT 的签发时间。该值也是一个 Unix 时间戳。
  • jti (JWT ID): JWT 的唯一标识符。它为 JWT 提供了一个唯一的标识符,可以用于防止重放攻击。

参考资料:https://datatracker.ietf.org/doc/html/rfc7519#section-4.1

除了预定义的声明,我们也可以声明自己的数据,例如上面的"name":"John Doe"

  1. 最后使用指定的哈希算法对头部和载荷签名
    1. 首先使用Base64URL分别对头部和载荷进行编码,然后用.号连接起来;
    2. 然后使用指定的哈希算法,用密钥对编码后的头部和载荷进行签名,最后使用Base64URL对签名结果编码,使用.将签名结果拼接到头部和载荷后,形成JWT。

示例如下: JWT示例

2. 在Java中使用生成JWT

本小节介绍如何在Java中自己生成JWT,以HmacSHA256哈希算法为例。

java
void testGenerateJWT(){
    // 1. 准备头部和载荷
    String header = "{\"alg\":\"HS256\",\"typ\":\"JWT\"}";
    String payload = "{\"sub\":\"1234567890\",\"name\":\"John Doe\",\"iat\":1516239022}";

    // 2. 将头部和载荷进行Base64UrlEncoded编码
    String headerBase64 = Base64.getUrlEncoder()
            .encodeToString(header.getBytes(StandardCharsets.UTF_8))
            .replaceAll("=","");
    String payloadBase64 = Base64.getUrlEncoder()
            .encodeToString(payload.getBytes(StandardCharsets.UTF_8))
            .replaceAll("=","");

    // 3. 使用 HMAC SHA256 算法生成签名
    String data = headerBase64 + "." + payloadBase64;
    String key = "your-256-bit-secret";
    String signature = hmacSha256(data, key);

    // 4. 生成JWT
    String jwt = data + "." +signature;

    Assertions.assertEquals("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c",jwt);
}

// 签名算法
public static String hmacSha256(String data, String key) {
    try {
        // 1. 创建 SecretKeySpec 对象,指定密钥和算法
        SecretKeySpec secretKeySpec = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "HmacSHA256");

        // 2. 创建 Mac 对象,指定算法
        Mac mac = Mac.getInstance("HmacSHA256");

        // 3. 使用密钥初始化 Mac 对象
        mac.init(secretKeySpec);

        // 4. 执行 MAC 操作,获取哈希值
        byte[] hmacBytes = mac.doFinal(data.getBytes(StandardCharsets.UTF_8));

        // 5. 将哈希值转换为Base64UrlEncoded字符串
        return Base64.getUrlEncoder().encodeToString(hmacBytes).replaceAll("=","");

    } catch (NoSuchAlgorithmException | InvalidKeyException e) {
        e.printStackTrace();
        return null;
    }
}

上面程序生成的JWT与下面相同:

image-20250109190244735

注意点:

  • JWT 使用了一种称为 Base64URL 的编码方式,它是 Base64 的变体。这种变体是为了使 JWT 更安全地在 URL 中传输。

    Base64 与 Base64URL 的区别

    • Base64: 使用 64 个字符(A-Z, a-z, 0-9, +, /)以及填充字符“=”来编码二进制数据。
    • Base64URL: 是 Base64 的一种修改版本,主要针对 URL 传输进行了优化。它使用 64 个字符(A-Z, a-z, 0-9, - , _),并且省略了填充字符“=”。(+被替换为了-/被替换为了_)。

3. 使用java-jwt库生成JWT

由于Java生态的丰富,我们不用自己从零开始编写代码生成JWT,可以使用java-jwt库来帮助我们生成JWT。

地址:https://github.com/auth0/java-jwt

首先在项目中引入依赖:

xml
<dependency>
  <groupId>com.auth0</groupId>
  <artifactId>java-jwt</artifactId>
  <version>4.4.0</version>
</dependency>

然后就可以使用java-jwt库中提供的工具类来生成JWT了。

生成JWT代码:

java
void testGenJWT(){
    try {
        Algorithm algorithm = Algorithm.HMAC256("your-256-bit-secret");
        String token = JWT.create()
                .withSubject("1234567890")
                .withClaim("name","John Doe")
                .withIssuedAt(Instant.ofEpochSecond(1516239022))
                .sign(algorithm);
      
      	System.out.println(token);
    } catch (JWTCreationException exception){
        exception.printStackTrace();
    }
}

校验并解析JWT的代码(这里主要是解析载荷信息):

java
void testValidJWT(){
    try {
        String jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c";

        Algorithm algorithm = Algorithm.HMAC256("your-256-bit-secret");
        JWTVerifier verifier = JWT.require(algorithm).build();

        // 校验JWT是否被修改,如果校验不通过,则会抛异常 JWTDecodeException
        DecodedJWT decodedJWT = verifier.verify(jwt);
        // 获取JWT中的载荷信息
        Map<String, Claim> claims = decodedJWT.getClaims();
        claims.forEach((k,v)->{
            System.out.println(k + " : " + v.toString());
        });
    }catch (Exception e){
        e.printStackTrace();
    }
}