Spring Security 详解与实操第六节 响应式编程及最后总结

564 篇文章 139 订阅

技术趋势:如何为 Spring Security 添加响应式编程特性?

对于大多数日常业务场景而言,软件系统在任何时候都需要确保具备即时响应性。而响应式编程(Reactive Programming)就是用来构建具有即时响应性的是一种新的编程技术。随着 Spring 5 的发布,我们迎来了响应式编程的全新发展时期。而 Spring Security 作为 Spring 家族的一员,同样实现了一系列的响应式组件。今天我们就将围绕这些组件展开讨论。

什么是响应式编程?

在引入响应式 Spring Security 之前,我们先来介绍响应式编程中的一些基本概念,并给出 Spring 5 中所集成的响应式编程组件。

响应式编程的基本概念

在响应式系统中,任何操作都可以被看作是一种事件,而这些事件构成了数据流。这个数据流对于技术栈而言是一个全流程的概念。也就是说,无论是从底层数据库,向上到达服务层,最后到 Web 层,亦或是在这个流程中所包含的任意中间层组件,整个数据传递链路都应该是采用事件驱动的方式来进行运作。这样,我们就可以不采用传统的同步调用方式来处理数据,而是由位于数据库上游的各层组件自动来执行事件。这就是响应式编程的核心特点。

针对数据流的具体操作方法都定义在响应式流(Reactive Stream)规范中。在 Java 的世界中,关于响应式流规范的实现也有一些主流的开源框架,包括 RxJava、Vert.x 以及 Project Reactor。

Project Reactor

Spring 5 选择 Project Reactor 作为它的内置响应式编程框架,该框架提供了两种数据流的表示方式,即代表包含 0 到 n 个元素异步序列的 Flux 组件,以及只包含 0 个或 1 个元素的 Mono 组件。我们可以通过一个简单的代码示例来创建一个 Flux 对象,如下所示:

private Flux<Order> getAccounts() {
        List<Account> accountList = new ArrayList<>();
 
        Account account = new Account();
        account.setId(1L);
        account.setAccountCode("DemoCode");
        account.setAccountName("DemoName");
        accountList.add(account);
                
        return Flux.fromIterable(accountList);
}

在以上代码中,我们通过 Flux.fromIterable() 方法构建了 Flux 对象并进行返回,Flux.fromIterable() 是构建 Flux 的一种常用方法。而 Mono 组件也提供了一组有用的方法来创建 Mono 数据流,例如:

private Mono<Account> getAccountById(Long id) {   
        Account account = new Account();
        account.setId(id);
        account.setAccountCode("DemoCode");
        account.setAccountName("DemoName");
        accountList.add(account);
           
        return Mono.just(account);
}

可以看到,这里首先构建一个 Account 对象,然后通过 Mono.just() 方法返回一个 Mono 对象。

Spring WebFlux

同时,针对一个完整的应用程序开发过程,Spring 5 还专门提供了针对 Web 层的 WebFlux 框架、针对数据访问层的 Spring Data Reactive 框架等。因为 Spring Security 主要用于 Web 应用程序,所以这里对 WebFlux 做一些展开。

想要在 Spring Boot 中使用 WebFlux,需要引入如下依赖:

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

请注意这里的 spring-boot-starter-webflux 就是构成响应式 Web 应用程序开发的基础。基于 WebFlux 构建响应式 Web 服务的编程模型,开发人员有两种选择。第一种是使用基于 Java 注解的方式,而第二种则是使用函数式编程模型。其中,基于 Java 注解的方式与使用 Spring MVC 完全一致,我们来看一个示例:

@RestController
public class HelloController {
 
    @GetMapping("/")
    public Mono<String> hello() {
        return Mono.just("Hello!");
    }
}

以上代码只有一个地方值得注意,即 hello() 方法的返回值从普通的 String 对象转化为了一个 Mono 对象。这点是完全可以预见的,使用 Spring WebFlux 与 Spring MVC 的不同之处,在于前者使用的类型都是 Reactor 中提供的 Flux 和 Mono 对象,而不是普通的 POJO。

