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

    • 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
    • 最佳实践

PostAuthorize

一句话定义:方法执行后权限校验注解,在目标方法执行完毕后,通过 SpEL 表达式评估返回值(returnObject)的权限属性,决定是放行返回还是抛出 AccessDeniedException。


定义与作用

@PostAuthorize 与 @PreAuthorize 的最大区别在于执行时机和可访问的变量。@PostAuthorize 在方法执行后评估,因此能够访问方法的返回值 returnObject,适用于"先查询数据,再判断用户是否有权查看该数据"的场景。

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface PostAuthorize {
    String value();  // SpEL 表达式,可访问 returnObject
}

核心表达式变量:

变量类型含义
returnObject方法返回值类型方法的返回值,仅在 @PostAuthorize 中可用
authenticationAuthentication当前安全上下文
principalObject当前用户主体
#paramName方法参数通过参数名引用方法入参
@beanNameSpring Bean调用 Spring Bean 方法

核心原理

执行时机对比

⚠️ 关键警告:@PostAuthorize 在方法执行后拦截,如果权限校验失败,方法已产生的副作用(如数据库修改、日志记录、消息发送)无法自动回滚。因此 @PostAuthorize 应仅用于无副作用的查询方法,或配合事务管理处理。


示例一:基于返回对象所有权的数据过滤

场景说明

文档查询系统,用户调用 getDocument(id) 查询文档。由于文档可能属于他人,需要查询出结果后再判断当前用户是否有权查看。

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

@Service
public class DocumentService {

    @PreAuthorize("hasRole('USER')")  // 只能检查角色,无法检查文档归属
    public Document getDocument(Long id) {
        return documentRepository.findById(id).orElseThrow();
        // 返回了他人的文档!
    }
}

问题:任何已登录用户传入任意 ID 都能查看他人文档,存在数据越权漏洞。

操作后配置(使用 @PostAuthorize)

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class DocumentSecurityConfig 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");
    }
}

@Service
public class DocumentService {

    @Autowired
    private DocumentRepository documentRepository;

    // 查询文档后,检查所有者或 ADMIN 角色
    @PostAuthorize("returnObject.owner == authentication.name or hasRole('ADMIN')")
    public Document getDocument(Long id) {
        return documentRepository.findById(id).orElseThrow(
            () -> new EntityNotFoundException("Document not found: " + id)
        );
    }

    // 更复杂场景:检查文档是否在用户可访问的部门
    @PostAuthorize("returnObject.deptCode == @userService.getUserDept(authentication.name)")
    public Document getDeptDocument(Long id) {
        return documentRepository.findById(id).orElseThrow();
    }
}

@Component("userService")
public class UserService {
    @Autowired
    private UserRepository userRepository;

    public String getUserDept(String username) {
        return userRepository.findByUsername(username)
            .map(User::getDeptCode)
            .orElse("");
    }
}

结果分析

调用用户文档 ID文档 owner结果表达式评估
alice1alice✅ 返回文档returnObject.owner == "alice" = true
alice2bob❌ 403"alice" == "bob" = false, hasRole('ADMIN') = false
admin2bob✅ 返回文档hasRole('ADMIN') = true(短路)
bob1alice❌ 403"bob" == "alice" = false

示例二:@PostAuthorize 在修改方法中的危险使用与补救

场景说明

开发者希望在修改文档后校验返回的更新结果是否合规。但由于 @PostAuthorize 在方法执行后拦截,修改操作已生效。

操作前配置(危险:修改已生效后才拒绝)

@Service
public class DocumentService {

    @PostAuthorize("returnObject.owner == authentication.name")
    public Document updateDocument(Document document) {
        Document updated = documentRepository.save(document);  // 数据库已更新!
        return updated;
    }
}

问题分析:

  1. 恶意用户传入不属于他的文档 ID 和修改内容
  2. documentRepository.save() 执行,数据库数据被篡改
  3. @PostAuthorize 检查 returnObject.owner,发现不等于当前用户
  4. 抛出 AccessDeniedException,但数据库修改已提交无法撤销

操作后配置(正确做法:Pre + Post 组合,或纯 Pre)

