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

Spring @EnableWebSecurity против @EnableGlobalMethodSecurity

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

1. Обзор

Мы можем захотеть применить несколько фильтров безопасности в разных путях наших приложений Spring Boot.

В этом руководстве мы рассмотрим два подхода к настройке нашей безопасности — с помощью @EnableWebSecurity и @EnableGlobalMethodSecurity .

Чтобы проиллюстрировать различия, мы будем использовать простое приложение, которое имеет некоторые административные ресурсы, аутентифицированные пользовательские ресурсы. Мы также предоставим ему раздел с общедоступными ресурсами, которые мы рады каждому загрузить.

2. Безопасность весенней загрузки

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

Какой бы подход мы ни выбрали, сначала нам нужно добавить стартер весенней загрузки для безопасности:

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>

2.2. Автоконфигурация Spring Boot

С Spring Security в пути к классам Spring Boot Security Auto-Configuration WebSecurityEnablerConfiguration активирует для нас @EnableWebSecurity . ``

Это применяет конфигурацию безопасности Spring по умолчанию к нашему приложению.

Безопасность по умолчанию активирует как фильтры безопасности HTTP, так и цепочку фильтров безопасности и применяет базовую аутентификацию к нашим конечным точкам.

3. Защита наших конечных точек

Для нашего первого подхода давайте начнем с создания класса MySecurityConfigurer , который расширяет WebSecurityConfigurerAdapter , убедившись, что мы аннотируем его с помощью @EnableWebSecurity.

@EnableWebSecurity
public class MySecurityConfigurer extends WebSecurityConfigurerAdapter {
}

Расширяя адаптер, мы получаем преимущества других средств защиты Spring Security, а также можем добавлять настройки.

3.1. Краткий обзор веб-безопасности по умолчанию

Во-первых, давайте взглянем на метод настройки WebSecurityConfigurerAdapter по умолчанию , чтобы мы знали, что мы собираемся переопределить: ``

@Override
protected void configure(HttpSecurity http) {
http.authorizeRequests((requests) -> requests.anyRequest().authenticated());
http.formLogin();
http.httpBasic();
}

Здесь мы видим, что любой полученный нами запрос аутентифицируется, и у нас есть базовая форма входа в систему для запроса учетных данных.

Когда мы хотим использовать HttpSecurity DSL, мы пишем это так:

http.authorizeRequests().anyRequest().authenticated()
.and().formLogin()
.and().httpBasic()

3.2. Требовать от пользователей соответствующей роли

Теперь давайте настроим нашу безопасность, чтобы разрешить доступ к нашей конечной точке /admin только пользователям с ролью ADMIN . Мы также разрешим доступ к нашей конечной точке /protected только пользователям с ролью USER . ``

Мы достигаем этого, переопределяя перегрузку HttpSecurity configure :

@Override
protected void configure(HttpSecurity http) {
http.authorizeRequests()
.antMatchers("/admin/**")
.hasRole("ADMIN")
.antMatchers("/protected/**")
.hasRole("USER");
}

3.3. Ослабьте безопасность для общедоступных ресурсов

Нам не нужна аутентификация для наших общедоступных ресурсов /hello , поэтому мы настроим WebSecurity так , чтобы они ничего не делали для них.

Как и раньше, давайте переопределим один из методов configure WebSecurityConfigurerAdapter , но на этот раз перегрузку WebSecurity :

@Override
public void configure(WebSecurity web) {
web.ignoring()
.antMatchers("/hello/*");
}

3.4. Замена безопасности Spring по умолчанию

Хотя большинство наших требований можно удовлетворить, расширив WebSecurityConfigurerAdapter , могут быть случаи, когда мы хотим полностью заменить конфигурацию безопасности Spring по умолчанию. Для этого мы можем реализовать WebSecurityConfigurer , а не расширять WebSecurityConfigurerAdapter .

Следует отметить, что реализуя WebSecurityConfigurer, мы теряем стандартную защиту безопасности Spring , поэтому нам следует очень тщательно все обдумать, прежде чем идти по этому пути.

4. Защитите наши конечные точки с помощью аннотаций

Чтобы применить безопасность с использованием подхода, основанного на аннотациях, мы можем использовать @EnableGlobalMethodSecurity.

4.1. Требовать от пользователей соответствующей роли с помощью аннотаций безопасности

Теперь давайте воспользуемся аннотациями методов для настройки нашей безопасности, чтобы разрешить доступ к нашей конечной точке /admin только пользователям ADMIN , а нашим пользователям USER доступ к нашей конечной точке /protected . ``

Давайте включим аннотации JSR-250 , установив jsr250Enabled=true в нашей аннотации EnableGlobalMethodSecurity :

