unit-test-security-authorization
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseUnit Testing Security and Authorization
安全与授权的单元测试
Overview
概述
This skill provides patterns for unit testing Spring Security authorization logic using @PreAuthorize, @Secured, @RolesAllowed, and custom permission evaluators. It covers testing role-based access control (RBAC), expression-based authorization, custom permission evaluators, and verifying access denied scenarios without full Spring Security context.
本技能提供使用@PreAuthorize、@Secured、@RolesAllowed以及自定义权限评估器进行Spring Security授权逻辑单元测试的模式。内容涵盖基于角色的访问控制(RBAC)测试、基于表达式的授权测试、自定义权限评估器测试,以及无需完整Spring Security上下文即可验证访问被拒绝的场景。
When to Use
适用场景
Use this skill when:
- Testing @PreAuthorize and @Secured method-level security
- Testing role-based access control (RBAC)
- Testing custom permission evaluators
- Verifying access denied scenarios
- Testing authorization with authenticated principals
- Want fast authorization tests without full Spring Security context
当你需要以下操作时使用本技能:
- 测试@PreAuthorize和@Secured方法级安全
- 测试基于角色的访问控制(RBAC)
- 测试自定义权限评估器
- 验证访问被拒绝的场景
- 测试已认证主体的授权
- 希望在无需完整Spring Security上下文的情况下快速进行授权测试
Instructions
操作步骤
Follow these steps to test Spring Security authorization:
按照以下步骤测试Spring Security授权:
1. Set Up Security Testing Dependencies
1. 配置安全测试依赖
Add spring-security-test to your test dependencies along with JUnit 5 and AssertJ.
在测试依赖中添加spring-security-test,同时引入JUnit 5和AssertJ。
2. Enable Method Security in Configuration
2. 在配置中启用方法安全
Use @EnableGlobalMethodSecurity(prePostEnabled = true) to activate @PreAuthorize annotations.
使用@EnableGlobalMethodSecurity(prePostEnabled = true)来激活@PreAuthorize注解。
3. Create Test with @WithMockUser
3. 使用@WithMockUser创建测试
Apply @WithMockUser annotation to simulate authenticated users with specific roles and authorities.
应用@WithMockUser注解来模拟具有特定角色和权限的已认证用户。
4. Test Both Allow and Deny Scenarios
4. 测试允许和拒绝两种场景
For each security rule, test that authorized users can access the method and unauthorized users receive AccessDeniedException.
针对每条安全规则,测试授权用户可以访问方法,未授权用户会收到AccessDeniedException。
5. Test Expression-Based Authorization
5. 测试基于表达式的授权
Verify complex expressions like authentication.principal.username == #owner work correctly.
验证复杂表达式(如authentication.principal.username == #owner)是否正常工作。
6. Test Custom Permission Evaluators
6. 测试自定义权限评估器
Unit test custom PermissionEvaluator implementations by creating Authentication objects and calling hasPermission directly.
通过创建Authentication对象并直接调用hasPermission来对自定义PermissionEvaluator实现进行单元测试。
7. Verify Method Interactions
7. 验证方法交互
Mock external dependencies and verify that security checks don't interfere with business logic.
模拟外部依赖,验证安全检查不会干扰业务逻辑。
Examples
示例
Setup: Security Testing
配置:安全测试
Maven
Maven
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>Gradle
Gradle
kotlin
dependencies {
implementation("org.springframework.boot:spring-boot-starter-security")
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("org.springframework.security:spring-security-test")
}kotlin
dependencies {
implementation("org.springframework.boot:spring-boot-starter-security")
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("org.springframework.security:spring-security-test")
}Basic Pattern: Testing @PreAuthorize
基础模式:测试@PreAuthorize
Simple Role-Based Access Control
简单基于角色的访问控制
java
// Service with security annotations
@Service
public class UserService {
@PreAuthorize("hasRole('ADMIN')")
public void deleteUser(Long userId) {
// delete logic
}
@PreAuthorize("hasRole('USER')")
public User getCurrentUser() {
// get user logic
}
@PreAuthorize("hasAnyRole('ADMIN', 'MANAGER')")
public List<User> listAllUsers() {
// list logic
}
}
// Unit test
import org.junit.jupiter.api.Test;
import org.springframework.security.test.context.support.WithMockUser;
import static org.assertj.core.api.Assertions.*;
class UserServiceSecurityTest {
@Test
@WithMockUser(roles = "ADMIN")
void shouldAllowAdminToDeleteUser() {
UserService service = new UserService();
assertThatCode(() -> service.deleteUser(1L))
.doesNotThrowAnyException();
}
@Test
@WithMockUser(roles = "USER")
void shouldDenyUserFromDeletingUser() {
UserService service = new UserService();
assertThatThrownBy(() -> service.deleteUser(1L))
.isInstanceOf(AccessDeniedException.class);
}
@Test
@WithMockUser(roles = "ADMIN")
void shouldAllowAdminAndManagerToListUsers() {
UserService service = new UserService();
assertThatCode(() -> service.listAllUsers())
.doesNotThrowAnyException();
}
@Test
void shouldDenyAnonymousUserAccess() {
UserService service = new UserService();
assertThatThrownBy(() -> service.deleteUser(1L))
.isInstanceOf(AccessDeniedException.class);
}
}java
// Service with security annotations
@Service
public class UserService {
@PreAuthorize("hasRole('ADMIN')")
public void deleteUser(Long userId) {
// delete logic
}
@PreAuthorize("hasRole('USER')")
public User getCurrentUser() {
// get user logic
}
@PreAuthorize("hasAnyRole('ADMIN', 'MANAGER')")
public List<User> listAllUsers() {
// list logic
}
}
// Unit test
import org.junit.jupiter.api.Test;
import org.springframework.security.test.context.support.WithMockUser;
import static org.assertj.core.api.Assertions.*;
class UserServiceSecurityTest {
@Test
@WithMockUser(roles = "ADMIN")
void shouldAllowAdminToDeleteUser() {
UserService service = new UserService();
assertThatCode(() -> service.deleteUser(1L))
.doesNotThrowAnyException();
}
@Test
@WithMockUser(roles = "USER")
void shouldDenyUserFromDeletingUser() {
UserService service = new UserService();
assertThatThrownBy(() -> service.deleteUser(1L))
.isInstanceOf(AccessDeniedException.class);
}
@Test
@WithMockUser(roles = "ADMIN")
void shouldAllowAdminAndManagerToListUsers() {
UserService service = new UserService();
assertThatCode(() -> service.listAllUsers())
.doesNotThrowAnyException();
}
@Test
void shouldDenyAnonymousUserAccess() {
UserService service = new UserService();
assertThatThrownBy(() -> service.deleteUser(1L))
.isInstanceOf(AccessDeniedException.class);
}
}Testing @Secured Annotation
测试@Secured注解
Legacy Security Configuration
传统安全配置
java
@Service
public class OrderService {
@Secured("ROLE_ADMIN")
public Order approveOrder(Long orderId) {
// approval logic
}
@Secured({"ROLE_ADMIN", "ROLE_MANAGER"})
public List<Order> getOrders() {
// get orders
}
}
class OrderSecurityTest {
@Test
@WithMockUser(roles = "ADMIN")
void shouldAllowAdminToApproveOrder() {
OrderService service = new OrderService();
assertThatCode(() -> service.approveOrder(1L))
.doesNotThrowAnyException();
}
@Test
@WithMockUser(roles = "USER")
void shouldDenyUserFromApprovingOrder() {
OrderService service = new OrderService();
assertThatThrownBy(() -> service.approveOrder(1L))
.isInstanceOf(AccessDeniedException.class);
}
}java
@Service
public class OrderService {
@Secured("ROLE_ADMIN")
public Order approveOrder(Long orderId) {
// approval logic
}
@Secured({"ROLE_ADMIN", "ROLE_MANAGER"})
public List<Order> getOrders() {
// get orders
}
}
class OrderSecurityTest {
@Test
@WithMockUser(roles = "ADMIN")
void shouldAllowAdminToApproveOrder() {
OrderService service = new OrderService();
assertThatCode(() -> service.approveOrder(1L))
.doesNotThrowAnyException();
}
@Test
@WithMockUser(roles = "USER")
void shouldDenyUserFromApprovingOrder() {
OrderService service = new OrderService();
assertThatThrownBy(() -> service.approveOrder(1L))
.isInstanceOf(AccessDeniedException.class);
}
}Testing Controller Security with MockMvc
使用MockMvc测试控制器安全
Secure REST Endpoints
安全的REST端点
java
@RestController
@RequestMapping("/api/admin")
public class AdminController {
@GetMapping("/users")
@PreAuthorize("hasRole('ADMIN')")
public List<UserDto> listAllUsers() {
// logic
}
@DeleteMapping("/users/{id}")
@PreAuthorize("hasRole('ADMIN')")
public void deleteUser(@PathVariable Long id) {
// delete logic
}
}
// Testing with MockMvc
import org.springframework.security.test.context.support.WithMockUser;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
class AdminControllerSecurityTest {
private MockMvc mockMvc;
@BeforeEach
void setUp() {
mockMvc = MockMvcBuilders
.standaloneSetup(new AdminController())
.apply(springSecurity())
.build();
}
@Test
@WithMockUser(roles = "ADMIN")
void shouldAllowAdminToListUsers() throws Exception {
mockMvc.perform(get("/api/admin/users"))
.andExpect(status().isOk());
}
@Test
@WithMockUser(roles = "USER")
void shouldDenyUserFromListingUsers() throws Exception {
mockMvc.perform(get("/api/admin/users"))
.andExpect(status().isForbidden());
}
@Test
void shouldDenyAnonymousAccessToAdminEndpoint() throws Exception {
mockMvc.perform(get("/api/admin/users"))
.andExpect(status().isUnauthorized());
}
@Test
@WithMockUser(roles = "ADMIN")
void shouldAllowAdminToDeleteUser() throws Exception {
mockMvc.perform(delete("/api/admin/users/1"))
.andExpect(status().isOk());
}
}java
@RestController
@RequestMapping("/api/admin")
public class AdminController {
@GetMapping("/users")
@PreAuthorize("hasRole('ADMIN')")
public List<UserDto> listAllUsers() {
// logic
}
@DeleteMapping("/users/{id}")
@PreAuthorize("hasRole('ADMIN')")
public void deleteUser(@PathVariable Long id) {
// delete logic
}
}
// Testing with MockMvc
import org.springframework.security.test.context.support.WithMockUser;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
class AdminControllerSecurityTest {
private MockMvc mockMvc;
@BeforeEach
void setUp() {
mockMvc = MockMvcBuilders
.standaloneSetup(new AdminController())
.apply(springSecurity())
.build();
}
@Test
@WithMockUser(roles = "ADMIN")
void shouldAllowAdminToListUsers() throws Exception {
mockMvc.perform(get("/api/admin/users"))
.andExpect(status().isOk());
}
@Test
@WithMockUser(roles = "USER")
void shouldDenyUserFromListingUsers() throws Exception {
mockMvc.perform(get("/api/admin/users"))
.andExpect(status().isForbidden());
}
@Test
void shouldDenyAnonymousAccessToAdminEndpoint() throws Exception {
mockMvc.perform(get("/api/admin/users"))
.andExpect(status().isUnauthorized());
}
@Test
@WithMockUser(roles = "ADMIN")
void shouldAllowAdminToDeleteUser() throws Exception {
mockMvc.perform(delete("/api/admin/users/1"))
.andExpect(status().isOk());
}
}Testing Expression-Based Authorization
测试基于表达式的授权
Complex Permission Expressions
复杂权限表达式
java
@Service
public class DocumentService {
@PreAuthorize("hasRole('ADMIN') or authentication.principal.username == #owner")
public Document getDocument(String owner, Long docId) {
// get document
}
@PreAuthorize("hasPermission(#docId, 'Document', 'WRITE')")
public void updateDocument(Long docId, String content) {
// update logic
}
@PreAuthorize("#userId == authentication.principal.id")
public UserProfile getUserProfile(Long userId) {
// get profile
}
}
class ExpressionBasedSecurityTest {
@Test
@WithMockUser(username = "alice", roles = "ADMIN")
void shouldAllowAdminToAccessAnyDocument() {
DocumentService service = new DocumentService();
assertThatCode(() -> service.getDocument("bob", 1L))
.doesNotThrowAnyException();
}
@Test
@WithMockUser(username = "alice")
void shouldAllowOwnerToAccessOwnDocument() {
DocumentService service = new DocumentService();
assertThatCode(() -> service.getDocument("alice", 1L))
.doesNotThrowAnyException();
}
@Test
@WithMockUser(username = "alice")
void shouldDenyUserAccessToOtherUserDocument() {
DocumentService service = new DocumentService();
assertThatThrownBy(() -> service.getDocument("bob", 1L))
.isInstanceOf(AccessDeniedException.class);
}
@Test
@WithMockUser(username = "alice", id = "1")
void shouldAllowUserToAccessOwnProfile() {
DocumentService service = new DocumentService();
assertThatCode(() -> service.getUserProfile(1L))
.doesNotThrowAnyException();
}
@Test
@WithMockUser(username = "alice", id = "1")
void shouldDenyUserAccessToOtherProfile() {
DocumentService service = new DocumentService();
assertThatThrownBy(() -> service.getUserProfile(999L))
.isInstanceOf(AccessDeniedException.class);
}
}java
@Service
public class DocumentService {
@PreAuthorize("hasRole('ADMIN') or authentication.principal.username == #owner")
public Document getDocument(String owner, Long docId) {
// get document
}
@PreAuthorize("hasPermission(#docId, 'Document', 'WRITE')")
public void updateDocument(Long docId, String content) {
// update logic
}
@PreAuthorize("#userId == authentication.principal.id")
public UserProfile getUserProfile(Long userId) {
// get profile
}
}
class ExpressionBasedSecurityTest {
@Test
@WithMockUser(username = "alice", roles = "ADMIN")
void shouldAllowAdminToAccessAnyDocument() {
DocumentService service = new DocumentService();
assertThatCode(() -> service.getDocument("bob", 1L))
.doesNotThrowAnyException();
}
@Test
@WithMockUser(username = "alice")
void shouldAllowOwnerToAccessOwnDocument() {
DocumentService service = new DocumentService();
assertThatCode(() -> service.getDocument("alice", 1L))
.doesNotThrowAnyException();
}
@Test
@WithMockUser(username = "alice")
void shouldDenyUserAccessToOtherUserDocument() {
DocumentService service = new DocumentService();
assertThatThrownBy(() -> service.getDocument("bob", 1L))
.isInstanceOf(AccessDeniedException.class);
}
@Test
@WithMockUser(username = "alice", id = "1")
void shouldAllowUserToAccessOwnProfile() {
DocumentService service = new DocumentService();
assertThatCode(() -> service.getUserProfile(1L))
.doesNotThrowAnyException();
}
@Test
@WithMockUser(username = "alice", id = "1")
void shouldDenyUserAccessToOtherProfile() {
DocumentService service = new DocumentService();
assertThatThrownBy(() -> service.getUserProfile(999L))
.isInstanceOf(AccessDeniedException.class);
}
}Testing Custom Permission Evaluator
测试自定义权限评估器
Create and Test Custom Permission Logic
创建并测试自定义权限逻辑
java
// Custom permission evaluator
@Component
public class DocumentPermissionEvaluator implements PermissionEvaluator {
private final DocumentRepository documentRepository;
public DocumentPermissionEvaluator(DocumentRepository documentRepository) {
this.documentRepository = documentRepository;
}
@Override
public boolean hasPermission(Authentication authentication, Object targetDomainObject, Object permission) {
if (authentication == null) return false;
Document document = (Document) targetDomainObject;
String userUsername = authentication.getName();
return document.getOwner().getUsername().equals(userUsername) ||
userHasRole(authentication, "ADMIN");
}
@Override
public boolean hasPermission(Authentication authentication, Serializable targetId, String targetType, Object permission) {
if (authentication == null) return false;
if (!"Document".equals(targetType)) return false;
Document document = documentRepository.findById((Long) targetId).orElse(null);
if (document == null) return false;
return hasPermission(authentication, document, permission);
}
private boolean userHasRole(Authentication authentication, String role) {
return authentication.getAuthorities().stream()
.anyMatch(auth -> auth.getAuthority().equals("ROLE_" + role));
}
}
// Unit test for custom evaluator
class DocumentPermissionEvaluatorTest {
private DocumentPermissionEvaluator evaluator;
private DocumentRepository documentRepository;
private Authentication adminAuth;
private Authentication userAuth;
private Document document;
@BeforeEach
void setUp() {
documentRepository = mock(DocumentRepository.class);
evaluator = new DocumentPermissionEvaluator(documentRepository);
document = new Document(1L, "Test Doc", new User("alice"));
adminAuth = new UsernamePasswordAuthenticationToken(
"admin",
null,
List.of(new SimpleGrantedAuthority("ROLE_ADMIN"))
);
userAuth = new UsernamePasswordAuthenticationToken(
"alice",
null,
List.of(new SimpleGrantedAuthority("ROLE_USER"))
);
}
@Test
void shouldGrantPermissionToDocumentOwner() {
boolean hasPermission = evaluator.hasPermission(userAuth, document, "WRITE");
assertThat(hasPermission).isTrue();
}
@Test
void shouldDenyPermissionToNonOwner() {
Authentication otherUserAuth = new UsernamePasswordAuthenticationToken(
"bob",
null,
List.of(new SimpleGrantedAuthority("ROLE_USER"))
);
boolean hasPermission = evaluator.hasPermission(otherUserAuth, document, "WRITE");
assertThat(hasPermission).isFalse();
}
@Test
void shouldGrantPermissionToAdmin() {
boolean hasPermission = evaluator.hasPermission(adminAuth, document, "WRITE");
assertThat(hasPermission).isTrue();
}
@Test
void shouldDenyNullAuthentication() {
boolean hasPermission = evaluator.hasPermission(null, document, "WRITE");
assertThat(hasPermission).isFalse();
}
}java
// Custom permission evaluator
@Component
public class DocumentPermissionEvaluator implements PermissionEvaluator {
private final DocumentRepository documentRepository;
public DocumentPermissionEvaluator(DocumentRepository documentRepository) {
this.documentRepository = documentRepository;
}
@Override
public boolean hasPermission(Authentication authentication, Object targetDomainObject, Object permission) {
if (authentication == null) return false;
Document document = (Document) targetDomainObject;
String userUsername = authentication.getName();
return document.getOwner().getUsername().equals(userUsername) ||
userHasRole(authentication, "ADMIN");
}
@Override
public boolean hasPermission(Authentication authentication, Serializable targetId, String targetType, Object permission) {
if (authentication == null) return false;
if (!"Document".equals(targetType)) return false;
Document document = documentRepository.findById((Long) targetId).orElse(null);
if (document == null) return false;
return hasPermission(authentication, document, permission);
}
private boolean userHasRole(Authentication authentication, String role) {
return authentication.getAuthorities().stream()
.anyMatch(auth -> auth.getAuthority().equals("ROLE_" + role));
}
}
// Unit test for custom evaluator
class DocumentPermissionEvaluatorTest {
private DocumentPermissionEvaluator evaluator;
private DocumentRepository documentRepository;
private Authentication adminAuth;
private Authentication userAuth;
private Document document;
@BeforeEach
void setUp() {
documentRepository = mock(DocumentRepository.class);
evaluator = new DocumentPermissionEvaluator(documentRepository);
document = new Document(1L, "Test Doc", new User("alice"));
adminAuth = new UsernamePasswordAuthenticationToken(
"admin",
null,
List.of(new SimpleGrantedAuthority("ROLE_ADMIN"))
);
userAuth = new UsernamePasswordAuthenticationToken(
"alice",
null,
List.of(new SimpleGrantedAuthority("ROLE_USER"))
);
}
@Test
void shouldGrantPermissionToDocumentOwner() {
boolean hasPermission = evaluator.hasPermission(userAuth, document, "WRITE");
assertThat(hasPermission).isTrue();
}
@Test
void shouldDenyPermissionToNonOwner() {
Authentication otherUserAuth = new UsernamePasswordAuthenticationToken(
"bob",
null,
List.of(new SimpleGrantedAuthority("ROLE_USER"))
);
boolean hasPermission = evaluator.hasPermission(otherUserAuth, document, "WRITE");
assertThat(hasPermission).isFalse();
}
@Test
void shouldGrantPermissionToAdmin() {
boolean hasPermission = evaluator.hasPermission(adminAuth, document, "WRITE");
assertThat(hasPermission).isTrue();
}
@Test
void shouldDenyNullAuthentication() {
boolean hasPermission = evaluator.hasPermission(null, document, "WRITE");
assertThat(hasPermission).isFalse();
}
}Testing Multiple Roles
测试多角色
Parameterized Role Testing
参数化角色测试
java
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
class RoleBasedAccessTest {
private AdminService service;
@BeforeEach
void setUp() {
service = new AdminService();
}
@ParameterizedTest
@ValueSource(strings = {"ADMIN", "SUPER_ADMIN", "SYSTEM"})
@WithMockUser(roles = "ADMIN")
void shouldAllowPrivilegedRolesToDeleteUser(String role) {
assertThatCode(() -> service.deleteUser(1L))
.doesNotThrowAnyException();
}
@ParameterizedTest
@ValueSource(strings = {"USER", "GUEST", "READONLY"})
void shouldDenyUnprivilegedRolesToDeleteUser(String role) {
assertThatThrownBy(() -> service.deleteUser(1L))
.isInstanceOf(AccessDeniedException.class);
}
}java
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
class RoleBasedAccessTest {
private AdminService service;
@BeforeEach
void setUp() {
service = new AdminService();
}
@ParameterizedTest
@ValueSource(strings = {"ADMIN", "SUPER_ADMIN", "SYSTEM"})
@WithMockUser(roles = "ADMIN")
void shouldAllowPrivilegedRolesToDeleteUser(String role) {
assertThatCode(() -> service.deleteUser(1L))
.doesNotThrowAnyException();
}
@ParameterizedTest
@ValueSource(strings = {"USER", "GUEST", "READONLY"})
void shouldDenyUnprivilegedRolesToDeleteUser(String role) {
assertThatThrownBy(() -> service.deleteUser(1L))
.isInstanceOf(AccessDeniedException.class);
}
}Best Practices
最佳实践
- Use @WithMockUser for setting authenticated user context
- Test both allow and deny cases for each security rule
- Test with different roles to verify role-based decisions
- Test expression-based security comprehensively
- Mock external dependencies (permission evaluators, etc.)
- Test anonymous access separately from authenticated access
- Use @EnableGlobalMethodSecurity in configuration for method-level security
- 使用@WithMockUser 设置已认证用户上下文
- 测试允许和拒绝两种场景 针对每条安全规则
- 测试不同角色 验证基于角色的决策
- 全面测试基于表达式的安全
- 模拟外部依赖(如权限评估器等)
- 单独测试匿名访问 与已认证访问区分开
- 在配置中使用@EnableGlobalMethodSecurity 启用方法级安全
Common Pitfalls
常见误区
- Forgetting to enable method security in test configuration
- Not testing both allow and deny scenarios
- Testing framework code instead of authorization logic
- Not handling null authentication in tests
- Mixing authentication and authorization tests unnecessarily
- 忘记在测试配置中启用方法安全
- 未测试允许和拒绝两种场景
- 测试框架代码而非授权逻辑
- 未在测试中处理空认证情况
- 不必要地混合认证和授权测试
Constraints and Warnings
约束与注意事项
- Method security requires proxy: @PreAuthorize works via proxies; direct method calls bypass security
- @EnableGlobalMethodSecurity: Must be enabled for @PreAuthorize, @Secured to work
- Role prefix: Spring adds "ROLE_" prefix automatically; use hasRole('ADMIN') not hasRole('ROLE_ADMIN')
- Authentication context: Security context is thread-local; be careful with async tests
- @WithMockUser limitations: Creates a simple Authentication; complex auth scenarios need custom setup
- SpEL expressions: Complex SpEL in @PreAuthorize can be difficult to debug; test thoroughly
- Performance impact: Method security adds overhead; consider security at layer boundaries
- 方法安全需要代理:@PreAuthorize通过代理工作;直接调用方法会绕过安全检查
- @EnableGlobalMethodSecurity:必须启用该注解,@PreAuthorize、@Secured才能生效
- 角色前缀:Spring会自动添加"ROLE_"前缀;使用hasRole('ADMIN')而非hasRole('ROLE_ADMIN')
- 认证上下文:安全上下文是线程局部的;异步测试时需注意
- @WithMockUser的局限性:创建简单的Authentication;复杂认证场景需要自定义配置
- SpEL表达式:@PreAuthorize中的复杂SpEL表达式难以调试;需全面测试
- 性能影响:方法安全会增加开销;考虑在层边界处设置安全检查
Examples
示例
Input: Service Without Security Testing
输入:未进行安全测试的服务
java
@Service
public class AdminService {
public void deleteUser(Long userId) {
// Delete logic without security check
}
}java
@Service
public class AdminService {
public void deleteUser(Long userId) {
// Delete logic without security check
}
}Output: Service With Security Test Coverage
输出:具备安全测试覆盖的服务
java
@Service
public class AdminService {
@PreAuthorize("hasRole('ADMIN')")
public void deleteUser(Long userId) {
// Delete logic
}
}
// Test
@Test
@WithMockUser(roles = "ADMIN")
void shouldAllowAdminToDeleteUser() {
assertThatCode(() -> adminService.deleteUser(1L))
.doesNotThrowAnyException();
}
@Test
@WithMockUser(roles = "USER")
void shouldDenyUserFromDeletingUser() {
assertThatThrownBy(() -> adminService.deleteUser(1L))
.isInstanceOf(AccessDeniedException.class);
}java
@Service
public class AdminService {
@PreAuthorize("hasRole('ADMIN')")
public void deleteUser(Long userId) {
// Delete logic
}
}
// Test
@Test
@WithMockUser(roles = "ADMIN")
void shouldAllowAdminToDeleteUser() {
assertThatCode(() -> adminService.deleteUser(1L))
.doesNotThrowAnyException();
}
@Test
@WithMockUser(roles = "USER")
void shouldDenyUserFromDeletingUser() {
assertThatThrownBy(() -> adminService.deleteUser(1L))
.isInstanceOf(AccessDeniedException.class);
}Input: Manual Security Check (Anti-Pattern)
输入:手动安全检查(反模式)
java
if (user.hasRole("ADMIN")) {
service.deleteUser(userId);
}java
if (user.hasRole("ADMIN")) {
service.deleteUser(userId);
}Output: Declarative Security with Testing
输出:声明式安全与测试
java
@PreAuthorize("hasRole('ADMIN')")
public void deleteUser(Long userId) {
// Business logic only, security is declarative
}
// Test verifies security enforcement
@Test
@WithMockUser(roles = "ADMIN")
void shouldExecuteDelete() {
service.deleteUser(1L);
verify(repository).deleteById(1L);
}java
@PreAuthorize("hasRole('ADMIN')")
public void deleteUser(Long userId) {
// Business logic only, security is declarative
}
// Test verifies security enforcement
@Test
@WithMockUser(roles = "ADMIN")
void shouldExecuteDelete() {
service.deleteUser(1L);
verify(repository).deleteById(1L);
}Constraints and Warnings
约束与注意事项
AccessDeniedException not thrown: Ensure is configured.
@EnableGlobalMethodSecurity(prePostEnabled = true)@WithMockUser not working: Verify Spring Security test dependencies are on classpath.
Custom PermissionEvaluator not invoked: Check .
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)未抛出AccessDeniedException:确保已配置。
@EnableGlobalMethodSecurity(prePostEnabled = true)@WithMockUser不生效:验证Spring Security测试依赖已在类路径中。
自定义PermissionEvaluator未被调用:检查是否配置了。
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)