我们知道传统的 Spring MVC 构建在 Java EE 的 Servlet 标准之上,该标准本身就是阻塞式和同步的。而 Spring WebFlux 则是构建在响应式流以及它的实现框架 Project Reactor 之上的一个开发框架,因此可以基于 HTTP 协议来构建异步非阻塞的 Web 服务。

最后,我们来看一下位于底部的容器支持。当你使用 Spring WebFlux 时,你会注意到它默认采用了 Netty 作为运行时容器。这是因为 Spring MVC 是运行在传统的 Servlet 容器之上,而 Spring WebFlux 则需要支持异步的运行环境,比如 Netty、Undertow 以及 Servlet 3.1 之上的 Tomcat 和 Jetty。这里多说一句,因为在 Servlet 3.1 中引入了异步 I/O 支持。

引入响应式 Spring Security

对于 Spring Security 而言,引入响应式编程技术同样会对传统实现方法带来一些变化。比方说,在《03 | 认证体系:如何深入理解 Spring Security 的认证机制?课时中,我们已经知道 UserDetailsService 的作用是用来获取用户信息,你可以把它理解为是一种数据源,这样针对数据源的数据访问过程同样需要支持响应式编程。下面让我们一起来看一下这些变化吧。

响应式用户认证

在响应式 Spring Security 中,提供了一个响应式版本的 UserDetailsService,即 ReactiveUserDetailsService,定义如下:

public interface ReactiveUserDetailsService {
 
    Mono<UserDetails> findByUsername(String username);
}

请注意,这里的 findByUsername() 方法返回的是一个 Mono 对象。ReactiveUserDetailsService 接口有一个实现类 MapReactiveUserDetailsService,提供了基于内存的用户信息存储方案,实现过程如下所示:

public class MapReactiveUserDetailsService implements ReactiveUserDetailsService, ReactiveUserDetailsPasswordService {
    private final Map<String, UserDetails> users;
 
    public MapReactiveUserDetailsService(Map<String, UserDetails> users) {
        this.users = users;
    }
 
    public MapReactiveUserDetailsService(UserDetails... users) {
        this(Arrays.asList(users));
    }
 
    public MapReactiveUserDetailsService(Collection<UserDetails> users) {
        Assert.notEmpty(users, "users cannot be null or empty");
        this.users = new ConcurrentHashMap<>();
        for (UserDetails user : users) {
             this.users.put(getKey(user.getUsername()), user);
        }
    }
 
    @Override
    public Mono<UserDetails> findByUsername(String username) {
        String key = getKey(username);
        UserDetails result = users.get(key);
        return result == null ? Mono.empty() : Mono.just(User.withUserDetails(result).build());
    }
 
    @Override
    public Mono<UserDetails> updatePassword(UserDetails user, String newPassword) {
        return Mono.just(user)
                 .map(u ->
                     User.withUserDetails(u)
                         .password(newPassword)
                         .build()
                 )
                 .doOnNext(u -> {
                     String key = getKey(user.getUsername());
                     this.users.put(key, u);
                 });
    }
 
    private String getKey(String username) {
        return username.toLowerCase();
    }
}

从上面的代码中可以看到,这里使用了一个 Map 来保存用户信息,然后在 findByUsername() 方法中,通过 Mono.just() 方法来返回一个 Mono 对象。然后,我们还注意到在 updatePassword() 方法中用到的 map() 方法,实际上是 Project Reactor 所提供的一个操作符,用于实现对一个对象执行映射操作。

基于 MapReactiveUserDetailsService,我们就可以在业务系统中通过以下方式构建一个 ReactiveUserDetailsService:

@Bean
public ReactiveUserDetailsService userDetailsService() {
        UserDetails u = User.withUsername("john")
                .password("12345")
                .authorities("read")
                .build();
 
        ReactiveUserDetailsService uds = new MapReactiveUserDetailsService(u);
 
        return uds;
}

当然,针对用户认证,响应式 Spring Security 也提供了响应式版本的 ReactiveAuthenticationManager 来执行具体的认证流程。

响应式授权机制

介绍完认证,我们接着来看授权,假设系统中存在这样一个简单的 HTTP 端点:

@RestController
public class HelloController {
 
