【问题标题】:Spring Security 5 with oauth2 causing 'principalName cannot be empty' error带有 oauth2 的 Spring Security 5 导致“principalName 不能为空”错误
【发布时间】:2020-11-30 19:05:37
【问题描述】:

我正在尝试使用 Spring Security 5 实现 oauth2 安全 Spring Boot API。我希望 API 成为 oauth2 资源服务器,并且能够使用 WebClient 访问外部 oauth2 资源服务器,并授予客户端凭据。

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

以下是将 API 配置为具有 Spring 安全性 5 的资源服务器的最小设置。我使用的是不透明令牌,因此为此配置了服务器。

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 端点以访问其受保护的资源。所以资源服务器配置本身就可以正常工作。

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

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 security 抛出 principalName cannot be empty 错误。


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>

【问题讨论】:

    标签: spring-boot spring-security oauth-2.0 spring-security-oauth2 spring-webclient


    【解决方案1】:

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

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

    OAuth2AuthorizedClient 需要 principalName,因为访问 令牌始终与主体相关联。这 ReactiveOAuth2AuthorizedClientService 验证 principalName 和 如果 principalName 不可用,预计会失败。

    principalName 不可用的原因是 JwtAuthenticationToken.getName() 将默认为 sub 声明,即 根据您的令牌,在 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;
    }
    

    【讨论】:

      【解决方案2】:

      问题是您的应用程序同时是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, clientId, 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()
          }
      }
      

      【讨论】:

        猜你喜欢
        • 2021-09-25
        • 2019-05-04
        • 2020-04-06
        • 1970-01-01
        • 2015-07-15
        • 2015-11-23
        • 2023-04-06
        • 2019-08-13
        • 2018-06-27
        相关资源
        最近更新 更多