🍉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生成流程。

相关推荐
2401_857439692 小时前
Spring Boot新闻推荐系统:用户体验优化
spring boot·后端·ux
进击的女IT3 小时前
SpringBoot上传图片实现本地存储以及实现直接上传阿里云OSS
java·spring boot·后端
杨半仙儿还未成仙儿4 小时前
Spring框架:Spring Core、Spring AOP、Spring MVC、Spring Boot、Spring Cloud等组件的基本原理及使用
spring boot·spring·mvc
一 乐4 小时前
学籍管理平台|在线学籍管理平台系统|基于Springboot+VUE的在线学籍管理平台系统设计与实现(源码+数据库+文档)
java·数据库·vue.js·spring boot·后端·学习
IT学长编程6 小时前
计算机毕业设计 二手图书交易系统的设计与实现 Java实战项目 附源码+文档+视频讲解
java·spring boot·毕业设计·课程设计·毕业论文·计算机毕业设计选题·二手图书交易系统
艾伦~耶格尔7 小时前
Spring Boot 三层架构开发模式入门
java·spring boot·后端·架构·三层架构
man20177 小时前
基于spring boot的篮球论坛系统
java·spring boot·后端
Java探秘者7 小时前
Maven下载、安装与环境配置详解:从零开始搭建高效Java开发环境
java·开发语言·数据库·spring boot·spring cloud·maven·idea
苹果醋38 小时前
大模型实战--FastChat一行代码实现部署和各个组件详解
java·运维·spring boot·mysql·nginx
潘多编程8 小时前
Spring Boot与GraphQL:现代化API设计
spring boot·后端·graphql