用于响应式应用程序的Spring Security 5

2023/05/13

1. 概述

在本文中,我们将探讨Spring Security 5框架用于保护响应式应用程序的新特性。此版本与Spring 5和Spring Boot 2保持一致。

在本文中,我们不会详细介绍响应式应用程序本身,这是Spring 5框架的新特性。请务必查看文章Reactor Core简介以了解更多详细信息。

2. Maven依赖

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

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

我们可以在Maven Central上查看Spring Boot Security启动器的当前版本。

3. 项目构建

3.1 启动响应式应用程序

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

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

@ComponentScan(basePackages = {"cn.tuyucheng.taketoday.reactive.security"})
@EnableWebFlux
public class SpringSecurity5Application {

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

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

Netty关闭后,Spring上下文会由try-with-resources块自动关闭。

我们还需要创建一个基于Netty的HTTP服务器、一个HTTP请求处理程序以及服务器和处理程序之间的适配器:

@Bean
public DisposableServer disposableServer(ApplicationContext context) {
    HttpHandler handler = WebHttpHandlerBuilder.applicationContext(context).build();
    ReactorHttpHandlerAdapter adapter = new ReactorHttpHandlerAdapter(handler);
    HttpServer httpServer = HttpServer.create().host("localhost").port(8083);
    return httpServer.handle(adapter).bindNow();
}

3.2 Spring Security配置类

对于我们基本的Spring Security配置,我们将创建一个配置类SecurityConfig。

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

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

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

这个类是Spring 5的一个新特性。它类似于HttpSecurity构建器,但仅适用于WebFlux应用程序。

ServerHttpSecurity已经预先配置了一些合理的默认值,因此我们可以完全跳过这个配置。但对于初学者而言,我们将提供以下最小配置:

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

此外,我们还需要一个UserDetailsService。Spring Security为我们提供了一个方便的模拟User构建器和UserDetailsService的内存中实现:

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

    return new MapReactiveUserDetailsService(user);
}

由于我们使用响应式框架,因此UserDetailsService也应该是响应式的。如果我们查看ReactiveUserDetailsService接口,我们会看到它的findByUsername方法实际上返回了一个Mono发布者

public interface ReactiveUserDetailsService {

    Mono<UserDetails> findByUsername(String username);
}

现在我们可以运行我们的应用程序并观察一个常规的HTTP基本身份验证表单。

4. 登录表单样式

Spring Security 5中一个小而显著的改进是使用Bootstrap 4 CSS框架的新样式登录表单。登录表单中的样式表链接到CDN,因此我们只能在连接到网络时才能看到此改进。

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

@Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
    return http.authorizeExchange()
        .pathMatchers("/admin").hasAuthority("ROLE_ADMIN")
        .anyExchange().authenticated()
        .and().formLogin()
        .and().csrf().disable()
        .build();
}

如果我们现在访问应用程序的主页,我们会看到它看起来比以前版本的Spring Security使用的默认表单要好得多:

请注意,这不是一个生产就绪的表单,但它是我们应用程序的一个很好的起步程序

如果我们现在登录,然后访问http://localhost:8083/logout URL,我们可以看到注销确认表单,该表单也是经过样式化的。

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));
}

现在让我们在我们的UserDetailsService中创建一个角色为ADMIN的第二个用户:

UserDetails admin = User
    .withUsername("admin")
    .password(passwordEncoder().encode("password"))
    .roles("ADMIN")
    .build();

现在,我们可以为“/admin” URL添加一个匹配器规则,该规则要求用户具有ROLE_ADMIN权限。

请注意,我们必须将匹配器放在.anyExchange()方法链调用之前。此调用适用于其他匹配器尚未覆盖的所有其他URL:

@Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
    return http.authorizeExchange()
        .pathMatchers("/admin").hasAuthority("ROLE_ADMIN")
        .anyExchange().authenticated()
        .and().formLogin()
        .and().build();
}

如果我们现在使用user或admin登录,我们可以看到他们都能成功访问主页,因为我们已经让所有经过身份验证的用户都可以访问它。

但只有admin用户可以访问http://localhost:8083/admin URL。

6. 响应式方法安全性

我们已经了解了如何保护URL,但是方法呢?

要为响应式应用启用基于方法的Spring Security,我们只需要将@EnableReactiveMethodSecurity注解添加到我们的SecurityConfig类:

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

现在让我们创建一个包含以下内容的响应式GreetingService:

@Service
public class GreetingService {

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

我们可以将其注入到控制器中,并添加一个端点调用该Service,然后访问http://localhost:8080/greetingService看看它是否确实有效:

@RestController
public class GreetingController {
    private final GreetingService greetingService;

    public GreetingController(GreetingService greetingService) {
        this.greetingService = greetingService;
    }

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

但是,如果我们现在将@PreAuthorize(“hasRole(‘ADMIN’)”)注解添加到Service方法上,那么普通用户将无法访问greetingService URL:

@Service
public class GreetingService {

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

7. 在测试中Mock用户

让我们看看如何为我们的响应式Spring应用程序编写测试。

首先,我们将使用注入的应用程序上下文创建一个测试类:

@ContextConfiguration(classes = SpringSecurity5Application.class)
public class SecurityTest {
    @Autowired
    ApplicationContext context;

    // ...
}

现在我们将设置一个简单的响应式WebTestClient,这是Spring 5测试框架的一个特性:

private WebTestClient webTestClient;

@BeforeEach
void setUp() {
    webTestClient = WebTestClient
        .bindToApplicationContext(context)
        .configureClient()
        .build();
}

下面是一个测试用例,当我们没有经过登录访问主页面时,我们应该重定向到登录页面:

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

如果我们现在将@WithMockUser注解添加到测试方法上,我们可以为该方法提供一个经过身份验证的用户

该用户的登录名和密码分别为user和password,角色为USER。当然,这些都可以使用@WithMockUser注解的参数进行配置。

现在我们可以检查经过授权的用户是否能得到正确的响应消息:

@Test
@WithMockUser
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上获得。

Show Disqus Comments

Post Directory

扫码关注公众号:Taketoday
发送 290992
即可立即永久解锁本站全部文章