乐途乐途
主页
  • 计算机基础

    • TCP/IP
    • Linux
    • HTTP
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C
    • C++
    • Java SE
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • Maven 入门
    • Maven 进阶
    • MyBatis
    • Spring
    • Spring MVC
  • Java

    • Spring Boot
    • Spring Cloud
    • Spring Cloud Alibaba
    • Spring Security
    • Spring AI
    • Spring Batch
    • Kafka
    • Java 设计模式
  • 缓存

    • Redis
  • 搜索引擎

    • Elasticsearch
  • 分布式协调

    • ZooKeeper
联系
阿里云
主页
  • 计算机基础

    • TCP/IP
    • Linux
    • HTTP
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C
    • C++
    • Java SE
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • Maven 入门
    • Maven 进阶
    • MyBatis
    • Spring
    • Spring MVC
  • Java

    • Spring Boot
    • Spring Cloud
    • Spring Cloud Alibaba
    • Spring Security
    • Spring AI
    • Spring Batch
    • Kafka
    • Java 设计模式
  • 缓存

    • Redis
  • 搜索引擎

    • Elasticsearch
  • 分布式协调

    • ZooKeeper
联系
阿里云
  • 学习路径
  • 第1章 Spring Security 基础

    • 本章定位
    • Spring Security 是什么
    • DelegatingFilterProxy
    • 安全过滤器链
    • SecurityFilterChain
    • 过滤器执行顺序
  • 第2章 认证

    • 本章定位
    • Authentication
    • 认证流程
    • AuthenticationManager
    • ProviderManager
    • DaoAuthenticationProvider
    • UserDetails
    • UserDetailsService
    • PasswordEncoder
    • BCryptPasswordEncoder
    • DelegatingPasswordEncoder
    • 表单登录
    • SecurityContext
    • SecurityContextHolder
    • UsernamePasswordAuthenticationToken
  • 第3章 授权

    • 本章定位
    • 授权模型
    • GrantedAuthority
    • AccessDecisionManager
    • AccessDecisionVoter
    • URL 级别授权
    • 方法级别安全
    • @PreAuthorize
    • @PostAuthorize
    • @PreFilter
    • @PostFilter
    • @Secured
    • RoleHierarchy
  • 第4章 过滤器链

    • 本章定位
    • FilterChainProxy
    • SecurityContextHolderFilter
    • LogoutFilter
    • BasicAuthenticationFilter
    • CsrfFilter
    • CorsFilter
    • HeaderWriterFilter
    • AnonymousAuthenticationFilter
    • RequestCacheAwareFilter
    • ExceptionTranslationFilter
    • FilterSecurityInterceptor
  • 第5章 会话管理

    • 本章定位
    • 会话管理
    • SessionFixation
    • 会话并发控制
    • SessionCreationPolicy
    • RememberMe
  • 第6章 JWT

    • 本章定位
    • JWT
    • JwtDecoder
    • JWT 认证
    • JwtAuthenticationConverter
  • 第7章 OAuth2

    • 本章定位
    • OAuth2 基础
    • OAuth2 Client
    • OAuth2 Resource Server
    • 第三方登录配置
  • 第8章 攻击防护

    • 本章定位
    • CSRF 跨站请求伪造防护
    • CORS 跨域防护
    • Clickjacking 点击劫持防护
    • 安全响应头
    • Session Fixation 会话固定防护
  • 第9章 测试

    • 本章定位
    • 安全测试
    • @WithMockUser
    • 最佳实践

PostFilter

一句话定义:方法执行后过滤返回值集合,使用 SpEL 表达式遍历方法返回的集合,只保留符合条件(表达式返回 true)的元素,将过滤后的集合返回给调用方。


定义与作用

@PostFilter 在方法执行完毕后,对返回的集合类型结果进行过滤。与 @PreFilter 不同,它作用于返回值而非参数。表达式中的 filterObject 代表返回集合中的单个元素,表达式返回 false 的元素会被移除,不会返回给调用方。

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface PostFilter {
    String value();  // SpEL 表达式,filterObject 代表返回集合元素
}

关键变量:

