Multiple HttpSecurity

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

Например, по адресам /api/* приложение предоставляет REST API с аутентификацией через JWT-токен. А все остальные url – это обычные страницы веб-сайта с перенаправлением на форму логина для неаутентифицированных, используют сессии.

Или же для каких-то url аутентификация будет базовая (Http Basic), а для остальных – обычная форма для входа (Form-Based). Именно такой пример мы и напишем.

Настройка двух HttpSecurity

По сути для разных url будет настроена разная цепочка фильтров, через которые проходит запрос. Чтобы такое реализовать, нужно настроить два бина HttpSecurity, а для этого дважды расширить класс WebSecurityConfigurerAdapter и переопределить в нем метод:

public class SecurityConfig extends WebSecurityConfigurerAdapter{
    ... 

    config(HttpSecurity http){
     ...
    }
}

Все то же, что в предыдущей статье, только дважды.

Задача

Мы решим такую задачу:

  1. Для  адресов /admin/** работает Http Basic аутентификация. Еще эти url будут доступны только для администратора.
  2. Для всех остальных url работает Form-Based аутентификация (если аутентификация нужна). Нужна она для /user – этот url доступен только для ролей пользователя и администратора.

Итак, напишем две конфигурации для этих двух пунктов:

@EnableWebSecurity 
//одна конфигурация
public class SecurityConfig extends WebSecurityConfigurerAdapter {


   //здесь будет настройка аутентификации 

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

        http
                .antMatcher("/**")
                .authorizeRequests(a -> a
                        .antMatchers("/user/**").hasAnyRole("ADMIN", "USER")
                        .anyRequest().permitAll())
                .formLogin();

    }


    @Configuration
    // другая конфигурация - приоритетная
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public static class AnotherSecurityConfig extends WebSecurityConfigurerAdapter {


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

            http
                    .antMatcher("/admin/**")
                    .authorizeRequests(a -> a.anyRequest().hasAnyRole("ADMIN"))
                    .httpBasic();

        }
    }

}

Конфигурация для п. 1 сделана как статический вложенный класс – это обычный класс, но так он компактно помещается на страницу внутри другого. Его можно вынести и в отдельный файл без слова static.

Отличаются конфигурации тем, что в первой используется Form-Based аутентификация:

Form-Based по адресу localhost:8080/user
Form-Based по адресу localhost:8080/user

а во второй – Http Basic:

Http Basic по адресу localhost:8080/admin
Http Basic по адресу localhost:8080/admin

Можно было сделать JWT-авторизацию для одной из конфигураций, про JWT есть статья. Там в HttpSecurity добавлен фильтр:

http.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);

Суть в том, что можно задать совершенно разные HttpSecurity для разных url, при этом разные запросы будут проходить через разные цепочки фильтров.

В классе SecurityConfig задан HttpSecurity для всех url:

htpp.antMatcher("/**")

а в классе AnotherSecurityConfig задан HttpSecurity для url, начинающихся с /admin:

htpp.antMatcher("/admin/**")

Обратите внимание, что метод тут называется antMatcher(..), а не antMatchers(..), разница в букву, но второй используется уже внутри для выдачи разрешений, а первый задает, для каких url собран весь HttpSecurity.

Как задать приоритет: какую HttpSecurity проверять первой на предмет соответствия запросу

То есть когда приходит запрос, приложение проверяет, соответствует ли он шаблону в htpp. antMatcher(…). И если да, применяет для пришедшего  запроса данный http.

При этом сначала оно проверяет конфигурацию  AnotherSecurityConfig, так как ее приоритет выше.

Приоритет задается с помощью аннотации @Order(Ordered.HIGHEST_PRECEDENCE)

То есть, например, если придет запрос /admin, то приложение в первую очередь проверит конфигурацию AnotherSecurityConfig.  В ней задан шаблон “/admin/**”, и путь /admin удовлетворяет шаблону. Так что для запроса конфигурация AnotherSecurityConfig выиграет несмотря на то, оба шаблона подходят: как  в AnotherSecurityConfig, так и в SecurityConfig.

Общие пользователи для двух конфигураций

Важно отметить, что пользователи должны быть общие для обеих конфигураций, а значит настроить AuthenticationManagerBuilder переопределением метода config(AuthenticationManagerBuilder auth), как это мы делали в предыдущей статье, не получится (иначе в каком из классов его переопределять). Вместо этого создадим метод configGlobal (можно назвать его как угодно) и внедрим в него AuthenticationManagerBuilder:

@EnableWebSecurity 
//одна конфигурация 
public class SecurityConfig extends WebSecurityConfigurerAdapter {
...    
    @Autowired
    public void configGlobal(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                .withUser("user")
                .password("password")
                .authorities("ROLE_USER")
                .and()
                .withUser("admin")
                .password("password")
                .authorities("ROLE_ADMIN");
    }
}

Так пользователи user и admin будут действовать для обеих конфигураций HttpSecurity.

Альтернативный способ задать пользователей

Как вариант, общих пользователей можно задать UserDetailsService:

    @Bean
    public UserDetailsService userDetailsService() {
        // ensure the passwords are encoded properly
        User.UserBuilder users = User.withDefaultPasswordEncoder();
        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
        manager.createUser(users.username("user").password("password")
                .roles("USER").build());
        manager.createUser(users.username("admin").password("password")
                .roles("ADMIN").build());
        return manager;
    }

В последнем случае бин PasswordEncoder в конфигурации НЕ нужен:

    @Bean
    public PasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }

Исходный код доступен на GitHub.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *