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。
相关推荐
Loo国昌4 小时前
【LangChain1.0】第二篇 快速上手实战
网络·人工智能·后端·算法·microsoft·语言模型
张彦峰ZYF4 小时前
Java+Python双语言开发AI工具全景分析与选型指南
java·人工智能·python
可儿·四系桜4 小时前
Kafka从入门到精通:分布式消息队列实战指南(Zookeeper 模式)
java·开发语言·zookeeper·kafka
小北方城市网4 小时前
SpringBoot 集成 Redis 实战(缓存优化与分布式锁):打造高可用缓存体系与并发控制
java·spring boot·redis·python·缓存·rabbitmq·java-rabbitmq
步步为营DotNet4 小时前
深度解析.NET 中Nullable<T>:灵活处理可能为空值的类型
java·前端·.net
努力d小白4 小时前
leetcode49.字母异位词分组
java·开发语言
爱笑的rabbit4 小时前
Linux和Windows的word模板导出转PDF下载保姆级教程,含PDF图片处理
java·spring
weixin_462446234 小时前
【实战】Java使用 Jsoup 将浏览器书签 HTML 转换为 JSON(支持多级目录)
java·html·json·书签
小北方城市网4 小时前
SpringBoot 集成 Elasticsearch 实战(全文检索与聚合分析):打造高效海量数据检索系统
java·redis·分布式·python·缓存
IMPYLH4 小时前
Lua 的 Table 模块
开发语言·笔记·后端·junit·游戏引擎·lua