Перейти к основному содержимому

Несколько точек входа в Spring Security

· 8 мин. чтения

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