方法级别安全
一句话定义:通过 Spring AOP 拦截机制,在方法调用前后执行权限校验,实现比 URL 级别更细粒度的业务逻辑层访问控制。
定义与作用
URL 级别授权只能控制"某个路径谁能访问",但无法应对同一端点内部不同业务方法的差异化权限需求。方法级别安全(Method Security)通过在 Service 层或 Controller 层的方法上添加注解,由 Spring AOP 生成的代理拦截方法调用,在调用前后执行授权决策。
核心原理
启用方法安全的开关
Spring Security 5.x 使用 @EnableGlobalMethodSecurity 注解启用方法级别安全:
@Configuration
@EnableGlobalMethodSecurity(
prePostEnabled = true, // 启用 @PreAuthorize / @PostAuthorize / @PreFilter / @PostFilter
securedEnabled = true, // 启用 @Secured
jsr250Enabled = true // 启用 @RolesAllowed
)
public class MethodSecurityConfig { }
三个开关的含义:
| 开关 | 启用注解 | 依赖 | 特点 |
|---|---|---|---|
prePostEnabled = true | @PreAuthorize, @PostAuthorize, @PreFilter, @PostFilter | Spring Security 自带 | 支持 SpEL 表达式,最灵活 |
securedEnabled = true | @Secured | Spring Security 自带 | 简单角色检查,无 SpEL |
jsr250Enabled = true | @RolesAllowed, @PermitAll, @DenyAll | JSR-250 标准 | 跨框架兼容 |
方法安全拦截器链
示例一:三层架构中的方法安全配置
场景说明
一个订单管理系统,Controller 层负责接收请求,Service 层处理业务逻辑。URL 级别只要求登录,具体业务操作权限在 Service 层通过注解控制。
操作前配置(仅 URL 级别,不安全)
@Configuration
@EnableWebSecurity
public class UnsafeConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests(auth -> auth
.antMatchers("/api/orders/**").authenticated() // 只要登录就能调用所有订单 API
.anyRequest().permitAll()
)
.httpBasic(withDefaults());
}
}
@Service
public class OrderService {
// 没有任何权限控制,任何登录用户都能调用
public void createOrder(Order order) { ... }
public void deleteOrder(Long id) { ... } // 危险!
public void updateOrder(Order order) { ... }
public List<Order> getAllOrders() { ... } // 返回所有订单!
}
漏洞:普通登录用户可以调用 deleteOrder 删除任意订单,可以调用 getAllOrders 查看所有用户的订单。
操作后配置(URL + 方法级别联合防护)
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true) // 开启方法安全
public class SafeConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests(auth -> auth
.antMatchers("/api/orders/**").authenticated()
.anyRequest().permitAll()
)
.httpBasic(withDefaults());
}
}
@Service
public class OrderService {
@PreAuthorize("hasAnyRole('USER', 'ADMIN')")
public void createOrder(Order order) { ... }
@PreAuthorize("hasRole('ADMIN')")
public void deleteOrder(Long id) { ... }
@PreAuthorize("hasRole('ADMIN') or #order.owner == authentication.name")
public void updateOrder(Order order) { ... }
@PreAuthorize("hasRole('ADMIN')")
@PostFilter("filterObject.owner == authentication.name")
public List<Order> getAllOrders() { ... }
}
结果分析
| 方法 | 普通用户调用 | Admin 调用 | 安全效果 |
|---|---|---|---|
createOrder | ✅ 通过 | ✅ 通过 | 所有已登录用户可创建 |
deleteOrder | ❌ 403 | ✅ 通过 | 仅 Admin 可删除 |
updateOrder | ✅ 自己的订单 | ✅ 任何订单 | 普通用户只能改自己的 |
getAllOrders | ❌ 403 | ✅ 自己的订单 | Admin 也只能看到自己的(@PostFilter) |
示例二:@EnableGlobalMethodSecurity 开关对比
场景说明
对比三种开关配置方式,理解不同注解的适用场景。
操作前配置(仅启用 prePostEnabled)
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class PrePostConfig { }
@Service
public class DocumentService {
@PreAuthorize("hasRole('ADMIN')")
public void deleteDocument(Long id) { ... }
@Secured("ROLE_ADMIN") // ❌ 不生效!securedEnabled 未开启
public void archiveDocument(Long id) { ... }
}
操作后配置(同时启用三种)
@Configuration
@EnableGlobalMethodSecurity(
prePostEnabled = true,
securedEnabled = true,
jsr250Enabled = true
)
public class FullMethodSecurityConfig { }
@Service
public class DocumentService {
// 方式一:SpEL 表达式(最灵活)
@PreAuthorize("hasRole('ADMIN') or #id == 0")
public void deleteDocument(Long id) { ... }
// 方式二:简单角色(无 SpEL)
@Secured("ROLE_ADMIN")
public void archiveDocument(Long id) { ... }
// 方式三:JSR-250 标准(跨框架兼容)
@RolesAllowed("ADMIN")
public void publishDocument(Long id) { ... }
@PermitAll
public Document getDocument(Long id) { ... }
}
结果分析
| 注解 | 来源 | 支持 SpEL | 支持参数/返回值 | 适用场景 |
|---|---|---|---|---|
@PreAuthorize | Spring Security | ✅ | ✅ | 复杂权限表达式(推荐) |
@Secured | Spring Security | ❌ | ❌ | 简单角色检查,代码简洁 |
@RolesAllowed | JSR-250 | ❌ | ❌ | 需要跨框架/标准兼容 |
@PermitAll | JSR-250 | — | — | 显式允许所有人 |
@DenyAll | JSR-250 | — | — | 显式拒绝所有人 |
易错场景:方法安全注解不生效的五大原因
场景:在方法上添加了 @PreAuthorize("hasRole('ADMIN')"),但测试时发现任何用户都能调用方法,注解似乎被忽略了。
常见原因排查清单:
1. 未开启 @EnableGlobalMethodSecurity
// 错误:只有 @EnableWebSecurity,没有方法安全开关
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter { }
// 正确:必须显式开启
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter { }
2. 方法不是 Spring 管理的 Bean
// 错误:普通 POJO,不在 Spring 容器中
public class OrderService { // 缺少 @Service
@PreAuthorize("hasRole('ADMIN')")
public void deleteOrder(Long id) { ... }
}
// 正确:必须是 Spring Bean
@Service
public class OrderService { ... }
3. 同类内部调用绕过代理
@Service
public class OrderService {
@PreAuthorize("hasRole('ADMIN')")
public void deleteOrder(Long id) { ... }
public void batchDelete(List<Long> ids) {
for (Long id : ids) {
this.deleteOrder(id); // ❌ 内部调用不走 AOP 代理!
}
}
}
正确做法:
@Service
public class OrderService {
@Autowired
private ApplicationContext context;
@PreAuthorize("hasRole('ADMIN')")
public void deleteOrder(Long id) { ... }
public void batchDelete(List<Long> ids) {
OrderService proxy = context.getBean(OrderService.class);
for (Long id : ids) {
proxy.deleteOrder(id); // ✅ 通过代理调用,触发 AOP 拦截
}
}
}
4. 方法非 public
@Service
public class OrderService {
@PreAuthorize("hasRole('ADMIN')")
private void deleteOrder(Long id) { ... } // ❌ 非 public 方法无法被 Spring AOP 代理拦截
}
5. 使用了 JDK 动态代理但没有接口
// 类没有实现接口,Spring 使用 CGLIB 代理
@Service
public class OrderService { ... } // ✅ CGLIB 代理可以拦截类方法
// 如果有接口,Spring 默认使用 JDK 动态代理(基于接口)
@Service
public class OrderServiceImpl implements OrderService { ... }
// 此时注入时应使用接口类型:
@Autowired
private OrderService orderService; // ✅ 接口类型
@Autowired
private OrderServiceImpl orderService; // ❌ 可能注入原始对象而非代理
面试考点:
问:
@PreAuthorize注解不生效,可能有哪些原因? 答:① 未开启@EnableGlobalMethodSecurity(prePostEnabled = true);② 目标类不是 Spring Bean(缺少@Service等);③ 同类内部调用this.method()绕过 AOP 代理;④ 方法不是public;⑤ 注入了实现类而非接口(JDK 代理场景)。最隐蔽的是第 3 点——内部调用不经过代理,导致权限注解被完全跳过,这是生产环境常见的安全漏洞。