nimbus-jose-jwt你都会吗?

nimbus-jose-jwt 概念

nimbus-jose-jwt是指一个受欢迎的JWT开源库,基于Apache 2.0开源协议,支持所有标准的签名(JWS)和加密(JWE)算法。它提供了一套简单易用的API来处理JWT,适用于对称加密和非对称加密场景。今天整理下这个框架相关的知识点,为授权服务器和资源服务器做铺垫。

🔐 什么是 JWK?

JWK(JSON Web Key) :一个 JSON 格式的密钥(如一个 RSA 公钥)

  1. JWK 是一个 JSON 对象,用来表示一个加密密钥。
  2. 它可以是公钥或私钥(但通常只包含公钥,出于安全考虑)。

例如,一个 RSA 公钥的 JWK 可能长这样:

json 复制代码
{
  "kty": "RSA",
  "n": "0vx7...Aw",
  "e": "AQAB",
  "kid": "2011-04-29",
  "alg": "RS256"
}
  1. kty: 密钥类型(如 RSA、EC)
  2. n, e: RSA 的模数和指数(公钥参数)
  3. kid: 密钥 ID,用于标识某个特定密钥
  4. alg: 推荐使用的算法(如 RS256)

📦 什么是 JWKSet?

JWKSet 就是一个包含多个 JWK 的集合

根据标准(RFC 7517),JWKSet 是一个 JSON 对象,其核心是 "keys" 字段,值是一个 JWK 数组。 下面这段话摘自

A JWK Set is a JSON object that represents a set of JWKs. The JSON object MUST have a "keys" member, with its value being an array of JWKs. This JSON object MAY contain whitespace and/or line breaks.

结构如下:

json 复制代码
{
  "keys": [
    { /* 第一个 JWK */ },
    { /* 第二个 JWK */ },
    ...
  ],
  "custom-member": "自定义字段也允许"
}

🌐 实际应用场景

最常见的使用场景是 OAuth 2.0 / OpenID Connect 协议中的 JWKS(JSON Web Key Set)端点

比如:
https://accounts.google.com/.well-known/jwks.json

访问这个 URL,你会得到 Google 发布的所有当前有效的公钥(JWKSet),像这样:

json 复制代码
{
  "keys": [
    {
      "kty": "RSA",
      "alg": "RS256",
      "use": "sig",
      "kid": "abc123",
      "n": "0vx7...",
      "e": "AQAB"
    },
    {
      "kty": "RSA",
      "alg": "RS256",
      "use": "sig",
      "kid": "def456",
      "n": "1a2b...",
      "e": "AQAB"
    }
  ]
}
🧩 用途举例:验证 JWT
  1. 你收到一个 JWT,header 中有 "kid": "abc123"
  2. 你去 JWKS 端点获取所有公钥。
  3. 找到 kidabc123 的 JWK。
  4. 使用该 JWK 中的公钥验证 JWT 的签名是否有效。

🧩 一、signedJWT 是什么?