    @GetMapping("/hello")
    public Mono<String> hello(Mono<Authentication> auth) {
        Mono<String> message = auth.map(a -> "Hello " + a.getName());
        return message;
    }
}

这里我们使用了 Spring Webflux 构建了一个响应式端点,注意到 hello() 的返回值是一个 Mono 对象。同时,我们输入的也是一个 Mono 对象,因此,访问这个端点显然是需要认证的。

在《访问授权:如何对请求的安全访问过程进行有效配置?》课时中,我们知道可以通过覆写 WebSecurityConfigurerAdapter 中的 configure(HttpSecurity http) 方法来设置访问权限。这种配置方法在响应式编程体系中就无法再使用了,取而代之的是使用一个叫 SecurityWebFilterChain 的配置接口来完成配置,该接口定义如下:

public interface SecurityWebFilterChain {
 
    //评估交互上下文 ServerWebExchange 是否匹配
    Mono<Boolean> matches(ServerWebExchange exchange);
    
    //一组过滤器
    Flux<WebFilter> getWebFilters();
}

从命名上看,我们不难理解 SecurityWebFilterChain 代表的实际上是一个过滤器链,而 ServerWebExchange 则是包含请求和响应的一种交互上下文,这在响应式环境中是一种固定属性,因为我们认为整个交互过程不是在单纯的发送请求和接受响应,而是在交换(Exchange)数据。如果想要使用 SecurityWebFilterChain,可以采用类似如下所示的代码示例:

@Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
        return http.authorizeExchange()
                .pathMatchers(HttpMethod.GET, "/hello").authenticated()
                .anyExchange().permitAll()
                    .and()
                .httpBasic()
                    .and()
                .build();
}

这里的 ServerHttpSecurity 可以用来构建 SecurityWebFilterChain 的实例,它的作用类似于非响应式系统中所使用的 HttpSecurity。同时,ServerHttpSecurity 也提供了一组我们熟悉的配置方法来设置各种认证和授权机制。

需要注意的是,在响应式系统中,因为处理的对象是 ServerWebExchange,而不是传统的 ServerRequest,所以在涉及与请求相关的方法命名时都统一做了调整。例如使用了 authorizeExchange() 方法来取代 authorizeRequests(),使用 anyExchange() 取代了 anyRequest(),而这里的 pathMatchers() 方法也可以等同于以前介绍的 mvcMatchers() 方法。

响应式方法级别访问控制

《 10 |全局方法:如何确保方法级别的安全访问?》课时中,我们介绍了 Spring Security 所提供的一个非常强大的功能,即全局安全方法机制。通过这种机制,无论是 Web 服务还是普通应用,都可以基于方法的执行过程来应用授权规则。

在响应式编程中,我们称这种方法级别的授权机制为响应式方法安全(Reactive Method Security)机制,以便与传统的全局方法安全机制进行区分。

想要在应用程序中使用响应式方法安全机制,我们还需要专门引入一个新的注解,即 @EnableReactiveMethodSecurity。这个注解与 @EnableGlobalMethodSecurity 注解类似,用来启用响应式安全方法机制:

@Configuration
@EnableGlobalMethodSecurity
public class SecurityConfig 

现在,我们来看一下使用响应式方法安全机制的代码示例:

@RestController
public class HelloController {
 
    @GetMapping("/hello")
    @PreAuthorize("hasRole('ADMIN')")
    public Mono<String> hello() {
        return Mono.just("Hello!");
    }
}

可以看到,这里使用了 @PreAuthorize 注解,并通过"hasRole('ADMIN')"这一 SpEL 表达式来实现基于角色的授权机制。就这个注解的使用方式而言,可以发现,它与传统应用程序中的使用方式是一致的。但不幸的是,就目前而言,响应式方法安全机制还不是很成熟,只提供了 @PreAuthorize 和 @PostAuthorize 这对注解,而 @PreFilter 和 @PostFilter 注解还没有实现。

小结与预告

