Spring Security @PreAuthorize 与自定义 @ss.hasPermission 权限控制

在基于 Spring Boot + Spring Security 的企业级项目中,我们经常在 Controller 层看到类似 @PreAuthorize("@ss.hasPermission('system:user:query')") 的注解。

这行代码虽短,却完美融合了 AOP 切面 、SpEL 表达式 以及 RBAC 模型。本文将为你拆解它的底层原理及使用技巧。

一、 核心概念拆解

这个注解由两部分组成:外层的 Spring Security 注解和内层的 SpEL 表达式。

1. @PreAuthorize(...)
  • 身份 :这是 Spring Security 提供的方法级安全注解。

  • 作用时机 :它会在方法被调用之前执行。

  • 功能:它接收一个布尔值(True/False)。

  • 如果表达式计算结果为 true,方法正常执行。

  • 如果为 false,框架抛出 AccessDeniedException(访问拒绝异常),前端通常收到 403 状态码。

  • 开启方式 :需在启动类或配置类上添加 @EnableGlobalMethodSecurity(prePostEnabled = true) (Spring Boot 2.x) 或 @EnableMethodSecurity (Spring Boot 3.x)。

2. @ss.hasPermission('...')

这是一段 SpEL (Spring Expression Language) 表达式,它的含义是:调用 Spring 容器中名为 ss 的 Bean,执行它的 hasPermission 方法。

  • @ss:

  • @ 符号在 SpEL 中表示引用 Bean。

  • ss 是 Bean 的名称(Alias)。在 Yudao 框架中,它通常指代 PermissionService 的实现类(Security 层面的封装)。

  • 注:为什么叫 ss?通常取自 S pring Security 的首字母缩写。

  • hasPermission:

  • 这是 ss Bean 中定义的一个 public boolean 方法。

  • 它负责具体的鉴权逻辑:获取当前登录用户 -> 读取用户拥有的权限列表 -> 判断是否包含传入的参数。

  • '...':

  • 传递给方法的字符串参数,即权限标识 (如 system:user:query),对应数据库 system_menu 表的 permission 字段。


二、 具体代码示例

以下展示了三种常见的用法场景:

场景 1:单权限控制(最常见)

只有拥有"查询用户"权限的管理员,才能调用分页接口。

java 复制代码
@GetMapping("/page")
// 翻译:喂,ss Bean,帮我查查这个人有没有 'system:user:query' 这把钥匙?
@PreAuthorize("@ss.hasPermission('system:user:query')")
public CommonResult<PageResult<UserRespVO>> getUserPage(@Valid UserPageReqVO reqVO) {
    return success(userService.getUserPage(reqVO));
}
场景 2:多权限控制(逻辑或 / 逻辑与)

SpEL 支持逻辑运算符 || (OR) 和 && (AND)。

示例 A:拥有"导出"或者"读取"权限均可访问

java 复制代码
@GetMapping("/export")
@PreAuthorize("@ss.hasPermission('system:user:export') || @ss.hasPermission('system:user:read')")
public void exportUser(...) { ... }

示例 B:必须同时拥有"创建"和"审批"权限才可访问

java 复制代码
@PostMapping("/create-high-level")
@PreAuthorize("@ss.hasPermission('system:user:create') && @ss.hasPermission('system:approval:pass')")
public void createHighLevelUser(...) { ... }
场景 3:自定义鉴权逻辑(调用其他方法)

既然 @ss 是一个 Bean,你完全可以在里面写其他方法。比如判断是否是超级管理员。

java 复制代码
// 假设 PermissionService 中写了一个 isSuperAdmin() 方法
@DeleteMapping("/delete-core")
@PreAuthorize("@ss.isSuperAdmin()") 
public void deleteCoreData() {
    // 只有超管能删核心数据
}

三、 底层实现原理(Backstage)

当你写下这行注解时,后端发生了什么?

  1. 定义 Bean (The Bean) :
    首先,必须有一个 Bean 被注册为 ss。
java 复制代码
// SecurityConfiguration.java 或者某个配置类
@Bean("ss") // 强行指定 Bean 名字为 ss
public PermissionService permissionService() {
    return new PermissionServiceImpl();
}
  1. 实现逻辑 (The Logic) :
    PermissionServiceImpl 实现了具体的校验。
java 复制代码
public class PermissionServiceImpl implements PermissionService {
    public boolean hasPermission(String permission) {
        // 1. 获取当前登录用户的 ID
        Long userId = SecurityFrameworkUtils.getLoginUserId();
        // 2. 如果是超管 (ID=1),直接放行
        if (userId == 1L) return true;
        // 3. 查缓存/DB,看该用户的权限集合里有没有这个字符串
        return permissionApi.hasAnyPermissions(userId, permission);
    }
}
  1. AOP 拦截 (The Interceptor) :
    当请求到达 Controller 方法前,Spring Security 的 AOP 拦截器截获请求,解析 SpEL,执行 ss.hasPermission()。
  • 返回 true -> 放行,进入 Controller。
  • 返回 false -> 拦截,抛出 403 Forbidden。
相关推荐
苍何6 小时前
开源微信流 Windows,微信聊天记录,可以直接给 Codex 和 Obsidan 了
后端
代码什么用8 小时前
Spring基础使用
java·后端·spring
代码什么用8 小时前
Spring对IoC的实现
数据库·spring
Su米苏8 小时前
Spring AI 中MCP 与普通 @Tool的区别
java·人工智能·spring
明月_清风8 小时前
Deno 终局来了:从挑战 Node 到被 Cloudflare 收编
前端·后端·node.js
码云数智-园园8 小时前
unique_ptr 还是 shared_ptr?C++ 智能指针选型与内存泄漏实战分析
java·开发语言
老板一杯拿铁8 小时前
Java 异常体系详解:从字节码原理到最佳实践
java
蜗牛互联网8 小时前
Java 17 HttpClient调用文件转写API的超时与失败回退
java·人工智能·后端
禾小西9 小时前
07丨Redis 哨兵机制:主库故障后,如何恢复服务?
java·开发语言·redis
我不是阵雨9 小时前
JDK 21虚拟线程Pinning陷阱:一文拔钉解困
java·开发语言