Spring Security 的方法级安全怎么用?@PreAuthorize 的原理是什么?
- 标签:Spring Security、认证授权、后端
🏛 业务架构师
Spring Security 方法级安全与 @PreAuthorize 原理:
用法:启动类开启 @EnableMethodSecurity(Spring Security 5.6+ 替代旧的 @EnableGlobalMethodSecurity),然后在方法上声明:
java
@PreAuthorize("hasAuthority('order:delete')") // 角色权限
public void deleteOrder(Long id) { ... }
@PreAuthorize("#userId == authentication.name") // 参数级:只能操作自己(防水平越权)
public UserVO getUser(Long userId) { ... }原理(AOP 动态代理):
@EnableMethodSecurity注册AuthorizationManagerBeforeMethodInterceptor——一个基于 AspectJ 切点的拦截器;- 容器创建 Bean 时,对含注解的方法生成动态代理(接口用 JDK 代理,类用 CGLIB);
- 调用经过代理 → 拦截器先执行:解析 SpEL 表达式(
authentication、#参数名可直接引用),求值交给 AuthorizationManager 判定; - 通过 → 放行真实方法;不通过 → 抛 AccessDeniedException → 403;
- 配套注解:@PostAuthorize(返回后校验,如
returnObject.owner == authentication.name)、@PreFilter/@PostFilter(集合参数/返回值按表达式过滤)。
业务落地要点(架构视角):
- 粒度策略:URL 级(SecurityFilterChain)管入口粗粒度,方法级管业务动作细粒度——双层防御;注解写在 Service 层(事务边界内、业务语义所在),Controller 层可省(避免重复但 Controller 直出数据的架构除外);
- 防越权的实战写法:权限注解只防"能不能做这类事",水平越权要靠参数校验(
#orderId归属校验)或 @PostAuthorize——这是安全审计最常抓的漏洞点; - 注意内部调用失效:同类内 this 调用不走代理,注解不生效——跨方法授权校验要拆 Bean 或用 AopContext。
方法级安全是 RBAC 从"表结构"落到"代码防线"的最后一公里,注解覆盖率应该进安全验收清单。
🏗 系统架构师
方法级安全 = 在业务方法上声明授权规则,AOP 切面统一执行:
java
@PreAuthorize("hasRole('ADMIN') and #order.userId == authentication.principal.id")
public void cancelOrder(Order order) { ... }
@PostAuthorize("returnObject.owner == authentication.name") // 返回后校验
public Order getOrder(Long id) { ... }@PreAuthorize 的原理(执行机制):
- 启用:
@EnableMethodSecurity(Spring Security 6,代理机制默认 AspectJ 切面 + AdviceMode);加在配置类上,容器启动时为含注解的 bean 创建代理; - 切面拦截:方法调用进入
AuthorizationManagerBeforeMethodInterceptor——提取注解里的 SpEL 表达式; - 表达式求值上下文:SpEL 拿到三样东西——方法参数(
#order,参数名依赖-parameters编译选项)、返回值(PostAuthorize 的returnObject)、Authentication(authentication、principal、hasRole/hasAuthority/permitAll等模板函数由 SecurityExpressionRoot 提供); - 决策执行:表达式求值为 true 放行、false 抛
AccessDeniedException(403 由异常翻译层处理)。
架构师视角的要点:
- 它把授权从"URL 前缀"下沉到"业务语义"——
#order.userId == principal.id是数据级授权(这条订单归不归你),URL 层做不到;Service 层加注解是最常见位置(Controller 注解与 URL 规则语义重叠,选一处统一); - 方法内调用失效(AOP 自调用陷阱):同类内
this.methodB()绕过代理——@PreAuthorize不生效,经典安全漏洞源(把敏感方法暴露成内部可调用的公共方法); - 性能:每次调用求值 SpEL(可表达式缓存解析结果);高并发热点方法的复杂表达式要测量;
- 测试:
@WithMockUser集成测试验证规则正确性——授权规则是安全契约,必须进 CI。
💻 全栈开发专家
用法:@EnableMethodSecurity(6.0+,替代旧 @EnableGlobalMethodSecurity)开启后,注解直接标方法:
java
@Service
public class OrderService {
@PreAuthorize("hasRole('ADMIN') or hasAuthority('order:write')")
public void update(Order o) { ... }
@PreAuthorize("#userId == authentication.principal.id") // SpEL 引参数
public Order myOrders(Long userId) { ... }
@PostAuthorize("returnObject.owner == authentication.name") // 验返回值
public Order detail(Long id) { ... }
@PreAuthorize("hasRole('MGR')")
@PostAuthorize("!returnObject.vip") // 组合使用
public Report report() { ... }
}原理(AOP 代理拦截):
@EnableMethodSecurity注册AuthorizationInterceptorsAdvisor(Around advice,基于 AspectJ pointcut 匹配注解);- 调用经过代理对象 → 拦截器取方法上的注解 → 解析 SpEL 成
AuthorizationManager(6.x 新模型)→ 传入MethodInvocation + Authentication(从 SecurityContextHolder 取)求值; - true 放行原方法;false 抛
AccessDeniedException→ExceptionTranslationFilter/@RestControllerAdvice翻译成 403。
工程必知的三个坑:
- 同类内部调用失效:
this.update(o)不走代理——AOP 万年坑(解决:拆 bean / 自注入@Lazy/ AopContext.currentProxy()); - 注解只认代理可见的方法:final/private 方法无法拦截(CGLIB 限制);
- 元注解与 SpEL 性能:SpEL 每次解析有开销——6.x 已做表达式缓存,但高频接口可用简单的
hasAuthority代替复杂 SpEL。
经验:URL 层管粗筛(permitAll / authenticated),方法层管业务权限与数据归属(水平越权在 #param == principal.id 这一关拦)——两层各司其职,缺一不可。