ini 复制代码
public static void testSignedJwt() {
    String token = "eyJraWQiOiIzMzc3MWZiNS1iODdjLTQ3Y2QtYWI3ZS0zOGI4YmQ2OGE2NGEiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ1c2VyIiwiYXVkIjoib2lkYy1jbGllbnQiLCJuYmYiOjE3NTYzNTY4NjQsInNjb3BlIjpbIm9wZW5pZCJdLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjkwMDAiLCJleHAiOjE3NTYzNTcxNjQsImlhdCI6MTc1NjM1Njg2NCwianRpIjoiMzBhNmZmMjctZTUzZi00ZTY3LWFmYjItNDgyMzU1MDAzOTFiIn0.YAX4kyf9R-2NWscRAXWD3gzV_ptkOrB3_-V63K9GnN1O_UEQCKeNk5o6bFGTyRYMjhJdFgDFnpAiHsa1Qe4uB0f3wMIB2Tx7Ob13tZPL9asZCgk9jwIbZojVI9jEBJkTVPHrjTvIhQ0cogQLxB10Pbw2PgPGZ-wVriP647STDi6QRGnHVoMB1u2JrQ16EEBz-8AtapnlOhqmriDh78ar1_na_SHaGj1f4t4IC-GZsbFE-sl3kJJW88aoOJE3OrI3-05-s7lcnHZ0CJE7j8Te7vIcwV4i1sID0hRaYdiWccfIcXUQZPUYrj30h5sXU0UT9lr4PMS_xWZuLID2d6_y0w";
    try {
        // 这一步只是解析 JWT 字符串,把它变成 Java 对象。 它不会验证签名是否有效!
        // 你现在可以安全地读取它的内容,但要相信它是"合法"的,必须经过后续的验证流程。
        SignedJWT signedJWT = SignedJWT.parse(token);
        System.out.println(signedJWT.getPayload().toJSONObject());

        JWSHeader header = signedJWT.getHeader();
        JWSAlgorithm algorithm = header.getAlgorithm();
        System.out.println(algorithm.getName());
        String keyID = header.getKeyID();
        System.out.println(keyID);

        JWTClaimsSet jwtClaimsSet = signedJWT.getJWTClaimsSet();
        System.out.println(jwtClaimsSet.getIssuer());
        System.out.println(jwtClaimsSet.getSubject());
        System.out.println(jwtClaimsSet.getAudience());
        System.out.println(jwtClaimsSet.getIssueTime());
        System.out.println(jwtClaimsSet.getExpirationTime());
        System.out.println(jwtClaimsSet.getNotBeforeTime());
        System.out.println(jwtClaimsSet.getListClaim("scope"));

        Base64URL signature = signedJWT.getSignature();
        System.out.println(signature.toString());

    } catch (ParseException e) {
        throw new RuntimeException(e);
    }
}

调用SignedJWT.parse(token) Nimbus 库会:

  1. . 拆分字符串
  2. 解码 Header 和 Payload 的 Base64URL
  3. 把它们转换成结构化的 Java 对象
  4. 返回一个 SignedJWT 实例

🔍 JWTParser 是什么?

JWTParserNimbus JOSE + JWT 库中的一个工具类(utility class) ,它的作用是:

根据传入的 JWT 字符串,自动判断它是哪种类型的 JWT(如:Signed、Encrypted、Plaintext),并返回对应的解析结果。

🧩 它能解析哪几种 JWT?

JWT 标准支持三种类型:

类型 说明 示例格式
1. Plain JWT 未签名、未加密的 JWT(明文) header.payload.
2. Signed JWT 签名的 JWT(最常见) header.payload.signature
3. Encrypted JWT (JWE) 加密的 JWT protectedHeader.encryptedKey.iv.ciphertext.authenticationTag

JWTParser.parse(token) 的强大之处在于:

👉 你不需要事先知道这个 token 是哪种类型,它会自动识别!

