使用Oauth2或Http-Basic身份验证的Spring安全性相同资源

52

我正在尝试实现一个API,其中的资源受到Oauth2或Http-Basic身份验证的保护。

当我加载WebSecurityConfigurerAdapter并首先将http-basic身份验证应用于资源时,Oauth2令牌身份验证不被接受。反之亦然。

示例配置: 这将对所有/user/**资源应用http-basic身份验证

@Configuration
@EnableWebMvcSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    private LoginApi loginApi;

    @Autowired
    public void setLoginApi(LoginApi loginApi) {
        this.loginApi = loginApi;
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.authenticationProvider(new PortalUserAuthenticationProvider(loginApi));
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .antMatchers("/users/**").authenticated()
                .and()
            .httpBasic();
    }

    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
}

这将在/user/**资源上应用OAuth令牌保护。

@Configuration
@EnableResourceServer
protected static class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http
            .requestMatchers().antMatchers("/users/**")
        .and()
            .authorizeRequests()
                .antMatchers("/users/**").access("#oauth2.clientHasRole('ROLE_CLIENT') and #oauth2.hasScope('read')");
    }
}

我确定我错过了一些神奇的代码,告诉Spring尝试第一个失败后再尝试第二个?

任何帮助都将不胜感激。

9个回答

57

在 Michael Ressler 的答案提示下,我进行了一些调整以完成此工作。

我的目标是允许在同一资源端点(例如 /leafcase/123)上同时使用基本身份验证和 OAuth。由于 filterChains 的排序(可以在 FilterChainProxy.filterChains 中查看),我被困扰了很长时间。默认顺序如下:

  • Oauth 认证服务器(如果在同一项目中启用)的 filterChains。默认顺序为 0(请参见 AuthorizationServerSecurityConfiguration)
  • Oauth 资源服务器的 filterChains。默认顺序为 3(请参见 ResourceServerConfiguration)。它具有请求匹配逻辑,匹配除 Oauth 认证端点之外的任何内容(例如 /oauth/token、/oauth/authorize 等。请参见 ResourceServerConfiguration$NotOauthRequestMatcher.matches())。
  • 对应于 config(HttpSecurity http) 的 filterChains,默认顺序为 100,请参见 WebSecurityConfigurerAdapter。

由于资源服务器的 filterChains 排名高于 WebSecurityConfigurerAdapter 配置的 filterchain,并且前者实际上与每个资源端点匹配,因此对于任何对资源端点的请求(即使请求使用 Authorization: Basic 标头),OAuth 资源服务器逻辑总是会起作用。你会收到以下错误信息:

{
    "error": "unauthorized",
    "error_description": "Full authentication is required to access this resource"
}

我进行了两个更改以使它工作:

首先,将WebSecurityConfigurerAdapter的顺序调整为资源服务器之上(顺序2比顺序3更高)。

@Configuration
@Order(2)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
其次,让configure(HttpSecurity)使用一个自定义的RequestMatcher,该匹配器仅匹配"Authorization: Basic"。
@Override
protected void configure(HttpSecurity http) throws Exception {

    http
        .anonymous().disable()
        .requestMatcher(new BasicRequestMatcher())
        .authorizeRequests()
            .anyRequest().authenticated()
            .and()
        .httpBasic()
             .authenticationEntryPoint(oAuth2AuthenticationEntryPoint())
            .and()
        // ... other stuff
 }
 ...
 private static class BasicRequestMatcher implements RequestMatcher {
    @Override
    public boolean matches(HttpServletRequest request) {
        String auth = request.getHeader("Authorization");
        return (auth != null && auth.startsWith("Basic"));
    }
 }

因此,它可以在资源服务器的过滤器链有机会匹配之前匹配和处理基本身份验证资源请求。 它还仅处理Authorization:Basic资源请求,因此任何带有Authorization:Bearer的请求将被忽略,然后由资源服务器的过滤器链处理(即,OAuth的过滤器生效)。此外,它的优先级低于AuthenticationServer(如果AuthenticationServer在同一个项目中启用),因此它不会阻止AuthenticationServer的过滤器链处理对/oauth/token等的请求。


12
oAuth2AuthenticationEntryPoint() 定义在哪里? - jax
4
有人知道如何正确地实现基于会话的身份验证吗?(表单登录) - kboom
1
回答 @jax 的问题: .authenticationEntryPoint(new OAuth2AuthenticationEntryPoint()) 中的 OAuth2AuthenticationEntryPoint 来自于以下导入: import org.springframework.security.oauth2.provider.error.OAuth2AuthenticationEntryPoint; - AntonioOtero

13

这可能接近你所寻找的内容:

@Override
public void configure(HttpSecurity http) throws Exception {
    http.requestMatcher(new OAuthRequestedMatcher())
    .authorizeRequests()
        .anyRequest().authenticated();
}

private static class OAuthRequestedMatcher implements RequestMatcher {
    @Override
    public boolean matches(HttpServletRequest request) {
        String auth = request.getHeader("Authorization");
        // Determine if the client request contained an OAuth Authorization
        return (auth != null) && auth.startsWith("Bearer");
    }
}

唯一缺少的是如果身份验证不成功,没有“回退”方式。

对我来说,这种方法很有道理。 如果用户直接通过基本身份验证向请求提供身份验证,则不需要OAuth。 如果客户端在执行操作,则需要此过滤器介入并确保请求得到正确的身份验证。


4

为什么不反其道而行之呢?如果没有附加令牌,就绕过资源服务器,然后回退到正常的安全过滤器链。顺便说一下,这就是资源服务器过滤器停止的地方。

@Configuration
@EnableResourceServer
class ResourceServerConfig : ResourceServerConfigurerAdapter() {


    @Throws(Exception::class)
    override fun configure(resources: ResourceServerSecurityConfigurer) {
        resources.resourceId("aaa")
    }

    /**
     * Resources exposed via oauth. As we are providing also local user interface they are also accessible from within.
     */
    @Throws(Exception::class)
    override fun configure(http: HttpSecurity) {
        http.requestMatcher(BearerAuthorizationHeaderMatcher())
                .authorizeRequests()
                .anyRequest()
                .authenticated()
    }

    private class BearerAuthorizationHeaderMatcher : RequestMatcher {
        override fun matches(request: HttpServletRequest): Boolean {
            val auth = request.getHeader("Authorization")
            return auth != null && auth.startsWith("Bearer")
        }
    }

}

