← 返回题目列表

Spring Method Security 如何实现?RBAC 应如何设计?

高频 中等 第 8 / 23 题 更新于 2026/07/25
Method SecurityRBACPreAuthorize

简化版

Method Security(方法级安全) 通过 AOP 代理在方法调用前后做权限判断,常用 @PreAuthorize(方法执行前判断,写 SpEL 表达式)保护服务层的业务方法——补足 URL 授权表达不了的细粒度规则。RBAC(基于角色的访问控制) 应把用户、角色、权限三者分开建模:用户拥有角色、角色拥有权限、权限对应资源动作,代码尽量判断「权限点」而非硬编码「角色名」。注意:Method Security 依赖代理,同类内部自调用会绕过拦截;且数据级权限(只能看自己的数据)不能只靠注解,要进业务查询条件

详细版

Method Security 用法:

@EnableMethodSecurity   // 开启方法级安全(Spring Security 6)
public class SecurityConfig { }

@Service
public class OrderService {
    @PreAuthorize("hasAuthority('order:refund')")   // 方法执行前判断
    public void refund(Long orderId) { ... }

    @PreAuthorize("hasRole('ADMIN') or #userId == authentication.principal.id")
    public User getUser(Long userId) { ... }        // 管理员 或 查自己

    @PreAuthorize("@auth.canReadOrder(#orderId)")   // 复杂逻辑封装成 Bean
    public Order getOrder(Long orderId) { ... }
}

常用注解:

注解时机说明
@PreAuthorize方法执行前最常用,SpEL 表达式
@PostAuthorize方法返回后可判断返回值,但业务已执行,慎用
@PreFilter/@PostFilter前/后过滤集合参数/返回值

RBAC 模型:

用户(User) ──多对多── 角色(Role) ──多对多── 权限(Permission)
                                              └─ 对应资源动作:order:refund

完整版教学

一、为什么需要方法级安全

