1. Обзор
В этом кратком руководстве мы рассмотрим, как определить несколько точек входа в приложении Spring Security .
В основном это влечет за собой определение нескольких блоков http
в файле конфигурации XML или нескольких экземпляров HttpSecurity путем многократного расширения класса
WebSecurityConfigurerAdapter
.
2. Зависимости Maven
Для разработки нам понадобятся следующие зависимости:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>2.6.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.6.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
<version>2.6.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>2.6.1</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<version>5.4.0</version>
</dependency>
Последние версии spring-boot-starter-security , spring-boot-starter-web , spring-boot-starter-thymeleaf , spring-boot-starter-test , spring-security-test можно загрузить с Maven Central.
3. Несколько точек входа
3.1. Несколько точек входа с несколькими элементами HTTP
Давайте определим основной класс конфигурации, который будет содержать пользовательский источник:
@Configuration
@EnableWebSecurity
public class MultipleEntryPointsSecurityConfig {
@Bean
public UserDetailsService userDetailsService() throws Exception {
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(User
.withUsername("user")
.password(encoder().encode("userPass"))
.roles("USER").build());
manager.createUser(User
.withUsername("admin")
.password(encoder().encode("adminPass"))
.roles("ADMIN").build());
return manager;
}
@Bean
public PasswordEncoder encoder() {
return new BCryptPasswordEncoder();
}
}
Теперь давайте посмотрим, как мы можем определить несколько точек входа в нашей конфигурации безопасности.
Мы собираемся использовать здесь пример, основанный на базовой аутентификации, и мы собираемся эффективно использовать тот факт, что Spring Security поддерживает определение нескольких элементов HTTP в наших конфигурациях.
При использовании конфигурации Java способ определения нескольких областей безопасности заключается в наличии нескольких классов @Configuration , которые расширяют базовый класс
WebSecurityConfigurerAdapter
— каждый со своей собственной конфигурацией безопасности. Эти классы могут быть статическими и размещаться внутри основного конфига.
Основная причина наличия нескольких точек входа в одном приложении — наличие разных типов пользователей, которые могут получить доступ к разным частям приложения.
Давайте определим конфигурацию с тремя точками входа, каждая с разными разрешениями и режимами аутентификации:
- один для административных пользователей, использующих базовую аутентификацию HTTP
- один для обычных пользователей, которые используют проверку подлинности с помощью формы
- и один для гостевых пользователей, которым не требуется аутентификация
Точка входа, определенная для пользователей с правами администратора, защищает URL-адреса формы /admin/**
, чтобы разрешить только пользователям с ролью ADMIN и требует базовой HTTP-аутентификации с точкой входа типа BasicAuthenticationEntryPoint
, которая устанавливается с помощью метода authenticationEntryPoint()
:
@Configuration
@Order(1)
public static class App1ConfigurationAdapter extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.antMatcher("/admin/**")
.authorizeRequests().anyRequest().hasRole("ADMIN")
.and().httpBasic().authenticationEntryPoint(authenticationEntryPoint());
}
@Bean
public AuthenticationEntryPoint authenticationEntryPoint(){
BasicAuthenticationEntryPoint entryPoint =
new BasicAuthenticationEntryPoint();
entryPoint.setRealmName("admin realm");
return entryPoint;
}
}
Аннотация @Order
для каждого статического класса указывает порядок, в котором будут рассматриваться конфигурации для поиска той, которая соответствует запрошенному URL-адресу. Значение заказа
для каждого класса должно быть уникальным.
Компонент типа BasicAuthenticationEntryPoint
требует установки свойства realName
.
3.2. Несколько точек входа, один и тот же элемент HTTP
Далее давайте определим конфигурацию для URL-адресов формы /user/**
, к которым могут получить доступ обычные пользователи с ролью USER, используя аутентификацию с помощью формы:
@Configuration
@Order(2)
public static class App2ConfigurationAdapter extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity http) throws Exception {
http.antMatcher("/user/**")
.authorizeRequests().anyRequest().hasRole("USER")
.and()
// formLogin configuration
.and()
.exceptionHandling()
.defaultAuthenticationEntryPointFor(
loginUrlauthenticationEntryPointWithWarning(),
new AntPathRequestMatcher("/user/private/**"))
.defaultAuthenticationEntryPointFor(
loginUrlauthenticationEntryPoint(),
new AntPathRequestMatcher("/user/general/**"));
}
}
Как мы видим, другим способом определения точек входа, кроме метода authenticationEntryPoint(), является использование метода defaultAuthenticationEntryPointFor()
. Это может определить несколько точек входа, которые соответствуют различным условиям на основе объекта RequestMatcher .
Интерфейс RequestMatcher
имеет реализации, основанные на различных типах условий, таких как совпадающий путь, тип мультимедиа или регулярное выражение. В нашем примере мы использовали AntPathRequestMatch для установки двух разных точек входа для URL-адресов форм /user/private/**
и /user/general/**
.
Затем нам нужно определить bean-компоненты точек входа в том же статическом классе конфигурации:
@Bean
public AuthenticationEntryPoint loginUrlauthenticationEntryPoint(){
return new LoginUrlAuthenticationEntryPoint("/userLogin");
}
@Bean
public AuthenticationEntryPoint loginUrlauthenticationEntryPointWithWarning(){
return new LoginUrlAuthenticationEntryPoint("/userLoginWithWarning");
}
Главное здесь заключается в том, как настроить эти множественные точки входа, а не в деталях реализации каждой из них.
В этом случае точки входа имеют тип LoginUrlAuthenticationEntryPoint
и используют разные URL-адреса страницы входа: /userLogin
для простой страницы входа и /userLoginWithWarning
для страницы входа, которая также отображает предупреждение при попытке доступа к /user/
private URL.
Эта конфигурация также потребует определения сопоставлений /userLogin
и /userLoginWithWarning
MVC и двух страниц со стандартной формой входа.
Для проверки подлинности с помощью формы очень важно помнить, что любой URL-адрес, необходимый для настройки, например URL-адрес обработки входа, также должен соответствовать формату /user/**
или быть настроен другим образом, чтобы быть доступным.
Обе приведенные выше конфигурации будут перенаправлять на URL-адрес /403
, если пользователь без соответствующей роли попытается получить доступ к защищенному URL-адресу.
Будьте осторожны, чтобы использовать уникальные имена для bean-компонентов, даже если они находятся в разных статических классах , иначе одно переопределит другое.
3.3. Новый элемент HTTP, без точки входа
Наконец, давайте определим третью конфигурацию для URL-адресов формы /guest/**
, которая разрешит все типы пользователей, включая неавторизованных:
@Configuration
@Order(3)
public static class App3ConfigurationAdapter extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity http) throws Exception {
http.antMatcher("/guest/**").authorizeRequests().anyRequest().permitAll();
}
}
3.4. XML-конфигурация
Давайте взглянем на эквивалентную конфигурацию XML для трех экземпляров HttpSecurity
в предыдущем разделе.
Как и ожидалось, он будет содержать три отдельных блока XML <http> .
Для URL-адресов /admin/**
конфигурация XML будет использовать атрибут entry-point-ref элемента
http-basic
:
<security:http pattern="/admin/**" use-expressions="true" auto-config="true">
<security:intercept-url pattern="/**" access="hasRole('ROLE_ADMIN')"/>
<security:http-basic entry-point-ref="authenticationEntryPoint" />
</security:http>
<bean id="authenticationEntryPoint"
class="org.springframework.security.web.authentication.www.BasicAuthenticationEntryPoint">
<property name="realmName" value="admin realm" />
</bean>
Здесь следует отметить, что при использовании конфигурации XML роли должны иметь форму ROLE_<ROLE_NAME>
.
Конфигурация для URL-адресов /user/**
должна быть разбита на два блока http
в xml, поскольку нет прямого эквивалента метода defaultAuthenticationEntryPointFor()
.
Конфигурация для URL-адресов /user/general/**:
<security:http pattern="/user/general/**" use-expressions="true" auto-config="true"
entry-point-ref="loginUrlAuthenticationEntryPoint">
<security:intercept-url pattern="/**" access="hasRole('ROLE_USER')" />
//form-login configuration
</security:http>
<bean id="loginUrlAuthenticationEntryPoint"
class="org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint">
<constructor-arg name="loginFormUrl" value="/userLogin" />
</bean>
Для URL-адресов /user/private/**
мы можем определить аналогичную конфигурацию:
<security:http pattern="/user/private/**" use-expressions="true" auto-config="true"
entry-point-ref="loginUrlAuthenticationEntryPointWithWarning">
<security:intercept-url pattern="/**" access="hasRole('ROLE_USER')"/>
//form-login configuration
</security:http>
<bean id="loginUrlAuthenticationEntryPointWithWarning"
class="org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint">
<constructor-arg name="loginFormUrl" value="/userLoginWithWarning" />
</bean>
Для URL-адресов /guest/**
у нас будет элемент http :
<security:http pattern="/**" use-expressions="true" auto-config="true">
<security:intercept-url pattern="/guest/**" access="permitAll()"/>
</security:http>
Также важно, чтобы по крайней мере один блок XML <http>
соответствовал шаблону /**.
4. Доступ к защищенным URL-адресам
4.1. Конфигурация MVC
Давайте создадим сопоставления запросов, соответствующие защищенным шаблонам URL:
@Controller
public class PagesController {
@GetMapping("/admin/myAdminPage")
public String getAdminPage() {
return "multipleHttpElems/myAdminPage";
}
@GetMapping("/user/general/myUserPage")
public String getUserPage() {
return "multipleHttpElems/myUserPage";
}
@GetMapping("/user/private/myPrivateUserPage")
public String getPrivateUserPage() {
return "multipleHttpElems/myPrivateUserPage";
}
@GetMapping("/guest/myGuestPage")
public String getGuestPage() {
return "multipleHttpElems/myGuestPage";
}
@GetMapping("/multipleHttpLinks")
public String getMultipleHttpLinksPage() {
return "multipleHttpElems/multipleHttpLinks";
}
}
Сопоставление /multipleHttpLinks
вернет простую HTML-страницу со ссылками на защищенные URL-адреса:
<a th:href="@{/admin/myAdminPage}">Admin page</a>
<a th:href="@{/user/general/myUserPage}">User page</a>
<a th:href="@{/user/private/myPrivateUserPage}">Private user page</a>
<a th:href="@{/guest/myGuestPage}">Guest page</a>
Каждая из HTML-страниц, соответствующих защищенным URL-адресам, будет иметь простой текст и обратную ссылку:
Welcome admin!
<a th:href="@{/multipleHttpLinks}" >Back to links</a>
4.2. Инициализация приложения
Мы запустим наш пример как приложение Spring Boot, поэтому давайте определим класс с основным методом:
@SpringBootApplication
public class MultipleEntryPointsApplication {
public static void main(String[] args) {
SpringApplication.run(MultipleEntryPointsApplication.class, args);
}
}
Если мы хотим использовать конфигурацию XML, нам также нужно добавить аннотацию @ImportResource({"classpath*:spring-security-multiple-entry.xml"})
к нашему основному классу.
4.3. Тестирование конфигурации безопасности
Давайте настроим тестовый класс JUnit, который мы можем использовать для проверки наших защищенных URL-адресов:
@RunWith(SpringRunner.class)
@WebAppConfiguration
@SpringBootTest(classes = MultipleEntryPointsApplication.class)
public class MultipleEntryPointsTest {
@Autowired
private WebApplicationContext wac;
@Autowired
private FilterChainProxy springSecurityFilterChain;
private MockMvc mockMvc;
@Before
public void setup() {
this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac)
.addFilter(springSecurityFilterChain).build();
}
}
Далее давайте проверим URL-адреса, используя пользователя- администратора
.
При запросе URL-адреса /admin/adminPage
без базовой HTTP-аутентификации мы должны ожидать получения кода состояния Unauthorized, а после добавления аутентификации код состояния должен быть 200 OK.
Если мы попытаемся получить доступ к URL-адресу /user/userPage
с правами администратора, мы должны получить статус 302 Forbidden:
@Test
public void whenTestAdminCredentials_thenOk() throws Exception {
mockMvc.perform(get("/admin/myAdminPage")).andExpect(status().isUnauthorized());
mockMvc.perform(get("/admin/myAdminPage")
.with(httpBasic("admin", "adminPass"))).andExpect(status().isOk());
mockMvc.perform(get("/user/myUserPage")
.with(user("admin").password("adminPass").roles("ADMIN")))
.andExpect(status().isForbidden());
}
Давайте создадим аналогичный тест, используя учетные данные обычного пользователя для доступа к URL-адресам:
@Test
public void whenTestUserCredentials_thenOk() throws Exception {
mockMvc.perform(get("/user/general/myUserPage")).andExpect(status().isFound());
mockMvc.perform(get("/user/general/myUserPage")
.with(user("user").password("userPass").roles("USER")))
.andExpect(status().isOk());
mockMvc.perform(get("/admin/myAdminPage")
.with(user("user").password("userPass").roles("USER")))
.andExpect(status().isForbidden());
}
Во втором тесте мы видим, что отсутствие аутентификации формы приведет к статусу 302 Found вместо Unauthorized, поскольку Spring Security перенаправит на форму входа.
Наконец, давайте создадим тест, в котором мы получаем доступ к URL-адресу /guest/guestPage
со всеми тремя типами аутентификации и убеждаемся, что получаем статус 200 OK:
@Test
public void givenAnyUser_whenGetGuestPage_thenOk() throws Exception {
mockMvc.perform(get("/guest/myGuestPage")).andExpect(status().isOk());
mockMvc.perform(get("/guest/myGuestPage")
.with(user("user").password("userPass").roles("USER")))
.andExpect(status().isOk());
mockMvc.perform(get("/guest/myGuestPage")
.with(httpBasic("admin", "adminPass")))
.andExpect(status().isOk());
}
5. Вывод
В этом руководстве мы продемонстрировали, как настроить несколько точек входа при использовании Spring Security.
Полный исходный код примеров можно найти на GitHub . Чтобы запустить приложение, раскомментируйте тег начального класса
MultipleEntryPointsApplication в
файле pom.xml
и выполните команду mvn spring-boot:run
, а затем получите доступ к URL -адресу /multipleHttpLinks
.
``
Обратите внимание, что при использовании базовой HTTP-аутентификации невозможно выйти из системы, поэтому вам придется закрыть и снова открыть браузер, чтобы удалить эту аутентификацию.
Чтобы запустить тест JUnit, используйте определенные входные точки профиля Maven со
следующей командой:
mvn чистая установка-PentryPoints