1
在尝试解决这个问题5个小时后,这个解决方案像魔法一样奏效了;-)今晚我将用一杯最好的苹果酒为您的健康干杯。 - Dave

3

@kca2ply提供的解决方案非常有效。我注意到浏览器没有发出挑战,所以我稍微调整了一下代码如下:

@Configuration
@Order(2)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

  @Override
  protected void configure(HttpSecurity http) throws Exception {

    // @formatter:off
    http.anonymous().disable()
      .requestMatcher(request -> {
          String auth = request.getHeader(HttpHeaders.AUTHORIZATION);
          return (auth != null && auth.startsWith("Basic"));
      })
      .antMatcher("/**")
      .authorizeRequests().anyRequest().authenticated()
    .and()
      .httpBasic();
    // @formatter:on
  }

  @Autowired
  public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
    auth.inMemoryAuthentication()
    .withUser("user").password("password").roles("USER");
  }
}

同时使用requestMatcher()antMatcher()可以使事情完美地运行起来。如果浏览器和HTTP客户端没有提供基本凭据,它们现在将首先要求基本凭据。如果没有提供凭据,则会转到OAuth2。


2
您可以将BasicAuthenticationFilter添加到安全过滤器链中,以在受保护的资源上获得OAuth2或基本身份验证安全性。下面是示例配置...
@Configuration
@EnableResourceServer
public class OAuth2ResourceServerConfig extends ResourceServerConfigurerAdapter {

    @Autowired
    private AuthenticationManager authenticationManagerBean;

    @Override
    public void configure(HttpSecurity http) throws Exception {
        // @formatter:off
        final String[] userEndpoints = {
            "/v1/api/airline"
        };

        final String[] adminEndpoints = {
                "/v1/api/jobs**"
            };

        http
            .requestMatchers()
                .antMatchers(userEndpoints)
                .antMatchers(adminEndpoints)
                .antMatchers("/secure/**")
                .and()
            .authorizeRequests()
                .antMatchers("/secure/**").authenticated()
                .antMatchers(userEndpoints).hasRole("USER")
                .antMatchers(adminEndpoints).hasRole("ADMIN");

        // @formatter:on
        http.addFilterBefore(new BasicAuthenticationFilter(authenticationManagerBean),
                UsernamePasswordAuthenticationFilter.class);
    }

}

BasicAuthenticationFilter需要一个能够处理UserPasswordAuthentication的AuthenticationManager,但是ResourceServerSecurityConfigurer没有注册这样的AuthenticationManager。为了使oauth2和basic auth在单个过滤器链中同时工作,需要编写更多的代码。 - Max