响应式编程是技术发展趋势,为我们构建高弹性的应用程序提供了一种新的编程模式。作为 Spring 家族中的重要组成部分,Spring Security 框架同样提供了对响应式编程的全面支持。本课时对响应式编程的基础概念做了阐述,并给出了 Spring Security 中关于用户认证、授权机制以方法级别访问控制等功能的响应式解决方案。

这里给你留一道思考题:在实现授权机制时,响应式编程模式与传统编程模式有什么区别?

介绍完响应式编程在 Spring Security 中的应用之后,下一课时我们将探讨整个课程的最后一个话题,即如何对 Spring Security 所提供的安全性功能进行测试。


测试驱动:如何基于 Spring Security 测试系统安全性?

作为整个课程最后一部分内容,我们将讨论基于 Spring Security 的测试解决方案。对于安全性而言,测试是一个难点,也是经常被忽略的一套技术体系。当我们使用 Spring Security 时,如何验证我们所使用的安全性功能是否正确呢?今天的内容将给出详细的答案。

如何对系统的安全性进行测试?

Spring Security 是一款安全性开发框架,提供的是内嵌到业务系统中的基础设施类功能。因此,势必会涉及大量组件之间的依赖关系,这是测试安全性功能所面临的最大挑战,需要采用特定的测试方法。因此,在介绍具体的测试用例之前,我们先来梳理一下安全性测试方法,以及 Spring Security 中为我们提供的测试解决方案。

安全性测试与 Mock 机制

正如前面提到的,验证安全性功能正确性的难点在于组件与组件之间的依赖关系,为了弄清楚这个关系,这里就需要引出测试领域非常重要的一个概念,即 Mock(模拟)。针对测试组件涉及的外部依赖,我们的关注点在于这些组件之间的调用关系,以及返回的结果或发生的异常等,而不是组件内部的执行过程。因此常见的技巧就是使用 Mock 对象来替代真实的依赖对象,从而模拟真实的调用场景。

我们以一个常见的三层 Web 服务架构为例来进一步解释 Mock 的实施方法。Controller 层会访问 Service 层,而 Service 层又会访问 Repository 层,我们对 Controller 层的端点进行验证时,就需要模拟 Service 层组件的功能。同样,对 Service 层组件进行测试时,也需要假定 Repository 层组件的结果是可以获取的,如下所示:

2.jpg

Web 服务中各层组件与 Mock 对象示意图

对于 Spring Security 而言,上图所展示的原理同样适用,例如我们可以通过模拟用户的方式来测试用户认证和授权功能的正确性。在本讲后面的内容中,我们会给出相关的代码示例。

Spring Security 中的测试解决方案

要想开展单元测试、集成测试以及基于 Mock 的测试,需要有一套完整的技术体系。和 Spring Boot 1.x 版本一样,Spring Boot 2.x 同样提供了针对测试的 spring-boot-starter-test 组件。在 Spring Boot 中集成该组件的方法就是在 pom 文件中添加如下依赖:

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

通过这个依赖,一系列组件被自动引入到了代码工程的构建路径中,包括 JUnit、JSON Path、AssertJ、Mockito、Hamcrest 等,这些测试组件都非常有用。同时,因为 Spring Boot 程序的入口是 Bootstrap 类,因此专门提供了一个 @SpringBootTest 注解来测试你的 Bootstrap 类。所有配置都会通过 Bootstrap 类去加载,而该注解可以引用 Bootstrap 类的配置。

另一方面,Spring Security 也提供了专门用于测试安全性功能的 spring-security-test 组件,如下所示:

<dependency>
     <groupId>org.springframework.security</groupId>
     <artifactId>spring-security-test</artifactId>
     <scope>test</scope>
</dependency>

该组件提供了相关的注解来模拟用户登录信息或者调用用户登录的方法,让我们一起来看一下。

测试 Spring Security 功能

测试用户

在使用 Spring Security 时,首先需要测试的无疑是合法的用户。假设我们实现了如下所示的一个简单 Controller:

@RestController
public class HelloController {
 
    @GetMapping("/hello")
    public String hello() {
        return "Hello";
    }
}

一旦我们启用 Spring Security 认证功能,那么对上述“/hello”端点就可以执行两种测试,分别面向认证和非认证用户。我们先来看一下针对非认证用户的测试方法:

