Spring 安全 OAuth2:'#oauth2.xxx' 表达式未通过映射到同一 FilterChain 的多个 RequestMatchers 进行评估

Spring Security OAuth2: '#oauth2.xxx' expressions not evaluted with multiple RequestMatchers mapped to the same FilterChain

在多模块应用程序中,我定义了 5 个 RequestMatchers 映射到同一个 FilterChain,如下所示:

@Configuration
public class Module1SecurityFilterChain extends ResourceServerConfigurerAdapter {

   @Override
   public void configure( HttpSecurity http ) throws Exception {
      http.sessionManagement().sessionCreationPolicy( STATELESS );
      http.requestMatchers().antMatchers( "/module1/**")
            .and()
            .authorizeRequests()
            .antMatchers( "/module1/resource").authenticated()
            .antMatchers( "/module1/test" ).access( "#oauth2.isClient()")
            .anyRequest().access( "#oauth2.hasScope('webclient')" );
   }
}

和模块 2:

@Configuration
public class Module2SecurityFilterChain extends ResourceServerConfigurerAdapter {

   @Override
   public void configure( HttpSecurity http ) throws Exception {
      http.sessionManagement().sessionCreationPolicy( STATELESS );
      http.requestMatchers().antMatchers( "/module2/**")
            .and()
            .authorizeRequests()
            .antMatchers( "/module2/resource").authenticated()
            .antMatchers( "/module2/test" ).access( "#oauth2.isClient()")
            .anyRequest().access( "#oauth2.hasScope('webclient')" );
   }
}

并启用方法安全性:

@Configuration
@EnableGlobalMethodSecurity( prePostEnabled = true, securedEnabled = true )
public class MethodSecurityConfiguration extends GlobalMethodSecurityConfiguration {

    @Override
    protected MethodSecurityExpressionHandler createExpressionHandler() {
        return new OAuth2MethodSecurityExpressionHandler();
    }
}

问题是所有 #oauth2.xx 表达式仅针对第一个模块 requestmatcher /module1/** 进行评估,而在其他模块中被忽略。当我对用户进行身份验证并尝试访问 /module1/test 时,访问被拒绝,而当访问 /module2/test 时,访问被授予(也应该被拒绝)。

有人可以向我解释为什么以及如何解决这个问题吗?我知道 Spring 安全一点也不容易... 再次感谢。

编辑 @Darren Forsythe(感谢您的评论) 创建的过滤器链是:

INFO | o.s.s.w.DefaultSecurityFilterChain | Creating filter chain: OrRequestMatcher [requestMatchers=[Ant [pattern='/oauth/token'], Ant [pattern='/oauth/token_key'], Ant [pattern='/oauth/check_token']]], [org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@f55a810b, org.springframework.security.web.context.SecurityContextPersistenceFilter@85021903, org.springframework.security.web.header.HeaderWriterFilter@1d0744d1, org.springframework.security.web.authentication.logout.LogoutFilter@2d15146a, org.springframework.security.web.authentication.www.BasicAuthenticationFilter@c38f3266, org.springframework.security.web.savedrequest.RequestCacheAwareFilter@8f9bf85, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@74a71be5, org.springframework.security.web.authentication.AnonymousAuthenticationFilter@e4eb6cc, org.springframework.security.web.session.SessionManagementFilter@22f6b39a, org.springframework.security.web.access.ExceptionTranslationFilter@960c464f, org.springframework.security.web.access.intercept.FilterSecurityInterceptor@f7a19dc5]
INFO | o.s.s.w.DefaultSecurityFilterChain | Creating filter chain: OrRequestMatcher [requestMatchers=[Ant [pattern='/module1/**'], Ant [pattern='/module2/**'], Ant [pattern='/module3/**'], Ant [pattern='/module4/**'], Ant [pattern='/module5/**'], Ant [pattern='/module6/**']]], [org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@38ef2427, org.springframework.security.web.context.SecurityContextPersistenceFilter@a26ff7af, org.springframework.security.web.header.HeaderWriterFilter@5344e710, org.springframework.security.web.authentication.logout.LogoutFilter@da0534c8, org.springframework.security.oauth2.provider.authentication.OAuth2AuthenticationProcessingFilter@2956c7ab, org.springframework.security.web.savedrequest.RequestCacheAwareFilter@5682f610, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@f4cbf7a4, org.springframework.security.web.authentication.AnonymousAuthenticationFilter@d1b1395a, org.springframework.security.web.session.SessionManagementFilter@d352f8ab, org.springframework.security.web.access.ExceptionTranslationFilter@9bb1d86, org.springframework.security.web.access.intercept.FilterSecurityInterceptor@73c7a695]
INFO | o.s.s.w.DefaultSecurityFilterChain | Creating filter chain: any request, [org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@1cc2056f, org.springframework.security.web.context.SecurityContextPersistenceFilter@259d95db, org.springframework.security.web.header.HeaderWriterFilter@de089e0b, org.springframework.security.web.authentication.logout.LogoutFilter@8b86b4c, org.springframework.security.web.savedrequest.RequestCacheAwareFilter@96304ca8, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@1d5b7e4b, org.springframework.security.web.session.SessionManagementFilter@bd586b4d, org.springframework.security.web.access.ExceptionTranslationFilter@7cff2571]