URL 授权只能保护「入口路径」/admin/**),但一个业务方法可能被多个入口调用——多个 Controller、消息消费者、定时任务、其他服务方法都可能调 orderService.refund()

如果只在 URL 层做授权,就有漏洞风险:某个 Controller 忘了配 URL 规则、或者方法被非 Web 入口(消息、定时任务)调用,权限检查就绕过了。把关键权限放在服务层(方法级),就让业务能力本身具备保护——不管从哪个入口调用,方法执行前都会检查权限。这是纵深防御:URL 层是第一道、方法层是第二道。

授权位置保护对象优点典型盲区
URL 授权Web 入口路径粗粒度、统一入口控制非 Web 入口或漏配路径
方法授权服务层业务能力多入口复用时仍能兜底AOP 自调用、数据范围
数据授权具体记录和查询结果能防跨租户/跨用户必须进入查询条件或业务校验

方法级安全的心法是“保护业务能力本身”,而不是只保护某个 URL。

二、Method Security 的实现方式(AOP 代理)

Spring Method Security 基于 AOP 代理:Spring 为标注了安全注解的 Bean 生成代理,代理在方法调用前后拦截——读取注解和 SpEL 表达式,结合当前 Authentication 做权限决策,通过才放行调用。

  • @PreAuthorize方法执行前判断。不满足直接抛 AccessDeniedException(不执行方法)。最常用、最推荐
  • @PostAuthorize方法返回后判断(可以基于返回值判断,如「只能看自己创建的订单」)。但要谨慎——此时业务方法已经执行了(如果方法有副作用,即使最后拒绝,副作用已发生)。适合纯查询、无副作用的场景。

三、SpEL 表达式里能做什么

@PreAuthorize 的表达式是 SpEL,能力很强:

  • hasRole('ADMIN') / hasAuthority('order:refund'):判断角色/权限。
  • 访问方法参数#orderId#userId(配合 @PreAuthorize 引用参数)。
  • 访问当前用户authentication.principal
  • 组合逻辑hasRole('ADMIN') or #userId == authentication.principal.id(管理员或查自己)。
  • 调用自定义 Bean@auth.canReadOrder(#orderId)

建议:复杂权限逻辑封装成方法/Bean,如 @PreAuthorize("@authService.canRefund(#orderId)")——不要在注解里塞满业务细节(注解里写一长串 SpEL 难读、难测、难维护),把逻辑放进可测试的 Java 方法里。

Controller A ┐
Controller B ├─> OrderService.refund() -> @PreAuthorize("hasAuthority('order:refund')")
MQ Consumer  ┘

四、RBAC 的基本模型

RBAC(Role-Based Access Control) 的核心是用「角色」作为「用户」和「权限」之间的桥梁,三者分开:

用户 ── 拥有 ──> 角色 ── 拥有 ──> 权限(对应资源动作)
  • 用户不直接绑定大量权限,而是通过角色获得权限
  • 角色表达岗位/职责(管理员、财务、客服)。
  • 权限表达具体动作order:readorder:refund)。

好处:调整权限时只改「角色-权限」关系,不用改每个用户、也不用改代码。比如「财务角色也要退款权限」,只需给财务角色加 order:refund 权限,所有财务用户自动获得——不用逐个改用户、不用改代码。

五、角色和权限的命名与代码判断

  • 角色(如 ROLE_ADMIN)偏「身份集合」。
  • 权限(如 order:readorder:refund)偏「能力点」。

代码层应优先判断「权限点」而非「角色名」

// ❌ 硬编码角色:新增「财务」角色也要退款时,得改代码
@PreAuthorize("hasRole('ADMIN')")

// ✅ 判断权限点:谁有这个权限谁能退款,代码不用动
@PreAuthorize("hasAuthority('order:refund')")

原因:如果代码到处写 hasRole('ADMIN'),将来「财务角色也应该能退款」时,要改所有相关代码(把 ADMIN 改成 ADMIN or FINANCE)。而写 hasAuthority('order:refund'),只要把权限点分配给财务角色即可,代码零改动角色管「分给谁」,权限点管「能做什么」——代码依赖权限点更稳定灵活。

六、数据权限不是简单 RBAC(重点)

RBAC 解决「能不能做某类操作」,但解决不了「能对哪些数据做」。举例:

用户 A 和用户 B 都有 order:read 权限,但 A 只能看自己部门的订单,B 只能看自己创建的订单

这种数据级权限(数据范围)——按部门、租户、创建人、区域限制能访问的数据——不能只靠方法注解表达。因为它依赖具体数据的归属,必须进入业务查询条件

// 数据权限:查询时加上数据范围条件
List<Order> orders = orderMapper.selectByDept(currentUser.getDeptId());
// 或在 @PostAuthorize / 业务里校验:这条订单属不属于当前用户

只靠 @PreAuthorize 很难完整表达数据隔离——RBAC 管「操作权限」,数据权限要靠查询条件 + 业务校验(更复杂的还要引入 ABAC 基于属性的访问控制)。这是越权漏洞的重灾区。

七、代理边界和测试

  • 代理边界(易错):Method Security 依赖 AOP 代理,所以同一个类内部的 this.方法() 自调用会绕过代理——注解不生效!(和 Spring 事务 @Transactional 自调用失效同理)。如果一个方法内部调用了本类的另一个 @PreAuthorize 方法,那个安全检查不会触发。解决:拆到不同 Bean,或通过代理调用。
  • 测试要覆盖反向场景授权成功、权限不足(403)、跨用户访问、以及绕过 Web 入口直接调服务方法——确保方法级安全真的拦住了越权,而不只是测「有权限的能调」。

八、常见误区与追问

  • 误区:URL 授权已经配置了,方法级安全就没必要。 关键服务方法可能被多个入口调用,方法级安全能在业务能力层形成第二道防线。
  • 误区:@PostAuthorize@PreAuthorize 安全效果一样。 @PostAuthorize 在方法执行后判断,有副作用的方法已经执行,通常只适合查询类场景。
  • 误区:RBAC 可以完整解决数据权限。 RBAC 解决能不能做某类操作,不能自动解决能操作哪些租户、部门或用户的数据。
  • 追问:为什么同类内部自调用会让注解失效? Method Security 依赖 AOP 代理,this.xxx() 不经过代理,自然不会触发安全拦截。
  • 追问:为什么代码更推荐判断权限点? 权限点表达稳定业务能力,新增角色时只改配置关系,不改业务代码。
  • 追问:复杂 SpEL 应该怎么写更可维护? 把复杂判断封装到 Bean 方法里,例如 @auth.canReadOrder(#orderId),便于测试和复用。

九、加强记忆

Method SecurityAOP 代理在方法前后做权限判断,@PreAuthorize(执行前,SpEL 表达式)最常用@PostAuthorize 方法已执行慎用),保护服务层业务方法——补足 URL 授权(URL 只管入口,方法被多入口调用时靠方法级兜底,纵深防御)。复杂逻辑封装成 Bean@auth.canX()),别在注解塞业务细节。RBAC用户→角色→权限三层,用户通过角色获得权限,调权限只改「角色-权限」关系不改代码;代码判断「权限点(order:refund)」而非「角色名(ADMIN)」(新角色要该权限时代码零改动)。数据权限(只能看自己/本部门数据)不是简单 RBAC,要进业务查询条件 + 校验(越权重灾区)。坑:同类内部自调用绕过代理注解失效(同 @Transactional);测试覆盖权限不足/跨用户/绕过入口等反向场景。