学完涨工资的技巧2:Spring Authorization Server如何签发JWTToken

在oauth2场景中 授权服务器在完成授权请求后,会给客户端签发accessToken。那么这个过程是如何的呢?

一.顶级抽象 JwtEncoder

java 复制代码
package org.springframework.security.oauth2.jwt;

@FunctionalInterface
public interface JwtEncoder {

   /**
    * Encode the JWT to it's compact claims representation format.
    * @param parameters the parameters containing the JOSE header and JWT Claims Set
    * @return a {@link Jwt}
    * @throws JwtEncodingException if an error occurs while attempting to encode the JWT
    */
   Jwt encode(JwtEncoderParameters parameters) throws JwtEncodingException;

}

springsecurity为oauth2提供了一个规范JwtEncoder,是一个函数式接口,正如接口文档描述的那样,接口的作用就是把JWT编码成它的紧凑格式

JWT 的紧凑格式 长这样:

text 复制代码
xxxx.yyyy.zzzz

可能有人会疑问 从返回值上看返回的是JWT对象,不是一个紧凑格式的字符串。但是我们看一下JWT的继承关系:

JWT继承于Oauth2Token,Oauth2Token也是springsecurity提供的接口规范,说明JWT可以通过getTokenValue方法返回字符串格式的token。

typescript 复制代码
package org.springframework.security.oauth2.core;

public interface OAuth2Token {

   /**
    * Returns the token value.
    * @return the token value
    */
   String getTokenValue();

   /**
    * Returns the time at which the token was issued.
    * @return the time the token was issued or {@code null}
    */
   @Nullable
   default Instant getIssuedAt() {
      return null;
   }

   /**
    * Returns the expiration time on or after which the token MUST NOT be accepted.
    * @return the token expiration time or {@code null}
    */
   @Nullable
   default Instant getExpiresAt() {
      return null;
   }

}

二.JwtEncoder的具体实现NimbusJwtEncoder

SpringSecurity也提供了默认的实现NimbusJwtEncoder,通过名称可以看出底层是对开源nimbus-jose-jwt的包装。我们姑且不去深入研究底层的实现,我们把目光停留在框架层面。

下面是NimbusJwtEncoder中的具体实现,我们发现如果要想进行jwt的生成,在框架层面必须先提供JwtEncoderParameters。

ini 复制代码
@Override
public Jwt encode(JwtEncoderParameters parameters) throws JwtEncodingException {
   Assert.notNull(parameters, "parameters cannot be null");

   JwsHeader headers = parameters.getJwsHeader();
   if (headers == null) {
      headers = DEFAULT_JWS_HEADER;
   }
   JwtClaimsSet claims = parameters.getClaims();

   JWK jwk = selectJwk(headers);
   headers = addKeyIdentifierHeadersIfNecessary(headers, jwk);

   String jws = serialize(headers, claims, jwk);

   return new Jwt(jws, claims.getIssuedAt(), claims.getExpiresAt(), headers.getHeaders(), claims.getClaims());
}

JwtEncoderParameters也是springsecurity为oauth2设计的一个类,里面包装了JwsHeader和 JwtClaimsSet

kotlin 复制代码
package org.springframework.security.oauth2.jwt;

public final class JwtEncoderParameters {

   private final JwsHeader jwsHeader;

   private final JwtClaimsSet claims;

   private JwtEncoderParameters(JwsHeader jwsHeader, JwtClaimsSet claims) {
      this.jwsHeader = jwsHeader;
      this.claims = claims;
   }
}

因为jwt的三个部分分别是header.claims.signature,所以用 JwtEncoderParameters封装了前两个部分,供JwtEncoder使用。

三.JwtClaimsSet 的构建

JWT Claims Set:JWT 的载荷(payload),比如:

json 复制代码
{ "sub": "123", "name": "张三", "exp": 1735689600 }

我们使用JwtClaimsSet来封装JWT的payload,例如如下代码: 通过构建者模式build了一个JwtClaimsSet对象。

