童雷 · 更新于 2021-05-11

单元测试

1. 前言

上节我们介绍了 Spring Security 安全框架中的加解密功能,本节我们讨论 Spring Security 项目单元测试的实现。

在程序开发过程中,单元测试环境往往贯穿始终。

本节我们主要讨论如何通过单元测试保障 Spring Security 应用的健壮性。

2. 方法安全测试

图片描述

2.1 场景:构造 MessageService 接口,要求认证过得用户才能访问它。

public class HelloMessageService implements MessageService {
    @PreAuthorize("authenticated")
    public String getMessage() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        return "Hello " + authentication;
    }
}

认证过的用户调用 getMessage 方法时,会得到如下返回:

Hello org.springframework.security.authentication.UsernamePasswordAuthenticationToken@ca25360: Principal: org.springframework.security.core.userdetails.User@36ebcb: Username: user; Password: [PROTECTED]; Enabled: true; AccountNonExpired: true; credentialsNonExpired: true; AccountNonLocked: true; Granted Authorities: ROLE_USER; Credentials: [PROTECTED]; Authenticated: true; Details: null; Granted Authorities: ROLE_USER

2.1.1 建立 Spring Security 单元测试

在使用 Spring Security 单元测试之前,首先需要做一些初始化,如下:

@RunWith(SpringJUnit4ClassRunner.class) 
@ContextConfiguration 
public class WithMockUserTests {
  • @RunWith 注解用于创建 ApplicationContext 对象,这里和其他的 Spring 应用单元测试一样,此处不再赘述。

  • @ContextConfiguration 注解用于上下文相关的配置选项,此处使用默认值即可,同样,这也是 Spring 单元测试所涉及的内容,此处不做赘述。

我们构造的 getMessage 方法是需要认证用户才能访问的,如果是非认证用户访问,则应该抛出相应异常。

针对这种场景建立以下单元测试方法:

@Test(expected = AuthenticationCredentialsNotFoundException.class)
public void getMessageUnauthenticated() {
    messageService.getMessage();
}

2.1.2 @WithMockUser

接下来我们要创造认证用户,是用的方法是增加 @WithMockUser 注解,该注解会构造一个用户名为「user」,密码为「password」,角色为「ROLE_USER」的用户:

@Test
@WithMockUser
public void getMessageWithMockUser() {
	String message = messageService.getMessage();
}

指定用户的用户名:

@Test
@WithMockUser("customUsername")
public void getMessageWithMockUserCustomUsername() {
    String message = messageService.getMessage();
}

指定用户的角色:

@Test
@WithMockUser(username="admin",roles={"USER","ADMIN"})
public void getMessageWithMockUserCustomUser() {
    String message = messageService.getMessage();
    ...
}

不指定用户的角色,直接定义用户的权限:

@Test
@WithMockUser(username = "admin", authorities = { "ADMIN", "USER" })
public void getMessageWithMockUserCustomAuthorities() {
    String message = messageService.getMessage();
    ...
}

冒烟测试用的用户可以定义在方法前,也可以定义在类声明前,使整个类范围内都使用相同用户:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
@WithMockUser(username="admin",roles={"USER","ADMIN"})
public class WithMockUserTests {
}

2.1.3 @WithAnonymousUser

如果需要测试匿名用户,可通过 @WithAnonymousUser 注解实现:

@RunWith(SpringJUnit4ClassRunner.class)
@WithMockUser
public class WithUserClassLevelAuthenticationTests {

    @Test
    public void withMockUser1() {
    }

    @Test
    public void withMockUser2() {
    }

    @Test
    @WithAnonymousUser
    public void anonymous() throws Exception {
      // 使用匿名用户访问该方法
    }
}

2.1.4 @WithUserDetails

@WithMockUser 不适用时,比如我们需要认证主体是一些特殊类型,这时就需要自定义 UserDetails,假设我们已经有了一个 UserDetailsService 的 bean 声明:

@Test
@WithUserDetails
public void getMessageWithUserDetails() {
    String message = messageService.getMessage();
    ...
}

我们也可以指定用户名,用来在 UserDetailsService 中返回指定用户:

@Test
@WithUserDetails("customUsername")
public void getMessageWithUserDetailsCustomUsername() {
    String message = messageService.getMessage();
    ...
}

我们还可以指定 UserDetailsService 的实现 bean:

@Test
@WithUserDetails(value="customUsername", userDetailsServiceBeanName="myUserDetailsService")
public void getMessageWithUserDetailsServiceBeanName() {
    String message = messageService.getMessage();
    ...
}

2.1.5 @WithSecurityContext

当我们不希望建立 Authentication 对象,而希望直接使用 SecurityContext 时,可用如下方法:

@Retention(RetentionPolicy.RUNTIME)
@WithSecurityContext(factory = WithMockCustomUserSecurityContextFactory.class)
public @interface WithMockCustomUser {

    String username() default "rob";

    String name() default "Rob Winch";
}

3. Spring MVC 测试集成

这部分需要配合 Spring MVC 的集成测试模块。

3.1 建立 Spring MVC 单元测试

import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.*;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SecurityConfig.class)
@WebAppConfiguration
public class CsrfShowcaseTests {

    @Autowired
    private WebApplicationContext context;

    private MockMvc mvc;

    @Before
    public void setup() {
        mvc = MockMvcBuilders
                .webAppContextSetup(context)
                .apply(springSecurity()) 
                .build();
    }

...

3.2 测试 CSRF

为请求增加有效的 CSRF Token:

mvc.perform(post("/").with(csrf()))

把 CSRF Token 增加到请求头中:

mvc.perform(post("/").with(csrf().asHeader()))

增加一个不合法的 CSRF Token:

mvc.perform(post("/").with(csrf().useInvalidToken()))

配置请求携带默认用户信息:

mvc.perform(get("/").with(user("user")))

配置请求携带自定义用户信息

mvc.perform(get("/admin").with(user("admin").password("pass").roles("USER","ADMIN")))

使用自定义的 userDetails 实例:

mvc.perform(get("/").with(user(userDetails)))

使用匿名用户:

mvc.perform(get("/").with(anonymous()))

使用自定义身份信息:

mvc.perform(get("/").with(authentication(authentication)))

使用自定义安全上下文:

mvc.perform(get("/").with(securityContext(securityContext)))

将用户信息应用到所有请求中:

mvc = MockMvcBuilders
        .webAppContextSetup(context)
        .defaultRequest(get("/").with(user("user").roles("ADMIN")))
        .apply(springSecurity())
        .build();

我们还可以使用注解方式配置用户信息:

@Test
@WithMockUser(roles="ADMIN")
public void requestProtectedUrlWithUser() throws Exception {
mvc
        .perform(get("/"))
        ...
}

3.3 测试 HTTP 基础认证

使用 httpBasic 测试基本认证:

mvc.perform(get("/").with(httpBasic("user","password")))

这一步相当于为请求增加了以下认证头:

Authorization: Basic dXNlcjpwYXNzd29yZA==

4. 小结

本节介绍了 Spring Security 项目实现单元测试的方法,主要内容有:

  • Spring Security 的单元测试是基于 Spring 单元测试扩展;
  • Spring Security 的单元测试核心思想是模拟出认证用户及相关用户信息;
  • Spring MVC 环境下的单元测试可以模拟各种安全头信息及用户认证信息。

至此,关于 Spring Security 的内容就全部结束了。