@Service
public class DocumentService {

    // 正确方案一:修改类操作只用 @PreAuthorize,禁止用 @PostAuthorize
    @PreAuthorize("@documentSecurityService.isOwner(#document.id, authentication.name)")
    public Document updateDocument(Document document) {
        return documentRepository.save(document);
    }

    // 正确方案二:如果必须 Post 检查,配合 @Transactional 事务回滚
    @Transactional
    @PostAuthorize("returnObject.owner == authentication.name")
    public Document safeUpdateDocument(Document document) {
        Document updated = documentRepository.save(document);
        // 如果 @PostAuthorize 失败,Spring 事务回滚数据
        return updated;
    }
}

@Component("documentSecurityService")
public class DocumentSecurityService {
    @Autowired
    private DocumentRepository documentRepository;

    public boolean isOwner(Long documentId, String username) {
        return documentRepository.findById(documentId)
            .map(doc -> doc.getOwner().equals(username))
            .orElse(false);
    }
}

结果分析

方案安全性性能适用场景
纯 @PreAuthorize + Bean 方法✅ 高多一次查询所有修改操作
@Transactional + @PostAuthorize✅ 高(事务回滚)事务开销复杂场景需先执行后验证
纯 @PostAuthorize 无事务❌ 危险正常绝不用于修改操作

易错场景:@PostAuthorize 与 null 返回值

场景:方法返回 null 时,@PostAuthorize 的 returnObject 为 null,SpEL 表达式中访问其属性会导致 NullPointerException 或 SpelEvaluationException。

@Service
public class DocumentService {

    // 错误:未处理 null 返回值
    @PostAuthorize("returnObject.owner == authentication.name")
    public Document getDocument(Long id) {
        return documentRepository.findById(id).orElse(null);  // 可能返回 null
    }
}

问题:当 id 不存在时,returnObject 为 null,returnObject.owner 触发 NullPointerException,Spring Security 将其包装为 SpelEvaluationException,返回 500 而不是 403 或 404。

正确做法:

@Service
public class DocumentService {

    // 方案一:使用 ?. 安全导航操作符(SpEL 支持)
    @PostAuthorize("returnObject == null or returnObject.owner == authentication.name")
    public Document getDocument(Long id) {
        return documentRepository.findById(id).orElse(null);
    }

    // 方案二:使用 T() 类型判断
    @PostAuthorize("returnObject == null or returnObject.owner == authentication.name")
    public Document getDocumentSafe(Long id) {
        return documentRepository.findById(id).orElse(null);
    }

    // 方案三:在方法内部处理 null,@PostAuthorize 只保护非 null 场景
    @PostAuthorize("returnObject.owner == authentication.name")
    public Document getDocumentThrow(Long id) {
        return documentRepository.findById(id)
            .orElseThrow(() -> new EntityNotFoundException("Document not found"));
    }
}

面试考点:

问:@PostAuthorize 和 @PreAuthorize 的核心区别是什么?@PostAuthorize 可以用在修改方法上吗? 答:核心区别是执行时机和可访问变量:@PreAuthorize 在方法执行前拦截,只能访问方法参数;@PostAuthorize 在方法执行后拦截,能访问 returnObject 和参数。@PostAuthorize 原则上不推荐用于修改操作,因为权限校验失败时方法已执行,副作用(如数据库更新、消息发送)无法撤销。如果必须使用,必须配合 @Transactional 事务管理,让 Spring 在抛出 AccessDeniedException 时回滚事务。此外,使用 @PostAuthorize 时必须处理 returnObject 为 null 的情况,否则 SpEL 访问属性会抛出异常。


@PostAuthorize 适用场景速查

场景推荐注解原因
查询单条记录并校验所有权@PostAuthorize需先查出数据再看 owner
查询列表并过滤@PostFilter批量过滤返回集合
修改前校验所有权@PreAuthorize + Bean 方法阻止未授权执行
创建资源@PreAuthorize纯前置权限判断
删除资源@PreAuthorize纯前置权限判断
上一页
@PreAuthorize
下一页
@PreFilter