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

Spring Security 5 для реактивных приложений

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

1. Введение

В этой статье мы рассмотрим новые функции среды Spring Security 5 для защиты реактивных приложений. Этот выпуск соответствует Spring 5 и Spring Boot 2.

В этой статье мы не будем вдаваться в подробности о самих реактивных приложениях, которые являются новой функцией фреймворка Spring 5. Обязательно ознакомьтесь со статьей Введение в Reactor Core для получения более подробной информации.

2. Настройка Мавена

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

Базовая настройка требует родительского объявления, веб-стартера и зависимостей стартера безопасности. Нам также понадобится среда тестирования Spring Security:

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.1</version>
<relativePath/>
</parent>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

Мы можем проверить текущую версию стартера безопасности Spring Boot на Maven Central .

3. Настройка проекта

3.1. Начальная загрузка реактивного приложения

Мы не будем использовать стандартную конфигурацию @SpringBootApplication , а вместо этого настроим веб-сервер на основе Netty. Netty — это асинхронная платформа на основе NIO, которая является хорошей основой для реактивных приложений.

Аннотация @EnableWebFlux включает стандартную конфигурацию Spring Web Reactive для приложения:

@ComponentScan(basePackages = {"com.foreach.security"})
@EnableWebFlux
public class SpringSecurity5Application {

public static void main(String[] args) {
try (AnnotationConfigApplicationContext context
= new AnnotationConfigApplicationContext(
SpringSecurity5Application.class)) {

context.getBean(NettyContext.class).onClose().block();
}
}

Здесь мы создаем новый контекст приложения и ждем завершения работы Netty, вызывая цепочку .onClose().block() в контексте Netty.

После закрытия Netty контекст будет автоматически закрыт с помощью блока try-with-resources .

Нам также потребуется создать HTTP-сервер на основе Netty, обработчик HTTP-запросов и адаптер между сервером и обработчиком:

@Bean
public NettyContext nettyContext(ApplicationContext context) {
HttpHandler handler = WebHttpHandlerBuilder
.applicationContext(context).build();
ReactorHttpHandlerAdapter adapter
= new ReactorHttpHandlerAdapter(handler);
HttpServer httpServer = HttpServer.create("localhost", 8080);
return httpServer.newHandler(adapter).block();
}

3.2. Класс конфигурации безопасности Spring

Для нашей базовой конфигурации Spring Security мы создадим класс конфигурации — SecurityConfig .

Чтобы включить поддержку WebFlux в Spring Security 5, нам нужно только указать аннотацию @EnableWebFluxSecurity :

@EnableWebFluxSecurity
public class SecurityConfig {
// ...
}

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

Этот класс является новой функцией Spring 5. Он похож на построитель HttpSecurity , но включен только для приложений WebFlux.

ServerHttpSecurity уже предварительно настроен с некоторыми разумными значениями по умолчанию, поэтому мы можем полностью пропустить эту настройку . Но для начала приведем следующий минимальный конфиг:

@Bean
public SecurityWebFilterChain securityWebFilterChain(
ServerHttpSecurity http) {
return http.authorizeExchange()
.anyExchange().authenticated()
.and().build();
}

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

@Bean
public MapReactiveUserDetailsService userDetailsService() {
UserDetails user = User
.withUsername("user")
.password(passwordEncoder().encode("password"))
.roles("USER")
.build();
return new MapReactiveUserDetailsService(user);
}

Поскольку мы находимся в реактивной стране, служба сведений о пользователях также должна быть реактивной. Если мы проверим интерфейс ReactiveUserDetailsService , то увидим, что его метод findByUsername фактически возвращает Mono издателя:

public interface ReactiveUserDetailsService {

Mono<UserDetails> findByUsername(String username);
}

Теперь мы можем запустить наше приложение и увидеть обычную форму базовой HTTP-аутентификации.

4. Стилизованная форма входа

Небольшое, но заметное улучшение в Spring Security 5 — это новая стилизованная форма входа, в которой используется CSS-фреймворк Bootstrap 4. Таблицы стилей в форме входа связаны с CDN, поэтому мы увидим улучшение только при подключении к Интернету.

Чтобы использовать новую форму входа, давайте добавим соответствующий метод построителя formLogin() в построитель ServerHttpSecurity :

public SecurityWebFilterChain securityWebFilterChain(
ServerHttpSecurity http) {
return http.authorizeExchange()
.anyExchange().authenticated()
.and().formLogin()
.and().build();
}

Если мы сейчас откроем главную страницу приложения, то увидим, что она выглядит намного лучше, чем форма по умолчанию, к которой мы привыкли со времен предыдущих версий Spring Security:

./e968b13714ca0be296c8c1cfebbcbc39.png

Обратите внимание, что это не готовая к производству форма, но это хорошая начальная загрузка нашего приложения.

Если мы сейчас войдем в систему, а затем перейдем по URL- адресу http://localhost:8080/logout , мы увидим форму подтверждения выхода, которая также оформлена.

5. Безопасность реактивного контроллера

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

@RestController
public class GreetingController {

@GetMapping("/")
public Mono<String> greet(Mono<Principal> principal) {
return principal
.map(Principal::getName)
.map(name -> String.format("Hello, %s", name));
}

}

После авторизации мы увидим приветствие. Давайте добавим еще один реактивный обработчик, доступный только администратору:

@GetMapping("/admin")
public Mono<String> greetAdmin(Mono<Principal> principal) {
return principal
.map(Principal::getName)
.map(name -> String.format("Admin access: %s", name));
}

Теперь давайте создадим второго пользователя с ролью ADMIN : в нашей службе сведений о пользователе:

UserDetails admin = User.withDefaultPasswordEncoder()
.username("admin")
.password("password")
.roles("ADMIN")
.build();

Теперь мы можем добавить правило сопоставления для URL-адреса администратора, которое требует от пользователя наличия полномочий ROLE_ADMIN .

Обратите внимание, что мы должны поместить сопоставители перед вызовом цепочки .anyExchange() . Этот вызов применяется ко всем другим URL-адресам, которые еще не были охвачены другими сопоставителями:

return http.authorizeExchange()
.pathMatchers("/admin").hasAuthority("ROLE_ADMIN")
.anyExchange().authenticated()
.and().formLogin()
.and().build();

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

Но только пользователь с правами администратора может перейти по URL- адресу http://localhost:8080/admin и увидеть ее приветствие .

6. Безопасность реактивного метода

Мы видели, как мы можем защитить URL-адреса, но как насчет методов?

Чтобы включить безопасность на основе методов для реактивных методов, нам нужно всего лишь добавить аннотацию @EnableReactiveMethodSecurity в наш класс SecurityConfig :

@EnableWebFluxSecurity
@EnableReactiveMethodSecurity
public class SecurityConfig {
// ...
}

Теперь давайте создадим реактивный сервис приветствия со следующим содержимым:

@Service
public class GreetingService {

public Mono<String> greet() {
return Mono.just("Hello from service!");
}
}

Мы можем внедрить его в контроллер, перейти по адресу http://localhost:8080/greetingService и убедиться, что он действительно работает:

@RestController
public class GreetingController {

private GreetingService greetingService

// constructor...

@GetMapping("/greetingService")
public Mono<String> greetingService() {
return greetingService.greet();
}

}

Но если мы сейчас добавим аннотацию @PreAuthorize к сервисному методу с ролью ADMIN , то URL сервиса приветствия не будет доступен обычному пользователю:

@Service
public class GreetingService {

@PreAuthorize("hasRole('ADMIN')")
public Mono<String> greet() {
// ...
}
}

7. Издевательство над пользователями в тестах

Давайте проверим, насколько просто протестировать наше реактивное приложение Spring.

Во-первых, мы создадим тест с внедренным контекстом приложения:

@ContextConfiguration(classes = SpringSecurity5Application.class)
public class SecurityTest {

@Autowired
ApplicationContext context;

// ...
}

Теперь мы настроим простой реактивный клиент веб-тестирования, который является функцией тестовой среды Spring 5:

@Before
public void setup() {
this.webTestClient = WebTestClient
.bindToApplicationContext(this.context)
.configureClient()
.build();
}

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

@Test
public void whenNoCredentials_thenRedirectToLogin() {
webTestClient.get()
.uri("/")
.exchange()
.expectStatus().is3xxRedirection();
}

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

Логин и пароль этого пользователя будут user и password соответственно, а роль USER . Конечно, все это можно настроить с помощью параметров аннотации @WithMockUser .

Теперь мы можем проверить, что авторизованный пользователь видит приветствие:

@Test
@WithMockUser
public void whenHasCredentials_thenSeesGreeting() {
webTestClient.get()
.uri("/")
.exchange()
.expectStatus().isOk()
.expectBody(String.class).isEqualTo("Hello, user");
}

Аннотация @WithMockUser доступна , начиная с Spring Security 4. Однако она также была обновлена в Spring Security 5, чтобы охватывать реактивные конечные точки и методы.

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

В этом руководстве мы обнаружили новые функции предстоящего выпуска Spring Security 5, особенно в области реактивного программирования.

Как всегда, исходный код статьи доступен на GitHub .