@SpringBootTest
@AutoConfigureMockMvc
public class HelloControllerTests {
 
    @Autowired
    private MockMvc mvc;
 
    @Test
    public void testUnauthenticatedUser() throws Exception {
        mvc.perform(get("/hello"))
                .andExpect(status().isUnauthorized());
	}
}

这里引入了一个 @AutoConfigureMockMvc 注解,通过将 @SpringBootTest 注解与 @AutoConfigureMockMvc 注解相结合,@AutoConfigureMockMvc 注解在通过 @SpringBootTest 加载的 Spring 上下文环境中会自动装配 MockMvc 这个测试工具类。

顾名思义,MockMvc 用来对 WebMVC 的执行过程进行模拟。MockMvc 类提供的基础方法如下所示。

  • perform:执行一个 RequestBuilder 请求,会自动执行 SpringMVC 流程,并映射到相应的 Controller 进行处理。

  • get/post/put/delete:声明发送一个 HTTP 请求的方式,根据 URI 模板和 URI 变量值得到一个 HTTP 请求,支持 GET、POST、PUT、DELETE 等 HTTP 方法。

  • param:添加请求参数,发送 JSON 数据时将不能使用这种方式,而应该采用 @ResponseBody 注解。

  • andExpect:添加 ResultMatcher 验证规则,通过对返回的数据进行判断来验证 Controller 执行结果是否正确。

  • andDo:添加 ResultHandler 结果处理器,比如调试时打印结果到控制台。

  • andReturn:最后返回相应的 MvcResult,然后执行自定义验证或做异步处理。

在上述代码示例中,我们通过 perform、accept 和 andExpect 方法最终模拟 HTTP 请求的整个过程并验证请求的返回状态是否为非认证。

接下来我们来模拟认证用户的测试场景,测试用例如下所示:

@Test
@WithMockUser
public void testAuthenticatedUser() throws Exception {
    mvc.perform(get("/hello"))
           .andExpect(content().string("Hello"))
           .andExpect(status().isOk());
}

显然,我们在这里看到了一个新的 @WithMockUser 注解,请注意这个注解是 Spring Security 所提供的,专门用来模拟认证用户。现在,既然已经有了认证用户,那么我们就可以验证响应的返回值以及状态,正如上述代码所示。

通过 @WithMockUser 注解,我们还可以指定用户的详细信息,例如如下所示的代码模拟了一个用户名为“admin”、角色为“USER”和“ADMIN”的认证用户:

@WithMockUser(username="admin",roles={"USER","ADMIN"})

进一步,我们还可以通过模拟 UserDetailsService 来提供自定义的 UserDetails 用户信息。为此,Spring Security 中专门提供了一个 @WithUserDetails 注解,示例代码如下所示:

@Test
@WithUserDetails("jianxiang")
public void testAuthenticatedUser() throws Exception {
    mvc.perform(get("/hello"))
           .andExpect(content().string("Hello"))
           .andExpect(status().isOk());
}
测试认证

测试完用户,我们接着来测试针对用户的认证过程。为了对整个认证过程有更多的定制化实现,这里专门提供一个 AuthenticationProvider 接口的实现类 MyAuthenticationProvider,如下所示:

@Component
public class MyAuthenticationProvider implements AuthenticationProvider {
 
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        String username = authentication.getName();
        String password = String.valueOf(authentication.getCredentials());
 
        if ("jianxiang".equals(username) && "123456".equals(password)) {
            return new UsernamePasswordAuthenticationToken(username, password, Arrays.asList());
        } else {
            throw new AuthenticationCredentialsNotFoundException("Error!");
        }
    }
 
    @Override
    public boolean supports(Class<?> authenticationType) {
        return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authenticationType);
    }
}

现在,我们基于 HTTP 基础认证机制来编写测试用例,如下所示:

@SpringBootTest
@AutoConfigureMockMvc
public class AuthenticationTests {
 
    @Autowired
    private MockMvc mvc;
 
    @Test
    public void testAuthenticatingWithValidUser() throws Exception {
        mvc.perform(get("/hello")
                .with(httpBasic("jianxiang","123456")))
                .andExpect(status().isOk());
    }
 