变量类型含义
filterObject集合元素类型当前遍历到的返回集合元素
returnObject完整返回值整个返回集合(也可通过 returnObject 访问)
authenticationAuthentication当前安全上下文
principalObject当前用户主体

核心原理

过滤执行流程

关键特性:@PostFilter 在方法执行后过滤,因此方法已经查询了全部数据,只是在返回给调用方前做了一次内存过滤。这意味着方法内部已经获取了所有数据,只是调用方看不到未授权的部分。


示例一:查询结果按用户过滤

场景说明

任务管理系统,调用 getAllTasks() 返回所有任务,但普通用户只能看到自己创建的任务,Admin 可以看到全部。

操作前配置(不安全的查询)

@Service
public class TaskService {

    @PreAuthorize("hasAnyRole('USER', 'ADMIN')")
    public List<Task> getAllTasks() {
        return taskRepository.findAll();  // 返回所有人的任务!
    }
}

问题:任何已登录用户都能看到所有任务,存在数据泄露。

操作后配置(使用 @PostFilter)

@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")
            .and()
            .withUser("admin").password("{noop}admin").roles("ADMIN");
    }
}

public class Task {
    private Long id;
    private String title;
    private String owner;  // 任务所有者
    // getters/setters
}

@Service
public class TaskService {

    @Autowired
    private TaskRepository taskRepository;

    // 返回所有任务,但只保留当前用户的任务(或 Admin 保留全部)
    @PreAuthorize("hasAnyRole('USER', 'ADMIN')")
    @PostFilter("hasRole('ADMIN') or filterObject.owner == authentication.name")
    public List<Task> getAllTasks() {
        return taskRepository.findAll();  // 查询全部,但返回前过滤
    }

    // 简化版:普通用户只看到自己的任务,Admin 看到全部
    @PreAuthorize("hasRole('USER')")
    @PostFilter("filterObject.owner == authentication.name")
    public List<Task> getMyTasks() {
        return taskRepository.findAll();
    }
}

结果分析

alice 调用 getAllTasks():

数据库返回owner表达式评估返回给 alice
Task1"alice""alice" == "alice"✅ 保留
Task2"bob""bob" == "alice"❌ 移除
Task3"alice""alice" == "alice"✅ 保留
Task4"bob""bob" == "alice"❌ 移除
  • alice 最终看到 [Task1, Task3]
  • admin 调用 getAllTasks():hasRole('ADMIN') 为 true,短路求值,所有任务保留

示例二:@PostFilter 与 @PostAuthorize 的区别

场景说明

对比两种"返回后校验"方式的差异。@PostAuthorize 是二元的(通过或拒绝整个方法),@PostFilter 是过滤的(允许部分元素通过)。

操作前配置(使用 @PostAuthorize,结果过于严格)

@Service
public class DocumentService {

    // 使用 @PostAuthorize:要求返回的文档必须属于当前用户
    @PostAuthorize("returnObject.owner == authentication.name")
    public Document getDocument(Long id) {
        return documentRepository.findById(id).orElse(null);
    }

    // 错误:@PostAuthorize 不适用于集合,因为 returnObject 是 List
    @PostAuthorize("???")  // 无法表达"只返回属于我的文档"
    public List<Document> getAllDocuments() {
        return documentRepository.findAll();
    }
}

问题:@PostAuthorize 只能判断单一返回值,无法处理集合过滤。当查询全部文档时,无法只返回部分文档。

操作后配置(@PostAuthorize 用于单条,@PostFilter 用于集合)

@Service
public class DocumentService {

    @Autowired
    private DocumentRepository documentRepository;

    // 单条查询:用 @PostAuthorize 判断所有权
    @PostAuthorize("returnObject == null or returnObject.owner == authentication.name")
    public Document getDocument(Long id) {
        return documentRepository.findById(id).orElse(null);
    }

    // 集合查询:用 @PostFilter 过滤返回结果
    @PreAuthorize("hasRole('USER')")
    @PostFilter("filterObject.owner == authentication.name")
    public List<Document> getMyDocuments() {
        return documentRepository.findAll();  // 查询全部,返回前过滤
    }

