Spring Security 测试与方法安全
概述
Spring Security 提供两种粒度的安全控制:
| 粒度 | 机制 | 适用场景 |
|---|---|---|
| URL 安全 | HttpSecurity.authorizeHttpRequests() | 按路径控制 |
| 方法安全 | @PreAuthorize / @Secured / @RolesAllowed | 按方法/类控制 |
方法安全允许你在 Service 层或 Controller 层的方法上直接声明权限规则,粒度更细。
一、启用方法安全
java
@Configuration
@EnableMethodSecurity // Spring Security 6+(替代 @EnableGlobalMethodSecurity)
public class MethodSecurityConfig {
// 无需额外配置
}Spring Security 5 及之前的旧配置:
java
@Configuration
@EnableGlobalMethodSecurity(
prePostEnabled = true, // 启用 @PreAuthorize / @PostAuthorize
securedEnabled = true, // 启用 @Secured
jsr250Enabled = true // 启用 @RolesAllowed (JSR-250)
)
public class OldMethodSecurityConfig {
}二、核心注解
2.1 @PreAuthorize
方法执行前检查权限:
java
@RestController
@RequestMapping("/orders")
public class OrderController {
// 只有 ADMIN 和管理员才能访问
@PreAuthorize("hasRole('ADMIN')")
@GetMapping("/all")
public List<Order> getAllOrders() {
return orderService.findAll();
}
// 只有订单所属用户或 ADMIN 才能访问
@PreAuthorize("#order.userId == authentication.name or hasRole('ADMIN')")
@GetMapping("/{id}")
public Order getOrder(@PathVariable Long id) {
return orderService.findById(id);
}
// 组合条件
@PreAuthorize("hasAnyRole('ADMIN', 'MANAGER') and #order.amount < 10000")
@PostMapping
public Order createOrder(@RequestBody Order order) {
return orderService.create(order);
}
}2.2 @PostAuthorize
方法执行后检查权限(可用于数据级权限):
java
@Service
public class DocumentService {
// 方法返回后,检查返回对象的 owner 是否等于当前用户
@PostAuthorize("returnObject.owner == authentication.name")
public Document findById(Long id) {
return documentRepository.findById(id)
.orElseThrow(() -> new DocumentNotFoundException(id));
}
// 拒绝时返回 null(不抛异常)
@PostAuthorize("returnObject == null or returnObject.owner == authentication.name")
public Document findByIdSafe(Long id) {
return documentRepository.findById(id).orElse(null);
}
}2.3 @PreFilter / @PostFilter
集合参数/返回值过滤:
java
// 只有当前用户有权限的订单才会保留
@PreFilter("filterObject.userId == authentication.name")
@PostMapping("/batch")
public List<Order> batchCreate(@RequestBody List<Order> orders) {
return orderService.batchCreate(orders);
}
// 只返回当前用户有权限的数据
@PostFilter("filterObject.owner == authentication.name")
@GetMapping("/documents")
public List<Document> getAllDocuments() {
return documentService.findAll();
}2.4 @Secured
java
// 基于角色的简单注解(不支持 SpEL)
@Secured("ROLE_ADMIN")
public void deleteUser(Long userId) {
userService.delete(userId);
}
@Secured({"ROLE_ADMIN", "ROLE_MANAGER"})
public void approveOrder(Long orderId) {
// ADMIN 或 MANAGER 都可以
}2.5 @RolesAllowed (JSR-250)
java
// 与 @Secured 类似,但遵循 JSR-250 标准
@RolesAllowed("ROLE_ADMIN")
public void adminOperation() {
}
@RolesAllowed({"ROLE_ADMIN", "ROLE_USER"})
public void userOrAdminOperation() {
}三、SpEL 上下文
方法安全注解中可用的 SpEL 变量:
| 表达式 | 说明 |
|---|---|
#paramName | 方法参数 |
authentication | 当前 Authentication 对象 |
principal | authentication.principal 的快捷方式 |
hasRole('ROLE') | 是否拥有指定角色 |
hasAnyRole('A','B') | 是否拥有任意指定角色 |
hasAuthority('perm') | 是否拥有指定权限 |
hasAnyAuthority('a','b') | 是否拥有任意指定权限 |
permitAll() | 始终允许 |
denyAll() | 始终拒绝 |
isAnonymous() | 是否匿名用户 |
isAuthenticated() | 是否已认证(非匿名) |
isRememberMe() | 是否通过记住我认证 |
isFullyAuthenticated() | 是否完整认证(非匿名且非记住我) |
#this | 当前对象(@PostFilter 中为集合元素) |
filterObject | @PreFilter/@PostFilter 中的当前元素 |
returnObject | @PostAuthorize/@PostFilter 中的返回值 |
四、AuthorizationManagerBeforeMethodInterceptor 源码
Spring Security 6+ 使用 AuthorizationManager 替代了旧的元数据源机制。
4.1 核心拦截器
java
// 方法执行前拦截
public class AuthorizationManagerBeforeMethodInterceptor
implements MethodInterceptor, Ordered {
private final Class<Annotation> annotation;
private final AuthorizationManager<MethodInvocation> authorizationManager;
@Override
@Nullable
public Object invoke(MethodInvocation mi) throws Throwable {
// 1. 检查是否有 @PreAuthorize 注解
if (skipAuthorization(mi)) {
return mi.proceed();
}
// 2. 构建认证上下文
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
// 3. 调用 AuthorizationManager.verify()
authorizationManager.verify(() -> authentication, mi);
// 4. 通过后执行目标方法
return mi.proceed();
}
}4.2 授权决策
java
// PreAuthorizeAuthorizationManager
public class PreAuthorizeAuthorizationManager
implements AuthorizationManager<MethodInvocation> {
@Override
public void verify(Supplier<Authentication> authentication, MethodInvocation invocation) {
// 1. 解析 @PreAuthorize 注解中的 SpEL 表达式
PreAuthorize annotation = findAnnotation(invocation);
Expression expression = expressionParser.parseExpression(annotation.value());
// 2. 创建评估上下文
EvaluationContext ctx = createEvaluationContext(authentication, invocation);
// 3. 评估表达式
Boolean result = expression.getValue(ctx, Boolean.class);
// 4. 拒绝则抛异常
if (!Boolean.TRUE.equals(result)) {
throw new AccessDeniedException("Access Denied");
}
}
}五、Spring Security 测试
5.1 依赖
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>5.2 注解方式测试
java
@SpringBootTest
@AutoConfigureMockMvc
public class OrderControllerTest {
@Autowired
private MockMvc mockMvc;
// 模拟 ADMIN 角色
@Test
@WithMockUser(roles = "ADMIN")
public void testAdminAccess() throws Exception {
mockMvc.perform(get("/orders/all"))
.andExpect(status().isOk());
}
// 模拟普通用户
@Test
@WithMockUser(username = "user1", roles = "USER")
public void testUserAccess() throws Exception {
mockMvc.perform(get("/orders/all"))
.andExpect(status().isForbidden()); // USER 无权访问
}
// 模拟匿名用户
@Test
public void testAnonymous() throws Exception {
mockMvc.perform(get("/orders/all"))
.andExpect(status().isUnauthorized());
}
// 自定义 UserDetails
@Test
@WithUserDetails("admin")
public void testWithUserDetails() throws Exception {
mockMvc.perform(get("/orders/all"))
.andExpect(status().isOk());
}
// 自定义 OAuth2 用户
@Test
@WithOAuth2User(
authorities = {@Authority("SCOPE_read")},
attributes = {"sub": "123"}
)
public void testOAuth2User() throws Exception {
mockMvc.perform(get("/api/orders"))
.andExpect(status().isOk());
}
}5.3 自定义注解
java
@Retention(RetentionPolicy.RUNTIME)
@WithMockUser(username = "admin", roles = {"ADMIN", "USER"})
public @interface WithAdminUser {
}
// 使用
@Test
@WithAdminUser
public void testWithCustomAnnotation() throws Exception {
mockMvc.perform(get("/orders/all"))
.andExpect(status().isOk());
}5.4 编程方式
java
@Test
public void testWithSecurityContext() {
// 手动设置认证信息
UsernamePasswordAuthenticationToken token =
UsernamePasswordAuthenticationToken.authenticated(
new User("user1", "password", AuthorityUtils.createAuthorityList("ROLE_USER")),
null,
AuthorityUtils.createAuthorityList("ROLE_USER")
);
SecurityContextHolder.getContext().setAuthentication(token);
try {
// 执行业务方法
List<Order> orders = orderService.getAllOrders();
assertNotNull(orders);
} finally {
SecurityContextHolder.clearContext();
}
}5.5 方法安全测试
java
@SpringBootTest
public class OrderServiceMethodSecurityTest {
@Autowired
private OrderService orderService;
@Test
@WithMockUser(roles = "ADMIN")
public void testAdminCanDelete() {
// ADMIN 可以删除
orderService.deleteOrder(1L);
}
@Test
@WithMockUser(roles = "USER")
public void testUserCannotDelete() {
// USER 删除会抛 AccessDeniedException
assertThrows(AccessDeniedException.class, () -> {
orderService.deleteOrder(1L);
});
}
@Test
@WithMockUser(username = "owner")
public void testOwnerCanAccess() {
Document doc = documentService.findById(1L);
assertEquals("owner", doc.getOwner());
}
@Test
@WithMockUser(username = "other")
public void testOtherCannotAccess() {
// @PostAuthorize 验证失败
assertThrows(AccessDeniedException.class, () -> {
documentService.findById(1L);
});
}
}六、实战:REST API 方法级权限落地
6.1 场景
电商系统需要控制订单、用户、商品不同角色的访问权限。
6.2 权限定义
java
// 权限常量
public final class Permissions {
// 订单权限
public static final String ORDER_CREATE = "order:create";
public static final String ORDER_VIEW = "order:view";
public static final String ORDER_DELETE = "order:delete";
// 用户权限
public static final String USER_CREATE = "user:create";
public static final String USER_VIEW = "user:view";
public static final String USER_DELETE = "user:delete";
// 商品权限
public static final String PRODUCT_CREATE = "product:create";
public static final String PRODUCT_UPDATE = "product:update";
}6.3 服务层方法安全
java
@Service
public class OrderService {
// 用户只能查看自己的订单
@PreAuthorize("hasPermission(#id, 'order', 'view')")
public Order findById(Long id) {
return orderRepository.findById(id)
.orElseThrow(() -> new OrderNotFoundException(id));
}
// 只有订单管理员才能删除订单
@PreAuthorize("hasAuthority('order:delete')")
public void deleteOrder(Long id) {
orderRepository.deleteById(id);
}
// 创建订单 — 任何已认证用户都可以
@PreAuthorize("isAuthenticated()")
public Order createOrder(Order order) {
order.setUserId(getCurrentUserId());
return orderRepository.save(order);
}
}
@Service
public class UserService {
@PreAuthorize("hasAuthority('user:view')")
public User findById(Long id) {
return userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException(id));
}
@PreAuthorize("hasAuthority('user:delete') and #id != authentication.principal.id")
public void deleteUser(Long id) {
userRepository.deleteById(id);
}
}
@Service
public class ProductService {
@PreAuthorize("hasAuthority('product:update')")
public Product updatePrice(Long id, BigDecimal price) {
Product product = productRepository.findById(id)
.orElseThrow(() -> new ProductNotFoundException(id));
product.setPrice(price);
return productRepository.save(product);
}
}6.4 权限映射
java
@Component
public class RolePermissionsMapping {
@PostConstruct
public void init() {
// ADMIN 拥有所有权限
GrantedAuthorityDefaults.ADMIN_AUTHORITIES = Arrays.asList(
"order:create", "order:view", "order:delete",
"user:create", "user:view", "user:delete",
"product:create", "product:update"
);
// MANAGER 拥有部分权限
GrantedAuthorityDefaults.MANAGER_AUTHORITIES = Arrays.asList(
"order:view", "order:delete",
"user:view",
"product:create", "product:update"
);
// USER 只有基本权限
GrantedAuthorityDefaults.USER_AUTHORITIES = Arrays.asList(
"order:create", "order:view"
);
}
}七、总结
| 知识点 | 说明 |
|---|---|
@PreAuthorize | 方法执行前检查(支持 SpEL) |
@PostAuthorize | 方法返回后检查(可用于数据级权限) |
@PreFilter | 方法执行前过滤集合参数 |
@PostFilter | 方法返回后过滤集合返回值 |
@Secured | 基于角色的简单注解 |
@RolesAllowed | JSR-250 标准注解 |
| SpEL 上下文 | #param、authentication、returnObject、filterObject |
| 测试注解 | @WithMockUser、@WithUserDetails、@WithOAuth2User |
| 源码设计 | AuthorizationManagerBeforeMethodInterceptor → SpEL 评估 → 放行/拒绝 |
参考链接: