PostAuthorize
一句话定义:方法执行后权限校验注解,在目标方法执行完毕后,通过 SpEL 表达式评估返回值(
returnObject)的权限属性,决定是放行返回还是抛出AccessDeniedException。
定义与作用
@PostAuthorize 与 @PreAuthorize 的最大区别在于执行时机和可访问的变量。@PostAuthorize 在方法执行后评估,因此能够访问方法的返回值 returnObject,适用于"先查询数据,再判断用户是否有权查看该数据"的场景。
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface PostAuthorize {
String value(); // SpEL 表达式,可访问 returnObject
}
核心表达式变量:
| 变量 | 类型 | 含义 |
|---|---|---|
returnObject | 方法返回值类型 | 方法的返回值,仅在 @PostAuthorize 中可用 |
authentication | Authentication | 当前安全上下文 |
principal | Object | 当前用户主体 |
#paramName | 方法参数 | 通过参数名引用方法入参 |
@beanName | Spring 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 | 结果 | 表达式评估 |
|---|---|---|---|---|
| alice | 1 | alice | ✅ 返回文档 | returnObject.owner == "alice" = true |
| alice | 2 | bob | ❌ 403 | "alice" == "bob" = false, hasRole('ADMIN') = false |
| admin | 2 | bob | ✅ 返回文档 | hasRole('ADMIN') = true(短路) |
| bob | 1 | alice | ❌ 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;
}
}
问题分析:
- 恶意用户传入不属于他的文档 ID 和修改内容
documentRepository.save()执行,数据库数据被篡改@PostAuthorize检查returnObject.owner,发现不等于当前用户- 抛出
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 | 纯前置权限判断 |