开放标准(RFC 7519):JSON Web Token (JWT)

开放标准:JSON Web Token

前言

JSON Web TokenJWT ) 是一种开放标准 (RFC 7519 ),它定义了一种紧凑且自包含的方式,用于将信息作为 JSON 对象在各方之间安全地传输。此信息是经过数字签名的,因此可以验证和信任。可以使用密钥(使用 HMAC 算法)或使用 RSAECDSA 的公钥/私钥对对 JWT 进行签名。

以下是 JSON Web Token 有用的一些情况:

  • 授权 :这是使用 JWT 的最常见场景。用户登录后,每个后续请求都将包含 JWT ,允许用户访问该令牌允许的路由、服务和资源。单点登录是当今广泛使用 JWT 的一项功能,因为它的开销小,并且能够轻松地跨不同域使用。
  • 信息交换JSON Web Token 是在各方之间安全地传输信息的好方法。由于 JWT 可以签名(例如,使用公钥/私钥对),因此您可以确保发件人是他们所声称的身份。此外,由于签名是使用标头和有效负载计算的,因此您还可以验证内容是否未被篡改。

什么是JWT?

JSON Web Token 由三个部分组成,由点 ( . ) 分隔

xxxxx.yyyyy.zzzzz

具体来说,这三个部分是:

  • Header 页眉 :标头通常由两部分组成:令牌的类型(JWT )和签名算法(HMAC SHA256RSA)。
java 复制代码
{
  "alg": "HS256",
  "typ": "JWT"
}

Base64 对这个JSON 编码就得到JWT的第一部分。

  • Payload 有效载荷 :令牌的第二部分是有效负载(JWT 中的主要数据,称为 payload ),其中包含声明。声明是关于实体(通常是用户)和其他数据的声明。 有三种类型的声明:registeredpublicprivate

    1. Registered claims :这些是一组预定义的声明,不是强制性的,但建议使用,以提供一组有用的、可互操作的声明。比如:iss(签发者)、exp(过期时间)、sub(主题)、aud(接收者)、iat(签发时间)、nbf(在此之前不可用)等。

    2. Public claims:这些声明可以由用户随意定义。但不建议添加敏感信息,因为该部分在客户端可解密。

    3. Private claims:提供者和消费者所共同定义的声明,一般不建议存放敏感信息

一个示例有效Payload如下(并不需要三个声明都设置):

java 复制代码
{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true
}

Payload 进行Base64 编码就得到JWT的第二部分。

请注意,对于带签名的令牌来说,这些信息虽然得到了保护,不会被擅自篡改,但仍然可以被任何人读取。不要将机密信息放在 JWTpayloadheader 元素中,除非它们是加密的。

  • Signature 签名 :要创建签名部分,您必须获取编码的Header 、编码的Payload、密钥、标头中指定的算法,并对其进行签名。

例如,如果您想使用 HMAC SHA256 算法,将按以下方式创建签名:

java 复制代码
HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret)
  1. Header页眉:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
  2. Payload 有效载荷:eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ
  3. 通过HMAC SHA256 算法得到:SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
  4. 最后,我们将上述的 3 个部分的字符串通过 "." 进行拼接得到完整JWT

每当用户想要访问受保护的路由时,它都应该发送 JWT ,通常在 Authorization 标头中使用 Bearer 模式。因此,标头的内容应如下所示。

Authorization: Bearer < token >

在某些情况下,这可以是无状态授权机制。服务器的受保护路由将检查 Authorization 标头中的有效 JWT ,如果存在,将允许用户访问受保护的资源。跨域资源共享 (CORS ) 不会成为问题,因为它不使用 Cookie

请注意,如果您通过 HTTP 标头发送 JWT 令牌,则应尽量防止它们变得太大。某些服务器不接受超过 8 KB 的标头。

下图显示了如何获取 JWT 并用于访问 API 或资源:

  1. 应用程序或客户端向授权服务器请求授权。
  2. 授予授权后,授权服务器将向应用程序返回访问令牌。
  3. 应用程序使用访问令牌访问受保护的资源(如 API)。

由于JSON 的表述方式比XML更简洁,因此在编码后,其体积也更小。

在大多数编程语言中,JSON 解析器都很常用,因为它们能够直接将数据映射为对象形式。而XML 则没有如此直接的"文档到对象"的转换方式。正因如此,JWT 成为在HTMLHTTP环境中传输数据的首选格式。

在应用方面,JWT 被广泛用于互联网领域。这说明,客户端可以在多种平台上轻松处理JSON Web令牌,尤其是在移动设备上。

