Иногда требуется, чтобы в приложении для разных url была настроена разная аутентификация и авторизация. Одну часть приложения надо обезопасить так, а другую этак.
Например, по адресам /api/* приложение предоставляет REST API с аутентификацией через JWT-токен. А все остальные url — это обычные страницы веб-сайта с перенаправлением на форму логина для неаутентифицированных, используют сессии.
Или же для каких-то url аутентификация будет базовая (Http Basic), а для остальных — обычная форма для входа (Form-Based). Именно такой пример мы и напишем.
Настройка двух HttpSecurity
По сути для разных url будет настроена разная цепочка фильтров, через которые проходит запрос. Чтобы такое реализовать, нужно настроить два бина HttpSecurity, а для этого дважды расширить класс WebSecurityConfigurerAdapter и переопределить в нем метод:
public class SecurityConfig extends WebSecurityConfigurerAdapter{ ... config(HttpSecurity http){ ... } }
Все то же, что в предыдущей статье, только дважды.
Задача
Мы решим такую задачу:
- Для адресов /admin/** работает Http Basic аутентификация. Еще эти url будут доступны только для администратора.
- Для всех остальных 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 аутентификация:
а во второй — Http Basic:
Можно было сделать 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.
Супер! Спасибо!!!