前言

在企业级后台管理系统中,菜单权限控制是必不可少的一环。一个按钮谁能点、谁能看、谁能操作,都需要精确到接口级别地管控。若依(RuoYi)作为国内最流行的开源后台管理框架之一,提供了从数据库到前端组件的全链路权限解决方案。

本文以若依 RuoYi-Vue-v3.8.2 前后端分离版本的学生管理模块为例,围绕「菜单权限测试」这一功能按钮,带你从零到一理解整个权限闭环:前端按钮如何通过 v-hasPermi 指令控制显隐 → Axios 发起请求 → 后端 Controller 接收 → @PreAuthorize 注解权限校验 → 权限拦截/放行 → 数据返回 → 前端弹窗提示。每一张截图对应一个关键环节,逐段分析代码作用,区分权限核心代码与辅助无关代码。

一、整体流程概述

在开始写代码之前,先把整个权限闭环的流程理清楚。以下是完整链路中 9 个关键节点

① 前端页面 → 用户点击「菜单权限测试」按钮
② v-hasPermi 指令 → 按钮显隐判断(前端第一道权限门)
③ testPermi() API 方法 → Axios 构造 GET 请求
④ Axios 拦截器 → 自动携带 Token、封装响应
⑤ 后端 Controller → @PreAuthorize("@ss.hasPermi('system:student:test')")
⑥ Spring Security → 权限拦截 / 放行判定
⑦ 权限通过 → 执行接口逻辑,返回 AjaxResult
⑧ 权限拦截 → 抛出拒绝异常,前端收到 403/异常信息
⑨ 前端弹窗 → $modal.msgSuccess / msgError 反馈结果

下面我们按这个顺序,从 前端 → 后端 → 配置 → 效果展示 逐一展开。

二、前端代码分析

2.1 Vue 页面 —— 按钮 + v-hasPermi 指令

源码文件ruoyi-ui/src/views/system/student/index.vue

代码位置:第 32-35 行。

<!-- 菜单权限测试按钮 已正确放入 -->
<el-col :span="2">
  <el-button type="primary" plain icon="el-icon-check" size="mini" @click="handleTestPermi" v-hasPermi="['system:student:test']">菜单权限测试</el-button>
</el-col>

逐段分析

代码片段 作用 是否权限核心
<el-col :span="2"> 栅格布局,控制按钮占位宽度 ❌ 辅助布局代码
type="primary" plain 按钮样式(蓝色描边主按钮) ❌ UI 样式代码
icon="el-icon-check" 按钮图标 ❌ UI 代码
size="mini" 按钮尺寸 ❌ UI 代码
@click="handleTestPermi" 点击事件,触发接口调用 ✅ 权限闭环触发点
v-hasPermi="['system:student:test']" 权限指令,核心!控制按钮是否渲染 ✅✅ 权限核心

核心要点v-hasPermi 是若依前端权限体系的第一道关卡。它是一个 Vue 自定义指令(将在 2.4 节详细剖析),读取 Vuex Store 中当前用户的权限标识数组,只有当数组中包含 'system:student:test' 时,才保留该按钮的 DOM 节点;否则直接移除。

2.2 Vue 页面 —— 方法处理函数

// 菜单权限测试方法
handleTestPermi() {
  testPermi().then(res => {
    this.$modal.msgSuccess(res.msg);
  }).catch(err => {
    this.$modal.msgError("权限校验失败或接口异常");
  });
}

逐段分析

代码片段 作用 备注
testPermi() 调用 API 文件中导出的权限测试接口 由 import { ..., testPermi } 引入(第 84 行)
.then(res => { ... }) 成功后弹窗提示,返回 res.msg 内容 即后端返回的"菜单权限测试接口调用成功!权限校验通过"
.catch(err => { ... }) 失败时弹窗提示错误信息 包括无权限(403)和网络异常两种情况

2.3 前端 API 文件 —— Axios 请求封装

