如何在Spring Boot+Keycloak中用Token Exchange实现身份模拟?
实现Keycloak Direct Naked Impersonation的Spring Boot OAuth2方案
核心思路
Direct Naked Impersonation的本质是先用Client Credentials获取服务自身的访问令牌,再将该令牌作为subject_token发起Token Exchange请求,换取目标用户的身份令牌。我们可以通过组合Spring OAuth2的原生组件串联这两个流程,无需自定义认证类。
步骤1:配置双Client Registration
在application.yml中配置两个客户端注册:一个用于Client Credentials授权,另一个用于Token Exchange授权(复用同一Keycloak客户端信息):
spring: security.oauth2.client: registration: keycloak-client-creds: client-id: '${SPRING_OAUTH_CLIENT_ID:service-s}' client-secret: '${SPRING_OAUTH_CLIENT_SECRET}' authorization-grant-type: client_credentials provider: keycloak keycloak-token-exchange: client-id: '${SPRING_OAUTH_CLIENT_ID:service-s}' client-secret: '${SPRING_OAUTH_CLIENT_SECRET}' authorization-grant-type: urn:ietf:params:oauth:grant-type:token-exchange provider: keycloak provider: keycloak: issuer-uri: '${SPRING_OAUTH_ISSUER_URI}'
步骤2:编写WebClient配置代码
通过组合Spring OAuth2的授权管理器和Token响应客户端,实现两个流程的串联:
@Bean fun legacyServerGraphQLClient( legacyServerProperties: LegacyServerProperties, clientRegistrationRepository: InMemoryReactiveClientRegistrationRepository, clientService: ReactiveOAuth2AuthorizedClientService ): WebClientGraphQLClient { // 1. 配置Client Credentials授权管理器,用于获取服务自身的令牌 val clientCredsManager = AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager( clientRegistrationRepository, clientService ).apply { setAuthorizedClientProvider(ClientCredentialsReactiveOAuth2AuthorizedClientProvider()) } // 2. 配置Token Exchange的响应客户端,动态注入subject_token等参数 val tokenExchangeClient = WebClientReactiveTokenExchangeTokenResponseClient().apply { addParametersConverter { grantRequest -> // 从授权请求属性中获取动态传入的目标用户和受众(可选,若固定可硬编码) val requestedSubject = grantRequest.attributes["requested_subject"] as String val audience = grantRequest.attributes["audience"] as String // 先获取Client Credentials令牌,作为subject_token clientCredsManager.authorize( OAuth2AuthorizeRequest.withClientRegistrationId("keycloak-client-creds") .principal(grantRequest.principal) .build() ).map { authorizedClient -> LinkedMultiValueMap<String, String>().apply { add("audience", audience) add("requested_subject", requestedSubject) add("subject_token", authorizedClient.accessToken.tokenValue) add("subject_token_type", "urn:ietf:params:oauth:token-type:access_token") } } } } // 3. 配置Token Exchange授权管理器 val tokenExchangeManager = AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager( clientRegistrationRepository, clientService ).apply { setAuthorizedClientProvider(TokenExchangeReactiveOAuth2AuthorizedClientProvider().apply { setAccessTokenResponseClient(tokenExchangeClient) }) } // 4. 配置WebClient的OAuth2过滤器 val oauthFilter = ServerOAuth2AuthorizedClientExchangeFilterFunction(tokenExchangeManager).apply { setDefaultClientRegistrationId("keycloak-token-exchange") } return MonoGraphQLClient.createWithWebClient(WebClient.builder() .baseUrl(legacyServerProperties.httpUrl) .filter(oauthFilter) .build() ) }
步骤3:动态传入目标用户和受众
在发起GraphQL请求时,通过attributes传递动态参数(若参数固定可跳过此步,直接在代码中硬编码):
// 示例:从事件中获取目标用户ID val targetUserId = event.originalUserId legacyServerGraphQLClient.document(query) .attribute(OAuth2AuthorizeRequestAttributes.withAuthorizedClientId("keycloak-token-exchange") .attribute("requested_subject", targetUserId) .attribute("audience", "legacy-api-A") .build()) .execute() .subscribe(...)
关键注意事项
- Keycloak客户端权限配置:确保你的服务客户端(
service-s)已开启Impersonation权限,并且在Keycloak的客户端设置中允许使用Token Exchange授权类型。 - 令牌缓存:Spring的
ReactiveOAuth2AuthorizedClientService会自动缓存有效的Client Credentials令牌,避免重复请求Keycloak。 - Reactive环境适配:所有流程均基于Reactive API实现,避免使用
block()等阻塞操作,确保服务的响应式特性。
内容的提问来源于stack exchange,提问作者Crystark
相关产品推荐
相关产品推荐

