Spring Security 教程(16)、测试

测试安全配置的痛点

testing 配图
testing 配图
testing 配图

安全配置经常涉及多个组件(Filter、AuthenticationManager、UserDetailsService)的协作,手动测试需要启动整个 Web 容器,非常耗时。Spring Security 提供了 MockMvc 支持,让安全测试变得简单。

集成测试基础

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@SpringBootTest
@AutoConfigureMockMvc
class SecurityTest {

@Autowired
private MockMvc mockMvc;

@Test
void unprotectedEndpoint_returns200() throws Exception {
mockMvc.perform(get("/public/hello"))
.andExpect(status().isOk());
}

@Test
void protectedEndpoint_withoutAuth_returns401() throws Exception {
mockMvc.perform(get("/api/admin"))
.andExpect(status().isUnauthorized());
}

@Test
void protectedEndpoint_withAuth_returns200() throws Exception {
mockMvc.perform(get("/api/admin")
.with(username("admin").password("password")))
.andExpect(status().isOk());
}
}

使用 WithMockUser 简化测试

WithMockUser 是最常用的测试注解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Test
@WithMockUser(username = "user1", roles = {"USER"})
void userCanAccessUserEndpoint() throws Exception {
mockMvc.perform(get("/api/user"))
.andExpect(status().isOk());
}

@Test
@WithMockUser(username = "admin", roles = {"ADMIN"})
void adminCanAccessAdminEndpoint() throws Exception {
mockMvc.perform(get("/api/admin"))
.andExpect(status().isOk());
}

@Test
@WithMockUser(roles = {"USER"})
void userCannotAccessAdminEndpoint() throws Exception {
mockMvc.perform(get("/api/admin"))
.andExpect(status().isForbidden());
}

使用 WithUserDetails 加载真实用户

当你需要测试真实的用户数据(来自数据库或 LDAP)时,使用 WithUserDetails:

1
2
3
4
5
6
@Test
@WithUserDetails("admin") // 调用 UserDetailsService.loadUserByUsername("admin")
void testWithRealUser() throws Exception {
mockMvc.perform(get("/api/user/profile"))
.andExpect(jsonPath("$.username").value("admin"));
}

方法级安全的测试

1
2
3
4
5
6
7
8
9
10
11
12
@Test
@WithMockUser(roles = {"ADMIN"})
void adminCanDeleteUser() {
userService.deleteUser(1L); // 不抛异常
}

@Test
@WithMockUser(roles = {"USER"})
void userCannotDeleteUser() {
assertThrows(AccessDeniedException.class,
() -> userService.deleteUser(1L));
}

测试 SecurityContext

1
2
3
4
5
6
7
8
@Test
void testSecurityContext() {
SecurityTestUtils.authenticatedWith("admin", "ROLE_ADMIN");

Authentication auth = SecurityContextHolder.getContext().getAuthentication();
assertTrue(auth.isAuthenticated());
assertEquals("admin", auth.getName());
}

测试配置的最佳实践

  • 为每个安全场景编写独立的测试方法
  • 使用 @WithMockUser 覆盖不同的角色组合
  • 测试认证成功、失败、权限不足三种情况
  • 为方法级安全编写单元测试,为 URL 级安全编写集成测试