带有oauth2的Spring Security 5导致'principalName不能为空'错误

问题描述

我正在尝试使用Spring Security 5实现一个oauth2安全的Spring Boot API。我希望该API成为oauth2资源服务器,并希望能够通过WebClient访问具有客户端证书授予权限的外部oauth2资源服务器。 / p>

我可以将API配置为oauth2资源服务器或oauth2客户端,但不能同时使用两者。

下面是将API配置为具有Spring security 5的资源服务器的最小设置。我使用的是Opaque令牌,因此为此配置了服务器。

application.properties

spring.security.oauth2.resourceserver.opaquetoken.introspection-uri=http://localhost:8086/auth/oauth/check_token
spring.security.oauth2.resourceserver.opaquetoken.client-id=test-api
spring.security.oauth2.resourceserver.opaquetoken.client-secret=e61aa5d6-074d-4216-b15f-1bf3fc71b833

WebSecurity配置类

@EnableWebSecurity
public class WebSecurity extends WebSecurityConfigurerAdapter {
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests(authorizeRequests ->
                    authorizeRequests
                        .anyRequest().authenticated()
            )
            .oauth2ResourceServer(OAuth2ResourceServerConfigurer::opaqueToken);

    }
}

通过此设置,我可以使用有效令牌访问此api端点以访问其受保护的资源。因此,资源服务器配置本身可以正常工作。

下面是最小的Spring security 5设置,配置为使用WebClient通过客户端凭据授予来访问外部受保护的资源。

application.properties

spring.security.oauth2.client.provider.my-oauth-provider.token-uri=http://127.0.0.1:8086/auth/oauth/token
spring.security.oauth2.client.registration.test-api.client-id=test-store
spring.security.oauth2.client.registration.test-api.client-secret=password
spring.security.oauth2.client.registration.test-api.provider=my-oauth-provider
spring.security.oauth2.client.registration.test-api.scope=read,write
spring.security.oauth2.client.registration.test-api.authorization-grant-type=client_credentials

WebSecurity配置类

@EnableWebSecurity
public class WebSecurity extends WebSecurityConfigurerAdapter {
    
    @Autowired
    ClientRegistrationRepository clientRegistrationRepository;
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .oauth2Client(oauth2 -> oauth2
                .clientRegistrationRepository(clientRegistrationRepository)
            );
    }
}

Oauth2Client配置类

@Configuration
public class Oauth2ClientConfig {
    
    @Bean
    public OAuth2AuthorizedClientManager authorizedClientManager(
            ClientRegistrationRepository clientRegistrationRepository,OAuth2AuthorizedClientRepository authorizedClientRepository) {

        OAuth2AuthorizedClientProvider authorizedClientProvider =
                OAuth2AuthorizedClientProviderBuilder.builder()
                        .clientCredentials()
                        .build();

        DefaultOAuth2AuthorizedClientManager authorizedClientManager =
                new DefaultOAuth2AuthorizedClientManager(
                        clientRegistrationRepository,authorizedClientRepository);
        authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);

        return authorizedClientManager;
    }

    
    @Bean
    WebClient webClient(OAuth2AuthorizedClientManager oAuth2AuthorizedClientManager) {
        ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Client =
                new ServletOAuth2AuthorizedClientExchangeFilterFunction(
                        oAuth2AuthorizedClientManager);
        
        // default registrationId
       oauth2Client.setDefaultClientRegistrationId("test-api");
        
        // set client to use oauth2 by default globally
       oauth2Client.setDefaultOAuth2AuthorizedClient(true);
        
        return WebClient.builder()
          .apply(oauth2Client.oauth2Configuration())
          .build();
    }
}

通过此设置,我可以使用客户端凭据授予访问外部受保护的端点以访问受保护的资源。

但是,当我将这两种配置放在一起时,当我尝试命中api端点(而该端点又试图使用WebClient访问外部资源)时,它将不起作用。

    @Autowired
    WebClient webClient;

    @GetMapping("test")
    public String test() {
        String message = webClient
                .get()
                .uri("http://localhost:8084/external/api/endpoint")
                .retrieve()
                .bodyToMono(String.class)
                .block();
        return message;
    }

Spring安全性引发 principalName不能为空错误。


