参考文档:spring-authorization-server【版本1.2.2】
问题
在spring-authorization-server官方文档中提供了JWK Set Endpoint相关介绍,此端点主要返回JWK Set ,此JWK Set包含了授权服务提供的所有公钥集,具体可通过访问端点:/oauth2/jwks来获取。
但获取到公钥集如何来运用呢?如何来验签的呢?这是此次我需要了解的问题。
在了解基于private_key_jwt的客户端身份验证方法时,我了解到授权服务是如何通过客户端暴露的JWK Set Endpoint来获取到公钥集,然后通过拿到的公钥集又是如何验签的,解决了我的疑问。
代码梳理
通过梳理源码,经层层调用,我定位到几个主要的方法。
首先是JwtClientAssertionDecoderFactory#createDecoder(registeredClient)的调用:
...
private final Map<String, JwtDecoder> jwtDecoders = new ConcurrentHashMap<>();@Override
public JwtDecoder createDecoder(RegisteredClient registeredClient) {Assert.notNull(registeredClient, "registeredClient cannot be null");return this.jwtDecoders.computeIfAbsent(registeredClient.getId(), (key) -> {NimbusJwtDecoder jwtDecoder = buildDecoder(registeredClient);// 设置相关的校验逻辑参考JwtClientAssertionDecoderFactory#defaultJwtValidatorFactory方法 jwtDecoder.setJwtValidator(this.jwtValidatorFactory.apply(registeredClient));return jwtDecoder;});
}
此方法用来往jwtDecoders对象中维护NimbusJwtDecoder,相同的客户端ID存在则返回NimbusJwtDecoder对象,不存在则创建。
再说NimbusJwtDecoder的创建,在创建NimbusJwtDecoder时会执行NimbusJwtDecoder#processor()方法的调用,为jwkSource(此处的jwkSource为RemoteJWKSet对象)对象提供了远程调用JWK Set Endpoint的地址和方法。这样就有了可以获取JWK Set的条件了。
JWTProcessor<SecurityContext> processor() {// 通过jwkSetRetriever来远程调用获取JWK SetResourceRetriever jwkSetRetriever = new RestOperationsResourceRetriever(this.restOperations);// jwkSetUri为客户端暴露的JWK Set端点地址String jwkSetUri = this.jwkSetUri.apply(this.restOperations);// 此处获取到的jwkSource为RemoteJWKSetJWKSource<SecurityContext> jwkSource = jwkSource(jwkSetRetriever, jwkSetUri);ConfigurableJWTProcessor<SecurityContext> jwtProcessor = new DefaultJWTProcessor<>();// 创建JWSVerificationKeySelector对象并给属性jwkSource、jwsAlgs初始化值,jwsAlgs包含一个对象JWSAlgorithm("RS256", Requirement.RECOMMENDED)jwtProcessor.setJWSKeySelector(jwsKeySelector(jwkSource));// Spring Security validates the claim set independent from NimbusjwtProcessor.setJWTClaimsSetVerifier((claims, context) -> {});this.jwtProcessorCustomizer.accept(jwtProcessor);return jwtProcessor;
}
然后通过调用RemoteJWKSet#updateJWKSetFromURL方法来获取jwkSet。此处的jwkSet对象对应到指定算法的key集合,例如如果Key Type对应“RSA”,那么返回的jwkSet对象为RSAKey对象集。
private JWKSet updateJWKSetFromURL()throws RemoteKeySourceException {Resource res;try {res = jwkSetRetriever.retrieveResource(jwkSetURL);} catch (IOException e) {throw new RemoteKeySourceException("Couldn't retrieve remote JWK set: " + e.getMessage(), e);}JWKSet jwkSet;try {jwkSet = JWKSet.parse(res.getContent());} catch (java.text.ParseException e) {throw new RemoteKeySourceException("Couldn't parse remote JWK set: " + e.getMessage(), e);}jwkSetCache.put(jwkSet);return jwkSet;
}
然后再把jwkSet中所有的JWK对象的publicKey获取到,通过SignedJWT#verify来验签。
...
// 获取所有的publicKey集
List<? extends Key> keyCandidates = selectKeys(signedJWT.getHeader(), claimsSet, context);if (keyCandidates == null || keyCandidates.isEmpty()) {throw new BadJOSEException("Signed JWT rejected: Another algorithm expected, or no matching key(s) found");
}ListIterator<? extends Key> it = keyCandidates.listIterator();while (it.hasNext()) {// 根据算法返回JWSVerifier对象,例如“RS256”返回RSASSAVerifier对象JWSVerifier verifier = getJWSVerifierFactory().createJWSVerifier(signedJWT.getHeader(), it.next());if (verifier == null) {continue;}// 根据算法获取Signature对象,例如“RS256”对应 Signature.getInstance("SHA256withRSA"),然后对verifier设置publicKey,最后进行验签final boolean validSignature = signedJWT.verify(verifier);if (validSignature) {return verifyClaims(claimsSet, context);}if (! it.hasNext()) {// No more keys to try outthrow new BadJWSException("Signed JWT rejected: Invalid signature");}
}
...
上面是跟踪代码摘取的部分代码,相对比较乱,其实如果针对上面的调用把主要的代码逻辑摘取出来就可以构建自己的获取JWK Set和验签的逻辑。
自定义方法
远程调用获取JWK Set
/*** 根据JWK Set Endpoint地址获取JWK集合* * @param jwksUrl JWK Set Endpoint地址* @return JWK Set*/
public static JWKSet loadJWKSByUrl(String jwksUrl) throws IOException, ParseException {URL jwkSetURL = new URL(jwksUrl);ResourceRetriever jwkSetRetriever = new DefaultResourceRetriever(RemoteJWKSet.resolveDefaultHTTPConnectTimeout(),RemoteJWKSet.resolveDefaultHTTPReadTimeout(),RemoteJWKSet.resolveDefaultHTTPSizeLimit());Resource res = jwkSetRetriever.retrieveResource(jwkSetURL);JWKSet jwkSet = JWKSet.parse(res.getContent());return jwkSet;}
验签
/*** 根据公钥验证JWT(JSON WEB TOKEN)** @param jwksUrl JWK Set Endpoint地址* @param jwt 要验证的JWT(JSON WEB TOKEN)* @return 验证结果,true正确,false不正确*/public static boolean verifyJWT(String jwksUrl, String jwt) throws ParseException, IOException, JOSEException {Base64URL[] parts = JOSEObject.split(jwt);SignedJWT signedJWT = new SignedJWT(parts[0], parts[1], parts[2]);JWSHeader jwsHeader = JWSHeader.parse(parts[0].decodeToString());JWKMatcher jwkMatcher = JWKMatcher.forJWSHeader(jwsHeader);JWKSelector jwkSelector = new JWKSelector(jwkMatcher);List<JWK> jwkMatches = jwkSelector.select(loadJWKSByUrl(jwksUrl));// 获取公钥List<Key> sanitizedKeyList = new LinkedList<>();for (Key key : KeyConverter.toJavaKeys(jwkMatches)) {if (key instanceof PublicKey || key instanceof SecretKey) {sanitizedKeyList.add(key);}}ListIterator<? extends Key> it = sanitizedKeyList.listIterator();JWSVerifierFactory jwsVerifierFactory = new DefaultJWSVerifierFactory();while (it.hasNext()) {JWSVerifier verifier = jwsVerifierFactory.createJWSVerifier(jwsHeader, it.next());if (verifier == null) {continue;}final boolean validSignature = signedJWT.verify(verifier);return validSignature;}return false;}