JWT验证

JSON Web TokenJWT)的验证对于保障安全性至关重要。

JWT 验证通常是对JWT的结构、格式和内容进行检查。

  • 格式检查

    • 确认有标准的三个组成部分:头部、有效载荷和签名,这三部分之间用点隔开。
    • 每个部分都经过正确的编码处理(采用Base64URL格式),同时确保有效载荷中包含预期的数据内容。
    • 检查有效载荷中的各项信息是否正确,比如有效期(exp)、发行时间(iat)、"不得早于"的时间限制(nbf)等。这样可以确保令牌没有过期,也没有在应该使用之前就被提前使用了。
  • 签名验证

    • 这是验证过程中的关键步骤,其目的是检查JWT 中的签名部分是否与头部信息和有效载荷内容一致。这一过程使用头部中指定的算法来执行,常见的算法有HMACRSAECDSA等。验证时会用到密钥或公钥。如果签名与预期不符,那么很可能是该令牌被篡改过,或者它并非来自可信任的来源。
    • 发行者验证:检查所声称的发行者是否与预期的发行者一致。
    • 受众匹配检查:确保所宣称的受众群体与实际目标受众相符。

JWT编码与解码

编码时,需要将头部信息和有效载荷转换成一种紧凑的、适合在URL 中使用的格式。头部信息中包含了签名算法和令牌类型等信息;而有效载荷则包含主题、有效期、颁发时间等数据。这些信息都被转换为JSON 格式后,再使用Base64URL 编码。之后,将这些编码后的部分用点连接起来。最后,利用头部信息中指定的算法以及密钥来生成签名。这个签名同样也经过Base64URL 编码处理。这样,就得到了最终的JWT字符串,该字符串以一种便于传输或存储的形式表示了相应的令牌。

解码JWT 的过程其实是逆向操作 :将经过Base64URL 编码的头部信息和有效载荷转换回JSON 格式。这样一来,任何人都可以无需密钥就能读取这些信息。不过,在这里,"解码"一词通常还包括对令牌签名的验证。这一验证步骤涉及使用与最初生成令牌时相同的算法和密钥,重新生成解码后的头部信息和有效载荷的签名,然后将新生成的签名与JWT中的原始签名进行比较。如果两者一致,那就说明该令牌是完整且真实的,没有被人篡改过。

基本使用

Java 后端最常用的两个库是 JJWTAuth0 java-jwt

JJWT

引入依赖:

xml 复制代码
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>0.13.0</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>
    <version>0.13.0</version>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId> <!-- or jjwt-gson if Gson is preferred -->
    <version>0.13.0</version>
    <scope>runtime</scope>
</dependency>

该依赖包含了 jjwt-apijjwt-impljjwt-jackson 三个模块的所有功能。

JJWT 保证其所有构件都符合语义化版本控制规范,但 jjwt-impl.jar 除外。对于 jjwt-impl.jar,不提供此类保证,该 JAR 内部的更改随时都可能发生。切勿将 jjwt-impl.jarcompile 范围添加到您的项目中,始终应将其声明为runtime 范围。

创建JWT

调用Jwts.builder()方法创建一个 JwtBuilder 实例,写入信息,示例如下:

java 复制代码
    public static void main(String[] args) {
        // 生成签名密钥
        SecretKey key = Jwts.SIG.HS256.key().build();
        String jwt = Jwts.builder()
        
                .header()
                .keyId("aKeyId")
                .and()
                
                .subject("Bob")
                //.content(aByteArray, "text/plain")
                
                .signWith(key)
                //.encryptWith(key, keyAlg, encryptionAlg)
                
                .compact();
        System.out.println(jwt);
        /** Output:
         *  eyJraWQiOiJhS2V5SWQiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJCb2IifQ.ZJwFiG-SBsb_d3cfNn-I59iC8D6x89Bi1sLclQax7vU
         */
    }

在这个例子中,设置header 参数;注册声明 sub(主题)将被设置为 Bob ;然后我们使用适合 HMAC-SHA-256 算法的密钥对 JWT 进行签名,调用 compact() 方法生成最终的紧凑型 JWT 字符串。

JWT payload 可能是 byte\[\] 内容(通过 content )或 JSON 声明(例如 subjectclaims ,等等),但不能同时是两者。

数字签名( signWith )或加密( encryptWith )可使用其一,但不能同时使用。

JWT Header

JWT 头是一个 JSON 对象,它提供了关于 JWT 内容、格式以及任何相关加密操作的元数据。JJWT 提供多种方式来设置整个头信息以及/或多个单独的头参数(名称/值对)。

