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

相关推荐
KK溜了溜了1 小时前
JAVA-springboot log日志
java·spring boot·logback
我命由我123452 小时前
Spring Boot 项目集成 Redis 问题:RedisTemplate 多余空格问题
java·开发语言·spring boot·redis·后端·java-ee·intellij-idea
面朝大海,春不暖,花不开2 小时前
Spring Boot消息系统开发指南
java·spring boot·后端
hshpy2 小时前
setting up Activiti BPMN Workflow Engine with Spring Boot
数据库·spring boot·后端
jay神2 小时前
基于Springboot的宠物领养系统
java·spring boot·后端·宠物·软件设计与开发
不知几秋3 小时前
Spring Boot
java·前端·spring boot
howard20054 小时前
5.4.2 Spring Boot整合Redis
spring boot·整合redis
TracyCoder1234 小时前
接口限频算法:漏桶算法、令牌桶算法、滑动窗口算法
spring boot·spring·限流
饮长安千年月4 小时前
JavaSec-SpringBoot框架
java·spring boot·后端·计算机网络·安全·web安全·网络安全
考虑考虑5 小时前
Jpa中的@ManyToMany实现增删
spring boot·后端·spring