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