java 复制代码
    public static void main(String[] args) {
        URI certUrl = URI.create("https://your-domain.com/certs/my-jwt-signer.crt");
        String jwt = Jwts.builder()
                .header()
                .keyId("aKeyId")
                .type("JWT")
                .x509Url(certUrl)
                .add("someName", "someValue")
                .delete("someName")
                .and()

                .subject("Joe")                 // resume JwtBuilder calls...
                .compact();
        /** Output:
         *  eyJraWQiOiJhS2V5SWQiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJCb2IifQ.ZJwFiG-SBsb_d3cfNn-I59iC8D6x89Bi1sLclQax7vU
         */
    }
  • keyId() :设置密钥 ID。多密钥场景下,指定签名所用密钥的标识,方便验签方匹配公钥。
  • type() :设置令牌类型。默认自动设为 JWT,极少手动修改。
  • x509Url() :声明一个 URL 地址,验证方可以从该地址下载对应的 X.509 公钥证书(或证书链),并用它来验证 JWT 的签名。
  • add() :持任意名称/值对:也支持Map对象。
  • delete() :删除 Header 里指定的某一个字段。
  • and() :是 JJWT 链式构建器里的返回上级方法,专门用来结束子配置(Header /Payload),回到主构建器,让你能流畅地写链式代码。

大多数情况来说,除了自定义的Header 和默认的Header,其它方法很少调用。

自动设置头部:您无需设置 algenczip 头部 - JJWT 会根据需要自动设置。

Jwts.header()Jwts.builder().header() 之间只有两个区别:

Jwts.header() 构建一个 '分离的' Header ,它与任何特定的 JWT 都没有关联(没有and()方法),而 Jwts.builder().header() 总是修改其父 JwtBuilder 正在构建的 JWT 的头部。

独立的头部可能很有用,如果您想将常见的头部参数聚合到一个单个的 '模板' 实例中,这样您就不必为每个 JwtBuilder 使用重复它们。

java 复制代码
    public static void main(String[] args) {
        Header commonHeaders = Jwts.header().add("someName", "someValue")
                .build();

        String jwt = Jwts.builder()

                .header()
                .add(commonHeaders)                   // <----
                .add("specificHeader", "specificValue") // jwt-specific headers...
                .and()

                .subject("whatever")
                // ... etc ...
                .compact();
    }
JWT Payload

一个 JWT payload 可以是任何东西 - 任何可以表示为字节数组的对象,例如文本、图像、文档等等。但既然 JWT header 始终是 JSON ,那么 payload 也可以是 JSON,特别是用于表示身份声明时。

JwtBuilder 支持两种不同的有效载荷选项:

  • content:如果您希望负载为任意字节数组内容。
java 复制代码
    public static void main(String[] args) {
        String jwt = Jwts.builder()
                .content("这是我的原始内容,不是JSON")
                //.content("自定义内容".getBytes(StandardCharsets.UTF_8))
                //.content("content", "text/plain") 参数一:有效载荷的实际字节内容 参数二:媒体类型的字符串标识符,自动设置 cty (内容类型)头部
                .compact();
    }
  • claims :如果您希望负载为 JSON 声明 Object
java 复制代码
    public static void main(String[] args) {
        String jwt = Jwts.builder()
                .subject("Bob")
                .issuer("me")
                .audience().add("you").and()
                .expiration(new Date())
                .notBefore(new Date(System.currentTimeMillis()+1000))
                .issuedAt(new Date())
                .id(UUID.randomUUID().toString())
                .claim("name", "张三")
                .compact();
    }

(1)subject():主题,一般放用户 ID。

(2)issuer() :签发者,谁发的 Token

(3)audience() :接收者,谁能用 Token

(4)expiration():过期时间,过了这个时间就不能用。

(5)notBefore():生效时间,没到这个时间,不能用。

(6)issuedAt() :签发时间,记录Token什么时候创建。

(7)id() :给这个 Token 一个唯一编号。

(8)claim():自定义声明

JWT 标准(RFC7519)原文规定:所有注册声明都是 OPTIONAL(可选的)

JWT Signed(JWS)

JWT 规范提供了对 JWT 进行加密签名的功能。对 JWT 进行签名,保证在 JWT 创建后,没有人对其进行篡改或更改(其完整性得到保持)。

java 复制代码
    public static void main(String[] args) {
        // HS256 密钥(对称加密)
        SecretKey key = Jwts.SIG.HS256.key().build();
        String jwt = Jwts.builder()
                .subject("1001")
                .expiration(new Date(System.currentTimeMillis() + 3600 * 1000))

                .signWith(key)  //签名
                .compact();
    }