ini 复制代码
public static void testJwtParser() {
    String token = "eyJraWQiOiIzMzc3MWZiNS1iODdjLTQ3Y2QtYWI3ZS0zOGI4YmQ2OGE2NGEiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ1c2VyIiwiYXVkIjoib2lkYy1jbGllbnQiLCJuYmYiOjE3NTYzNTY4NjQsInNjb3BlIjpbIm9wZW5pZCJdLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjkwMDAiLCJleHAiOjE3NTYzNTcxNjQsImlhdCI6MTc1NjM1Njg2NCwianRpIjoiMzBhNmZmMjctZTUzZi00ZTY3LWFmYjItNDgyMzU1MDAzOTFiIn0.YAX4kyf9R-2NWscRAXWD3gzV_ptkOrB3_-V63K9GnN1O_UEQCKeNk5o6bFGTyRYMjhJdFgDFnpAiHsa1Qe4uB0f3wMIB2Tx7Ob13tZPL9asZCgk9jwIbZojVI9jEBJkTVPHrjTvIhQ0cogQLxB10Pbw2PgPGZ-wVriP647STDi6QRGnHVoMB1u2JrQ16EEBz-8AtapnlOhqmriDh78ar1_na_SHaGj1f4t4IC-GZsbFE-sl3kJJW88aoOJE3OrI3-05-s7lcnHZ0CJE7j8Te7vIcwV4i1sID0hRaYdiWccfIcXUQZPUYrj30h5sXU0UT9lr4PMS_xWZuLID2d6_y0w";
    try {
        // 这一步只是解析 JWT 字符串,把它变成 Java 对象。 它不会验证签名是否有效!
        // 你现在可以安全地读取它的内容,但要相信它是"合法"的,必须经过后续的验证流程。
        JWT jwt = JWTParser.parse(token);
        if (jwt instanceof SignedJWT) {
            System.out.println("this is a signed token");
        }

        JWSHeader header = (JWSHeader) jwt.getHeader();
        JWSAlgorithm algorithm = header.getAlgorithm();
        System.out.println(algorithm.getName());
        String keyID = header.getKeyID();
        System.out.println(keyID);

        JWTClaimsSet jwtClaimsSet = jwt.getJWTClaimsSet();
        System.out.println(jwtClaimsSet.getIssuer());
        System.out.println(jwtClaimsSet.getSubject());
        System.out.println(jwtClaimsSet.getAudience());
        System.out.println(jwtClaimsSet.getIssueTime());
        System.out.println(jwtClaimsSet.getExpirationTime());
        System.out.println(jwtClaimsSet.getNotBeforeTime());
        System.out.println(jwtClaimsSet.getListClaim("scope"));

        String  originalString= jwt.getParsedString();
        System.out.println(originalString);

    } catch (ParseException e) {
        throw new RuntimeException(e);
    }
}

🧠 jwtProcessor 到底是什么?

它的类型是:

DefaultJWTProcessor<SecurityContext>

你可以把它理解为:

🔍 一个"全自动 JWT 验证机器人"

给它一个 JWT,它会完成:找密钥 → 验签名 → 校验时间 → 返回数据

🔍 三、process() 方法到底做了什么?

当你调用:

java 复制代码
jwtProcessor.process(parsedJwt, null)

它会执行以下几步:

✅ 步骤 1:解析 JWT 的 header

