学完涨工资的技巧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;
    }
}
相关推荐
珹洺4 小时前
Java-Spring入门指南(二十六)Android Studio下载与安装
java·spring·android studio
JAVA学习通4 小时前
JDK高版本特性总结与ZGC实践
java·jvm·算法
cxyxiaokui0014 小时前
JDK 动态代理 vs CGLIB:原理、区别与 Spring AOP 底层揭秘
java·后端·spring
代码充电宝4 小时前
LeetCode 算法题【中等】189. 轮转数组
java·算法·leetcode·职场和发展·数组
我命由我123454 小时前
PDFBox - PDDocument 与 byte 数组、PDF 加密
java·服务器·前端·后端·学习·java-ee·pdf
EF@蛐蛐堂4 小时前
WUJIE VS QIANKUN 微前端框架选型(一)
前端·vue.js·微服务·架构
花哥码天下4 小时前
Oracle下载JDK无需登录
java·开发语言
摇滚侠5 小时前
Spring Boot 3零基础教程,yml语法细节,笔记16
java·spring boot·笔记
RunningShare5 小时前
高可用架构实战:SpringBoot+MongoDB构建AI原生应用
spring boot·mongodb·架构