调用 compact() 方法进行压缩和签名,生成最终的 JWSJWT 是统称;JWS 是 带签名的、最安全、最常用的 JWT)。

JWT 规范确定了 13 种标准签名算法------3 种对称密钥算法和 10 种非对称密钥算法,这些都在 io.jsonwebtoken.Jwts.SIG 注册类中以常量形式表示。

JWS 压缩

如果您的 JWT 负载很大(包含大量数据),并且您确信 JJWT 也将是读取/解析您的 JWS 的库,那么您可能希望压缩 JWS 以减小其大小。

JWS 不符合标准:JJWT 支持对 JWS 进行压缩,但这不是 JWS 的标准功能。JWT RFC 规范仅将此标准化为 JWE ,并且其他 JWT 库可能不会支持 JWS 压缩。只有当您确信 JJWT (或支持 JWS 压缩的另一个库)将解析 JWS 时,才使用 JWS 压缩。

java 复制代码
    public static void main(String[] args) {
        // HS256 密钥(对称加密)
        SecretKey key = Jwts.SIG.HS256.key().build();
        String jwt = Jwts.builder()
                .subject("1001")
                .expiration(new Date(System.currentTimeMillis() + 3600 * 1000))
                .compressWith(Jwts.ZIP.DEF) // 压缩,DEFLATE
                //.compressWith(Jwts.ZIP.GZIP) // 更高压缩率(非标准,跨系统需统一)
                .signWith(key)  //签名
                .compact();
        System.out.println(jwt);
    }

必须 先压缩 Payload,再签名(仅 Payload 压缩)。若先签名再压缩,会篡改签名内容,导致验签失败。

无需手动解压,JJWT 自动识别 zip Header 并处理

读取JWT

使用 Jwts.parser() 方法创建一个 JwtParserBuilder 实例。

如果您期望解析已签名或加密的 JWT ,可以选择调用 keyLocatorverifyWithdecryptWith 方法。

如果解析、签名验证或解密失败,请将 parse* 调用包裹在 try/catch 块中。

java 复制代码
    public static void main(String[] args) {
        SecretKey key = Jwts.SIG.HS256.key().build();
        try {
            String jwt = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJCb2IiLCJpc3MiOiJtZSIsImF1ZCI6WyJ5b3UiXSwiZXhwIjoxNzc1OTgwMTk5LCJuYmYiOjE3NzU5ODAyMDAsImlhdCI6MTc3NTk4MDE5OSwianRpIjoiYjE1MzUyZDEtNzUwNS00MTU5LTlhZDMtZGU3NWNlZjBmMTI1IiwibmFtZSI6IuW8oOS4iSJ9.x2LZKFQgzNcB2dwGfpkov4a0n69tKsZ9P5z4t_x2Yro";
            Jws<Claims> claimsJws = Jwts.parser().verifyWith(key).build().parseSignedClaims(jwt);

            // 读取 Header
            JwsHeader header = claimsJws.getHeader();
            String keyId = header.getKeyId();
            String type = header.getType();
            String algorithm = header.getAlgorithm();

            // 读取 Payload
            Claims payload = claimsJws.getPayload();
            // 标准字段
            String sub = payload.getSubject();       // 用户ID
            String jti = payload.getId();            // 令牌唯一ID
            Date iat = payload.getIssuedAt();        // 签发时间
            Date exp = payload.getExpiration();     // 过期时间
            Date nbf = payload.getNotBefore();      // 生效时间
            String iss = payload.getIssuer();        // 签发者
            Set<String> aud = payload.getAudience();     // 接收方
            // 自定义字段
            String username = payload.get("username", String.class);

            System.out.println("JWT is valid");
        } catch (JwtException e) {
            //don't trust the JWT!
            System.out.println("JWT is invalid!");
        }
    }

JJWT 提供好几个 parse

  • parseSignedClaims() :带签名的 JWT(最常用)。
java 复制代码
Jws<Claims> claimsJws = Jwts.parser().build().parseSignedClaims(jwt);
  • parseEncryptedClaims() :解析加密 JWT(JWE)。
java 复制代码
Jws<Claims> claimsJws = Jwts.parser().build().parseEncryptedClaims(jwt);
  • parseUnsecuredClaims() :只解析 Header + Payload,不校验签名。
java 复制代码
Jwt<Header, Claims> claimsJws = Jwts.parser().build().parseUnsecuredClaims(jwt);

如果您期望一个带有内容 payloadJWS ,请调用 JwtParserparseSignedContent 方法。