    @Test
    public void testAuthenticatingWithInvalidUser() throws Exception {
        mvc.perform(get("/hello")
                .with(httpBasic("noexiseduser","123456")))
                .andExpect(status().isUnauthorized());
    }
}

这里使用了前面介绍的 @AutoConfigureMockMvc 注解和 MockMvc 工具类,然后通过 httpBasic() 方法来实现 HTTP 基础认证。我们分别针对正确和错误的用户名/密码组合来执行 HTTP 请求并根据返回状态对认证结果进行校验。

测试方法安全

前面讨论的内容都是面向 Web 应用,也就是说测试的对象都是 HTTP 端点。那么,如何针对方法级别的安全性进行测试呢?

针对全局方法安全机制,前面介绍的 @WithMockUser 注解 @WithUserDetails 注解实际上也都是可以正常使用的。但因为已经脱离了 Web 环境,所以 MockMvc 工具类显然是无效的。这时候,你要做的事情就是在测试用例中直接注入目标方法即可,我们来看一下代码示例,假设一个非 Web 类的应用程序中存在如下一个 Service 类:

@Service
public class HelloService {
 
    @PreAuthorize("hasAuthority('write')")
    public String hello() {
        return "Hello";
    }
}

可以看到这里使用了 @PreAuthorize 注解限制了只有具备“write”权限的用户才能访问这个方法。

现在让我们编写针对方法访问安全的第一个测试用例,如下所示:

@Autowired
private HelloService helloService;
 
@Test
void testMethodWithNoUser() {
      assertThrows(AuthenticationException.class,
           () -> helloService.hello());
}

当我们在没有认证的情况下访问 helloService 的 hello() 方法,应该抛出一个 AuthenticationException 异常,上述测试用例验证了这一点。而如果我们使用一个具备不同权限的认证用户去访问这个方法时,会发生什么呢?对应测试用例如下所示:

@Test
@WithMockUser(authorities = "read")
void testMethodWithUserButWrongAuthority() {
     assertThrows(AccessDeniedException.class,
           () -> helloService.hello());
}

可能看到这里使用了 @WithMockUser 模拟了一个具有“read”权限的认证用户,但因为 @PreAuthorize 注解中指定只有“write”权限的用户才能访问这个方法,所以会抛出一个 AccessDeniedException。

最后,我们来测试正常流程下的结果,测试用例如下所示:

@Test
@WithMockUser(authorities = "write")
void testMethodWithUserButCorrectAuthority() {
     Stringresult = helloService.hello();
 
     assertEquals("Hello", result);
}
测试 CSRF 和 CORS 配置

基于我们在09 |攻击应对:如何实现 CSRF 保护和 CORS?》中的讨论,对于 POST、PUT 和 DELETE 等 HTTP 请求,我们需要添加针对 CSRF 的安全保护。为了测试 CSRF 配置的正确性,假设我们存在这样一个 HTTP 端点,请注意它的 HTTP 方法是 POST:

@RestController
public class HelloController {
 
    @PostMapping("/hello")
    public String postHello() {
        return "Post Hello!";
    }
}

现在,我们通过 MockMvc 工具类发起 post 请求,测试用例如下所示:

@Test
public void testHelloUsingPOST() throws Exception {
     mvc.perform(post("/hello"))
            .andExpect(status().isForbidden());
}

请注意,这个 post 请求并没有携带 CSRF Token,所以响应的状态应该是 HTTP 403 Forbidden。

现在,让我们重构上述测试用例,如下所示:

@Test
public void testHelloUsingPOSTWithCSRF() throws Exception {
     mvc.perform(post("/hello").with(csrf()))
            .andExpect(status().isOk());
}

上述 csrf() 方法的作用就是在请求中添加 CSRF Token,显然,这时候的响应结果应该是正确的。

讨论完 CSRF,我们再来看 CORS。在09 |攻击应对:如何实现 CSRF 保护和 CORS?》中,我们已经通过 CorsConfiguration 设置了 HTTP 响应消息头,如下所示:

