🍉Spring Authorization Server (10) 授权服务的JWK密钥对生成

架构版本
Spring Boot 3.1
Spring Authorization Server 1.1.1
spring-cloud 2022.0.3
spring-cloud-alibaba 2022.0.0.0
完整代码👉watermelon-cloud

JWK 密钥生成

为什么JWK密钥对要固定生成?

目前代码中每次重启授权服务都会重新生成密钥对,Spring Authorization Server 默认的token加密就是 jwt,所以要做这个事情,如果不用jwt生成的token,自定义去扩展,要实现很多的东西,涉及到到服务也非常多,后面再单独讲讲不用jwt,自己扩展一个玩玩。

java 复制代码
  @Bean
  public JWKSource<SecurityContext> jwkSource() throws NoSuchAlgorithmException, IOException, InvalidKeySpecException {
      RSAKey rsaKey = JwtKeyUtil.generateRsa();
      JWKSet jwkSet = new JWKSet(rsaKey);
      return (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
  }

每次重启服务这个bean就重新实例化了,JwtKeyUtil.generateRsa() 是每次重新生成的,并不是读取指定的密钥对文件。

有这样一种场景:当我们客户端、资源服务都没有重启的情况下,授权服务器重启了,密钥对重新生成了,这个时候客户端或资源服务器到授权服务器访问 /oauth2/jwks ,token还是之前的token,那么密钥对都改变了,那肯定是解密失败的,这个时候就需要重新授权(生成token才行了)。

如何解决?

让它不变不就行了?

能想到有以下方案

1.将 JwtKeyUtil.generateRsa() 生成的 RSAKey 持久化(redis)

2.生成密钥对文件,然后读取。

选择方案2

原因是jwk密钥对这样的存在就不是需要变动的,直接选择文件,简单又好维护。

生成密钥对文件

win上打开小黑窗口

生成私钥:openssl genpkey -algorithm RSA -out private_key.pem -pkeyopt rsa_keygen_bits:2048

一定是2048 ,少于2048 Spring Authorization Server 是不允许的会抛异常。

提取公钥:openssl rsa -pubout -in private_key.pem -out public_key.pem
你的小黑窗口在哪个目录下打开的,就会生成在哪个目录下,我选择的实在resource的static目录下生成的。

读取密钥对

private_key.pem 里面长这样,我们只要中间的数据,将它转换为PrivateKey就可以了

text 复制代码
-----BEGIN PRIVATE KEY-----
MIIEwAIBADANBgkqhkiG9w0BAQEFAASCBKowggSmAgEAAoIBAQDr2aZI9TFmeS9F
3W9OqaGoG3iw+A97BKgEaQOn/4X6PQ3UCd1VN7F5lJ242Q8RM5I8sz8Ho70k+BUi
3JwL5ADbeer25OBK2PGVFUVm8mNd6z3Qy8EEYd1OcI5SI/lfKe1findUFG/0zrby
jDqYu10L/mAyPoPXAahOPJr1K4jpoaxDkb09rtuG7wkN7NnzRAbodemjNVxBybU1
BUv7MloT/jGXly+7YXA6FU8FuKLwWNrQ7dGn7V12gSQ6wBsyraYm1b8dZS2StjC6
8Lec7klEJCVjEhTGakG3pfltl4S4fyOA2t9i6pcovKJ7vx3vW5a1gYIjZQSZ41gA
9IXYAKthAgMBAAECggEBAI98L5UNTsuYCHGJwRDrVIUQiYGouMpPz+Q2+1l2tEzE
XihVBAm3Q0rDZp0xuN/vLxWsuzjrncPjBgDalDkLspXT+2XPYsFGcNsRQNLbviZC
Wq4vd7Mx0tDI210Ps8P4nwhUFjrZ4C7goB65v2ByBK6qSF3o+I6S3JEUf/WOUdJI
NZRfW4K00jw/CSgohUEP+wvgy6b1222e+PZPsNdXeVzgRKO3tua3GtR0t9gOZ2pB
8/i/+EtD6PrtSaR4A1zBXJ6S36G0l+O4Rw+2siytUwyt3ppnu4zMaDl0po668mJm
Ba5OpI69//2hSmea5IXkPQ3a70hoAY8CZyq/H5KBI0ECgYEA+3XR6ekzDuht/eWI
y/zp3emejTFYm8i4u5Op7uMU8ef2Fp/hH/QMEJpzcEXLaXq0UWK1g/jTKLDHdBUo
jm/GNofT8x8ewopz4mbf3EjCjWH+70udBp0RqmIurM01+NVRyVTebInmYe5HzxI7
gTmPI4Bc5QkSOW9vm6yGnSkak4kCgYEA8BuvK8MSW9u3pIcn94fyViwnN+zkgxGR
gEbxVmj4A75UcBSiWj5FR5zprw0+Q3pfVuN+mNmFzYJZ5NYzwP7KsCfze9il9ffJ
8Dn/1GrvejMUMdHxzA6SbcEVk3n+rFkpVRWiMZDgT7xDEKw8dqZLTeVxX1ISku1A
CpqKpV1OaxkCgYEAgALbyPt5jaZPkEhQmp/3IoxytaggVrYZLQygHselevy+L4hW
n+CqX61xBP/S7LCVqTTZ+QQr4vQTpYm76r8GJe6BvKvkCd9X3TLH1amIuVbg5EsW
9i3xt05iOoABcNqP1zGIRbLyAHrAPa8nccKulsEbCVHT4D9VjueGY+1v5RkCgYEA
rgeSzohELToyf9jKehoZ5qV4A4v7EJjSOgSxdaz9XlE8iEQcbIZH1qD/qzZRE72F
jsezAXxgA9Vf7IHo3xCNvmImk3QyzfW8cxbGu6KKUqrlDzsZI4rITS6uwcahdS/m
ylm0xnI4cvKENXhxFppvaFVN+AXXmpDFYyoiJbtcVDkCgYEArSjHW98MmSMXdEz0
XMs8TXZRVDnmm9RnQ+3d0SeXC5zZz4OZOLcx8GR9dwiTkJiK92h1rd7uvRVRD17L
VcLRPomXai6ny+ExKGPkwow6MfLDmORbUX8yIMwbgy7eI8t10lc4xwo1e4NSE5/K
sLHtHH4yXEOekXfHNBOUeQOUmfY=
-----END PRIVATE KEY-----    

我也不知道怎么转,直接问chatgpt吧,或者科大讯飞的讯飞星火也可以,工具类方法这事就找他们代劳了。

java 复制代码
public static PrivateKey getPrivateKey() throws IOException, NoSuchAlgorithmException, >InvalidKeySpecException {
       ClassPathResource resource = new ClassPathResource("static/private_key.pem");
       byte[] privateKeyBytes = FileCopyUtils.copyToByteArray(resource.getInputStream());
       String key = new String(privateKeyBytes, StandardCharsets.UTF_8);
       String privateStr = key
               .replace("-----BEGIN PRIVATE KEY-----", "")
               .replaceAll(System.lineSeparator(), "")
               .replace("-----END PRIVATE KEY-----", "");

       byte[] privateKeyDecodedBytes = Base64.getDecoder().decode(privateStr);

       PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(privateKeyDecodedBytes);
       KeyFactory keyFactory = KeyFactory.getInstance("RSA");
       PrivateKey privateKey = keyFactory.generatePrivate(keySpec);

       return privateKey;
   }   

public_key.pem 也同样找gpt代工。

java 复制代码
public static RSAPublicKey getPublicKey() throws IOException, NoSuchAlgorithmException, InvalidKeySpecException {
       ClassPathResource resource = new ClassPathResource("static/public_key.pem");
       byte[] publicKeyBytes = FileCopyUtils.copyToByteArray(resource.getInputStream());
       String key = new String(publicKeyBytes, StandardCharsets.UTF_8);
       String publicStr = key
               .replace("-----BEGIN PUBLIC KEY-----", "")
               .replaceAll(System.lineSeparator(), "")
               .replace("-----END PUBLIC KEY-----", "");

       KeyFactory keyFactory = KeyFactory.getInstance("RSA");
       byte[] publicKeyBase64Bytes = Base64.getDecoder().decode(publicStr);
       X509EncodedKeySpec publicKeySpec = new X509EncodedKeySpec(publicKeyBase64Bytes);
       PublicKey rsaPublicKey = keyFactory.generatePublic(publicKeySpec);
       return (RSAPublicKey) rsaPublicKey;
   }    

最后参考已有的封装一个 生成 RSAKey 的工具方法,这样也就完成了

java 复制代码
public static RSAKey generateRsa() throws IOException, NoSuchAlgorithmException, InvalidKeySpecException {
       // @formatter:off
       return new RSAKey.Builder(getPublicKey())
               .privateKey(getPrivateKey())
               .build();
       // @formatter:on
   }    

替换工具类方法就完事了😁

JWKSource<SecurityContext> 最后是用在哪里的呢?

这个我们就要从token生成这里找入口了,因为token的加密是和它有关系的。

SmsAuthenticationProvider 种这样一段代码

java 复制代码
/ ----- Access token -----
     OAuth2TokenContext tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.ACCESS_TOKEN).build();
     OAuth2Token generatedAccessToken = this.tokenGenerator.generate(tokenContext);    