java 复制代码
Jws<byte[]> claimsJws = Jwts.parser().verifyWith(key).build().parseSignedContent(jwt);
byte[] contentBytes = claimsJws.getPayload();
String content = new String(contentBytes);

除此之外也支持内容的解密和不校验签名,获取Header + Payload

java 复制代码
// 解密
Jwe<byte[]> claimsJws = Jwts.parser().verifyWith(key).build().parseEncryptedContent(jwt);
//获取Header、Payload
Jwt<Header, byte[]> headerJwt = Jwts.parser().build().parseUnsecuredContent(jwt);
验证 JWT

调用Jwts.parser()方法,JJWT 内部检查这个 Token 是不是合法、有效、没被篡改、没过期。

java 复制代码
    public static void main(String[] args) {
        String jws = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJKb2UifQ.vDv3eouyPSGC-t9g21XDgPtKIOvxPg91xIvaV_Tux74";
        boolean equals = Jwts.parser().verifyWith(key).build().parseSignedClaims(jws).getPayload().getSubject().equals("Joe");
        System.out.println(equals);
        /** Output:
         *  true
         */
    }

如果验证失败,会抛出一个异常。

Exception in thread "main" io.jsonwebtoken.security.SignatureException: JWT signature does not match locally computed signature. JWT validity cannot be asserted and should not be trusted.

at io.jsonwebtoken.impl.DefaultJwtParser.verifySignature(DefaultJwtParser.java:340)

at io.jsonwebtoken.impl.DefaultJwtParser.parse(DefaultJwtParser.java:576)

at io.jsonwebtoken.impl.DefaultJwtParser.parse(DefaultJwtParser.java:364)

at io.jsonwebtoken.impl.DefaultJwtParser.parse(DefaultJwtParser.java:94)

at io.jsonwebtoken.impl.io.AbstractParser.parse(AbstractParser.java:36)

at io.jsonwebtoken.impl.io.AbstractParser.parse(AbstractParser.java:29)

at io.jsonwebtoken.impl.DefaultJwtParser.parseSignedClaims(DefaultJwtParser.java:827)

但如果解析或签名验证失败了怎么办?你可以及时发现并做出相应反应:

java 复制代码
    public static void main(String[] args) {
        try {
        	// 生成签名密钥
            SecretKey key = Jwts.SIG.HS256.key().build();
            String jws = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJKb2UifQ.vDv3eouyPSGC-t9g21XDgPtKIOvxPg91xIvaV_Tux75";
            boolean equals = Jwts.parser().verifyWith(key).build().parseSignedClaims(jws).getPayload().getSubject().equals("Joe");
            System.out.println(equals);
            //OK, we can trust this JWT
            System.out.println("JWT is valid");
        } catch (JwtException e) {
            //don't trust the JWT!
            System.out.println("JWT is invalid!");
        }
        /** Output:
         *  JWT is invalid!
         */
    }

常见验证失败异常:

  • MalformedJwtException:格式错误。
java 复制代码
    public static void main(String[] args) {
        // 生成签名密钥
        SecretKey key = Jwts.SIG.HS256.key().build();
        String jws = "eyJhbGciOiJIUzI1NiJ9.";
        Jws<Claims> jwt= Jwts.parser().verifyWith(key).build().parseSignedClaims(jws);
    }
  • UnsupportedJwtException :不支持的JWT 异常,使用了 JWK + 不支持的密钥类型。
java 复制代码
    public static void main(String[] args) {
        // 生成签名密钥
        SecretKey key = Jwts.SIG.HS256.key().build();
        String jws = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJKb2UifQ.vDv3eouyPSGC-t9g21XDgPtKIOvxPg91xIvaV_Tux75";
        Jws<Claims> jwt= Jwts.parser().build().parseSignedClaims(jws); // 未调用verifyWith方法进行验证
    }
  • SignatureException:签名错误,密钥不正确或者签名串不一致。
java 复制代码
    public static void main(String[] args) {
        // 生成签名密钥
        SecretKey key = Jwts.SIG.HS256.key().build();
        String jws = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJKb2UifQ.vDv3eouyPSGC-t9g21XDgPtKIOvxPg91xIvaV_Tux75";
        System.out.println(jws);
        Jws<Claims> jwt= Jwts.parser().verifyWith(key).build().parseSignedClaims(jws); // 重新生成的key进行验证
    }
  • ExpiredJwtException:签名过期,过期时间小于当前时间。
