Spring Security 5 for Reactive Applications – 面向反应式应用的Spring Security 5

最后修改: 2017年 11月 19日

中文/混合/英文(键盘快捷键:t)

1. Introduction

1.介绍

In this article, we’ll explore new features of the Spring Security 5 framework for securing reactive applications. This release is aligned with Spring 5 and Spring Boot 2.

在本文中,我们将探讨Spring Security 5框架的新功能,以确保反应式应用程序的安全。该版本与Spring 5和Spring Boot 2保持一致。

In this article, we won’t go into details about the reactive applications themselves, which is a new feature of the Spring 5 framework. Be sure to check out the article Intro to Reactor Core for more details.

在这篇文章中,我们不会详细介绍反应式应用程序本身,这是Spring 5框架的一个新功能。请务必查看Intro to Reactor Core一文以了解更多细节。

2. Maven Setup

2.Maven的设置

We’ll use Spring Boot starters to bootstrap our project together with all required dependencies.

我们将使用Spring Boot启动器来引导我们的项目和所有需要的依赖。

The basic setup requires a parent declaration, web starter, and security starter dependencies. We’ll also need the Spring Security test framework:

基本设置需要一个父声明、Web启动器和安全启动器的依赖。我们还需要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>

We can check out the current version of the Spring Boot security starter over at Maven Central.

我们可以在Maven中心查看Spring Boot安全启动器的当前版本

3. Project Setup

3.项目设置

3.1. Bootstrapping the Reactive Application

3.1.引导反应式应用程序

We won’t use the standard @SpringBootApplication configuration but instead, configure a Netty-based web server. Netty is an asynchronous NIO-based framework that is a good foundation for reactive applications.

我们将不使用标准的@SpringBootApplication配置,而是配置一个基于Netty的Web服务器。Netty是一个基于NIO的异步框架,是反应式应用程序的良好基础。

The @EnableWebFlux annotation enables the standard Spring Web Reactive configuration for the application:

@EnableWebFlux注解为应用程序启用标准的Spring Web Reactive配置。

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

    public static void main(String[] args) {
        try (AnnotationConfigApplicationContext context 
         = new AnnotationConfigApplicationContext(
            SpringSecurity5Application.class)) {
 
            context.getBean(NettyContext.class).onClose().block();
        }
    }

Here, we create a new application context and wait for Netty to shut down by calling .onClose().block() chain on the Netty context.

在这里,我们创建一个新的应用程序上下文,并通过在Netty上下文上调用.onClose().block()链来等待Netty关闭。

After Netty is shut down, the context will be automatically closed using the try-with-resources block.

在Netty关闭后,上下文将使用try-with-resources块自动关闭。

We’ll also need to create a Netty-based HTTP server, a handler for the HTTP requests, and the adapter between the server and the handler:

我们还需要创建一个基于Netty的HTTP服务器,一个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 Security Configuration Class

3.2.Spring安全配置类

For our basic Spring Security configuration, we’ll create a configuration class – SecurityConfig.

对于我们的基本Spring安全配置,我们将创建一个配置类 – SecurityConfig

To enable WebFlux support in Spring Security 5, we only need to specify the @EnableWebFluxSecurity annotation:

要在Spring Security 5中启用WebFlux支持,我们只需要指定@EnableWebFluxSecurity注解。

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

Now we can take advantage of the class ServerHttpSecurity to build our security configuration.

现在我们可以利用ServerHttpSecurity类来构建我们的安全配置。

This class is a new feature of Spring 5. It’s similar to HttpSecurity builder, but it’s only enabled for WebFlux applications.

该类是Spring 5的一个新特性。它类似于HttpSecurity构建器,但它只对WebFlux应用程序启用。

The ServerHttpSecurity is already preconfigured with some sane defaults, so we could skip this configuration completely. But for starters, we’ll provide the following minimal config:

ServerHttpSecurity已经用一些合理的默认值进行了预配置,所以我们可以完全跳过这个配置。但对于初学者来说,我们将提供以下最小的配置。

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

Also, we’ll need a user details service. Spring Security provides us with a convenient mock user builder and an in-memory implementation of the user details service:

此外,我们还需要一个用户细节服务。Spring Security为我们提供了一个方便的模拟用户构建器和一个用户细节服务的内存实现。

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

Since we’re in reactive land, the user details service should also be reactive. If we check out the ReactiveUserDetailsService interface, we’ll see that its findByUsername method actually returns a Mono publisher:

既然我们在反应式领域,用户详情服务也应该是反应式的。如果我们查看一下ReactiveUserDetailsService接口,我们会看到它的findByUsername方法实际上返回一个Mono发布器:

public interface ReactiveUserDetailsService {

    Mono<UserDetails> findByUsername(String username);
}

Now we can run our application and observe a regular HTTP basic authentication form.

现在我们可以运行我们的应用程序,并观察一个常规的HTTP基本认证形式。

4. Styled Login Form

4.风格化的登录表格

A small but striking improvement in Spring Security 5 is a new styled login form that uses the Bootstrap 4 CSS framework. The stylesheets in the login form link to CDN, so we’ll only see the improvement when connected to the Internet.

Spring Security 5的一个小但引人注目的改进是使用Bootstrap 4 CSS框架的新风格化登录表。登录表单中的样式表链接到CDN,所以我们只有在连接到互联网时才会看到这种改进。

