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

1. Обзор

В этом кратком руководстве мы рассмотрим, как определить несколько точек входа в приложении Spring Security

Это в основном влечет за собой определение нескольких блоков http в файле конфигурации XML или нескольких экземпляров HttpSecurity путем расширения класса WebSecurityConfigurerAdapter несколько раз.

2. Зависимости Maven

Для разработки нам понадобятся следующие зависимости:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
    <version>2.0.0.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <version>2.0.0.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
    <version>2.0.0.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <version>2.0.0.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-test</artifactId>
    <version>5.0.4.RELEASE</version>
</dependency>

Последние версии spring-boot-starter-security , spring-boot-starter-web , spring-boot-starter-thymeleaf , https://search . maven.org/classic/#search%7Cga%7C1%7Ca%3A%22spring-boot-starter-test%22[spring-boot-starter-test], https://search.maven.org/classic/#search % 7Cga% 7C1% 7Ca% 3A% 22spring-security-test% 22[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. Значение order для каждого класса должно быть уникальным.

Бобу типа 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 для страницы входа в систему, которая также отображает предупреждение при попытке доступа к частным URL-адресам /user/ .

Эта конфигурация также потребует определения отображений /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, поэтому давайте определим класс с методом main

@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 с помощью пользователя admin .

При запросе URL-адреса /admin/adminPage без базовой HTTP-аутентификации следует ожидать получения неавторизованного кода состояния, а после добавления аутентификации код состояния должен быть 200 OK.

При попытке получить доступ к URL-адресу /user/userPage с правами администратора, мы должны получить статус 302 Запрещено:

@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 «Найден» вместо «Несанкционированный», поскольку 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.

Полный исходный код для примеров можно найти over на GitHub . Чтобы запустить приложение, раскомментируйте тег MultipleEntryPointsApplication start-class в pom.xml и выполните команду mvn spring-boot: run , затем получите доступ к URL-адресу /multipleHttpLinks _. _

Обратите внимание, что невозможно выйти из системы при использовании HTTP Basic Authentication, поэтому вам придется закрыть и снова открыть браузер, чтобы удалить эту аутентификацию.

Чтобы запустить тест JUnit, используйте определенный профиль Maven entryPoints со следующей командой:

mvn чистой установки -PentryPoints