javascript 复制代码
    public static void main(String[] args) {
        // HS256 密钥(对称加密)
        SecretKey key = Jwts.SIG.HS256.key().build();
        String jwt = Jwts.builder()
                .subject("1001")
                .expiration(new Date(System.currentTimeMillis() - 1000)) // 设置当前时间之前
                .signWith(key)  //签名
                .compact();
        Jws<Claims> jws = Jwts.parser().verifyWith(key).build().parseSignedClaims(jwt);
    }
  • PrematureJwtException:签名过期,当前时间小于设置未来时间。
java 复制代码
    public static void main(String[] args) {
        // HS256 密钥(对称加密)
        SecretKey key = Jwts.SIG.HS256.key().build();
        String jwt = Jwts.builder()
                .notBefore(new Date(System.currentTimeMillis() + 100000))
                .signWith(key)  //签名
                .compact();
        Jws<Claims> jws = Jwts.parser().verifyWith(key).build().parseSignedClaims(jwt);
    }
  • IncorrectClaimException :声明异常。比如:issueraudience等不匹配(如果设置了就必须要校验,验证时不调用不会校验就毫无意义)。
java 复制代码
    public static void main(String[] args) {
        // HS256 密钥(对称加密)
        SecretKey key = Jwts.SIG.HS256.key().build();
        String jwt = Jwts.builder()
                .subject("Bob")
                .signWith(key)  //签名
                .compact();
        Jws<Claims> jws = Jwts.parser().verifyWith(key)
                .requireId("123456") // 验证id不为空且匹配
                .requireIssuer("issuer") // 验证issuer不为空且匹配
                .requireAudience("audience") // 验证audience不为空且匹配
                .requireSubject("subject") // 验证subject不为空且匹配
                .build()
                .parseSignedClaims(jwt);
    }
JWE

JWT 规范还提供了加密和解密 JWT 的能力。保证除了预期的 JWT 接收者之外,没有人能看到 JWT payload (它是机密的),并且保证在 JWT 创建后,没有人对其进行篡改或更改(其完整性得到保持)。

JWT 规范定义了 6 种标准的认证加密算法,用于加密 JWT 。这些都作为常量在 io.jsonwebtoken.Jwts.ENC 注册单例中表示,作为 io.jsonwebtoken.security.AeadAlgorithm 接口的实现。

java 复制代码
    public static void main(String[] args) {
        // 生成签名密钥
        SecretKey secretKey = Keys.secretKeyFor(SignatureAlgorithm.HS256);
        String jwt = Jwts.builder()
                .subject("1001")
                .expiration(new Date(System.currentTimeMillis() + 3600 * 1000))
                .encryptWith(secretKey , Jwts.ENC.A128CBC_HS256)  // 加密
                .compact();

        /** Output:
         *  eyJhbGciOiJkaXIiLCJlbmMiOiJBMTI4Q0JDLUhTMjU2In0..h4xwPcC1-RRLYeXpNOfluA.0Lf6C-SLHv4Dzmh91lCWuY_k3vdnvry-MTwNFpIXvgM.JhOJKtxV9MfZ71_CpuUbyg
         */

        // 解析 JWE 令牌
        try {
            Claims payload = Jwts.parser()
                    .decryptWith(secretKey)
                    .build()
                    .parseEncryptedClaims(jwt)
                    .getPayload();

            System.out.println("Decrypted Payload: " + payload.getSubject());
        } catch (Exception e) {
            System.err.println("Failed to decrypt JWE: " + e.getMessage());
        }
    }

大多数情况下,不会使用JWE ,一方面:再登录Token 中不允许放敏感信息;另一方面:JWE再加解密的过程比较消耗性能。

JWK

JSON Web 密钥(JWKs )是加密密钥的 JSON 序列化,允许将密钥材料嵌入 JWT 或以标准 JSON 基于文本格式在各方之间传输。它们本质上是一种基于 JSON 的替代其他基于文本的密钥格式,例如 DERPEMPKCS12 文本字符串或文件,这些格式通常用于配置 Web 服务器上的 TLS

简单来说:就是用 JSON 格式来表示一个密钥(公钥 / 私钥 / 对称密钥)

java 复制代码
    public static void main(String[] args) {
        SecretKey key = Jwts.SIG.HS256.key().build();
        SecretJwk jwk = Jwks.builder().key(key).idFromThumbprint().build();
        /** Output:
         *  {alg=HS256, kty=oct, k=<redacted>, kid=zRAW7muU4kX00ChWJugUSJPA5YoqTaH0lLCVECu-f5Y}
         */
    }

你可以通过构建 JwkParser 并使用其 parse 方法解析 JWK JSON 字符串来读取/解析 JWK

