Иногда требуется, чтобы в приложении для разных 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.
Супер! Спасибо!!!