You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.29 22:35:19