@Override
protected void configure(HttpSecurity http) throws Exception {
        http.cors(c -> {
            CorsConfigurationSource source = request -> {
                CorsConfiguration config = new CorsConfiguration();
                config.setAllowedOrigins(Arrays.asList("*"));
                config.setAllowedMethods(Arrays.asList("*"));
                return config;
            };
            c.configurationSource(source);
        });
        …
}

对上述配置进行测试的方法也很简单,我们通过 MockMvc 发起请求,然后对响应的消息头进行验证即可,测试用例如下所示:

@SpringBootTest
@AutoConfigureMockMvc
public class MainTests {
 
    @Autowired
    private MockMvc mvc;
 
    @Test
    public void testCORSForTestEndpoint() throws Exception {
        mvc.perform(options("/hello")
                .header("Access-Control-Request-Method", "POST")
                .header("Origin", "http://www.test.com")
        )
        .andExpect(header().exists("Access-Control-Allow-Origin"))
        .andExpect(header().string("Access-Control-Allow-Origin", "*"))
        .andExpect(header().exists("Access-Control-Allow-Methods"))
        .andExpect(header().string("Access-Control-Allow-Methods", "POST"))
        .andExpect(status().isOk());
    }
}

可以看到,针对 CORS 配置,我们分别获取了响应结果的"Access-Control-Allow-Origin"和"Access-Control-Allow-Methods"消息头并进行了验证。

小结与预告

对于一个应用程序而言,无论其表现形式是否是一个 Web 服务,我们都需要对其进行安全性测试,为此 Spring Security 也提供了专门的测试解决方案,这其中很大程度上依赖于对 Mock 机制的合理应用。本讲对 Spring Security 所提供的用户、认证、全局方法、CSRF 以及 CORS 设计了测试用例,并给出了对应的示例代码。

这里给你留一道思考题:在使用 Spring Security 测试用户和认证过程中,如果对用户进行有效的 Mock?


以终为始,Spring Security 的学习总结

如今,虽然越来越多的开发人员开始意识到安全问题,但不幸的是,从应用程序设计和开发之初就充分考虑安全性问题并不是一种常见的做法。这种态度应该改变,每个参与软件系统开发的团队或个人都必须从一开始就学会考虑安全性。

Spring Security 是 Spring 家族的重要组成成员,也是业界领先的一款应用程序开发框架,提供了多项核心功能,帮忙我们构建完整的安全解决方案。本专栏作为针对 Spring Security 的一门系统化课程,在课程的最后,我想和你一起回顾和总结一下 Spring Security 核心功能,分享一些我在写作过程中的一些思考和心得。

学习 Spring Security 的意义

在 Java 领域中,Spring Security 是应用非常广泛的一个开发框架,也是 Spring 家族中历史比较悠久的一个框架。Spring Security 同样是 Spring Cloud 等综合性开发框架的底层基础框架之一,功能完备且强大,因此在 Spring 家族的其他框架中应用十分广泛。

另一方面,随着 Spring Boot 的大规模流行,Spring Security 也迎来了全新的发展时期。基于 Spring Boot 的自动配置原理,以往在传统 Spring 应用程序中集成和配置 Spring Security 框架的复杂过程将成为历史。在日常开发过程中,Spring Security 与 Spring Boot 等框架可以无缝集成,为构建完整的安全性解决方案提供保障。

Spring Security 内置了很多强大的功能,针对整个框架,我们可以从以下几个方面对其进行分析和学习:

  • Spring Security 的体系结构和基本组件,以及如何使用它来保护应用程序;

  • 使用 Spring Security 进行身份验证和授权,以及如何将它们应用于面向生产的应用程序;

  • 如何在应用程序的不同层中使用 Spring Security;

  • 在应用程序中使用不同的安全配置方式和最佳实践;

  • 将 Spring Security 用于响应式应用程序;

  • 对安全性解决方案进行测试。

但是,基于我自己的学习过程,以及从周围开发人员接收到的信息,我们在学习如何正确地使用 Spring Security 来保护应用程序免受常见漏洞的攻击时,或多或少都会遇到困难。当然,我们可以在网上找到有关 Spring Security 的所有细节。但是如果你希望在使用框架时花费最少的精力,那么就需要将相关知识按正确、合理的顺序放在一起进行学习,这通常需要大量的时间和经验。因此我设计这门课程的初衷,就是为了帮助你节省学习时间,提高学习效率。

