架构版本
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吧,或者科大讯飞的讯飞星火也可以,工具类方法这事就找他们代劳了。
javapublic 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代工。
javapublic 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
的工具方法,这样也就完成了
javapublic 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
javapublic 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
javapublic 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生成流程。