java 复制代码
    public static void main(String[] args) {
        String json = getJwkJsonString();
        Jwk<?> jwk = Jwks.parser()
                .build()
                .parse(json);                 // 实际上解析 JSON 数据

        Key key = jwk.toKey();            // 转换为 Java Key 实例
    }
工具类

创建一个JWT工具类,方便使用:

java 复制代码
public class JwtUtil {

    // 生成方式:Jwts.SIG.HS256.key().build() → base64
    private static final String SECRET_KEY_BASE64 = "你自己生成的32字节base64密钥";

    // 过期时间:1天
    private static final long EXPIRATION = 1000 * 60 * 60 * 24;

    // 获取密钥对象
    private SecretKey getSecretKey() {
        return Keys.hmacShaKeyFor(java.util.Base64.getDecoder().decode(SECRET_KEY_BASE64));
    }

    /**
     * 生成 JWT Token
     */
    public String generateToken(String userId) {
        return Jwts.builder()
                .subject(userId)
                .issuedAt(new Date())
                .expiration(new Date(System.currentTimeMillis() + EXPIRATION))
                .signWith(getSecretKey())
                .compact();
    }

    /**
     * 生成带自定义声明的 Token
     */
    public String generateToken(String userId, Map<String, Object> claims) {
        return Jwts.builder()
                .subject(userId)
                .claims(claims)
                .issuedAt(new Date())
                .expiration(new Date(System.currentTimeMillis() + EXPIRATION))
                .signWith(getSecretKey())
                .compact();
    }

    /**
     * 解析 Token 获取 Claims
     */
    public Claims parseToken(String token) {
        return Jwts.parser()
                .verifyWith(getSecretKey())
                .build()
                .parseSignedClaims(token)
                .getPayload();
    }

    /**
     * 获取用户ID
     */
    public String getUserId(String token) {
        return parseToken(token).getSubject();
    }

    /**
     * 验证 Token 是否有效
     */
    public boolean validateToken(String token) {
        try {
            parseToken(token);
            return true;
        } catch (ExpiredJwtException e) {
            System.out.println("Token 已过期");
        } catch (MalformedJwtException e) {
            System.out.println("Token 格式错误");
        } catch (SecurityException | SignatureException e) {
            System.out.println("签名无效");
        } catch (UnsupportedJwtException e) {
            System.out.println("不支持的 Token 类型");
        } catch (IllegalArgumentException e) {
            System.out.println("Token 参数为空");
        }
        return false;
    }
}

Auth0 java-jwt

引入java-jwt 依赖,对比JJWT引入的少:

xml 复制代码
<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>4.4.0</version>
</dependency>
创建 JWT

要创建一个 JWT ,你需要使用 JWT.create() 方法,然后添加必要的声明(claims),最后使用你的密钥和算法进行签名。示例代码如下:

java 复制代码
public class Test {
    public static void main(String[] args) {
        // 创建 JWT
        String token = JWT.create()
//                .withHeader(map) 自定义Header,可以传map或json
                .withIssuer("auth0")         // 发行人
                .withSubject("1234567890")    // 主题
                .withAudience("app_audience") // 观众
                .withIssuedAt(new Date())     // 发行时间
                .withExpiresAt(new Date(System.currentTimeMillis() + 3600 * 1000)) // 过期时间(1小时后)
//                .withPayload(map) 自定义payload,可以传map或json
//                .withClaim("test", "test") 自定义payload,指定name和value
                .sign(Algorithm.HMAC256("123345")); // 使用 HMAC256 算法和密钥进行签名,默认用该参数的加密类型当作Header
        System.out.println(token);
        /** Output
         * eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
         * .eyJpc3MiOiJhdXRoMCIsInN1YiI6IjEyMzQ1Njc4OTAiLCJhdWQiOiJhcHBfYXVkaWVuY2UiLCJpYXQiOjE3MzYxMjc0ODksImV4cCI6MTczNjEzMTA4OX0
         * .O4VD5DOn77tzPUZabPhPhkFKW9vS31dTpTAOPmkRv2o
         */
    }
}

然后去官网可以查看解密的数据,如图所示:

验证JWT

验证一个 JWT ,我们需要创建一个 JWTVerifier 实例,该实例定义了验证 JWT 时所需的条件(如算法和密钥、发行人、观众等)。然后,我们使用 verify() 方法对 JWT 进行验证,并返回一个 DecodedJWT 实例,该实例包含了 JWT 中的所有声明。