源码文件ruoyi-ui/src/api/system/student.js

// 菜单权限测试
export function testPermi() {
  return request({
    url: '/system/student/testPermi',
    method: 'get'
  })
}

逐段分析

代码片段 作用 备注
export function testPermi() ES6 模块导出,供 Vue 组件调用 函数名与后端接口名保持一致
request 封装的 Axios 实例,来源于 @/utils/request 全局配置在此文件中统一管理
url: '/system/student/testPermi' 请求路径,与后端 @RequestMapping 对应 格式:/模块/实体/接口名
method: 'get' HTTP 方法 此接口仅做权限校验,使用 GET 即可

注意:此文件中其他函数(listStudentgetStudentaddStudentupdateStudentdelStudent)均为 CRUD 常规操作,与权限验证无关,但权限校验注解同样绑定了各自的权限字符串。

2.4 v-hasPermi 自定义指令源码剖析

源码文件ruoyi-ui/src/directive/permission/hasPermi.js

import store from '@/store'

export default {
  inserted(el, binding, vnode) {
    const { value } = binding
    const all_permission = "*:*:*"
    const permissions = store.getters && store.getters.permissions

    if (value && value instanceof Array && value.length > 0) {
      const permissionFlag = value

      const hasPermissions = permissions.some(permission => {
        return all_permission === permission || permissionFlag.includes(permission)
      })

      if (!hasPermissions) {
        el.parentNode && el.parentNode.removeChild(el)
      }
    } else {
      throw new Error(`请设置操作权限标签值`)
    }
  }
}

执行原理

1.inserted 钩子:Vue 指令在元素插入 DOM 时触发,此时执行权限判断逻辑。

2.store.getters.permissions:从 Vuex Store 获取当前登录用户的所有权限标识(数组),这个数组由登录时后端返回并存储在前端全局状态中。

3.permissions.some(...):使用数组的 some 方法遍历,只要匹配到任意一个权限标识即返回 true

4.all_permission === permission:如果用户拥有 *:*:*(超级管理员全权限),则始终放行。

5.permissionFlag.includes(permission):用户权限数组中包含按钮所需权限时放行。