java.lang.IllegalArgumentException: principalName cannot be empty
    at org.springframework.util.Assert.hasText(Assert.java:287) ~[spring-core-5.2.8.RELEASE.jar:5.2.8.RELEASE]
    Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException: 
Error has been observed at the following site(s):
    |_ checkpoint ⇢ Request to GET http://localhost:8084/ocr/document/test [DefaultWebClient]
Stack trace:
        at org.springframework.util.Assert.hasText(Assert.java:287) ~[spring-core-5.2.8.RELEASE.jar:5.2.8.RELEASE]
        at org.springframework.security.oauth2.client.InMemoryOAuth2AuthorizedClientService.loadAuthorizedClient(InMemoryOAuth2AuthorizedClientService.java:73) ~[spring-security-oauth2-client-5.3.3.RELEASE.jar:5.3.3.RELEASE]
        at org.springframework.security.oauth2.client.web.AuthenticatedPrincipalOAuth2AuthorizedClientRepository.loadAuthorizedClient(AuthenticatedPrincipalOAuth2AuthorizedClientRepository.java:73) ~[spring-security-oauth2-client-5.3.3.RELEASE.jar:5.3.3.RELEASE]
        at org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizedClientManager.authorize(DefaultOAuth2AuthorizedClientManager.java:144) ~[spring-security-oauth2-client-5.3.3.RELEASE.jar:5.3.3.RELEASE]
        at org.springframework.security.oauth2.client.web.reactive.function.client.ServletOAuth2AuthorizedClientExchangeFilterFunction.lambda$authorizeClient$24(ServletOAuth2AuthorizedClientExchangeFilterFunction.java:534) ~[spring-security-oauth2-client-5.3.3.RELEASE.jar:5.3.3.RELEASE]
...

高度赞赏有关解决此问题的任何指示。当API还是资源服务器时,我应该配置WebClient来工作的另一种方式吗?

我的maven项目中有这些依赖项。 Spring安全性版本是5.3.3.RELEASE pom.xml

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.2.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    ...

        <!-- Spring Boot Web starter -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- Security Starter -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <!-- Oauth2 resource server -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
        </dependency>
        <dependency>
            <groupId>com.nimbusds</groupId>
            <artifactId>oauth2-oidc-sdk</artifactId>
        </dependency>
        <!-- Oauth2 client -->
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-oauth2-client</artifactId>
        </dependency>
        <!-- WebClient -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webflux</artifactId>
        </dependency>

解决方法

从这里: https://www.gitmemory.com/issue/spring-projects/spring-security/8398/614615036

这是预期的行为->“ principalName不能为空”。

由于访问,OAuth2AuthorizedClient需要principalName 令牌始终与主体相关联。的 ReactiveOAuth2AuthorizedClientService验证principalName和 如果principalName不可用,则预期会失败。

principalName不可用的原因是 JwtAuthenticationToken.getName()将默认为子声明,即 根据您的令牌,在Jwt的声明集中不可用 上面的例子。您需要做的是指定user_name声明 用作principalName。

看看这个 sample 展示了如何将JwtDecoder配置为默认为 JwtAuthenticationToken.getName()的用户名。

链接的示例无法按原样工作,因为convertClaims中没有“ user_name”值,因此我只填写了自己的用户名。 这是我添加到配置中的内容:

http.authorizeExchange(exchangeSpec -> exchangeSpec.anyExchange().authenticated())
                .oauth2ResourceServer(oAuth2 -> oAuth2.jwt(jwt -> jwt.jwtDecoder(jwtDecoder())));
http.oauth2Login();

public class UsernameSubClaimAdapter implements Converter<Map<String,Object>,Map<String,Object>> {
    private final MappedJwtClaimSetConverter delegate =
            MappedJwtClaimSetConverter.withDefaults(Collections.emptyMap());

    public Map<String,Object> convert(Map<String,Object> claims) {
        Map<String,Object> convertedClaims = this.delegate.convert(claims);

        String username = "anonymous_user";
        convertedClaims.put("sub",username);

        return convertedClaims;
    }
}

@Bean
public NimbusReactiveJwtDecoder jwtDecoder() {
    NimbusReactiveJwtDecoder jwtDecoder = new NimbusReactiveJwtDecoder(processor());
    jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(_jwtIssuerUrl));
    jwtDecoder.setClaimSetConverter(new UsernameSubClaimAdapter());
    return jwtDecoder;
}
,

