方法级安全 @PreAuthorize 如何工作?RBAC 权限模型如何设计?
结论先行:方法安全 = AOP 在业务方法执行前/后插入授权检查。@PreAuthorize 用 SpEL 表达式按“当前登录用户能否调用”裁决,失败抛 AccessDeniedException。落地常用 RBAC:用户→角色→权限三级模型,角色把权限打包,用户只挂角色,授权表达式尽量对着权限(authority)而非角色硬编码。
一、开启与注解
@Configuration
@EnableMethodSecurity // Security 6.x;替代旧 @EnableGlobalMethodSecurity
public class MethodSecurityConfig {}
@PreAuthorize("hasRole('ADMIN')") // 角色:自动带 ROLE_ 前缀
@PreAuthorize("hasAuthority('order:update')") // 权限
@PreAuthorize("@permService.check(#order.userId)") // 自定义 Bean + 参数
public void updateOrder(@Param("order") Order order) { }
二、关键注解与语义
| 注解 | 时机 | 说明 |
|---|---|---|
| @PreAuthorize | 执行前 | 不满足直接拒绝,最常用 |
| @PostAuthorize | 执行后 | 可依据返回值再校验 |
| @PreFilter / @PostFilter | 前后 | 对集合参数/返回值过滤 |
| @Secured / @RolesAllowed | 执行前 | 旧式角色写法,语义较简单 |
- SpEL 可用对象:authentication、principal、参数名(需 -parameters 编译或 @Param);
- hasRole 与 hasAuthority:hasRole('ADMIN') 内部等价 hasAuthority('ROLE_ADMIN');
- 表达式可组合:hasAnyRole、hasAnyAuthority、!、&&、||。
三、RBAC 权限模型
用户(USER) ──多对多──> 角色(ROLE) ──多对多──> 权限(PERMISSION)
权限示例:order:create / order:update / order:delete(资源:操作)
设计要点:
- 接口不做死权限名,菜单/按钮由权限点驱动,改角色即生效;
- 角色支持层级(RoleHierarchy)时注意别把层级写进 JWT 等静态缓存;
- 授权应同时覆盖 URL 层(SecurityFilterChain 规则)与方法层(@PreAuthorize),纵深防御;
- 超级管理员用“角色含全部权限”表达,避免到处判断 isAdmin;
- 权限变更及时性:无状态 JWT 里嵌权限时需配合刷新或每次查库,避免“改权限不生效”事故。
常见追问 / 记忆点
- 追问:URL 鉴权与 @PreAuthorize 区别?答:前者过滤器链做粗粒度门禁,后者 AOP 做方法级细粒度,通常两者都配。
- 追问:为什么推荐按权限而非角色授权?答:角色是权限集合的别名,直接依赖角色会把权限变更耦合进代码。
- 记忆点:方法安全=AOP+SpEL;RBAC 三级“用户-角色-权限”,表达式用 authority 不硬编码角色名。