6.el.parentNode.removeChild(el):权限不满足时,物理移除 DOM 节点,按钮彻底不存在于页面中,而非简单的隐藏。

    三、后端代码分析

    3.1 Controller 层 —— 权限注解 + 接口定义

    源码文件ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/MyStudentController.java

    /**
     * 菜单权限测试
     */
    @PreAuthorize("@ss.hasPermi('system:student:test')")
    @GetMapping("/testPermi")
    public AjaxResult testPermi()
    {
        return AjaxResult.success("菜单权限测试接口调用成功!权限校验通过");
    }

    逐段分析

    代码片段 作用 权限核心度
    @PreAuthorize("@ss.hasPermi('system:student:test')") Spring Security 权限校验注解,核心! ✅✅ 权限拦截核心
    @GetMapping("/testPermi") 路由映射,定义接口访问路径 接口定义
    public AjaxResult testPermi() 接口方法,返回 JSON 统一格式 逻辑执行
    return AjaxResult.success(...) 构造成功响应体,前端 res.msg 即为此文本 数据返回

    核心要点@PreAuthorize 是 Spring Security 提供的方法级权限注解@ss 是若依框架注册的 Spring Bean(PermissionService),.hasPermi() 方法内部通过 SecurityUtils 获取当前登录用户的所有权限标识,与注解中传入的 'system:student:test' 进行匹配校验。匹配成功则方法放行,否则抛出 AccessDeniedException,前端收到异常并进入 .catch 分支弹窗报错。

    3.2 其他接口的权限注解对照表

    同一个 Controller 中,所有业务接口均绑定了各自的权限字符:

    接口方法 HTTP 方法 路径 权限字符
    list GET /list system:student:query
    getInfo GET /{id} system:student:query
    add POST / system:student:add
    edit PUT / system:student:edit
    remove DELETE /{ids} system:student:remove
    export POST /export system:student:export
    testPermi GET /testPermi system:student:test

    可以看到,若依的权限字符串采用 模块:实体:操作 的三级格式(如 system:student:test),清晰直观,便于管理员在角色管理页面进行配置。

    四、权限配置截图演示

    4.1 菜单管理 —— 新增权限测试按钮菜单项

    该截图展示的是在菜单管理中,将「权限测试」按钮项(类型为"按钮")添加到「学生管理」菜单树下:

    • 上级菜单:学生管理(system/student
    • 菜单类型:按钮
    • 菜单名称:权限测试
    • 权限字符system:student:test此字符串需与前端 v-hasPermi 和后端 @PreAuthorize 完全一致!
    • 菜单状态:正常

    ⚠️ 关键配置点:权限字符是连接前端、后端、配置的唯一纽带,三端必须保持完全一致才能正常工作。

    4.2 角色管理 —— 为角色分配权限

    该截图展示的是在角色管理中,为普通角色配置菜单权限的树形选择界面。关键点如下:

    • 角色名称:普通角色(编号 2,超级管理员为编号 1)
    • 功能选项:已勾选「父子联动」,保证树形选择的一致性。
    • 学生管理节点已展开,其下包含:
      • ✅ 学生查询
      • ✅ 学生新增
      • ✅ 学生修改
      • ✅ 学生删除
      • ✅ 学生导出
      • 权限测试 ← 红框标注的核心新增权限项,权限字符为 system:student:test

    配置流程:管理员进入「角色管理」→「普通角色」→「编辑」→ 勾选「权限测试」权限项 → 确定。配置保存后,普通角色的用户即可在前端看到「菜单权限测试」按钮,且调用接口时后端权限校验会通过。

    补充说明:「超级管理员」角色默认拥有 *:*:* 全权限,不需要单独配置任何菜单权限,所有按钮对其均可见且可用。

    五、权限拦截原理讲解

    5.1 整体拦截链路

    浏览器发起 GET /system/student/testPermi 请求
             ↓
    Spring Security Filter Chain(过滤器链)
             ↓
    进入 MyStudentController.testPermi()
             ↓
    执行 @PreAuthorize("@ss.hasPermi('system:student:test')")
             ↓
    调用 PermissionService.hasPermi("system:student:test")
             ↓
    获取当前登录用户的权限标识集合(从 SecurityUtils 获取)
             ↓
    权限集合中是否包含 "system:student:test" ?
             ↓
    ┌──────────────┐     ┌────────────────┐
    │    是 ✅     │     │    否 ❌       │
    │  放行执行     │     │ 抛出拒绝异常     │
    │  返回成功     │     │ 返回 403        │
    └──────────────┘     └────────────────┘
             ↓                    ↓
       前端 .then()           前端 .catch()
             ↓                    ↓
       弹窗绿色提示           弹窗红色提示

    5.2 前端权限与后端权限的关系

    层次 实现机制 作用
    前端显隐层 v-hasPermi 指令,移除无权限按钮 DOM 提升用户体验,减少无效请求
    后端拦截层 @PreAuthorize 注解,方法级校验 安全防线,防止绕过前端直接调用接口
    数据库权限层 sys_role_menu 关联表,角色-菜单绑定 数据持久化存储权限关系

    ⚠️ 重要提醒:前端权限按钮显隐只是用户体验优化,并不是安全防线。恶意用户可以通过 Postman、curl 等工具直接构造请求绕过前端。因此,后端 Controller 的 @PreAuthorize 才是真正的安全关卡,二者缺一不可。

    六、运行效果展示

    6.1 页面效果 —— 菜单权限测试按钮

    效果说明:用户以「普通角色」登录后,正确配置了 system:student:test 权限的账号可以看到并点击该按钮。若未配置该权限,按钮不会显示在页面上。

    6.2 接口请求 —— 浏览器 Network 面板

    关键信息

    • 请求路径GET /system/student/testPermi
    • 响应内容:json
      {
        "msg": "菜单权限测试接口调用成功!权限校验通过",
        "code": 200
      }

    6.3 弹窗提示 —— 权限通过

    该弹窗由前端 handleTestPermi 方法的 .then() 分支触发:

    this.$modal.msgSuccess(res.msg);  // res.msg === "菜单权限测试接口调用成功!权限校验通过"

    七、完整流程总结

    7.1 权限闭环全链路回顾

    步骤 环节 关键代码 / 操作 截图
    管理员在角色管理中勾选「权限测试」权限 角色管理 → 编辑 → 勾选 system:student:test 图6
    用户登录,后端返回权限标识集合 用户权限数据写入 Session / Token -
    前端 Vue 初始化,store.getters.permissions 填充 登录成功后权限数组存入 Vuex Store -
    页面渲染,v-hasPermi 指令执行权限判断 v-hasPermi="['system:student:test']" 图1 / 图3
    按钮可见,用户点击「菜单权限测试」 @click="handleTestPermi" 图8
    Axios 发起 GET 请求 testPermi() → request({ url: '/system/student/testPermi' }) 图2
    后端 Controller 接收请求 @GetMapping("/testPermi") 图1
    @PreAuthorize 执行权限校验 @PreAuthorize("@ss.hasPermi('system:student:test')") 图1
    权限通过,执行接口方法 return AjaxResult.success(...) -
    前端 .then() 收到响应 $modal.msgSuccess(res.msg) 图9
    弹窗显示绿色成功提示 "菜单权限测试接口调用成功!权限校验通过" 图9

    7.2 权限字符串三端一致对照

    配置位置 权限字符串 截图
    后端 Controller 注解 system:student:test 图1
    前端 v-hasPermi 指令 ['system:student:test'] 图3
    前端 API 文件 /system/student/testPermi(路径不含权限字符) 图2
    数据库菜单表 system:student:test 图7
    角色权限分配 勾选「权限测试」项 图6

    三端一致是权限系统正常工作的铁律,任一处不匹配都会导致权限失效。

    7.3 开发者日常开发 Checklist

    ✅ 新增按钮 → 在 Vue 模板中添加 v-hasPermi="['模块:实体:操作']"
    ✅ 新增接口 → 在 Controller 方法上添加 @PreAuthorize("@ss.hasPermi('模块:实体:操作')")
    ✅ 新增 API 方法 → 在 api/xx.js 中导出对应的 Axios 请求函数
    ✅ 配置菜单 → 在菜单管理中新增"按钮"类型菜单项,填入权限字符
    ✅ 分配权限 → 在角色管理中为相应角色勾选该菜单权限
    ✅ 测试验证 → 以普通用户登录,点击按钮,确认弹窗提示

    结语

    本文通过若依 RuoYi-Vue-v3.8.2 学生管理模块的「菜单权限测试」功能,完整串联了前端按钮 → v-hasPermi 指令 → API 方法 → Axios 请求 → 后端 Controller → @PreAuthorize 注解 → 权限校验 → 响应返回 → 前端弹窗提示的全链路闭环。

    核心要点归纳为三点:

    1.前端 v-hasPermi 指令是按钮显隐的第一道关卡,提升用户体验,但不能作为安全防线

    2.后端 @PreAuthorize 注解是真正的权限安全防线,即使绕过前端直接请求接口,后端也会拦截。

    3.权限字符串三端一致(后端注解 / 前端指令 / 数据库菜单)是整个权限系统的灵魂,务必保持完全一致。

      若依框架将这套权限体系封装得非常完善,开发者在代码生成器生成的模板基础上,仅需关注业务逻辑实现,权限配置交由管理员在运维阶段通过可视化界面完成。

      更多推荐