@EnableGlobalMethodSecurity(jsr250Enabled = true)
@Controller
public class AnnotationSecuredController {
@RolesAllowed("ADMIN")
@RequestMapping("/admin")
public String adminHello() {
return "Hello Admin";
}

@RolesAllowed("USER")
@RequestMapping("/protected")
public String jsr250Hello() {
return "Hello Jsr250";
}
}

4.2. Обеспечение безопасности всех общедоступных методов

Когда мы используем аннотации как способ реализации безопасности, мы можем забыть аннотировать метод. Это непреднамеренно создало бы дыру в безопасности.

Чтобы защититься от этого, мы должны запретить доступ ко всем методам, которые не имеют аннотаций авторизации .

4.3. Разрешить доступ к общедоступным ресурсам

Безопасность Spring по умолчанию обеспечивает аутентификацию для всех наших конечных точек, независимо от того, добавляем ли мы безопасность на основе ролей или нет.

Хотя наш предыдущий пример применяет безопасность к нашим конечным точкам /admin и /protected , мы все же хотим разрешить доступ к файловым ресурсам в /hello .

Хотя мы могли бы снова расширить WebSecurityAdapter , Spring предоставляет нам более простую альтернативу.

Защитив наши методы аннотациями, теперь мы можем добавить WebSecurityCustomizer для открытия ресурсов /hello/* :

public class MyPublicPermitter implements WebSecurityCustomizer {
    public void customize(WebSecurity webSecurity) {
        webSecurity.ignoring()
.antMatchers("/hello/*");
    }
}

В качестве альтернативы мы можем просто создать bean-компонент, который реализует его внутри нашего класса конфигурации:

@Configuration
public class MyWebConfig {
@Bean
public WebSecurityCustomizer ignoreResources() {
return (webSecurity) -> webSecurity
.ignoring()
.antMatchers("/hello/*");
}
}

Когда Spring Security инициализируется, он вызывает любой найденный WebSecurityCustomizer , включая наш.

5. Проверка нашей безопасности

Теперь, когда мы настроили нашу безопасность, мы должны проверить, работает ли она так, как мы задумали.

В зависимости от подхода, который мы выбрали для нашей безопасности, у нас есть один или два варианта наших автоматических тестов. Мы можем либо отправлять веб-запросы в наше приложение, либо напрямую вызывать методы нашего контроллера.

5.1. Тестирование через веб-запросы

Для первого варианта мы создадим тестовый класс @SpringBootTest с @TestRestTemplate :

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = RANDOM_PORT)
public class WebSecuritySpringBootIntegrationTest {
@Autowired
private TestRestTemplate template;
}

Теперь давайте добавим тест, чтобы убедиться, что наши общедоступные ресурсы доступны:

@Test
public void givenPublicResource_whenGetViaWeb_thenOk() {
ResponseEntity<String> result = template.getForEntity("/hello/foreach.txt", String.class);
assertEquals("Hello From ForEach", result.getBody());
}

Мы также можем увидеть, что происходит, когда мы пытаемся получить доступ к одному из наших защищенных ресурсов:

@Test
public void whenGetProtectedViaWeb_thenForbidden() {
ResponseEntity<String> result = template.getForEntity("/protected", String.class);
assertEquals(HttpStatus.FORBIDDEN, result.getStatusCode());
}

Здесь мы получаем ответ ЗАПРЕЩЕНО , так как в нашем анонимном запросе нет нужной роли.

Таким образом, мы можем использовать этот метод для тестирования нашего защищенного приложения, какой бы подход к безопасности мы ни выбрали.

5.2. Тестирование с помощью автоматического подключения и аннотаций

Теперь давайте посмотрим на наш второй вариант. Давайте настроим @SpringBootTest и автоматически подключим наш AnnotationSecuredController:

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = RANDOM_PORT)
public class GlobalMethodSpringBootIntegrationTest {
@Autowired
private AnnotationSecuredController api;
}

Давайте начнем с тестирования нашего общедоступного метода с помощью @WithAnonymousUser :

@Test
@WithAnonymousUser
public void givenAnonymousUser_whenPublic_thenOk() {
assertThat(api.publicHello()).isEqualTo(HELLO_PUBLIC);
}

Теперь, когда мы получили доступ к нашему общедоступному ресурсу, давайте воспользуемся аннотациями @WithMockUser для доступа к нашим защищенным методам.

Во-первых, давайте протестируем наш защищенный метод JSR-250 с пользователем с ролью «ПОЛЬЗОВАТЕЛЬ»:

@WithMockUser(username="foreach", roles = "USER")
@Test
public void givenUserWithRole_whenJsr250_thenOk() {
assertThat(api.jsr250Hello()).isEqualTo("Hello Jsr250");
}

А теперь давайте попробуем получить доступ к тому же методу, когда у нашего пользователя нет нужной роли:

@WithMockUser(username="foreach", roles = "NOT-USER")
@Test(expected = AccessDeniedException.class)
public void givenWrongRole_whenJsr250_thenAccessDenied() {
api.jsr250Hello();
}

Наш запрос был перехвачен Spring Security, и было выдано исключение AccessDeniedException .

Мы можем использовать этот подход только тогда, когда выбираем безопасность на основе аннотаций.

6. Предостережения в отношении аннотаций

Когда мы выбираем подход, основанный на аннотациях, следует отметить несколько важных моментов.

Наша аннотированная безопасность применяется только тогда, когда мы входим в класс через общедоступный метод .

6.1. Косвенный вызов метода

Ранее, когда мы вызывали аннотированный метод, мы видели успешное применение нашей системы безопасности. Однако теперь давайте создадим общедоступный метод в том же классе, но без аннотации безопасности. Мы заставим его вызывать наш аннотированный метод jsr250Hello :

@GetMapping("/indirect")
public String indirectHello() {
return jsr250Hello();
}

Теперь давайте вызовем нашу конечную точку «/indirect», просто используя анонимный доступ:

@Test
@WithAnonymousUser
public void givenAnonymousUser_whenIndirectCall_thenNoSecurity() {
assertThat(api.indirectHello()).isEqualTo(HELLO_JSR_250);
}

Наш тест проходит успешно, поскольку наш «защищенный» метод был вызван без срабатывания какой-либо защиты. Другими словами, к внутренним вызовам внутри одного класса не применяется никакой защиты .

6.2. Косвенный вызов метода для другого класса

Теперь давайте посмотрим, что происходит, когда наш незащищенный метод вызывает аннотированный метод другого класса.

Во-первых, давайте создадим DifferentClass с аннотированным методом DifferentJsr250Hello :

@Component
public class DifferentClass {
@RolesAllowed("USER")
public String differentJsr250Hello() {
return "Hello Jsr250";
}
}

Теперь давайте автоматически подключим DifferentClass к нашему контроллеру и добавим незащищенный публичный метод DifferentClassHello для его вызова.

@Autowired
DifferentClass differentClass;

@GetMapping("/differentclass")
public String differentClassHello() {
return differentClass.differentJsr250Hello();
}

И, наконец, давайте проверим вызов и убедимся, что наша безопасность обеспечена:

@Test(expected = AccessDeniedException.class)
@WithAnonymousUser
public void givenAnonymousUser_whenIndirectToDifferentClass_thenAccessDenied() {
api.differentClassHello();
}

Итак, мы видим, что, хотя наши аннотации безопасности не будут соблюдаться, когда мы вызываем другой метод в том же классе, когда мы вызываем аннотированный метод в другом классе, они соблюдаются.

6.3. Последнее предостережение

Мы должны убедиться, что мы правильно настроили наш @EnableGlobalMethodSecurity . Если мы этого не сделаем, то, несмотря на все наши аннотации безопасности, они могут вообще не иметь никакого эффекта.

Например, если мы используем аннотации JSR-250, но вместо jsr250Enabled=true указываем prePostEnabled=true , то наши аннотации JSR-250 ничего не сделают!

@EnableGlobalMethodSecurity(prePostEnabled = true)

Мы можем, конечно, объявить, что будем использовать более одного типа аннотаций, добавив их оба в нашу аннотацию @EnableGlobalMethodSecurity :

@EnableGlobalMethodSecurity(jsr250Enabled = true, prePostEnabled = true)

7. Когда нам нужно больше

По сравнению с JSR-250 мы также можем использовать Spring Method Security . Это включает в себя использование более мощного языка Spring Security Expression Language (SpEL) для более сложных сценариев авторизации. Мы можем включить SpEL в нашей аннотации EnableGlobalMethodSecurity , установив prePostEnabled=true:

@EnableGlobalMethodSecurity(prePostEnabled = true)

Кроме того, когда мы хотим обеспечить безопасность на основе того, принадлежит ли объект домена пользователю, мы можем использовать Spring Security Access Control Lists .

Также следует отметить, что когда мы пишем реактивные приложения, вместо этого мы используем @EnableWebFluxSecurity и @EnableReactiveMethodSecurity .

8. Заключение

В этом руководстве мы впервые рассмотрели, как защитить наше приложение с помощью централизованного подхода к правилам безопасности с помощью @EnableWebSecurity.

Затем мы использовали это, настроив нашу систему безопасности, чтобы поместить эти правила ближе к коду, на который они влияют. Мы сделали это, используя @EnableGlobalMethodSecurity и аннотируя методы, которые хотели защитить.

Наконец, мы представили альтернативный способ ослабления безопасности общедоступных ресурсов, которым он не нужен.

Как всегда, код примера доступен на GitHub .