如您所见,所有模块的 url 都映射到具有此过滤器列表的相同过滤器链:

Security filter chain: [
  WebAsyncManagerIntegrationFilter
  SecurityContextPersistenceFilter
  HeaderWriterFilter
  LogoutFilter
  OAuth2AuthenticationProcessingFilter
  RequestCacheAwareFilter
  SecurityContextHolderAwareRequestFilter
  AnonymousAuthenticationFilter
  SessionManagementFilter
  ExceptionTranslationFilter
  FilterSecurityInterceptor
]

我不明白的是为什么对于其他模块 #oauth2.xx 表达式没有被评估,因为 FilterChain 是相同的?

请求匹配器(在 antMatchersanyRequest 等中指定)由过滤器链按照它们指定的顺序进行处理。因为多个 ResourceServiceConfiguredAdapter 实例只是从 HttpSecurity 的同一个实例进行配置,所以匹配器会针对您的每个请求进行如下处理:

if (uri == "/module1/resource") {
    // ...
} else if (uri == "/module1/test") { 
    // ...
} else if (true) { // anyRequest
    // ...
} else if (uri = "/module2/resource") {
    // ...
} else if (uri = "/module2/test") {
    // ...
}

如您所见,最后两个 if 条件永远不会被命中。

您可以考虑以下两点:

替换anyRequest()

anyRequest 通常非常方便;但是,在这种情况下,您实际上并不意味着 "any request" 因为您试图将范围缩小到某些模块路径。你可以改为:

http
    .requestMatchers()
        .antMatchers("/module2/**")
        .and()
    .authorizeRequests()
        .antMatchers("/module2/resource").authenticated()
        .antMatchers("/module2/test").access( "#oauth2.isClient()")
        .antMatchers("/module2/**").access( "#oauth2.hasScope('webclient')" );

这样,模块就不会越界并尝试指定它可能不知道的行为。

说实话,调用 anyRequest 通常是无害的,因为您已经使用 requestMatchers 缩小了过滤器链的范围。但是,因为您正在使用多个适配器组合单个 HttpSecurity,所以存在这种隐藏的复杂性。

oauth2ResourceServer() - Spring 安全 5.1+

如果您使用 Spring Security 5.1,那么 WebSecurityConfigurerAdapter 中实际上内置了支持,因此您不需要再使用 ResourceServerConfigurerAdapter,至少对于 JWT 编码而言在这一点上的令牌。这也很好,因为两个 WebSecurityConfigurerAdapter 被视为单独的过滤器链。

根据您需要的 OAuth 2.0 功能,您也许可以这样做:

@EnableWebSecurity
public class Module1Config extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .requestMatchers()
                .antMatchers("/module1/**")
                .and()
            .authorizeRequests()
                .antMatchers("/module1/resource").authenticated()
                .anyRequest.hasRole("SCOPE_webclient")
            .oauth2ResourceServer()
                .jwt();
    }
}

这是目前 Spring 安全性的一个活跃开发领域 - 将功能移植到 WebSecurityConfigurerAdapter;所以一定要联系你的具体用例,以确保它在尚未到位的情况下得到优先考虑。