PreFilter
一句话定义:方法执行前过滤参数集合,使用 SpEL 表达式遍历传入的集合参数,移除不符合权限条件的元素,只保留合法元素传递给目标方法。
定义与作用
@PreFilter 在方法执行前拦截,对传入的集合类型参数进行过滤。表达式中的 filterObject 代表集合中的单个元素,表达式返回 false 的元素会被从集合中移除,不会进入目标方法。
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface PreFilter {
String value(); // SpEL 表达式,filterObject 代表集合元素
}
关键变量:
| 变量 | 类型 | 含义 |
|---|---|---|
filterObject | 集合元素类型 | 当前遍历到的集合元素 |
filterTarget | 隐式 | Spring Security 自动检测的集合参数 |
#paramName | 方法参数 | 其他参数引用 |
authentication | Authentication | 当前安全上下文 |
支持的集合类型:
Collection接口实现(List,Set,ArrayList,HashSet等)- 数组(
Object[]) Map(filterObject为Map.Entry)
核心原理
过滤执行流程
核心机制:@PreFilter 通过 MethodSecurityExpressionHandler 执行过滤,实际上是修改(或替换)传入的集合参数。对于不可变集合(如 Collections.emptyList()),会抛出异常或创建新集合。
示例一:批量操作前过滤非所属数据
场景说明
用户提交一批任务 ID 进行批量更新。系统只允许用户更新自己创建的任务,需要过滤掉不属于当前用户的任务。
操作前配置(不安全的批量处理)
@Service
public class TaskService {
@PreAuthorize("hasRole('USER')")
public void batchUpdate(List<Task> tasks) {
for (Task task : tasks) {
taskRepository.save(task); // 可以更新任何人的任务!
}
}
}
// 调用方
taskService.batchUpdate(Arrays.asList(
myTask, // 自己的任务
othersTask // 别人的任务 ❌ 也能更新!
));
问题:@PreAuthorize 只检查角色,无法逐个检查集合元素的所有权,导致越权修改。
操作后配置(使用 @PreFilter)
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class TaskSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("alice").password("{noop}alice").roles("USER")
.and()
.withUser("bob").password("{noop}bob").roles("USER");
}
}
public class Task {
private Long id;
private String title;
private String owner; // 任务所有者
// getters/setters
}
@Service
public class TaskService {
@Autowired
private TaskRepository taskRepository;
// 过滤掉不属于当前用户的任务,只保留自己创建的
@PreAuthorize("hasRole('USER')")
@PreFilter("filterObject.owner == authentication.name")
public void batchUpdate(List<Task> tasks) {
// 此时 tasks 已被过滤,只包含当前用户的任务
for (Task task : tasks) {
taskRepository.save(task);
}
System.out.println("实际更新了 " + tasks.size() + " 条任务");
}
}
结果分析
alice 调用 batchUpdate(Arrays.asList(aliceTask1, aliceTask2, bobTask1)):
| 元素 | owner | 表达式评估 | 结果 |
|---|---|---|---|
| aliceTask1 | "alice" | "alice" == "alice" | ✅ 保留 |
| aliceTask2 | "alice" | "alice" == "alice" | ✅ 保留 |
| bobTask1 | "bob" | "bob" == "alice" | ❌ 移除 |
- 进入
batchUpdate方法的tasks实际只有[aliceTask1, aliceTask2] bobTask1被静默过滤,不会报错也不会被更新
示例二:多集合参数时指定 filterTarget
场景说明
方法有多个集合参数,需要显式指定哪个参数应该被过滤。
操作前配置(错误:多集合参数未指定目标)
@Service
public class ProjectService {
// 错误:有多个集合参数,Spring 不知道过滤哪个
@PreFilter("filterObject.owner == authentication.name")
public void mergeTasks(List<Task> sourceTasks, List<Task> targetTasks) {
// 可能报错或行为不确定
}
}
操作后配置(使用 filterTarget 指定)
@Service
public class ProjectService {
// 明确指定过滤 sourceTasks 参数
@PreFilter(value = "filterObject.owner == authentication.name",
filterTarget = "sourceTasks")
public void mergeTasks(List<Task> sourceTasks, List<Task> targetTasks) {
// sourceTasks 已被过滤,只包含当前用户的任务
// targetTasks 完整保留,不做过滤
for (Task task : sourceTasks) {
targetTasks.add(task.clone());
}
}
}
结果分析
alice 调用 mergeTasks(sourceList, targetList):
sourceList包含[aliceTask, bobTask]→ 过滤后只剩[aliceTask]targetList包含[existingTask]→ 完整保留,不被过滤- 方法执行后
targetList增加aliceTask的副本
易错场景:不可变集合导致 UnsupportedOperationException
场景:调用方传入不可变集合(如 Arrays.asList() 在某些 JDK 版本中返回固定大小列表,或 Collections.unmodifiableList()),@PreFilter 尝试移除元素时抛出异常。
// 调用方
List<Task> tasks = Arrays.asList(task1, task2, task3); // 固定大小列表
taskService.batchUpdate(tasks); // 可能抛出异常
@Service
public class TaskService {
@PreFilter("filterObject.owner == authentication.name")
public void batchUpdate(List<Task> tasks) {
// 如果 tasks 是不可变集合,Spring Security 尝试 remove() 时抛出异常
}
}
问题根源:@PreFilter 默认尝试在原集合上调用 remove() 方法。如果集合不可变(如 Arrays.asList() 返回的列表或 Collections.unmodifiableList()),会抛出 UnsupportedOperationException。
正确做法:
@Service
public class TaskService {
// 方案一:方法内部复制为可变集合(绕过 @PreFilter,改用 @PreAuthorize + 手动过滤)
@PreAuthorize("hasRole('USER')")
public void safeBatchUpdate(List<Task> tasks) {
// 手动复制和过滤
List<Task> filtered = tasks.stream()
.filter(t -> t.getOwner().equals(
SecurityContextHolder.getContext().getAuthentication().getName()
))
.collect(Collectors.toList());
for (Task task : filtered) {
taskRepository.save(task);
}
}
// 方案二:调用方传入可变集合
// List<Task> tasks = new ArrayList<>(Arrays.asList(task1, task2, task3));
// taskService.batchUpdate(tasks);
}
面试考点:
问:
@PreFilter和@PostFilter对集合有什么要求?传入不可变集合会怎样? 答:@PreFilter和@PostFilter底层都通过迭代器remove()操作集合。如果传入的是不可变集合(如Arrays.asList()在某些 JDK 版本返回固定大小列表、Collections.unmodifiableList()、Java 9+ 的List.of()),调用remove()会抛出UnsupportedOperationException。解决方案:① 调用方传入ArrayList等可变集合;② 方法内部不使用@PreFilter,改为手动 Stream 过滤;③ 使用@PreAuthorize配合方法内部逻辑处理。生产环境中,如果集合来源不确定,应避免依赖@PreFilter的自动过滤,改用显式代码过滤更安全可控。
@PreFilter 速查表
| 特性 | 说明 |
|---|---|
| 执行时机 | 方法调用前,参数传入后 |
| 核心变量 | filterObject(集合元素) |
| 多集合参数 | 使用 filterTarget = "paramName" 指定 |
| 返回值 | 过滤后的集合(原地修改或替换) |
| 不可变集合 | 会抛出 UnsupportedOperationException |
| 空集合 | 不过滤,直接传入空集合 |
| 数组支持 | 支持,但过滤后数组大小不变(null 填充) |
与 @PreAuthorize 配合 | 通常同时使用,先角色检查再元素过滤 |