ini 复制代码
JwtClaimsSet.Builder claimsBuilder = JwtClaimsSet.builder();
Instant issuedAt = Instant.now();
Instant expiresAt = issuedAt.plus(30, ChronoUnit.MINUTES);
claimsBuilder
        .issuer("http://localhost:9000")
        .subject("admin")
        // 全称是 Audience,中文意思是"受众"或"目标接收者"
        // 告诉接收方 ------ 这个 token 是不是发给你的
        .audience(Collections.singletonList("client01"))
         // 这个 JWT 是什么时候签发的。  "iat" = "Issued At"
        .issuedAt(issuedAt)
         // 这个 JWT 什么时候过期。  "exp" = "Expiration Time"
        .expiresAt(expiresAt)
        .id(UUID.randomUUID().toString());
// 这个 JWT 在此时间之前不能被接受处理。 在这个时间点之前,任何接收方都应拒绝使用该 token;
claimsBuilder.notBefore(issuedAt);
// 多个scopes
claimsBuilder.claim("scope", List.of("read", "write"));

JwtClaimsSet claims = claimsBuilder.build();

JwtClaimsSet里面就是个map,将所有的payload通过key value的形式存储在map中

四.JwsHeader的构建

JOSE Header:JWT 的头部,比如:

json 复制代码
 { "alg": "HS256", "typ": "JWT" }

springsecurity使用 JwsHeader来封装JWT的头部 如下:

ini 复制代码
JwsAlgorithm jwsAlgorithm = SignatureAlgorithm.RS256;
JwsHeader.Builder jwsHeaderBuilder = JwsHeader.with(jwsAlgorithm);
JwsHeader jwsHeader = jwsHeaderBuilder.build();

JwsHeader的底层也是有个map属性,用来保存头部中所有指定的键值对,如下图:

五.详细说说NimbusJwtEncoder

1. NimbusJwtEncoder 是什么?

  • 是 Spring Security 中用于生成 JWT 的一个具体实现。
  • 基于 Nimbus JOSE + JWT 库。
  • 负责把 JwtClaimsSetJwsHeader 打包并签名成一个 JWT 字符串。

NimbusJwtEncoder要想实现对jwt的签名,必须使用到秘钥信息。在nimbus中 JWK 用来表示秘钥。

我们在创建NimbusJwtEncoder的时候需要提供jwkSetJWKSet 就是密钥的集合。

ini 复制代码
NimbusJwtEncoder nimbusJwtEncoder = new NimbusJwtEncoder(new ImmutableJWKSet<>(jwkSet));

你是在告诉 encoder:

"我有一组密钥(jwkSet),你从中选一个合适的来签名 JWT。"

2.使用非对称密钥(RSA)

如果我想用RSA非对称加密方式,那么在创建NimbusJwtEncoder的时候,你需要告诉encoder,在你的jwkSet集合中有一个是RSA.

我们可以先生成一堆KeyPair

ini 复制代码
private static KeyPair generateRsaKey() {
    KeyPair keyPair;
    try {
        KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
        keyPairGenerator.initialize(2048);
        keyPair = keyPairGenerator.generateKeyPair();
    }
    catch (Exception ex) {
        throw new IllegalStateException(ex);
    }
    return keyPair;
}

然后将其包装成JWK,RSAKey就是Nimbus对jwk的一种实现。也就是说RSAKey继承了JWK

ini 复制代码
KeyPair keyPair = generateRsaKey();

RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();


// package com.nimbusds.jose.jwk;
RSAKey rsaKey = new RSAKey.Builder(publicKey)
        .privateKey(privateKey)
        .keyID("ilabx-oauth2-20250830")
        .build();
System.out.println(rsaKey.toJSONString());
JWKSet jwkSet = new JWKSet(rsaKey);

上面的代码将生成的私钥包装成RSAKey,最后放到了jwkset集合中,被encoder使用

五.最后一步 生成jwt

ini 复制代码
JwtEncoderParameters jwtEncoderParameters = JwtEncoderParameters.from(jwsHeader, claims);
NimbusJwtEncoder nimbusJwtEncoder = new NimbusJwtEncoder(new ImmutableJWKSet<>(jwkSet));

// 签发 JWT 格式的 Access Token,但签名需要 私钥(private key)
Jwt jwt = nimbusJwtEncoder.encode(jwtEncoderParameters);

System.out.println(jwt);
System.out.println(jwt.getTokenValue());

六. 完整demo代码如下

java 复制代码
public class JwtTest {


    public static void main(String[] args) {

        JwtClaimsSet.Builder claimsBuilder = JwtClaimsSet.builder();
        Instant issuedAt = Instant.now();
        Instant expiresAt = issuedAt.plus(30, ChronoUnit.MINUTES);
        claimsBuilder
                .issuer("http://localhost:9000")
                .subject("admin")
                // 全称是 Audience,中文意思是"受众"或"目标接收者"
                // 告诉接收方 ------ 这个 token 是不是发给你的
                .audience(Collections.singletonList("client01"))
                 // 这个 JWT 是什么时候签发的。  "iat" = "Issued At"
                .issuedAt(issuedAt)
                 // 这个 JWT 什么时候过期。  "exp" = "Expiration Time"
                .expiresAt(expiresAt)
                .id(UUID.randomUUID().toString());
        // 这个 JWT 在此时间之前不能被接受处理。 在这个时间点之前,任何接收方都应拒绝使用该 token;
        claimsBuilder.notBefore(issuedAt);
        // 多个scopes
        claimsBuilder.claim("scope", List.of("read", "write"));

        JwtClaimsSet claims = claimsBuilder.build();

        System.out.println(claims);
        System.out.println(claims);


        JwsAlgorithm jwsAlgorithm = SignatureAlgorithm.RS256;
        JwsHeader.Builder jwsHeaderBuilder = JwsHeader.with(jwsAlgorithm);
        JwsHeader jwsHeader = jwsHeaderBuilder.build();

        JwtEncoderParameters jwtEncoderParameters = JwtEncoderParameters.from(jwsHeader, claims);

        KeyPair keyPair = generateRsaKey();

        RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
        RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();


        // package com.nimbusds.jose.jwk;
        RSAKey rsaKey = new RSAKey.Builder(publicKey)
                .privateKey(privateKey)
                .keyID("ilabx-oauth2-20250830")
                .build();
        System.out.println(rsaKey.toJSONString());
        JWKSet jwkSet = new JWKSet(rsaKey);


        NimbusJwtEncoder nimbusJwtEncoder = new NimbusJwtEncoder(new ImmutableJWKSet<>(jwkSet));

        // 签发 JWT 格式的 Access Token,但签名需要 私钥(private key)
        Jwt jwt = nimbusJwtEncoder.encode(jwtEncoderParameters);

        System.out.println(jwt);
        System.out.println(jwt.getTokenValue());

    }

    private static KeyPair generateRsaKey() {
        KeyPair keyPair;
        try {
            KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
            keyPairGenerator.initialize(2048);
            keyPair = keyPairGenerator.generateKeyPair();
        }
        catch (Exception ex) {
            throw new IllegalStateException(ex);
        }
        return keyPair;
    }
}
相关推荐
咩咩啃树皮8 小时前
第40篇:Vue3组件化开发精讲——组件拆分、复用、父子通信、工程化架构
java·前端·架构
鱟鲥鳚8 小时前
Spring Boot 集成 LangChain4j:从模型调用到 Tool Calling(Demo版)
java·spring boot
大模型码小白10 小时前
【Python零基础教程】继承、多态与魔法函数:面向对象编程三大核心特性详解
java·大数据·开发语言·人工智能·python·ai编程
腾渊信息科技公司10 小时前
Spring Boot对接MES实战:视觉检测数据自动同步方案
java·人工智能·spring boot·后端·计算机视觉·ai·软件需求
爱笑的源码基地11 小时前
高并发 Redis 缓存门诊HIS系统源码,含财务统计药房进销存
java·程序·门诊系统·诊所系统·云诊所源码
wuqingshun31415911 小时前
TCP超时重传机制是为了解决什么问题?
java
莫逸风14 小时前
【AgentScope 2.0】 0. 学习指南
java·llm·agent·agentscope
隔窗听雨眠14 小时前
Spring Boot在云原生时代的编程范式革新研究
spring boot·后端·云原生
z1234567898614 小时前
2026最新两款AI编程工具深度对比实测
java·数据库·ai编程
yaoxin52112315 小时前
470. Java 反射 - Member 接口与 AccessFlag
java·开发语言·python