To use the new login form, let’s add the corresponding formLogin() builder method to the ServerHttpSecurity builder:

为了使用新的登录表单,让我们将相应的formLogin()构建器方法添加到ServerHttpSecurity构建器。

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

If we now open the main page of the application, we’ll see that it looks much better than the default form we’re used to since previous versions of Spring Security:

如果我们现在打开应用程序的主页面,我们会发现它看起来比我们从以前的Spring Security版本中习惯的默认表单好得多。

 

Note that this is not a production-ready form, but it’s a good bootstrap of our application.

注意,这不是一个可用于生产的表单,但它是我们应用程序的一个很好的引导。

If we now log in and then go to the http://localhost:8080/logout URL, we’ll see the logout confirmation form, which is also styled.

如果我们现在登录,然后转到http://localhost:8080/logout URL,我们会看到注销确认表单,它也是有风格的。

5. Reactive Controller Security

5.反应式控制器的安全性

To see something behind the authentication form, let’s implement a simple reactive controller that greets the user:

为了看到认证表单背后的东西,让我们实现一个简单的反应式控制器来迎接用户。

@RestController
public class GreetingController {

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

}

After logging in, we’ll see the greeting. Let’s add another reactive handler that would be accessible by admin only:

登录后,我们会看到问候语。让我们添加另一个反应式处理程序,只有管理员可以访问。

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

Now let’s create a second user with the role ADMIN: in our user details service:

现在,让我们在用户详情服务中创建第二个具有ADMIN角色的用户:。

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

We can now add a matcher rule for the admin URL that requires the user to have the ROLE_ADMIN authority.

我们现在可以为管理员的URL添加一个匹配器规则,要求用户拥有ROLE_ADMIN权限。

Note that we have to put matchers before the .anyExchange() chain call. This call applies to all other URLs which were not yet covered by other matchers:

注意,我们必须把匹配器放在.anyExchange()链调用之前。这个调用适用于所有其他尚未被其他匹配器覆盖的URL。

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

If we now log in with user or admin, we’ll see that they both observe the initial greeting, as we’ve made it accessible for all authenticated users.

如果我们现在用useradmin登录,我们会看到他们都观察到最初的问候语,因为我们已经让所有认证用户都可以访问它。

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

但只有admin用户可以进入http://localhost:8080/admin网址,看到她的问候语

6. Reactive Method Security

6.反应式方法的安全性

We’ve seen how we can secure the URLs, but what about methods?

我们已经看到了如何确保URL的安全,但方法呢?

To enable method-based security for reactive methods, we only need to add the @EnableReactiveMethodSecurity annotation to our SecurityConfig class:

要为反应式方法启用基于方法的安全,我们只需要将@EnableReactiveMethodSecurity注解添加到我们的SecurityConfig类。

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

Now let’s create a reactive greeting service with the following content:

现在让我们创建一个具有以下内容的反应式问候服务。

@Service
public class GreetingService {

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

We can inject it into the controller, go to http://localhost:8080/greetingService and see that it actually works:

我们可以把它注入控制器,进入http://localhost:8080/greetingService,看看它是否真的能工作。

@RestController
public class GreetingController {

    private GreetingService greetingService

    // constructor...

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

}

But if we now add the @PreAuthorize annotation on the service method with the ADMIN role, then the greet service URL won’t be accessible to a regular user:

但如果我们现在在带有ADMIN角色的服务方法上添加@PreAuthorize注解,那么普通用户就无法访问问候服务URL。

@Service
public class GreetingService {

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

7. Mocking Users in Tests

7.在测试中模拟用户

Let’s check out how easy it is to test our reactive Spring application.

让我们看看测试我们的反应式Spring应用程序是多么容易。

First, we’ll create a test with an injected application context:

首先,我们将创建一个具有注入应用上下文的测试。

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

    @Autowired
    ApplicationContext context;

    // ...
}

Now we’ll set up a simple reactive web test client, which is a feature of the Spring 5 test framework:

现在我们将建立一个简单的反应式网络测试客户端,这是Spring 5测试框架的一个特点。

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

This allows us to quickly check that the unauthorized user is redirected from the main page of our application to the login page:

这使我们能够快速检查未经授权的用户是否从我们应用程序的主页面重定向到登录页面。

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

If we now add the @WithMockUser annotation to a test method, we can provide an authenticated user for this method.

如果我们现在将@WithMockUser注解添加到一个测试方法中,我们可以为这个方法提供一个认证用户。

The login and password of this user would be user and password respectively, and the role is USER. This, of course, can all be configured with the @WithMockUser annotation parameters.

这个用户的登录名和密码将分别是userpassword,而角色是USER。当然,这些都可以通过@WithMockUser注解参数来配置。

Now we can check that the authorized user sees the greeting:

现在我们可以检查授权用户是否看到问候语。

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

The @WithMockUser annotation is available since Spring Security 4. However, this was also updated in Spring Security 5 to cover reactive endpoints and methods.

@WithMockUser注解从Spring Security 4开始可用。然而,这在Spring Security 5中也得到了更新,以涵盖反应式端点和方法。

8. Conclusion

8.结论

In this tutorial, we’ve discovered new features of the upcoming Spring Security 5 release, especially in the reactive programming arena.

在本教程中,我们发现了即将发布的Spring Security 5的新特性,特别是在反应式编程领域。

As always, the source code for the article is available over on GitHub.

一如既往,该文章的源代码可在GitHub上获取。