此外,不完整的知识可能导致你设计并实现了难以维护的解决方案,甚至可能暴露安全漏洞。很多时候,当我们去 Review 这些问题时,会发现 Spring Security 的使用方式本身可能就是不合理的。而且,在许多情况下,主要原因还是开发人员对“如何使用 Spring Security”缺乏必要的了解。Spring Security 中的一些功能看上去比较简单,但用起来却经常会因为一些细小的配置,导致整个功能无法使用。即使发现了问题,也不太不容易找到原因。

因此,我决定设计一个系统化的专栏,帮助所有使用 Spring 框架的开发人员理解“如何正确使用 Spring Security”。这门课程应该是一个资源,以帮助开发人员逐步了解 Spring Security 框架,希望能为你带来价值,避免在应用程序中引入所有可能的安全漏洞。

下面我们再来回顾一下这门课程具体讲了哪些内容,这些内容又具备哪些特色呢?

Spring Security 这门课有什么特色?

在设计这门课程时,我关注的是将框架提供的各项功能进行合理的组织,并详细介绍它们的应用方式。课程组织上按照“基础功能篇→高级主题篇→OAuth2 与微服务篇 → 框架扩展篇”的主线来展开内容,呈递进关系。这是本课程的一大特色。我们将 Spring Security 的各项功能按照基础和高级等不同维度进行划分,由浅入深进行讲解。

  • 基础功能篇中,我们介绍 Spring Security 的一些基础性功能,包括认证、授权和加密;

  • 高级主题篇的功能面向特定需求,可以用于构建比较复杂的应用场景,包括过滤器、CSRF 保护、跨域 CORS,以及针对非 Web 应用程序的全局方法安全机制;

  • 而 OAuth2 与微服务篇的内容关注微服务开发框架 Spring Cloud 与 Spring Security 之间的整合,我们对 OAuth2 协议和 JWT 进行了全面的展开,并使用这些技术体系构建了安全的微服务系统,以及单点登录系统。

  • 最后,在框架扩展篇中,我们对 Spring Security 框架在应用上的一些扩展进行讨论,包括在 Spring Security 中引入全新的响应式编程技术,以及如何对应用程序安全性进行测试的系统方法。

课程的第二大特色在于案例驱动。整个专栏分别在基础功能篇、高级主题篇和微服务安全篇中结合本篇内容提供一个完整的案例,分别介绍 Spring Security 的基础认证授权功能、过滤器功能、基于 OAuth2 协议的单点登录和微服务访问授权体系的实战技巧。我们在案例中使用到的很多示例代码都可以直接使用在面向生产的应用程序中。

第三个特色是技术创新。随着 Spring 5 的发布涌现出了响应式编程这种新型技术体系,新版本的 Spring Security 中也提供了对响应式编程的全面支持。本课程对响应式 Spring Security 也做了介绍,这部分内容应该在目前 Spring Security 相关资料中还是首创。

第四个特色是深度和广度并重。从内容广度上,我们对 Spring Security 框架可以说是面面俱到,相关知识点娓娓道来。而在内容深度上,我们对框架最核心的认证和授权机制进行底层原理的分析,让你知其然更值其所以然。Spring Security 内置了很多可扩展性组件,通过对框架底层实现机制的理解和把握,可以帮助我们更好的实现扩展性。

写在最后

整个课程从平时的积累,到酝酿的启动再到上线经历了小半年的时间,伴随着这个过程,我把 Spring Security 的部分源代码系统地梳理了一遍,并对内部的设计思想和实现原理也做了一些提炼和总结。

总体而言,Spring Security 是一款代码质量非常高的开源框架,其中关于对用户认证和访问授权模型的抽象、内置的过滤器机制、全局方法安全机制、OAuth2 协议,以及响应式编程支持等诸多功能都给我留下了深刻的印象,使我受益良多。相信对于坚持学习到今天的你而言也是一样。


  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

办公模板库 素材蛙

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值