Spring с аутентификацией Oauth2 или Http-Basic для одного и того же ресурса

Я пытаюсь реализовать API с ресурсами, которые защищены аутентификацией Oauth2 ИЛИ Http-Basic.

Когда я загружаю WebSecurityConfigurerAdapter, который сначала применяет HTTP-базовую аутентификацию к ресурсу, аутентификация маркера Oauth2 не принимается. И наоборот.

Примеры конфигураций: Это относится к HTTP-базовой аутентификации ко всем /user/ ** resources

@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();
    }
}

Это относится к защите маркера oauth к ресурсу /user/ **

@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 попробовать оба, если первый не сработал?

Любая помощь будет наиболее оценена.

Ответ 1

Мне удалось получить эту работу на основе намеков Майкла Ресслера, но с некоторыми настройками.

Моя цель состояла в том, чтобы позволить как Basic Auth, так и Oauth на тех же конечных точках ресурсов, например, /leafcase/ 123. Я был захвачен в течение довольно долгого времени из-за упорядочивания filterChains (можно проверить в FilterChainProxy.filterChains); порядок по умолчанию выглядит следующим образом:

  • Сервер аутентификации Oauth (если включен в том же проекте) filterChains. по умолчанию 0 (см. AuthorizationServerSecurityConfiguration)
  • Фильтр ресурсов ресурса OauthChains. по умолчанию 3 (см. ResourceServerConfiguration). Он имеет логику сопряжения запросов, которая соответствует чему-либо, кроме конечных точек аутентификации Oauth (например,/oauth/token,/oauth/authorize и т.д. См. ResourceServerConfiguration $NotOauthRequestMatcher.matches()).
  • ФильтрChains, который соответствует config (HttpSecurity http) - по умолчанию 100, см. WebSecurityConfigurerAdapter.

Поскольку ресурсный сервер filterChains занимает более высокое значение, чем у настроенного фильтра WebSecurityConfigurerAdapter, и первый соответствует практически каждой конечной точке ресурса, тогда логика сервера Oauth всегда запускается для любого запроса на конечные точки ресурсов (даже если запрос использует авторизацию: Basic заголовок). Ошибка, которую вы получите:

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

Я сделал 2 изменения, чтобы получить эту работу:

Во-первых, закажите WebSecurityConfigurerAdapter выше, чем сервер ресурсов (заказ 2 выше, чем заказ 3).

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

Во-вторых, пусть configure (HttpSecurity) использует клиент RequestMatcher, который соответствует только "Authorization: Basic".

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

    // @formatter:off
    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"));
    }
 }

В результате он сопоставляет и обрабатывает запрос ресурса Basic Auth до того, как фильтр ресурса сервера имеет шанс сопоставить его. Он также ТОЛЬКО обрабатывает Authorizaiton: запрос базового ресурса, поэтому любые запросы с авторизацией: Bearer будет проваливаться, а затем обрабатываться фильтром filterChain (т.е. Фильтром Oauth). Кроме того, он находится ниже, чем AuthenticationServer (в случае, если AuthenticationServer включен в том же проекте), поэтому он не мешает процессу фильтрования AuthenticaitonServer обрабатывать запрос в/oauth/токен и т.д.

Ответ 2

Это может быть близко к тому, что вы искали:

@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");
    }
}

Единственное, что не предусмотрено, это способ "отступить", если аутентификация не удалась.

Для меня этот подход имеет смысл. Если пользователь напрямую предоставляет аутентификацию для запроса через Basic auth, то OAuth не требуется. Если Клиент является действующим, нам нужен этот фильтр для входа и убедитесь, что запрос правильно аутентифицирован.

Ответ 3

Я считаю, что невозможно получить обе аутентификации. У вас может быть базовая аутентификация и аутентификация oauth2, но для разных конечных точек. Как и вы, первая конфигурация преодолеет вторую, в этом случае будет использоваться http basic.

Ответ 4

Вы можете добавить BasicAuthenticationFilter в цепочку фильтров безопасности, чтобы получить защиту OAuth2 OR Basic на защищенном ресурсе. Пример config ниже...

@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);
    }

}

Ответ 5

Решение @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.

Ответ 6

Не могу предоставить вам полный пример, но здесь есть подсказки для копания:

Грубо говоря, spring auth - это просто комбинация фильтра запросов, который извлекает данные аутентификации из запроса (заголовки) и диспетчера аутентификации, который предоставляет объект аутентификации для этого auth.

Итак, чтобы получить базовый и oauth на том же URL-адресе, вам нужно установить 2 фильтра в цепочке фильтров BasicAuthenticationFilter и OAuth2AuthenticationProcessingFilter.

Я думаю, проблема в том, что ConfiguringAdapters хороши для более простых confs, поскольку они, как правило, переопределяют друг друга. Итак, как первый шаг, попробуйте переместить

.httpBasic();

вызов ResourceServerConfiguration Обратите внимание, что вам также необходимо предоставить 2 разных менеджера auth: один для базового auth и один для oauth

Ответ 7

И почему бы не сделать это наоборот? Просто обходите сервер ресурсов, если нет подключенного токена, а затем отмените его на обычную цепочку фильтров безопасности. Это, кстати, то, что фильтр сервера ресурсов останавливается.

@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")
        }
    }

}