Spring boot Spring Boot 2 OIDC(OAuth2)客户端/资源服务器未在WebClient中传播访问令牌

Spring boot Spring Boot 2 OIDC(OAuth2)客户端/资源服务器未在WebClient中传播访问令牌,spring-boot,spring-security,openid-connect,keycloak,spring-oauth2,Spring Boot,Spring Security,Openid Connect,Keycloak,Spring Oauth2,我已经成功地将两个Spring Boot 2应用程序2配置为客户端/资源服务器,它们之间的SSO很好 此外,我正在测试彼此之间经过身份验证的REST调用,将访问令牌传播为授权:承载访问\u令牌头 在启动keydrope和应用程序后,我在keydrope登录页面中访问或并进行身份验证。HomeController使用WebClient调用另一个资源服务器的HelloRestController/rest/hello端点 @Controller class HomeController(privat

我已经成功地将两个Spring Boot 2应用程序2配置为客户端/资源服务器,它们之间的SSO很好

此外,我正在测试彼此之间经过身份验证的REST调用,将访问令牌传播为
授权:承载访问\u令牌

在启动keydrope和应用程序后,我在keydrope登录页面中访问或并进行身份验证。HomeController使用WebClient调用另一个资源服务器的
HelloRestController
/rest/hello
端点

@Controller
class HomeController(private val webClient: WebClient) {

    @GetMapping
    fun home(httpSession: HttpSession,
             @RegisteredOAuth2AuthorizedClient authorizedClient: OAuth2AuthorizedClient,
             @AuthenticationPrincipal oauth2User: OAuth2User): String {
        val authentication = SecurityContextHolder.getContext().authentication
        println(authentication)

        val pair = webClient.get().uri("http://localhost:8282/resource-server-2/rest/hello").retrieve()
                .bodyToMono(Pair::class.java)
                .block()

        return "home"
    }

}
此调用返回302,因为请求未经过身份验证(它没有传播访问令牌):

OAuth2配置:

@Configuration
class OAuth2Config : WebSecurityConfigurerAdapter() {

    @Bean
    fun webClient(): WebClient {
        return WebClient.builder()
                .filter(ServletBearerExchangeFilterFunction())
                .build()
    }

    @Bean
    fun clientRegistrationRepository(): ClientRegistrationRepository {
        return InMemoryClientRegistrationRepository(keycloakClientRegistration())
    }

    private fun keycloakClientRegistration(): ClientRegistration {
        val clientRegistration = ClientRegistration
                .withRegistrationId("resource-server-1")
                .clientId("resource-server-1")
                .clientSecret("c00670cc-8546-4d5f-946e-2a0e998b9d7f")
                .clientAuthenticationMethod(ClientAuthenticationMethod.BASIC)
                .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
                .redirectUriTemplate("{baseUrl}/login/oauth2/code/{registrationId}")
                .scope("openid", "profile", "email", "address", "phone")
                .authorizationUri("http://localhost:8080/auth/realms/insight/protocol/openid-connect/auth")
                .tokenUri("http://localhost:8080/auth/realms/insight/protocol/openid-connect/token")
                .userInfoUri("http://localhost:8080/auth/realms/insight/protocol/openid-connect/userinfo")
                .userNameAttributeName(IdTokenClaimNames.SUB)
                .jwkSetUri("http://localhost:8080/auth/realms/insight/protocol/openid-connect/certs")
                .clientName("Keycloak")
                .providerConfigurationMetadata(mapOf("end_session_endpoint" to "http://localhost:8080/auth/realms/insight/protocol/openid-connect/logout"))
                .build()
        return clientRegistration
    }

    override fun configure(http: HttpSecurity) {
        http.authorizeRequests { authorizeRequests ->
            authorizeRequests
                    .anyRequest().authenticated()
        }.oauth2Login(withDefaults())
                .logout { logout ->
                    logout.logoutSuccessHandler(oidcLogoutSuccessHandler())
                }
    }

    private fun oidcLogoutSuccessHandler(): LogoutSuccessHandler? {
        val oidcLogoutSuccessHandler = OidcClientInitiatedLogoutSuccessHandler(clientRegistrationRepository())
        oidcLogoutSuccessHandler.setPostLogoutRedirectUri(URI.create("http://localhost:8181/resource-server-1"))
        return oidcLogoutSuccessHandler
    }
}
如您所见,我正在
WebClient
中设置一个
servletBeareExchangeFilterFunction
。这是我在调试中看到的:

SubscriberContext没有设置任何内容,因为AbstractOAuth2Token的
authentication.getCredentials()instanceof
为false。实际上,它只是一个字符串:

public class OAuth2AuthenticationToken extends AbstractAuthenticationToken {

    ... 

    @Override
    public Object getCredentials() {
        // Credentials are never exposed (by the Provider) for an OAuth2 User
        return "";
    }


这里有什么问题?如何自动传播令牌?

对于纯OAuth2/OIDC登录应用程序,似乎没有现成的解决方案,我为此创建了一个解决方案

同时,我创建了一个特定的
ServletBeareExchangeFilterFunction
,该函数从
OAuth2AuthorizedClient存储中检索访问令牌

这是我的自定义解决方案:

    @Autowired
    lateinit var oAuth2AuthorizedClientRepository: OAuth2AuthorizedClientRepository

    @Bean
    fun webClient(): WebClient {
        val servletBearerExchangeFilterFunction = ServletBearerExchangeFilterFunction("resource-server-1", oAuth2AuthorizedClientRepository)
        return WebClient.builder()
                .filter(servletBearerExchangeFilterFunction)
                .build()
    }

    ...

    private fun keycloakClientRegistration(): ClientRegistration {
        return ClientRegistration
                .withRegistrationId("resource-server-1")
                ...
const val SECURITY\u REACTOR\u CONTEXT\u ATTRIBUTES\u KEY=“org.springframework.SECURITY.SECURITY\u CONTEXT\u ATTRIBUTES”
类ServletBeareExchangeFilterFunction(私有val clientRegistrationId:String,
私有val OAuth2AuthorizedClient存储:OAuth2AuthorizedClient存储?:ExchangeFilterFunction{
/**
*{@inheritardoc}
*/
覆盖趣味过滤器(请求:ClientRequest,下一步:ExchangeFunction):Mono{
返回oauth2Token()
.map{token:AbstractOAuth2Token->bearer(请求,令牌)}
.defaultIfEmpty(请求)
.flatMap{request:ClientRequest->next.exchange(request)}
}
私人娱乐oauth2Token():Mono{
返回Mono.subscriberContext()
.flatMap{ctx:Context->currentAuthentication(ctx)}
.map{身份验证->
val authorizedClient=OAuth2AuthorizedclientPository?.loadAuthorizedClient(clientRegistrationId,身份验证,null)
if(authorizedClient!=null){
authorizedClient.accessToken
}否则{
单位
}
}
.filter{it!=null}
.cast(AbstractOAuth2Token::class.java)
}
私有身份验证(ctx:Context):Mono{
返回Mono.justOrEmpty(getAttribute(ctx,Authentication::class.java))
}
private-fun-getAttribute(ctx:Context,clazz:Class):T?{//注意:SecurityReactorContextConfiguration.SecurityReactorContextSubscriber添加此密钥
if(!ctx.hasKey(安全性\反应器\上下文\属性\密钥)){
返回空
}
val属性:Map=ctx[安全性\反应器\上下文\属性\密钥]
返回属性[clazz]
}
私人娱乐持有者(请求:ClientRequest,令牌:AbstractOAuth2Token):ClientRequest{
返回ClientRequest.from(请求)
.headers{headers:HttpHeaders->headers.setbeareauth(token.tokenValue)}
.build()
}
}
    @Autowired
    lateinit var oAuth2AuthorizedClientRepository: OAuth2AuthorizedClientRepository

    @Bean
    fun webClient(): WebClient {
        val servletBearerExchangeFilterFunction = ServletBearerExchangeFilterFunction("resource-server-1", oAuth2AuthorizedClientRepository)
        return WebClient.builder()
                .filter(servletBearerExchangeFilterFunction)
                .build()
    }

    ...

    private fun keycloakClientRegistration(): ClientRegistration {
        return ClientRegistration
                .withRegistrationId("resource-server-1")
                ...
const val SECURITY_REACTOR_CONTEXT_ATTRIBUTES_KEY = "org.springframework.security.SECURITY_CONTEXT_ATTRIBUTES"

class ServletBearerExchangeFilterFunction(private val clientRegistrationId: String,
                                          private val oAuth2AuthorizedClientRepository: OAuth2AuthorizedClientRepository?) : ExchangeFilterFunction {

    /**
     * {@inheritDoc}
     */
    override fun filter(request: ClientRequest, next: ExchangeFunction): Mono<ClientResponse> {
        return oauth2Token()
                .map { token: AbstractOAuth2Token -> bearer(request, token) }
                .defaultIfEmpty(request)
                .flatMap { request: ClientRequest -> next.exchange(request) }
    }

    private fun oauth2Token(): Mono<AbstractOAuth2Token> {
        return Mono.subscriberContext()
                .flatMap { ctx: Context -> currentAuthentication(ctx) }
                .map { authentication ->
                    val authorizedClient = oAuth2AuthorizedClientRepository?.loadAuthorizedClient<OAuth2AuthorizedClient>(clientRegistrationId, authentication, null)
                    if (authorizedClient != null) {
                        authorizedClient.accessToken
                    } else {
                        Unit
                    }
                }
                .filter { it != null }
                .cast(AbstractOAuth2Token::class.java)
    }

    private fun currentAuthentication(ctx: Context): Mono<Authentication> {
        return Mono.justOrEmpty(getAttribute(ctx, Authentication::class.java))
    }

    private fun <T> getAttribute(ctx: Context, clazz: Class<T>): T? { // NOTE: SecurityReactorContextConfiguration.SecurityReactorContextSubscriber adds this key
        if (!ctx.hasKey(SECURITY_REACTOR_CONTEXT_ATTRIBUTES_KEY)) {
            return null
        }
        val attributes: Map<Class<T>, T> = ctx[SECURITY_REACTOR_CONTEXT_ATTRIBUTES_KEY]
        return attributes[clazz]
    }

    private fun bearer(request: ClientRequest, token: AbstractOAuth2Token): ClientRequest {
        return ClientRequest.from(request)
                .headers { headers: HttpHeaders -> headers.setBearerAuth(token.tokenValue) }
                .build()
    }

}