java 复制代码
public class Test {
    public static void main(String[] args) {
        // 验证 JWT
        JWTVerifier verifier = JWT.require(Algorithm.HMAC256("123456"))
                .build(); // 可重用验证器实例
        DecodedJWT jwt = verifier.verify("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9" +
                ".eyJpc3MiOiJhdXRoMCIsInN1YiI6IjEyMzQ1Njc4OTAiLCJhdWQiOiJhcHBfYXVkaWVuY2UiLCJpYXQiOjE3MzYxMjc0ODksImV4cCI6MTczNjEzMTA4OX0" +
                ".O4VD5DOn77tzPUZabPhPhkFKW9vS31dTpTAOPmkRv2o");
        System.out.println("Verified Token: " + jwt);
        System.out.println("Verified Token: " + jwt.getId());
        System.out.println("Verified Token: " + jwt.getIssuer());
        /** Output
         *  Verified Token: com.auth0.jwt.JWTDecoder@2aece37d
         *  Verified Token: null
         *  Verified Token: auth0
         */
    }
}

如果验证过程中出现密钥不匹配或者token过期都会抛出异常,如图所示:

工具类

创建一个JWT工具类,方便使用:

java 复制代码
public class JwtUtil {
    // 生成方式:Jwts.SIG.HS256.key().build() → base64
    private static final String SECRET_KEY_BASE64 = "你自己生成的32字节base64密钥";

    // 过期时间:1天
    private static final long EXPIRATION = 1000 * 60 * 60 * 24;

    // 获取密钥对象
    private SecretKey getSecretKey() {
        return Keys.hmacShaKeyFor(java.util.Base64.getDecoder().decode(SECRET_KEY_BASE64));
    }

    /**
     * 生成 JWT Token
     */
    public String generateToken(String userId) {
        return JWT.create()
                .withSubject(userId)
                .withIssuedAt(new Date())     // 发行时间
                .withExpiresAt(new Date(System.currentTimeMillis() + EXPIRATION))
                .sign(Algorithm.HMAC256(SECRET_KEY_BASE64));
    }

    /**
     * 生成带自定义声明的 Token
     */
    public String generateToken(String userId, Map<String, Object> claims) {
        return JWT.create()
                .withSubject(userId)
                .withIssuedAt(new Date())     // 发行时间
                .withExpiresAt(new Date(System.currentTimeMillis() + EXPIRATION))
                .withPayload(claims)
                .sign(Algorithm.HMAC256(SECRET_KEY_BASE64));
    }

    /**
     * 解析 Token 获取 Claims
     */
    public DecodedJWT parseToken(String token) {
        return JWT.require(Algorithm.HMAC256(SECRET_KEY_BASE64)).build().verify(token);
    }

    /**
     * 获取用户ID
     */
    public String getUserId(String token) {
        return parseToken(token).getSubject();
    }

    /**
     * 验证 Token 是否有效
     */
    public boolean validateToken(String token) {
        try {
            parseToken(token);
            return true;
        } catch (Exception e) {
            System.out.println("Token 失效");
        }
        return false;
    }
}

JJWT和Auth0对比

经过介绍也能知道JJWT 提供的功能非常多,Auth0就比较简洁。

Spring Boot 项目最推荐JJWT ,链式 builder ,和 Spring 风格统一。

JJWT 提供的功能非常多,Auth0比较简洁。

一般接入 Auth0 第三方登录才用 Auth0 ,除非你非常喜欢极简 API,不想引入多个依赖。

相关推荐
IT爱学堂13 小时前
Go开发疑难杂症终结者通关指南
后端
大勇前进13 小时前
从10分钟到10秒:一个真实慢查询的SQL Server优化全记录
后端
站大爷IP13 小时前
被Python异步坑了三年,才搞懂await和create_task的区别
后端
大白8013 小时前
你的数据库密码还在用明文?SQL Server 凭据管理的 3 种现代方案
后端
凤山老林13 小时前
Spring Boot 集成 ShardingSphere-Encrypt 实现字段级实时脱敏
java·spring boot·后端·数据脱敏
vipxieliang13 小时前
ValidX v1.2.0 更新日志
java·后端
Zane199413 小时前
只改一个方向的引用,循环引用就能立刻被回收?一文讲透 weakref 弱引用
后端·python
长大198813 小时前
窗口函数用不好反而更慢?SQL Server中OVER子句的4个性能陷阱
后端
大黄评测13 小时前
MERGE语句有Bug?SQL Server官方不推荐的背后真相与替代方案
后端
长大198813 小时前
TempDB 爆满导致系统卡死?SQL Server TempDB 瓶颈诊断与根治方案
后端