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

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

1. Вступление

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

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

2. Maven Setup

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

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


    org.springframework.boot
    spring-boot-starter-parent
    2.0.0.RELEASE
    



    
        org.springframework.boot
        spring-boot-starter-web
    
    
        org.springframework.boot
        spring-boot-starter-security
    
    
        org.springframework.security
        spring-security-test
        test
    

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

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

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

Мы не будем использовать стандартную конфигурацию@SpringBootApplication, а вместо этого настроим веб-сервер на основе Netty. Netty is an asynchronous NIO-based framework which is a good foundation for reactive applications.

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

@ComponentScan(basePackages = {"com.example.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 для построения нашей конфигурации безопасности.

This class is a new feature of Spring 5. Он похож на конструкторHttpSecurity, но доступен только для приложений WebFlux.

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

@Bean
public SecurityWebFilterChain securitygWebFilterChain(
  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,we’ll see that its findByUsername method actually returns a Mono publisher:

public interface ReactiveUserDetailsService {

    Mono findByUsername(String username);
}

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

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

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

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

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

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

image

 

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

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

5. Реактивная безопасность контроллера

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

@RestController
public class GreetController {

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

}

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

@GetMapping("/admin")
public Mono greetAdmin(Mono 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.

Цепной вызовNote that we have to put matchers before the .anyExchange(). Этот вызов применяется ко всем другим URL-адресам, которые еще не были охвачены другими сопоставителями:

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

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

But only the admin user can go to the http://localhost:8080/admin URL and see her greeting.

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

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

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

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

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

@Service
public class GreetService {

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

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

@RestController
public class GreetController {

    private GreetService greetService

    @GetMapping("/greetService")
    public Mono greetService() {
        return greetService.greet();
    }

    // standard constructors...
}

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

@Service
public class GreetService {

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

7. Мокинг пользователей в тестах

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

Сначала мы создадим тест с внедренным контекстом приложения:

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

    @Autowired
    ApplicationContext context;

    // ...
}

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

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

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

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

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

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

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

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

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

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

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

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