开放标准(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,不想引入多个依赖。

相关推荐
回家路上绕了弯4 小时前
Git worktree 终极指南:告别分支切换烦恼,实现多分支并行开发
git·后端
@atweiwei4 小时前
用 Rust 构建agent的 LLM 应用的高性能框架
开发语言·后端·rust·langchain·eclipse·llm·agent
skilllite作者4 小时前
Spec + Task 作为「开发协议层」:Rust 大模型辅助的标准化、harness 化与可回滚
开发语言·人工智能·后端·安全·架构·rust·rust沙箱
懒得起名_yyf4 小时前
Http---详细格式介绍
后端
程序员cxuan4 小时前
今天看到很多人讨论 Linux 终于要接受 AI 提交的代码了,我的第一反应是,真的吗?作为喷 AI 最狠的祖师爷到底咋看这件事儿?
后端·程序员
何陋轩4 小时前
GitHub Copilot深度使用指南:手把手教你在IDEA中榨干AI生产力
人工智能·后端
fish20264 小时前
车载日志限流稽查系统
后端
云边有个稻草人4 小时前
NFS 环境 KES 安装 Operation not permitted 报错排查 + 最佳实践
后端
ZC跨境爬虫4 小时前
Scrapy多级请求实战:5sing伴奏网爬取踩坑与优化全记录(JSON提取+Xpath解析)
爬虫·scrapy·html·json