tokenGenerator.generate 就是生成token的,我们跟进去能找到对应实现就是JwtGenerator

java 复制代码
public final class JwtGenerator implements OAuth2TokenGenerator<Jwt> {
  private final JwtEncoder jwtEncoder;
  private OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer;

  @Nullable
  @Override
  public Jwt generate(OAuth2TokenContext context) {
  	
  	if (this.jwtCustomizer != null) {
  		
  		JwtEncodingContext jwtContext = jwtContextBuilder.build();
  		this.jwtCustomizer.customize(jwtContext);
  	}

  	JwsHeader jwsHeader = jwsHeaderBuilder.build();
  	JwtClaimsSet claims = claimsBuilder.build();

  	Jwt jwt = this.jwtEncoder.encode(JwtEncoderParameters.from(jwsHeader, claims));

  	return jwt;
  }

  
}    

jwtEncoder 的实现继续往下 只有一个默认实现 NimbusJwtEncoder

java 复制代码
public final class NimbusJwtEncoder implements JwtEncoder {

  private final JWKSource<SecurityContext> jwkSource;
  /**
   * Constructs a {@code NimbusJwtEncoder} using the provided parameters.
   * @param jwkSource the {@code com.nimbusds.jose.jwk.source.JWKSource}
   */
  public NimbusJwtEncoder(JWKSource<SecurityContext> jwkSource) {
  	Assert.notNull(jwkSource, "jwkSource cannot be null");
  	this.jwkSource = jwkSource;
  }

  @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());
  }

  private JWK selectJwk(JwsHeader headers) {
  	List<JWK> jwks;
  	try {
  		JWKSelector jwkSelector = new JWKSelector(createJwkMatcher(headers));
  		jwks = this.jwkSource.get(jwkSelector, null);
  	}
  	catch (Exception ex) {
  		throw new JwtEncodingException(String.format(ENCODING_ERROR_MESSAGE_TEMPLATE,
  				"Failed to select a JWK signing key -> " + ex.getMessage()), ex);
  	}

  	if (jwks.size() > 1) {
  		throw new JwtEncodingException(String.format(ENCODING_ERROR_MESSAGE_TEMPLATE,
  				"Found multiple JWK signing keys for algorithm '" + headers.getAlgorithm().getName() + "'"));
  	}

  	if (jwks.isEmpty()) {
  		throw new JwtEncodingException(
  				String.format(ENCODING_ERROR_MESSAGE_TEMPLATE, "Failed to select a JWK signing key"));
  	}

  	return jwks.get(0);
  }
}    

NimbusJwtEncoder 就用到了 JWKSource<SecurityContext> ,jwt生成token也是在这里完成的 ,后面如果想扩展,可以参考jwt的token生成流程。

相关推荐
indexsunny1 小时前
互联网大厂Java求职面试实战:核心技术与业务场景解析
java·spring boot·redis·微服务·kafka·互联网大厂·面试技巧
程序猿大波1 小时前
基于java,SpringBoot和Vue餐饮公司食堂管理系统设计
java·vue.js·spring boot
wuyaolong0071 小时前
Spring Boot 3.4 正式发布,结构化日志!
java·spring boot·后端
a5629916193 小时前
【springboot】Spring 官方抛弃了 Java 8!新idea如何创建java8项目
java·spring boot·spring
编程小风筝5 小时前
如何用redission实现springboot的分布式锁?
spring boot·分布式·后端
码喽7号5 小时前
Springboot学习六:MybatisPlus的多表查询以及分页查询
java·spring boot·学习
不吃香菜学java6 小时前
苍穹外卖-新增菜品需求分析
java·spring boot·spring·tomcat·maven·ssm
智能工业品检测-奇妙智能7 小时前
开源知识库平台有哪些
服务器·人工智能·spring boot·开源·openclaw·奇妙智能
冬天豆腐7 小时前
Springcloud,Nacos管理,打jar包后,启动报错
java·spring cloud·maven·jar
计算机学姐8 小时前
基于SpringBoot的中药材店铺管理系统
java·vue.js·spring boot·后端·spring·tomcat·推荐算法