如何在Spring Security中优雅实现client_secret_jwt客户端认证
问题:实现client_secret_jwt客户端认证的简洁配置方式
背景
我需要实现client_secret_jwt客户端认证,Spring Security文档提到:
若仅需自定义请求参数,可通过为OAuth2ClientCredentialsGrantRequestEntityConverter.setParametersConverter()提供自定义Converter<OAuth2ClientCredentialsGrantRequest, MultiValueMap<String, String>>来完全覆盖请求发送的参数。这通常比直接构造RequestEntity更简单。
但我不清楚该在哪里进行这项配置。
当前实现方式
我目前的解决方案是复制ClientCredentialsOAuth2AuthorizedClientProvider的代码,创建新类JWKClientCredentialsOAuth2AuthorizedClientProvider,并修改accessTokenResponseClient的初始化逻辑:
class JWKClientCredentialsOAuth2AuthorizedClientProvider : OAuth2AuthorizedClientProvider{ private var accessTokenResponseClient: OAuth2AccessTokenResponseClient<OAuth2ClientCredentialsGrantRequest> = DefaultClientCredentialsTokenResponseClient() private var clockSkew = Duration.ofSeconds(60) private var clock = Clock.systemUTC() constructor() { val jwkResolver = Function<ClientRegistration, JWK?> { clientRegistration: ClientRegistration -> if (clientRegistration.clientAuthenticationMethod == ClientAuthenticationMethod.CLIENT_SECRET_JWT) { val secretKey = SecretKeySpec( clientRegistration.clientSecret.toByteArray(StandardCharsets.UTF_8), "HmacSHA256" ) OctetSequenceKey.Builder(secretKey) .keyID(UUID.randomUUID().toString()) .build() } null } val requestEntityConverter = OAuth2ClientCredentialsGrantRequestEntityConverter() requestEntityConverter.addParametersConverter( NimbusJwtClientAuthenticationParametersConverter(jwkResolver) ) (accessTokenResponseClient as DefaultClientCredentialsTokenResponseClient).setRequestEntityConverter(requestEntityConverter) } /** * Attempt to authorize (or re-authorize) the * [client][OAuth2AuthorizationContext.getClientRegistration] in the provided * `context`. Returns `null` if authorization (or re-authorization) is not * supported, e.g. the client's [ authorization grant type][ClientRegistration.getAuthorizationGrantType] is not [ client_credentials][AuthorizationGrantType.CLIENT_CREDENTIALS] OR the [access][OAuth2AuthorizedClient.getAccessToken] is not expired. * @param context the context that holds authorization-specific state for the client * @return the [OAuth2AuthorizedClient] or `null` if authorization (or * re-authorization) is not supported */ @Nullable override fun authorize(context: OAuth2AuthorizationContext): OAuth2AuthorizedClient? { Assert.notNull(context, "context cannot be null") val clientRegistration = context.clientRegistration if (AuthorizationGrantType.CLIENT_CREDENTIALS != clientRegistration.authorizationGrantType) { return null } val authorizedClient = context.authorizedClient if (authorizedClient != null && !hasTokenExpired(authorizedClient.accessToken)) { // If client is already authorized but access token is NOT expired than no // need for re-authorization return null } // As per spec, in section 4.4.3 Access Token Response // https://tools.ietf.org/html/rfc6749#section-4.4.3 // A refresh token SHOULD NOT be included. // // Therefore, renewing an expired access token (re-authorization) // is the same as acquiring a new access token (authorization). val clientCredentialsGrantRequest = OAuth2ClientCredentialsGrantRequest( clientRegistration ) val tokenResponse = getTokenResponse(clientRegistration, clientCredentialsGrantRequest) return OAuth2AuthorizedClient( clientRegistration, context.principal.name, tokenResponse.accessToken ) } private fun getTokenResponse( clientRegistration: ClientRegistration, clientCredentialsGrantRequest: OAuth2ClientCredentialsGrantRequest ): OAuth2AccessTokenResponse { return try { accessTokenResponseClient.getTokenResponse(clientCredentialsGrantRequest) } catch (ex: OAuth2AuthorizationException) { throw ClientAuthorizationException(ex.error, clientRegistration.registrationId, ex) } } private fun hasTokenExpired(token: OAuth2Token): Boolean { return clock.instant().isAfter(token.expiresAt!!.minus(clockSkew)) } /** * Sets the client used when requesting an access token credential at the Token * Endpoint for the `client_credentials` grant. * @param accessTokenResponseClient the client used when requesting an access token * credential at the Token Endpoint for the `client_credentials` grant */ fun setAccessTokenResponseClient( accessTokenResponseClient: OAuth2AccessTokenResponseClient<OAuth2ClientCredentialsGrantRequest> ) { Assert.notNull(accessTokenResponseClient, "accessTokenResponseClient cannot be null") this.accessTokenResponseClient = accessTokenResponseClient } /** * Sets the maximum acceptable clock skew, which is used when checking the * [access token][OAuth2AuthorizedClient.getAccessToken] expiry. The default is * 60 seconds. * * * * An access token is considered expired if * `OAuth2AccessToken#getExpiresAt() - clockSkew` is before the current time * `clock#instant()`. * @param clockSkew the maximum acceptable clock skew */ fun setClockSkew(clockSkew: Duration) { Assert.notNull(clockSkew, "clockSkew cannot be null") Assert.isTrue(clockSkew.seconds >= 0, "clockSkew must be >= 0") this.clockSkew = clockSkew } /** * Sets the [Clock] used in [Instant.now] when checking the access * token expiry. * @param clock the clock */ fun setClock(clock: Clock) { Assert.notNull(clock, "clock cannot be null") this.clock = clock } }
同时修改了AuthorizedClientManager的配置:
@Bean fun authorizedClientManager(clientRegistrationRepository : ClientRegistrationRepository, oAuth2AuthorizedClientService: OAuth2AuthorizedClientService): OAuth2AuthorizedClientManager { val authorizedClientProvider = JWKClientCredentialsOAuth2AuthorizedClientProvider() val authorizedClientManager = AuthorizedClientServiceOAuth2AuthorizedClientManager( clientRegistrationRepository, oAuth2AuthorizedClientService ) authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider) return authorizedClientManager }
我认为当前实现过于繁琐,希望找到更简洁、更优雅的配置方式。
内容的提问来源于stack exchange,提问作者SaebaAtHot
相关产品推荐
相关产品推荐