问题是您的应用程序同时是resourceServeroauth2Client,听起来很奇怪。 将.oauth2ResourceServer(OAuth2ResourceServerConfigurer::opaqueToken)配置包含到httpSecurity中后,所有身份验证现在将成为BearerTokenAuthentication的实例。由于您的不透明令牌没有name字段,因此这些名称将没有关联的名称。如果排除.oauth2ResourceServer(OAuth2ResourceServerConfigurer::opaqueToken),则所有身份验证都将是AnonymousAuthenticationToken的实例,默认主体为anonymousUser

此问题的解决方案是为不透明令牌实现自己的自省,这将为身份验证设置主体名称。我通过扩展NimbusReactiveOpaqueTokenIntrospector并复制具有默认主体名称的Oauth2Authentication来做到这一点。

内省者:

class ExtendedNimbusReactiveOpaqueTokenIntrospector(introspectionUri: String,clientId: String,clientSecret: String) : NimbusReactiveOpaqueTokenIntrospector(introspectionUri,clientId,clientSecret) {
    override fun introspect(token: String?): Mono<OAuth2AuthenticatedPrincipal> {
        return super.introspect(token).map { auth ->
            val name = SecurityContextHolder.getContext().authentication?.name ?: "defaultPrincipal"
            DefaultOAuth2AuthenticatedPrincipal(name,auth.attributes,auth.authorities)
    }
}

}

WebSecurityConfig:

@Configuration
@EnableWebFluxSecurity
class OAuthConfig
@Autowired constructor(@Value("\${spring.security.oauth2.resourceserver.opaquetoken.introspection-uri}") private val introspectionUri: String,@Value("\${spring.security.oauth2.resourceserver.opaquetoken.client-id}") private val clientId: String,@Value("\${spring.security.oauth2.resourceserver.opaquetoken.client-secret}") private val clientSecret: String){

    @Bean
    fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
        return http
            .oauth2Client {

            }
            .authorizeExchange { authExchange ->
                authExchange.pathMatchers(
                        "/apidoc","/swagger-ui.html","/webjars/**","/swagger-resources/**","/v2/api-docs")
                        .permitAll()
                        .anyExchange()
                        .permitAll()
            }
            .oauth2Client {}
            .oauth2ResourceServer {
                it.opaqueToken {opaqueTokenSpec ->
                    opaqueTokenSpec.introspector(ReweNimbusReactiveOpaqueTokenIntrospector(introspectionUri,clientSecret))
                }
            }
            .build()
    }

}

WebClientConfig:

@Configuration
class WebClientConfig @Autowired constructor() {

    @Bean
    fun authorizedClientManager(
            clientRegistrationRepository: ReactiveClientRegistrationRepository?,authorizedClientService: ReactiveOAuth2AuthorizedClientService?
    ): ReactiveOAuth2AuthorizedClientManager? {
        val authorizedClientProvider = ReactiveOAuth2AuthorizedClientProviderBuilder.builder()
            .clientCredentials()
            .build()
        val authorizedClientManager = AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager(clientRegistrationRepository,authorizedClientService)
        authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider)
        return authorizedClientManager
    }

    @Bean
    fun webClientBuilder(authorizedClientManager: ReactiveOAuth2AuthorizedClientManager?): WebClient.Builder? {
        val oauth = ServerOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager)
    oauth.setDefaultClientRegistrationId("rewe")
        return WebClient.builder()
            .filter(oauth)
    }

    @Bean
    fun authorizedWebClient(webClientBuilder: WebClient.Builder,@Value("\${host}") host: String): WebClient {
        return webClientBuilder.baseUrl(host).build()
    }
}

相关问答

依赖报错 idea导入项目后依赖报错,解决方案:https://blog....
错误1:代码生成器依赖和mybatis依赖冲突 启动项目时报错如下...
错误1:gradle项目控制台输出为乱码 # 解决方案:https://bl...
错误还原:在查询的过程中,传入的workType为0时,该条件不起...
报错如下,gcc版本太低 ^ server.c:5346:31: 错误:‘struct...