基于 spring-security-oauth2-authorization-server 0.2.3
客户端使用 client_id和client_secret 进行认证,且使用Jwt加密请求时,加密的方式 JwsAlgorithm
有:
@Bean
public RegisteredClientRepository jdbcRegisteredClientRepository(JdbcTemplate jdbcTemplate, PasswordEncoder passwordEncoder) {
RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
// 客户端id 需要唯一
.clientId("apple")
// 客户端密码
.clientSecret(passwordEncoder.encode("apple_secret"))
// 默认基于 basic 的方式 认证 client_id和client_secret
// Base64.encode(string(client_id:clientSecret))
//!!必须
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_JWT)
// 授权码
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
// 刷新token
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
// 客户端模式
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
// 密码模式, 目前没有实现
.authorizationGrantType(AuthorizationGrantType.PASSWORD)
//废弃
.authorizationGrantType(AuthorizationGrantType.IMPLICIT)
// 重定向url
.redirectUri("https://www.baidu.com")
// 客户端申请的作用域,也可以理解这个客户端申请访问用户的哪些信息,比如:获取用户信息,获取用户照片等
.scope("user_info")
.scope("user_photo")
.clientSettings(ClientSettings.builder()
//必须!!
.tokenEndpointAuthenticationSigningAlgorithm(MacAlgorithm.HS256)
.requireAuthorizationConsent(true)
.build())
.tokenSettings(TokenSettings.builder()
//使用透明方式,默认是 OAuth2TokenFormat SELF_CONTAINED
.accessTokenFormat(OAuth2TokenFormat.REFERENCE)
// 授权码的有效期
.accessTokenTimeToLive(Duration.ofHours(1))
// 刷新token的有效期
.refreshTokenTimeToLive(Duration.ofDays(3))
.reuseRefreshTokens(true).build())
.build();
JdbcRegisteredClientRepository jdbcRegisteredClientRepository = new JdbcRegisteredClientRepository(jdbcTemplate);
jdbcRegisteredClientRepository.save(registeredClient);
return jdbcRegisteredClientRepository;
}
client_secret_jwt 三个要点:
参数 | 参数值 |
---|---|
scope | 域 |
grant_type | 授权类型:client_credentials |
client_assertion_type | jwt type: urn:ietf:params:oauth:client-assertion-type:jwt-bearer |
client_assertion | Jwt token 值 |
client_id | client_id |
client_assertion 参数生成 jwt 请求参数:
public static String gen() throws Exception {
String clientId = "apple";
// 我这里使用 BCryptPasswordEncoder 加密,和 授权服务器中 保存 RegisteredClient 的secret 一致!!
String clientSecret = "apple_secret";
//algorithm 必须是 HMACSHA256
SecretKeySpec secretKeySpec = new SecretKeySpec(clientSecret.getBytes(StandardCharsets.UTF_8),
"HmacSHA256");
JWSSigner signer = new MACSigner(secretKeySpec);
//subject, issuer, audience, expirationTime 这四个参数是必须的
//服务器那边会校验
JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
//主题
.subject(clientId)
//签发人
.issuer(clientId)
//受众 必填
.audience("http://localhost:9000")
//过期时间
.expirationTime(new Date(System.currentTimeMillis() + 60 * 60 * 60 * 1000))
.build();
SignedJWT signedJWT = new SignedJWT(new JWSHeader(JWSAlgorithm.HS256), claimsSet);
signedJWT.sign(signer);
String token = signedJWT.serialize();
System.out.println(token);
return token;
}
如生成:
yJhbGciOiJIUzI1NiJ9.eyJpc3MioiaHR0cDpcL1wvbG9jYWxob3N0OjkwMDAiLCJleHAiOjE2NTE3ODMyNzV9.rpVurCOh
请求 token:
curl --location --request POST 'http://127.0.0.1:9000/oauth2/token?scope=user_info&grant_type=client_credentials&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer&client_assertion=eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJhcHBsZSIsInN1YiI6ImFwcGxlIiwiYXVkIjoiaHR0cDpcL1wvbG9jYWxob3N0OjkwMDAiLCJleHAiOjE2NTE3ODMyNzV9.rpVurCOhnxqJy83jLGwmxwMdAxvTiB76cvfmeYEryX4=&client_id=apple'
{
"access_token": "oSORyWslthM9irNVnLxHB8bX....4P",
"scope": "user_info",
"token_type": "Bearer",
"expires_in": 3599
}
urn:ietf:params:oauth:client-assertion-type:jwt-bearer
文章开头要求 client_secret_jwt 要遵守的三个点,原因是什么呢?
public final class JwtClientAssertionAuthenticationProvider implements AuthenticationProvider {
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
OAuth2ClientAuthenticationToken clientAuthentication =
(OAuth2ClientAuthenticationToken) authentication;
if (!JWT_CLIENT_ASSERTION_AUTHENTICATION_METHOD.equals(clientAuthentication.getClientAuthenticationMethod())) {
return null;
}
String clientId = clientAuthentication.getPrincipal().toString();
RegisteredClient registeredClient = this.registeredClientRepository.findByClientId(clientId);
if (registeredClient == null) {
throwInvalidClient(OAuth2ParameterNames.CLIENT_ID);
}
if (!registeredClient.getClientAuthenticationMethods().contains(ClientAuthenticationMethod.PRIVATE_KEY_JWT) &&
!registeredClient.getClientAuthenticationMethods().contains(ClientAuthenticationMethod.CLIENT_SECRET_JWT)) {
throwInvalidClient("authentication_method");
}
if (clientAuthentication.getCredentials() == null) {
throwInvalidClient("credentials");
}
Jwt jwtAssertion = null;
JwtDecoder jwtDecoder = this.jwtClientAssertionDecoderFactory.createDecoder(registeredClient);
try {
jwtAssertion = jwtDecoder.decode(clientAuthentication.getCredentials().toString());
} catch (JwtException ex) {
throwInvalidClient(OAuth2ParameterNames.CLIENT_ASSERTION, ex);
}
// Validate the "code_verifier" parameter for the confidential client, if available
this.codeVerifierAuthenticator.authenticateIfAvailable(clientAuthentication, registeredClient);
ClientAuthenticationMethod clientAuthenticationMethod =
registeredClient.getClientSettings()
.getTokenEndpointAuthenticationSigningAlgorithm() instanceof SignatureAlgorithm ?
ClientAuthenticationMethod.PRIVATE_KEY_JWT :
ClientAuthenticationMethod.CLIENT_SECRET_JWT;
return new OAuth2ClientAuthenticationToken(registeredClient, clientAuthenticationMethod, jwtAssertion);
}
}
看 jwtClientAssertionDecoderFactory.createDecoder 方法
static {
Map<JwsAlgorithm, String> mappings = new HashMap<>();
mappings.put(MacAlgorithm.HS256, "HmacSHA256");
mappings.put(MacAlgorithm.HS384, "HmacSHA384");
mappings.put(MacAlgorithm.HS512, "HmacSHA512");
JCA_ALGORITHM_MAPPINGS = Collections.unmodifiableMap(mappings);
}
@Override
public JwtDecoder createDecoder(RegisteredClient registeredClient) {
Assert.notNull(registeredClient, "registeredClient cannot be null");
return this.jwtDecoders.computeIfAbsent(registeredClient.getId(), (key) -> {
NimbusJwtDecoder jwtDecoder = buildDecoder(registeredClient);
jwtDecoder.setJwtValidator(createJwtValidator(registeredClient));
return jwtDecoder;
});
}
private static NimbusJwtDecoder buildDecoder(RegisteredClient registeredClient) {
JwsAlgorithm jwsAlgorithm = registeredClient.getClientSettings().getTokenEndpointAuthenticationSigningAlgorithm();
if (jwsAlgorithm instanceof SignatureAlgorithm) {
String jwkSetUrl = registeredClient.getClientSettings().getJwkSetUrl();
if (!StringUtils.hasText(jwkSetUrl)) {
OAuth2Error oauth2Error = new OAuth2Error(OAuth2ErrorCodes.INVALID_CLIENT,
JWT_CLIENT_AUTHENTICATION_ERROR_URI);
throw new OAuth2AuthenticationException(oauth2Error);
}
return NimbusJwtDecoder.withJwkSetUri(jwkSetUrl).jwsAlgorithm((SignatureAlgorithm) jwsAlgorithm).build();
}
if (jwsAlgorithm instanceof MacAlgorithm) {
String clientSecret = registeredClient.getClientSecret();
if (!StringUtils.hasText(clientSecret)) {
OAuth2Error oauth2Error = new OAuth2Error(OAuth2ErrorCodes.INVALID_CLIENT)
throw new OAuth2AuthenticationException(oauth2Error);
}
SecretKeySpec secretKeySpec = new SecretKeySpec(clientSecret.getBytes(StandardCharsets.UTF_8),
JCA_ALGORITHM_MAPPINGS.get(jwsAlgorithm));
return NimbusJwtDecoder.withSecretKey(secretKeySpec).macAlgorithm((MacAlgorithm) jwsAlgorithm).build();
}
OAuth2Error oauth2Error = new OAuth2Error(OAuth2ErrorCodes.INVALID_CLIENT,
throw new OAuth2AuthenticationException(oauth2Error);
}
这里涉及两种 jwt 签名方式:
在创建客户端的 jwt 要求有字段必有, 是因为解析jwt的时候增加了校验:
private static OAuth2TokenValidator<Jwt> createJwtValidator(RegisteredClient registeredClient) {
String clientId = registeredClient.getClientId();
return new DelegatingOAuth2TokenValidator<>(
new JwtClaimValidator<>(JwtClaimNames.ISS, clientId::equals),
new JwtClaimValidator<>(JwtClaimNames.SUB, clientId::equals),
new JwtClaimValidator<>(JwtClaimNames.AUD, containsProviderAudience()),
new JwtClaimValidator<>(JwtClaimNames.EXP, Objects::nonNull),
new JwtTimestampValidator()
);
}
再看看 jwtDecoder.decode
方法, 委托给 NimbusJwtDecoder
public final class NimbusJwtDecoder implements JwtDecoder {
@Override
public Jwt decode(String token) throws JwtException {
JWT jwt = parse(token);
if (jwt instanceof PlainJWT) {
this.logger.trace("Failed to decode unsigned token");
throw new BadJwtException("Unsupported algorithm of " + jwt.getHeader().getAlgorithm());
}
Jwt createdJwt = createJwt(token, jwt);
return validateJwt(createdJwt);
}
private JWT parse(String token) {
try {
return JWTParser.parse(token);
}
catch (Exception ex) {
this.logger.trace("Failed to parse token", ex);
throw new BadJwtException(String.format(DECODING_ERROR_MESSAGE_TEMPLATE, ex.getMessage()), ex);
}
}
private Jwt createJwt(String token, JWT parsedJwt) {
try {
// Verify the signature
JWTClaimsSet jwtClaimsSet = this.jwtProcessor.process(parsedJwt, null);
Map<String, Object> headers = new LinkedHashMap<>(parsedJwt.getHeader().toJSONObject());
Map<String, Object> claims = this.claimSetConverter.convert(jwtClaimsSet.getClaims());
// @formatter:off
return Jwt.withTokenValue(token)
.headers((h) -> h.putAll(headers))
.claims((c) -> c.putAll(claims))
.build();
// @formatter:on
}
catch (RemoteKeySourceException ex) {
this.logger.trace("Failed to retrieve JWK set", ex);
if (ex.getCause() instanceof ParseException) {
throw new JwtException(String.format(DECODING_ERROR_MESSAGE_TEMPLATE, "Malformed Jwk set"), ex);
}
throw new JwtException(String.format(DECODING_ERROR_MESSAGE_TEMPLATE, ex.getMessage()), ex);
}
catch (JOSEException ex) {
this.logger.trace("Failed to process JWT", ex);
throw new JwtException(String.format(DECODING_ERROR_MESSAGE_TEMPLATE, ex.getMessage()), ex);
}
catch (Exception ex) {
this.logger.trace("Failed to process JWT", ex);
if (ex.getCause() instanceof ParseException) {
throw new BadJwtException(String.format(DECODING_ERROR_MESSAGE_TEMPLATE, "Malformed payload"), ex);
}
throw new BadJwtException(String.format(DECODING_ERROR_MESSAGE_TEMPLATE, ex.getMessage()), ex);
}
}
}
很简单:
jwtProcessor.process
方法:
//DefaultJWTProcessor.java
@Override
public JWTClaimsSet process(final JWT jwt, final C context)
throws BadJOSEException, JOSEException {
if (jwt instanceof SignedJWT) {
return process((SignedJWT)jwt, context);
}
if (jwt instanceof EncryptedJWT) {
return process((EncryptedJWT)jwt, context);
}
if (jwt instanceof PlainJWT) {
return process((PlainJWT)jwt, context);
}
// Should never happen
throw new JOSEException("Unexpected JWT object type: " + jwt.getClass());
}
如果 jwt 不是摘要签名方式, 就需要提供 jwk 的获取方式, 如 private_key_jwt 方式,就需要配置:
@Bean
public JWKSource<SecurityContext> jwkSource() {
RSAKey rsaKey = Jwks.generateRsa();
JWKSet jwkSet = new JWKSet(rsaKey);
return (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
}
在 JWEDecryptionKeySelector 使用到:
public class JWEDecryptionKeySelector<C extends SecurityContext>
extends AbstractJWKSelectorWithSource<C> implements JWEKeySelector<C> {
@Override
public List<Key> selectJWEKeys(final JWEHeader jweHeader, final C context)
throws KeySourceException {
if (! jweAlg.equals(jweHeader.getAlgorithm()) || ! jweEnc.equals(jweHeader.getEncryptionMethod())) {
// Unexpected JWE alg or enc
return Collections.emptyList();
}
JWKMatcher jwkMatcher = createJWKMatcher(jweHeader);
// 这里获取 JWKSouce!!
List<JWK> jwkMatches = getJWKSource().get(new JWKSelector(jwkMatcher), context);
List<Key> sanitizedKeyList = new LinkedList<>();
for (Key key: KeyConverter.toJavaKeys(jwkMatches)) {
if (key instanceof PrivateKey || key instanceof SecretKey) {
sanitizedKeyList.add(key);
} // skip public keys
}
return sanitizedKeyList;
}
}