    // Admin 查询:不过滤,返回全部
    @PreAuthorize("hasRole('ADMIN')")
    public List<Document> getAllDocumentsAdmin() {
        return documentRepository.findAll();
    }

    // 组合场景:混合返回
    @PreAuthorize("hasAnyRole('USER', 'ADMIN')")
    @PostFilter("hasRole('ADMIN') or filterObject.owner == authentication.name or filterObject.isPublic")
    public List<Document> getVisibleDocuments() {
        return documentRepository.findAll();
    }
}

结果分析

方法注解用户行为
getDocument(1)@PostAuthorizealice如果文档 1 属于 alice 则返回,否则 403
getMyDocuments()@PostFilteralice返回 alice 的所有文档,不报错
getMyDocuments()@PostFilterbob返回 bob 的所有文档
getVisibleDocuments()@PostFilter匿名无法调用(@PreAuthorize 拦截)
getVisibleDocuments()@PostFilteralice返回自己的 + 公开的文档
getVisibleDocuments()@PostFilteradmin返回全部(短路)

易错场景:@PostFilter 的内存消耗与性能陷阱

场景:@PostFilter 配合 findAll() 查询,在数据量巨大时导致内存溢出或性能问题。

@Service
public class OrderService {

    @PreAuthorize("hasRole('USER')")
    @PostFilter("filterObject.owner == authentication.name")
    public List<Order> getAllOrders() {
        return orderRepository.findAll();  // 查询 100万 条订单!
    }
}

问题:

  1. orderRepository.findAll() 加载了全表数据到内存
  2. @PostFilter 只保留属于当前用户的几十条
  3. 浪费了 99.99% 的查询结果和内存
  4. 高并发时数据库和 JVM 压力巨大

正确做法:

@Service
public class OrderService {

    @Autowired
    private OrderRepository orderRepository;

    // 方案一:查询时直接过滤(最优,数据库层面过滤)
    @PreAuthorize("hasRole('USER')")
    public List<Order> getMyOrders() {
        String currentUser = SecurityContextHolder.getContext()
            .getAuthentication().getName();
        return orderRepository.findByOwner(currentUser);  // 数据库只返回相关数据
    }

    // 方案二:如果必须用 @PostFilter,配合分页避免全表加载
    @PreAuthorize("hasRole('USER')")
    @PostFilter("filterObject.owner == authentication.name")
    public List<Order> getRecentOrders(Pageable pageable) {
        return orderRepository.findRecentOrders(pageable);  // 只查最近 N 条
    }

    // 方案三:@PostFilter 仅用于小数据量、多维度权限的复杂场景
    @PreAuthorize("hasRole('ADMIN')")
    @PostFilter("filterObject.deptCode == authentication.principal.deptCode")
    public List<Report> getDeptReports() {
        return reportRepository.findAll();  // 仅用于管理后台,数据量可控
    }
}

面试考点:

问:@PostFilter 和 @PreAuthorize + 数据库查询过滤有什么区别?@PostFilter 有什么性能问题? 答:@PostFilter 是在方法执行后对内存中的集合进行过滤,方法内部已经获取了全部数据,只是在返回前做了一次内存裁剪。这种方式的问题在于:① 方法内部已经查询了全部数据,数据库 I/O 和内存占用没有节省;② 对于大数据量场景(如 findAll()),会加载海量数据到内存后过滤掉大部分,造成严重的性能浪费和 OOM 风险。最佳实践是:对于数据量大的场景,应在数据库查询层直接过滤(如 WHERE owner = ?),配合 @PreAuthorize 做角色检查;@PostFilter 只适用于数据量可控、权限规则复杂难以在 SQL 中表达的场景(如多维度组合权限)。


@PostFilter vs @PostAuthorize 速查

特性@PostAuthorize@PostFilter
执行时机方法执行后方法执行后
适用返回类型任意类型(单对象)集合/数组
核心变量returnObjectfilterObject, returnObject
失败行为抛出 AccessDeniedException移除元素,不抛异常
内存效率正常方法已获取全部数据,可能浪费
典型场景单条查询后校验所有权集合查询后过滤可见数据
上一页
@PreFilter
下一页
@Secured