parsedJwt.getHeader() 中读取:

  • "alg":签名算法(如 RS256
  • "kid":密钥 ID(如 "abc123"

✅ 步骤 2:使用 JWSKeySelector 查找匹配的公钥

  • 根据 algkid,去 JWKSource(比如远程 JWKS)中查找对应的 JWK
  • 把 JWK 转成 RSAPublicKey 或其他类型的公钥

🔁 如果找不到 kid 对应的密钥,会抛异常

✅ 步骤 3:验证签名

  • 使用找到的公钥 + 指定算法(如 RS256)
  • 对 JWT 的 header.payload 部分重新计算签名
  • 和原始 JWT 的 signature 部分对比

❌ 如果不一致 → 签名无效 → 抛 BadJWSException

✅ 步骤 4:验证标准时间字段

自动检查:

  • exp(过期时间):是否已过期?
  • nbf(生效时间):是否还没到生效时间?
  • iat(签发时间):是否合理?

❌ 如果 exp 已过 → 抛 ExpiredJWTException

✅ 步骤 5:返回可信的 JWTClaimsSet

如果以上全部通过,说明这个 JWT 是:

  • ✅ 签名合法(没被篡改)
  • ✅ 时间有效(没过期)

于是返回一个 可信的 JWTClaimsSet ,里面包含了 payload 中的所有用户信息(如 sub, name, email 等)。

🧩 什么是 JWKSource?("图书馆")

在校验jwt的时候,一定要用到对应的公钥(JWK)才可以进行校验,那么公钥在哪里呢?

JWKSource定义

java 复制代码
public interface JWKSource<C extends SecurityContext> {
    List<JWK> get(JWKSelector selector, C context) throws KeySourceException;
}

它是一个接口,代表一个"可以获取 JWK 的地方"。

✅ JWKSource它能做什么?

  • 提供一组 JWK(公钥)
  • 支持从不同地方加载:远程 URL、本地文件、内存、数据库等

✅ 常见实现类

实现类 来源 说明
RemoteJWKSet Nimbus 库 https://.../jwks.json 远程加载
ImmutableJWKSet Nimbus 库 从本地 JSON 字符串或对象加载,只读
自定义实现 你自己写 从数据库、缓存、配置中心加载

当调用get从JWKSource中获取一组JWK的时候 需要提供给一个参数叫JWKSelector

🔍 什么是 JWKSelector?("检索器")

JWKSelector 是一个"查询条件对象",用来告诉 JWKSource

"我要找什么样的公钥?"

ini 复制代码
public static void testJwkSource() throws MalformedURLException {
    String jwkSetUrl = "http://localhost:9000/oauth2/jwks";
    URL url = new URL(jwkSetUrl);



    JWKSource<SecurityContext> jwkSource = new RemoteJWKSet<>(url);
    JWKMatcher jwkMatcher = new JWKMatcher.Builder()
             .keyID("33771fb5-b87c-47cd-ab7e-38b8bd68a64a")
            .build();
    JWKSelector jwkSelector = new JWKSelector(jwkMatcher);
    try {
        List<JWK> jwks = jwkSource.get(jwkSelector, null);
        for (JWK jwk : jwks) {
            System.out.println(jwk.toJSONObject());
        }
    } catch (KeySourceException e) {
        throw new RuntimeException(e);
    }
}

jwkselector提供一个select方法用来选择匹配的jwk。

从下面的源码可以看出select方法参数是JWKSet,意思是给我一组jwk,我通过matcher去匹配,把所有符合条件的jwk返回 matcher是JWKMatcher类型,我们在构建JWKSelector的时候作为参数传递到构造器中,也就是说selector底层进行选择的时候需要依赖matcher。matcher定义了匹配机制

ini 复制代码
JWKMatcher jwkMatcher = new JWKMatcher.Builder()
         .keyID("33771fb5-b87c-47cd-ab7e-38b8bd68a64a")
        .build();
JWKSelector jwkSelector = new JWKSelector(jwkMatcher);
vbnet 复制代码
/**
 * Selects the keys from the specified JWK set according to the
 * matcher's criteria.
 *
 * @param jwkSet The JWK set. May be {@code null}.
 *
 * @return The selected keys, ordered by their position in the JWK set,
 *         empty list if none were matched or the JWK is {@code null}.
 */
public List<JWK> select(final JWKSet jwkSet) {

   List<JWK> selectedKeys = new LinkedList<>();

   if (jwkSet == null)
      return selectedKeys;

   for (JWK key: jwkSet.getKeys()) {

      if (matcher.matches(key)) {
         selectedKeys.add(key);
      }
   }

   return selectedKeys;
}

🧠 jwtProcessor的执行流程

上面我们提到过jwtProcessor的功能是: 给它一个 JWT,它会完成:找密钥 → 验签名 → 校验时间 → 返回数据

  1. 因为它要寻找秘钥(JWKSET),所以需要定义一个JWKSource
  2. 验签的时候还需要匹配符合条件的(JWK),那就需要JWKSelector,JWSVerificationKeySelector是一个专门用来进行签名校验时的key选择器,底层包装了JWKSelector。
  3. JWTClaimsSetVerifier是用来进行验证claims各项声明是否符合要求,比如时间过期啊,像下面这种就是将JWTClaimsSetVerifier置空,也就是不进行校验。springsecurity就采用了这种方式,因为springsecurity单独进行的校验,脱离了JOSE-JWT框架。

jwtProcessor.setJWTClaimsSetVerifier((claims, context) -> { });

ini 复制代码
public static void testProcessor() throws MalformedURLException {
    String jwkSetUrl = "http://localhost:9000/oauth2/jwks";
    URL url = new URL(jwkSetUrl);
    JWKSource<SecurityContext> jwkSource = new RemoteJWKSet<>(url);



    ConfigurableJWTProcessor<SecurityContext> jwtProcessor = new DefaultJWTProcessor<>();
    // Creates a new JWS verification key selector.  见名之意 专门用来进行签名验证的key选择器,底层会包装一个JWKSelector
    JWSVerificationKeySelector<SecurityContext> jwsVerificationKeySelector = new JWSVerificationKeySelector<>(JWSAlgorithm.RS256, jwkSource);
    jwtProcessor.setJWSKeySelector(jwsVerificationKeySelector);

    // Spring Security validates the claim set independent from Nimbus
    // 覆盖了默认的校验逻辑,这样就不会校验claims中的字段是否符合 比如是否过期
    jwtProcessor.setJWTClaimsSetVerifier((claims, context) -> {
    });

    // Verify the signature
    String token = "eyJraWQiOiIzMzc3MWZiNS1iODdjLTQ3Y2QtYWI3ZS0zOGI4YmQ2OGE2NGEiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ1c2VyIiwiYXVkIjoib2lkYy1jbGllbnQiLCJuYmYiOjE3NTYzNTY4NjQsInNjb3BlIjpbIm9wZW5pZCJdLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjkwMDAiLCJleHAiOjE3NTYzNTcxNjQsImlhdCI6MTc1NjM1Njg2NCwianRpIjoiMzBhNmZmMjctZTUzZi00ZTY3LWFmYjItNDgyMzU1MDAzOTFiIn0.YAX4kyf9R-2NWscRAXWD3gzV_ptkOrB3_-V63K9GnN1O_UEQCKeNk5o6bFGTyRYMjhJdFgDFnpAiHsa1Qe4uB0f3wMIB2Tx7Ob13tZPL9asZCgk9jwIbZojVI9jEBJkTVPHrjTvIhQ0cogQLxB10Pbw2PgPGZ-wVriP647STDi6QRGnHVoMB1u2JrQ16EEBz-8AtapnlOhqmriDh78ar1_na_SHaGj1f4t4IC-GZsbFE-sl3kJJW88aoOJE3OrI3-05-s7lcnHZ0CJE7j8Te7vIcwV4i1sID0hRaYdiWccfIcXUQZPUYrj30h5sXU0UT9lr4PMS_xWZuLID2d6_y0w";
    SignedJWT parsedJwt = null;
    try {
        parsedJwt = SignedJWT.parse(token);
    } catch (ParseException e) {
        throw new RuntimeException(e);
    }

    try {
        JWTClaimsSet jwtClaimsSet = jwtProcessor.process(parsedJwt, null);
        System.out.println(jwtClaimsSet.getIssuer());
        System.out.println(jwtClaimsSet.getSubject());
        System.out.println(jwtClaimsSet.getAudience());
        System.out.println(jwtClaimsSet.getIssueTime());
    } catch (BadJOSEException | JOSEException e) {
        throw new RuntimeException(e);
    }
}

总结

本文主要分享了nimbus-jose-jwt中一些常用类的用法和概念

  1. 什么是JWK
  2. 什么是JWKSET
  3. 什么是JWKSource
  4. 什么是JWKMatcher
  5. 什么是JWKSelector
  6. 什么是JWTProcessor

最后奉上一张图 希望大家了解jwt

相关推荐
用户3497979837226 分钟前
优化后端 10 万数据
后端
渣哥12 分钟前
没有 Optional,Java 程序员每天都在和 NullPointerException 打架
java
天天摸鱼的java工程师12 分钟前
Maven 能为我们解决什么问题?8 年 Java 开发:从踩坑到实战(附核心配置代码)
后端
智_永无止境13 分钟前
Java集合操作:Apache Commons Collections4启示录
java·apache
AAA修煤气灶刘哥17 分钟前
网络编程原来这么好懂?TCP 三次握手像约会,UDP 像发朋友圈
后端·python·网络协议
悟能不能悟26 分钟前
java去图片水印的方法
java·人工智能·python
ftpeak34 分钟前
Rust Web开发指南 第六章(动态网页模板技术-MiniJinja速成教程)
开发语言·前端·后端·rust·web
编码浪子41 分钟前
趣味学Rust基础篇(数据类型)
开发语言·后端·rust
南囝coding1 小时前
Claude Code 官方内部团队最佳实践!
前端·后端·程序员
从零开始学习人工智能1 小时前
PDFMathTranslate:让科学PDF翻译不再难——技术原理与实践指南
java·开发语言·pdf