2
在Spring Boot的最新版本中,WebSecurityConfigurerAdapter 类已被弃用。
为了使一些端点具有HTTP基本授权并使其他端点具有OAuth2授权,解决方案是在HttpSecurityFilterChain Bean调用中先进行HTTP基本配置调用,然后再进行OAuth2配置调用。 例如:
@Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
        
        .authorizeRequests()
        .antMatchers("/auth/basic/endpoints/**","/basic-auth/**").authenticated().and().httpBasic()
        .and()
        
        .authorizeRequests()
        .antMatchers(HttpMethod.POST,"/auth/oauth2/test/**").hasAnyAuthority("SCOPE_somescope/test")
        .antMatchers(HttpMethod.POST,"/auth/oauth2/other/**").hasAnyAuthority("SCOPE_somescope/other")
        .and()
        
        .oauth2ResourceServer().jwt().decoder(jwtDecoder())
        .and()
        
        .accessDeniedHandler(accessDeniedHandler)
        .and()
        
        .authorizeRequests()
        .anyRequest().permitAll()
        .and()
        
        .csrf().disable();
        
        return http.build();
    }

jwtDecoder() 必须在配置类中定义为 bean,如下所示:

    @Bean
JwtDecoder jwtDecoder() {
    return JwtDecoders.fromIssuerLocation(issuerUri);
}

此外,您应该在配置中定义一个UserDetailsService bean。例如,为了开发目的最简单的方法是定义一个InMemoryUserDetailsManager:
@Bean
    public UserDetailsService users() {
        // The builder will ensure the passwords are encoded before saving in memory
        UserDetails admin = User.builder()
                .username("user-basic-auth")
                .password("{bcrypt}"+bcryptPassword)
                .roles("USER", "ADMIN")
                .build();
        return new InMemoryUserDetailsManager(admin);
    }

其中{bcrypt}用于解码存储在某处(例如数据库表中)的密码。 如果密码是明文的(不建议,除非用于测试和开发目的),您也可以在密码前面使用{noop}。

所有这些bean都应该在一个带有以下注释的类内定义:

@Configuration  
@EnableWebSecurity
public class WebSecurityConfig{
...
}

不要忘记在配置文件中指定JWT发行者URI,以使Oauth2正常工作:

spring.security.oauth2.resourceserver.jwt.issuer-uri

1

很抱歉无法提供完整的示例,但以下是挖掘的提示:

大致上,Spring认证只是请求过滤器和身份验证管理器的组合,请求过滤器从请求(标头)中提取身份验证数据,而身份验证管理器为该身份验证提供身份验证对象。

因此,要在同一URL上获取基本身份验证和OAuth,您需要在过滤器链中安装2个过滤器:BasicAuthenticationFilter和OAuth2AuthenticationProcessingFilter。

我认为问题在于ConfiguringAdapters适用于更简单的配置,因为它们倾向于相互覆盖。因此,作为第一步,请尝试移动

.httpBasic();

调用ResourceServerConfiguration。请注意,您还需要提供2个不同的身份验证管理器:一个用于基本身份验证,一个用于OAuth。


谢谢你的建议!我很快就会尝试解决这个问题,并且如果成功了,我会回来发布结果。 - user3613594

1
如果有人想在Spring WebFlux中实现此功能,确定请求是否被处理的方法称为"securityMatcher",而不是"requestMatcher"。
即:
fun configureBasicAuth(http: ServerHttpSecurity): SecurityWebFilterChain {
    return http
        .securityMatcher(BasicAuthServerWebExchangeMatcher())
        .authorizeExchange()
        ...

0

我认为不可能同时具有两种身份验证方式。您可以使用基本身份验证和OAuth2身份验证,但是针对不同的端点。就像您所做的那样,第一个配置将覆盖第二个配置,在这种情况下,将使用HTTP基本身份验证。


哼...有什么办法可以绕过这个限制吗?还是说它就是“运作方式”? - user3613594
1
也许你可以使用过滤器来实现,但我觉得这会变得过于复杂。那么使用不同的端点怎么样?比如:/basic/users 和 /oauth/users。 - raonirenosto
我们现在正在考虑这个问题。会继续深入挖掘,看看能否想出一种使其正常工作的方法。感谢您的反馈和建议! - user3613594
很高兴能够帮助您。如果这个答案对您有所帮助,请接受答案,谢谢。 - raonirenosto

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接