方法级安全到底是“锦上添花”,还是你迟早要补的那道防盗门?
🏆本文收录于《滚雪球学SpringBoot 3》:
https://blog.csdn.net/weixin_43970743/category_12795608.html,专门攻坚指数提升,本年度国内最系统+最专业+最详细(永久更新)。
本专栏致力打造最硬核 SpringBoot3 从零基础到进阶系列学习内容,🚀均为全网独家首发,打造精品专栏,专栏持续更新中…欢迎大家订阅持续学习。 如果想快速定位学习,可以看这篇【SpringBoot3教程导航帖】https://blog.csdn.net/weixin_43970743/article/details/151115907,你想学习的都被收集在内,快速投入学习!!两不误。
若还想学习更多,可直接前往《滚雪球学SpringBoot(全版本合集)》:https://blog.csdn.net/weixin_43970743/category_11599389.html,涵盖SpringBoot所有版本教学文章。
演示环境说明:
- 开发工具:IDEA 2021.3
- JDK版本: JDK 17(推荐使用 JDK 17 或更高版本,因为 Spring Boot 3.x 系列要求 Java 17,Spring Boot 3.5.4 基于 Spring Framework 6.x 和 Jakarta EE 9,它们都要求至少 JDK 17。)
- Spring Boot版本:3.5.4(于25年7月24日发布)
- Maven版本:3.8.2 (或更高)
- Gradle:(如果使用 Gradle 构建工具的话):推荐使用 Gradle 7.5 或更高版本,确保与 JDK 17 兼容。
- 操作系统:Windows 11
前言
很多项目一开始只在 HttpSecurity 里拦 URL,感觉已经“很安全”了。可一旦业务逻辑复杂起来(同一个接口不同参数权限不同、返回值也要看归属、集合要过滤……),你就会发现:请求进来了不代表就该被允许“做这件事”。
方法级安全(Method Security)就是为这个场景准备的:在服务层/方法调用这个粒度上做授权,并且能把参数、返回值、集合元素都纳入判断。Spring Security 官方方法安全文档对 @EnableMethodSecurity 以及 @PreAuthorize/@PostAuthorize/@PreFilter/@PostFilter 的定位写得很明确。
1)开启方法安全:@EnableMethodSecurity(替代 @EnableGlobalMethodSecurity)
1.1 为什么要换?
Spring Security 6 之后推荐用 @EnableMethodSecurity,它取代了老的 @EnableGlobalMethodSecurity,并且方法安全整体转向更简化、更现代的配置与扩展方式(官方说明它是替代方案,并列出改进点)。
1.2 最小可用配置(Spring Boot 3 / Spring Security 6)
@Configuration
@EnableMethodSecurity // ✅ 开启方法级安全
public class SecurityConfig {
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
.authorizeHttpRequests(reg -> reg.anyRequest().authenticated())
.httpBasic(withDefaults())
.build();
}
}
小提醒:官方也提到 Spring Boot Starter Security 默认不会自动启用方法级授权,所以你得显式加
@EnableMethodSecurity。
2)@PreAuthorize 与 @PostAuthorize 实战(最常用的两把刀)
2.1 @PreAuthorize:方法执行前拦住(最常见)
典型场景:只有 ADMIN 才能删用户;或者“本人才能改自己的资料”。
@Service
public class UserService {
// 需要权限(authority)或角色(role)
@PreAuthorize("hasAuthority('user:delete')")
public void deleteUser(Long userId) {
// ...
}
// 只有本人 or 管理员可访问
@PreAuthorize("#userId == authentication.principal.id or hasRole('ADMIN')")
public UserProfile getProfile(Long userId) {
// ...
return profile;
}
}
几个你会反复用到的 SpEL 变量:
authentication:当前认证对象(用户名、权限等)principal:主体(常见是 UserDetails/JWT 解析后的对象)#参数名:方法参数(如#userId)hasRole('ADMIN') / hasAuthority('xxx'):内置表达式能力
这些注解就是官方提到的“你可以立即用它们授权方法调用(包含参数与返回值)”。
2.2 @PostAuthorize:方法执行后再校验返回值(很关键但常被忽略)
场景:你需要先查出来对象,才能判断“返回这个对象给你合不合适”。
@Service
public class CustomerService {
@PostAuthorize("returnObject.ownerUsername == authentication.name")
public Customer loadCustomer(String id) {
// 先查库
return repository.findById(id).orElseThrow();
}
}
说句人话:
@PostAuthorize像是“拿到结果后再验身份证”。适合“授权依赖返回值属性”的业务。官方同样把它列为方法安全的核心注解之一。
3)@PreFilter 与 @PostFilter:对集合数据自动过滤(让你少写一堆 for 循环)
很多时候你想要的不是“能不能调用这个方法”,而是——能调用,但只能看到你该看的那部分数据。
3.1 @PreFilter:过滤入参集合(进方法前先清洗)
场景:批量更新时,传入一堆 id,但你只允许改自己名下的。
@Service
public class OrderService {
@PreAuthorize("hasRole('USER')")
@PreFilter(filterTarget = "orderIds", value = "@authz.canAccessOrder(filterObject)")
public void batchUpdateStatus(List<Long> orderIds, String status) {
// 这里拿到的 orderIds 已经是过滤后的
}
}
filterObject:当前遍历的元素filterTarget:指定要过滤哪个参数(当方法参数不止一个集合时非常重要)
3.2 @PostFilter:过滤返回集合(出方法时再裁剪)
场景:方法返回一堆订单,但用户只能看到属于自己的那部分。
@Service
public class OrderQueryService {
@PreAuthorize("hasRole('USER')")
@PostFilter("@authz.canAccessOrder(filterObject.id)")
public List<Order> listOrders() {
return repository.findAll(); // 返回前会被过滤
}
}
这两类过滤注解同样在官方方法安全文档中被明确列为可直接使用的能力。
小吐槽:
@PostFilter很爽,但别滥用。因为它常意味着“先查出来再过滤”,如果数据量大可能不划算。更优做法往往是:把过滤条件下沉到查询层(SQL/Repository),方法级安全只负责兜底。
4)自定义 SpEL 表达式扩展权限校验逻辑(把权限逻辑写得像人话)
当 hasRole/hasAuthority 不够用了(比如“按资源归属判断”“多租户隔离”“复杂审批状态”),你就会想要这种效果:
@PreAuthorize("@authz.canEditPost(#postId)")
4.1 方案 A:暴露一个鉴权 Bean,直接在 SpEL 里调用(最实用)
先写一个组件:
@Component("authz")
public class Authz {
private final PostRepository repo;
public Authz(PostRepository repo) { this.repo = repo; }
public boolean canEditPost(Long postId) {
var auth = SecurityContextHolder.getContext().getAuthentication();
var username = auth.getName();
var post = repo.findById(postId).orElse(null);
if (post == null) return false;
return post.getOwnerUsername().equals(username) || auth.getAuthorities().stream()
.anyMatch(a -> a.getAuthority().equals("ROLE_ADMIN"));
}
}
然后在方法上直接用:
@PreAuthorize("@authz.canEditPost(#postId)")
public void editPost(Long postId, String content) { ... }
优点:简单、可测试、业务语义强。
缺点:你要注意性能(别在鉴权里做重查询)和循环调用(别在鉴权里再调用被保护的方法)。
4.2 方案 B:自定义权限表达式体系(PermissionEvaluator / ExpressionHandler)(更“框架化”)
如果你希望用更标准的写法,比如:
@PreAuthorize("hasPermission(#postId, 'Post', 'edit')")
通常会走 PermissionEvaluator / 自定义表达式处理链这条路。Baeldung 给了比较完整的实现思路与示例(工程上很常见)。
实战建议:
- 团队规模不大、需求不复杂:优先用 方案 A(@authz Bean),直观、落地快。
- 多资源类型、多权限动作、要统一 DSL:再上 方案 B(hasPermission 体系)。
你很可能会踩的 5 个坑(提前说,省你半夜抓头发🥲)
- 角色 vs 权限写混:
hasRole('ADMIN')实际会匹配ROLE_ADMIN前缀(团队得统一约定)。 - 参数名拿不到:
#userId依赖参数名可用性(编译参数/调试符号),否则得用#p0/#a0这种索引写法。 @PostAuthorize/@PostFilter性能:先查全量再过滤会很痛。- 在鉴权表达式里做“写操作”:鉴权应该是纯判断,别顺手改库(别给自己埋雷)。
- 误以为方法安全能替代 URL 安全:它是互补关系。URL 层挡一批,方法层兜底细粒度。
🧧福利赠与你🧧
无论你是计算机专业的学生,还是对编程有兴趣的小伙伴,都建议直接毫无顾忌的学习此专栏「滚雪球学SpringBoot」,bug菌郑重承诺,凡是学习此专栏的同学,均能获取到所需的知识和技能,全网最快速入门SpringBoot,就像滚雪球一样,越滚越大, 无边无际,指数级提升。
最后,如果这篇文章对你有所帮助,帮忙给作者来个一键三连,关注、点赞、收藏,您的支持就是我坚持写作最大的动力。
同时欢迎大家关注公众号:「猿圈奇妙屋」 ,以便学习更多同类型的技术文章,免费白嫖最新BAT互联网公司面试题、4000G PDF编程电子书、简历模板、技术文章Markdown文档等海量资料。
ps:本文涉及所有源代码,均已上传至Gitee:
https://gitee.com/bugjun01/SpringBoot-demo开源,供同学们一对一参考 Gitee传送门https://gitee.com/bugjun01/SpringBoot-demo,同时,原创开源不易,欢迎给个star🌟,想体验下被🌟的感jio,非常感谢❗
🫵 Who am I?
我是 bug菌:
- 热活跃于 CSDN:
https://blog.csdn.net/weixin_43970743| 掘金:https://juejin.cn/user/695333581765240| InfoQ:https://www.infoq.cn/profile/4F581734D60B28/publish| 51CTO:https://blog.51cto.com/u_15700751| 华为云:https://bbs.huaweicloud.com/community/usersnew/id_1582617489455371| 阿里云:https://developer.aliyun.com/profile/uolxikq5k3gke| 腾讯云:https://cloud.tencent.com/developer/user/10216480/articles等技术社区; - CSDN 博客之星 Top30、华为云多年度十佳博主&卓越贡献奖、掘金多年度人气作者 Top40;
- 掘金、InfoQ、51CTO 等平台签约及优质作者;
- 全网粉丝累计 30w+。
更多高质量技术内容及成长资料,可查看这个合集入口 👉 点击查看:https://bbs.csdn.net/topics/612438251 👈️
硬核技术公众号 「猿圈奇妙屋」https://bbs.csdn.net/topics/612438251 期待你的加入,一起进阶、一起打怪升级。
- End -
- 点赞
- 收藏
- 关注作者
评论(0)