吃透 RuoYi-Vue 权限体系:彻底搞懂登录鉴权、功能权限、数据权限,一篇就够
文章目录
RuoYi-Vue 权限学习
前言
本文聚焦 RuoYi-Vue 框架的权限体系展开深度解析,通过系统化的梳理与实战演示,你将收获:
- 清晰掌握 RuoYi-Vue 基于 Spring Security + JWT 的登录认证授权全流程,理解身份校验、Token 生成与校验的核心逻辑;
- 精准理解功能权限(菜单、按钮、接口)与数据权限的核心概念,区分两类权限在系统中的定位与价值;
- 深入剖析 RuoYi-Vue 功能权限(菜单权限、按钮权限、接口权限)和数据权限的底层实现原理,包括动态路由、自定义指令、AOP 切面、动态 SQL 拼接等关键技术的应用;
- 熟练掌握 RuoYi-Vue 中功能权限(菜单、按钮、接口)和数据权限的配置方法,能够结合实际业务场景完成权限的自定义配置与落地,解决企业级系统中的权限管控需求。
RuoYi-Vue 权限总共分为三大类:
- 登录权限:控制能不能进入系统
- 功能权限:控制前端组件的可见/用性(其中可细分为:菜单权限、按钮权限、接口权限)
- 数据权限:控制后端数据的可见性
PS:严格来讲登录权限并不是真正的权限,它是 “身份认证”(Authentication)功能,不属于权限范畴,这里只是我个人为了统一方便学习,我把它当作权限体系的入口统一梳理,同时也是因为功能权限和数据权限高度依赖于登录(它们都需要到 SpringSecurity 上下文中拿权限数据)
| 核心实现方式 | 权限类型 | 控制内容 | 控制层面 | 核心依赖表 | 关键判断逻辑 | 数据流向示例 |
|---|---|---|---|---|---|---|
| Spring Security 认证 + Token | 登录权限 | 能不能进系统 | 登录入口 | sys_user、sys_role |
用户名密码校验、账号状态 status=0、验证码校验 | 登录页 → 校验账号密码 → 生成 JWT Token → 写入 Redis → 登录成功 |
动态路由 (Vue Router) + sys_menu 表 |
菜单权限 | 用户能看到左侧导航栏的哪些页面 | 前端菜单 | sys_menu sys_role_menu sys_user_role |
sys_menu.menu_type in (‘M’, ‘C’) |
用户ID → 查角色 → 查菜单ID → 过滤出类型为菜单的记录 → 生成左侧导航树 |
自定义指令 v-hasPermi |
按钮权限 | 用户在页面内能看到哪些操作按钮 | 前端按钮 | sys_menu sys_role_menu sys_user_role |
sys_menu.menu_type = ‘F’ 提取 perms 字段 |
用户ID → 查角色 → 查菜单ID → 过滤出类型为按钮的记录 → 提取 perms (如 user:add) → 前端 v-hasPermi |
Spring Security + @PreAuthorize / @RequiresPermissions |
接口权限 | 用户能否调用具体的API 接口 | 后端接口 | sys_menu sys_role_menu sys_user_role |
比对注解中的字符串与 sys_menu.perms |
请求到达 Controller → Spring Security 拦截 → 获取用户 perms 列表 → 比对 @PreAuthorize('xxx') 中的字符串是否存在 |
AOP + @DataScope + 动态 SQL |
数据权限 | 用户能看到查询结果中的哪些行数据 | 后端数据 | sys_role sys_role_dept sys_user sys_dept |
sys_role.data_scope 的值 |
拦截 Service 方法 → 读 data_scope → 若是"自定义",查 sys_role_dept 获取部门ID列表 → 拼接 SQL WHERE dept_id IN (...) |
登录权限
相关接口:
login后端相关代码:
com.ruoyi.web.controller.system.SysLoginController#login前端相关代码:
ruoyi-ui\src\views\login.vue
登录权限体系主要基于 Spring Security + JWT + Redis 构建,整个登录流程可以分为获取验证码和用户登录两个主要阶段。
PS:本文主要是讲解 RuoYi-Vue 的登录流程,并没有过于详细是介绍 SpringSecurity,RuoYi-Vue 底层核心是依赖于 SpringSecurity 实现登录认证授权的,关于 SpringSecurity 登录相关的详细学习请参考博主的这篇文章 初识SpringSecurity

第一阶段:获取验证码
- Step1:前端请求。用户打开登录页面时,前端通过
mounted方法触发getCode方法自动向后端发起一个captchaImage请求获取验证码 - Step2:后端响应。
- 请求首先会经过
JwtAuthenticationTokenFilter过滤器,由于是登录请求且没有 Token,过滤器会放行 - 请求会到达
CaptchaController,CaptchaController调用验证码生成器DefaultKaptcha来创建验证码图片和对应的答案 - 然后生成一个唯一的
uuid,并将uuid和验证码答案作为键值对存入Redis中,并设置一个较短的过期时间(如2分钟) - 最后将生成的验证码图片(转为 Base64 格式)和
uuid返回给前端。前端展示图片,并将uuid暂存,用于后续登录时校验
- 请求首先会经过
第二阶段:用户登录
-
Step1:前端请求。用户输入用户名、密码、验证码后,点击登录按钮,前端携带登录信息向后端服务器发送
login请求。 -
Step2:后端响应。
-
请求同样先被
JwtAuthenticationTokenFilter拦截,因为此时并没有登录所以会放行。 -
请求到达
SysLoginController,SysLoginController调用SysLoginService的login方法,将用户名、密码、验证码和uuid传递过去。 -
验证码校验:核心方法是
validateCaptchaa. 在
SysLoginService中,首先会根据前端传来的uuid从RedisCache中取出对应的验证码答案。校验取出的答案和用户输入的是否一致b. 校验完成后,会立即从
Redis中删除该验证码,确保其一次性有效。如果验证码为空或错误,则抛出异常,登录失败 -
登录参数校验:非空判断、非法参数判断、IP 黑名单判断
-
用户身份认证:核心方法是
authenticate
a. 验证码通过后,
SysLoginService会调用 Spring Security 的核心认证组件AuthenticationManagerb.
AuthenticationManager会委托UserDetailsServiceImpl去根据用户名从数据库(通过ISysUserService)查询用户信息c.
UserDetailsServiceImpl将查询到的用户信息封装成一个LoginUser对象(实现了UserDetails接口),其中包含了用户的基本信息、密码和权限列表d.
AuthenticationManager使用DaoAuthenticationProvider来比对LoginUser中的密码和用户提交的密码是否匹配。 -
生成令牌 (Token):核心方法是
createTokena. 认证成功后,
SysLoginService会调用TokenService来创建 JWT 令牌。b.
TokenService会生成一个唯一的 token,并将登录用户LoginUser的详细信息缓存到Redis中,key 为这个uuid,并设置一个较长的过期时间(如30分钟)。c. 然后,
TokenService使用 JWT 工具,将用户的部分信息(如 userId)和这个 token 一起生成为一个最终的 JWT 字符串。 -
登陆成功,则后端将这个 JWT 令牌返回给前端。前端会将其存储在
localStorage或Cookie中。
-
阶段三:后续请求与退出登录
-
Step1:前端在发起后续的业务请求时,会在请求头(Header)中携带这个 JWT 令牌(格式为
Bearer <token>) -
Step2:后端进行 Token 校验。
- 首先登陆后的请求,会被
JwtAuthenticationTokenFilter过滤器拦截,从请求头中获取 token,然后使用 JWT 工具解析,并从Redis中获取缓存的用户信息 - 如果 token 有效且未过期,则将用户信息存入
SecurityContextHolder,代表用户已登录,请求得以继续。
- 首先登陆后的请求,会被
-
退出登录:用户点击退出时,前端调用退出接口。后端的
LogoutFilter会拦截该请求,并从Redis中删除对应的 token 缓存,完成登出。
功能权限
功能权限表结构分析
功能权限(菜单权限、按钮权限、接口权限)核心都是依赖
sys_menu表中的perms字段进行鉴权
sys_user(用户表) 与sys_role(角色表)是多对多关系,两者的关系存储在sys_user_role(用户角色关联关系表) 中sys_role(角色表)与sys_menu(菜单表)是多对多关系,两者的关系存储在sys_role_menu(角色菜单关联关系表)中
PS:权限数据获取的流程图,并不是说从数据库中查询,而是表示数据源来自哪里,实际上有的数据是直接从SpringSecurity 上下文中拿的,并没有去查数据库表
菜单权限
相关接口:
getRouters前端相关代码:
ruoyi-ui/src/permission.js后端相关代码:
com.ruoyi.web.controller.system.SysLoginController#getRouters
在学习菜单权限之前,我们需要搞清楚menu_type字段,通过sys_menu 表我们可以发现菜单还有按钮的权限都是通过 perms 字段来存储的,其中 menu_type 字段用于区分类型,“M” 和 “C” 代表菜单,“F” 代表按钮
-
目录(M):只用来分组,没有实际页面,点击不能跳转
-
菜单(C):对应具体页面,点击能跳转,有对应的 Vue 组件
-
按钮(F):对应具体的按钮
菜单权限流程分析
菜单权限流程解析:
-
Step1:前端发送
getRouters请求首次登录或者页面刷新就会触发
ruoyi-ui/src/permission.js中前置路由守卫调用,向后端发送getRouters请求
-
Step2:后端响应数据
后端会根据当前登录用户的 id 到数据库中查出当前用户所拥有角色所有有的菜单,具体查询链路就是
- 从 Security 上下文中拿到当前用户的 userId
- 通过 userId 从 sys_user_role 表中找出当前用户所拥有的角色 roleIds
- 通过 roleIds 从 sys_role_menu 表中找出当前用户所有的可访问菜单 menuIds
- 通过 menuIds 从 sys_menu 表中找出所有的菜单信息
具体的 SQL 如下:
select distinct m.menu_id, m.parent_id, m.menu_name, m.path, m.component, m.`query`, m.route_name, m.visible, m.status, ifnull(m.perms,'') as perms, m.is_frame, m.is_cache, m.menu_type, m.icon, m.order_num, m.create_time from sys_menu m left join sys_role_menu rm on m.menu_id = rm.menu_id left join sys_user_role ur on rm.role_id = ur.role_id left join sys_role ro on ur.role_id = ro.role_id left join sys_user u on ur.user_id = u.user_id where u.user_id = #{userId} and m.menu_type in ('M', 'C') and m.status = 0 AND ro.status = 0 order by m.parent_id, m.order_num后端查出当前用户的菜单数据后当前是扁平化的,因为前端需要使用动态路由,所以还需要构造为子父级格式,方便前端处理,具体的处理逻辑参考
com.ruoyi.system.service.impl.SysMenuServiceImpl#getChildPerms -
Step3:前端渲染数据
前端在接收到后端返回的菜单数据后,直接将其添加为路由

菜单权限实战示例
这里演示新增一个菜单,并配置一个菜单权限
-
Step1:生成一个页面。
这里我们借助 Ruoyi-Vue 提供的代码生成功能,为
tb_book表生成一个页面-
建表
-- 图书表 CREATE TABLE `tb_book` ( `book_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '图书ID', `book_name` varchar(100) DEFAULT NULL COMMENT '书名', `author` varchar(50) DEFAULT NULL COMMENT '作者', `price` decimal(10,2) DEFAULT NULL COMMENT '价格', `publish_date` date DEFAULT NULL COMMENT '出版日期', `description` varchar(500) DEFAULT NULL COMMENT '简介', `create_by` varchar(64) DEFAULT '' COMMENT '创建者', `create_time` datetime DEFAULT NULL COMMENT '创建时间', `update_by` varchar(64) DEFAULT '' COMMENT '更新者', `update_time` datetime DEFAULT NULL COMMENT '更新时间', `del_flag` char(1) DEFAULT '0' COMMENT '删除标志(0代表存在 2代表删除)', PRIMARY KEY (`book_id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='图书信息表';
-
生成代码:点击生成代码按钮后,会下载一个下压缩包,这个压缩包就是生成的代码

-
执行SQL,放置代码:解压压缩包,发现里面不光有代码,还有SQL,这是因为我们前端是采用了动态路由,展示的菜单需要从后端获取,这个 SQL 的作用就是往
sys_menu添加数据,这样前端就能够把我们前端的菜单展示出来了
PS:其实这里也可以不执行SQL,选择手动到菜单管理哪里添加
-
-
Step2:配置菜单权限,其实这一步已经通过执行
bookMenu.sql完成了,这里我们就讲解一下如何手动配置菜单权限
这里介绍以下几个比较核心的配置项
- 路由地址:用于路由缓存、跳转定位,建议与组件名保持一致,避免冲突
- 组件路径:对应 Vue 路由的
path属性,同时这个组件路径system/user/index也对应具体页面的路径位置,比如src/views/system/user/index.vue,如果index.vue的位置发生了变化,组件路径需要同步修改,负责页面会无法正常展示 - 权限字符:菜单的权限标识符,目录菜单无需配置,页面菜单需要配置,通常配置为
system:user:list,用于控制页面可见性,list 接口的权限标识符通常也是system:user:list,不同它是用于控制数据的可见性,两者共用(都存储在sys_menu的perms字段中),但是作用不同
按钮权限
相关接口:
getInfo前端相关代码:
ruoyi-ui/src/directive/permission/hasPermi.js后端相关代码:
com.ruoyi.web.controller.system.SysLoginController#getInfo
按钮权限流程分析
按钮权限在 RuoYi-Vue 中主要通过自定义操作符 v-hasRole和v-hasPermi 来实现,这里以 v-hasPermi 为例,介绍按钮权限流程解析(v-hasRole鉴权逻辑是一样的):
-
Step1:前端发送
getInfo请求首次登录或者页面刷新就会触发
ruoyi-ui/src/permission.js中前置路由守卫调用,向后端发送getInfo请求
-
Step2:后端响应数据
后端会根据当前登录用户的 id 到数据库中查出当前用户所拥有角色所有有的权限操作符,具体查询链路就是
- 从 Security 上下文中拿到当前用户的 userId
- 通过 userId 从 sys_user_role 表中找出当前用户所拥有的角色 roleIds
- 通过 roleIds 从 sys_role_menu 表中找出当前用户所有的可访问菜单 menuIds
- 通过 menuIds 从 sys_menu 表中找出所有菜单的权限信息(也就是 perms 字段信息)
具体的代码逻辑请参考
com.ruoyi.web.controller.system.SysLoginController#getInfo -
Step3:前端渲染数据
前端拿到后端的数据后,会暂存在
store中,每次页面加载时,会触发自定义鉴权指令的函数,进而判断当前用户是否拥有该按钮的权限操作符,如果没有就不展示(物理上移除DOM),有就展示。具体的判断代码请参考
ruoyi-ui/src/directive/permission/hasPermi.js

按钮权限实战示例
这里演示新增一个按钮,然后演示如何设置按钮权限项
-
Step1:新增按钮
这里我们直接沿用菜单权限实战示例中生成的代码,那个 index.vue 里面已经有按钮了
-
Step2:配置权限
-
前端代码配置好
v-hasPermi指令
-
后端数据库添加按钮权限数据
同样的在
bookMenu.sql中已经包含了,这里我们介绍一下手动配置,我们来到菜单管理页,在我们新增的页面下新增一个按钮菜单,配置好权限字符即可,例如这里配置好了一个用户管理页下的用户查询按钮的权限
-
接口权限
前面已经讲了按钮权限,按钮权限通过前端控制使得按钮不可见,从而杜绝用户越权操作,这是对于一般用户而言,对于懂技术的用户,可能会直接越过前端,直接向后端发起接口请求,此时我们就需要配置接口权限,从这里我们也可以看出来接口权限是对按钮权限的补充
后端相关代码:
com.ruoyi.framework.web.service.PermissionService#hasPermi
接口权限流程分析
接口权限主要借助 SpringSecurity 的 @PreAuthorize 注解实现,核心原理其实是 AOP
以下是对接口权限流程的解析:
-
Step1:前端发送请求
-
Step2:后端接收请求,此时如果前端请求的接口添加了
@PreAuthorize注解,比如@PreAuthorize("@ss.hasPermi('system:user:list')")此时请求会被拦截鉴权,
ss是注入 IOC 的 Bean 的名字,在 RuoYi-Vue 中对应com.ruoyi.framework.web.service.PermissionService这个类,hasPermi是这个类的一个具体的鉴权方法,他会从 SpringSecurity 上下文中获取当前登录用的所有权限操作符,然后比较一下是否包含了“system:user:list”这个指定的权限操作符,如果包含了就鉴权成功,接口可以访问,然后就是走接口对应的 service 一系列方法并响应数据给前端,如果不包含说明鉴权失败,直接拒绝访问返回“500 + 提示 “权限不足”

接口权限实战示例
接口权限实际上就是按钮权限的一个补充,接口权限标识符与按钮权限标识符是共用的
-
Step1:配置接口权限数据
因为权限标识符和按钮权限是共用的,所以这里可以参考按钮权限数据的配置,两者都是在
sys_menu中添加一条数据,并补充perms字段 -
Step2:后端添加接口权限注解

数据权限
Ruo-Vue 中数据权限主要依赖于 AOP+SQL动态拼接 实现,核心注解是
@DataScope注解,它是基于 “用户-角色-部门” 模型实现,是 RBAC 模型的一种具体实现,是一种行级数据权限,控制用户只能看到权限范围内的数据行。数据权限按照数据范围进行分类:
- 行级数据权限:用于控制能看到那些行的数据,依赖于在 where 条件后动态拼 SQL 实现
- 列级数据权限:也叫字段级数据权限,用于控制能看到那些 列/字段 的数据,依赖于动态修改 select 后的字段(或序列化时过滤)实现
数据权限表结构分析
sys_user(用户表) 与sys_dept(部门表)是一对多的关系,一个用户只能拥有一个部门,一个部门下可以有多个用户,关系依赖于sys_user表中的dept_id字段维护sys_user(用户表) 与sys_role(角色表)是多对多关系,两者的关系存储在sys_user_role(用户角色关联关系表)中sys_role(角色表)与sys_dept(部门表)是多对多关系,两者的关系存储在sys_role_dept(角色部门关联关系表)中
数据权限注解参数介绍
在 RuoYi-Vue 中,数据权限核心注解是 @DataScope,他有三个参数
| 参数名 | 默认值 | 作用说明 | 生效场景 | 示例 |
|---|---|---|---|---|
| deptAlias | 空字符串 | SQL 中部门表的别名,用于拼接 dept_id 过滤条件 |
本部门、本部门及以下、自定义部门 | deptAlias = "d" |
| userAlias | 空字符串 | SQL 中用户表的别名,用于拼接 user_id 过滤条件 |
仅本人数据权限 | userAlias = "u" |
| permission | 空字符串 | 权限字符,多角色场景下指定生效的权限标识,多个用逗号分隔 | 用户拥有多个角色时,精准匹配该接口应使用的角色数据权限 | permission = "system:user:list" |
数据权限类型介绍
此外数据权限的类型,通过 sys_role 表的 data_scope 字段区分,总共5中类型
| 范围 | 标识 | 说明 | 生成的 SQL 片段示例 |
|---|---|---|---|
| 全部数据 | 1 | 无限制 | 空字符串 |
| 自定义部门 | 2 | 自选可访问部门 | AND d.dept_id IN (101,102) |
| 本部门 | 3 | 仅当前用户部门 | AND d.dept_id = 100 |
| 本部门及以下 | 4 | 当前部门 + 所有子部门 | AND d.dept_id IN (100,101,102) |
| 仅本人 | 5 | 仅自己创建的数据 | AND u.user_id = 1001 |
数据权限流程解析
数据权限流程解析:
-
Step1:前端发送请求
-
Step2:后端接收请求,此时如果前端请求的接口添加了
@DataScope注解,则会被切面类的前置通知 进行逻辑处理,接下来我们以data_scope为 3 的用户进行具体的流程分析- 拼接权限 SQL 前先清空 params.dataScope 参数防止注入
- 从 Security 的上下文中获取当前登录用户
- 获取当前用户所拥有的权限操作符(如果数据权限注解的
permission参数未指定) - 从当前登录用户中获取对应所有的角色
- 然后遍历所有的角色,根据
data_scope字段拼接SQL(以 OR d.dept_id = user.deptId 拼接) - 最后将第一个 OR 替换为 AND,比如 OR d.dept_id = 2 替换为 AND d.dept_id = 2
- 最终执行的 SQL,我们只需要在 where 条件之后拼接一个
${params.dataScope}
详情代码请参考
com.ruoyi.framework.aspectj.DataScopeAspect#doBefore下面是具体的流程图:
可能疑惑的点:
-
如果一个 SQL 既要给有数据权限的 Service 使用,又要给不需要数据权限的 Service 使用,拼接
${params.dataScope}的 SQL 能复用吗?SQL 能复用。从前面的流程分析过程来看,params.dataScope 只有当我们走 AOP 切面时,才会被设置,如果我们不走,该参数不会被设置,此时是空字符串,我们在执行的 SQL 后面拼接一个空字符串,这个 SQL 照样能够被正常执行
数据权限实战示例
现在我们基于 RuoYi-Vue 开发一个管理系统,有如下的需求:
- 同一部门的员工只能看自己和本部门的数据;
- 部门领导可以看本部门所有人的数据;
- 管理员可以看全部数据。
实现思路:
- 通过
@DataScope注解注入数据权限 SQL - 在 Mapper 层自动拼接
dept_id/user_id过滤条件
具体实现:
-
Step1:实体类
class User { Long userId; Long deptId; // ... } -
Step2:添加数据权限注解
一般都是加在 Service 接口上,也可以加载 Controller 或者 Mapper 接口上
@DataScope(deptAlias = "d", userAlias = "u") List<User> getUserList(); -
Step3:为用户配置数据权限
这个需要在后台管理页面中使用管理员账号进行配置,需要新增三个角色,部门领导、普通员工、管理员(普通员工和管理员角色已经有了,只需要新增部门领导角色即可)
-
新增角色

配置完后,数据库 sys_role 表里面会多一条记录
-
配置数据权限


配置完后,数据库 sys_role 表部门领导角色这条记录的 data_scope 字段会变为 4
-
分配用户,将配好了数据权限的角色分配给具体的用户即可


-
-
Step4:编写 SQL
只需要在 SQL where 条件上拼接上
${params.dataScope}即可<select id="selectUserList" parameterType="SysUser" resultMap="SysUserResult"> select u.user_id, u.dept_id, u.nick_name, u.user_name, u.email, u.avatar, u.phonenumber, u.sex, u.status, u.del_flag, u.login_ip, u.login_date, u.create_by, u.create_time, u.remark, d.dept_name, d.leader from sys_user u left join sys_dept d on u.dept_id = d.dept_id where u.del_flag = '0' <if test="userId != null and userId != 0"> AND u.user_id = #{userId} </if> <if test="userName != null and userName != ''"> AND u.user_name like concat('%', #{userName}, '%') </if> <if test="status != null and status != ''"> AND u.status = #{status} </if> <if test="phonenumber != null and phonenumber != ''"> AND u.phonenumber like concat('%', #{phonenumber}, '%') </if> <if test="params.beginTime != null and params.beginTime != ''"><!-- 开始时间检索 --> AND date_format(u.create_time,'%Y%m%d') >= date_format(#{params.beginTime},'%Y%m%d') </if> <if test="params.endTime != null and params.endTime != ''"><!-- 结束时间检索 --> AND date_format(u.create_time,'%Y%m%d') <= date_format(#{params.endTime},'%Y%m%d') </if> <if test="deptId != null and deptId != 0"> AND (u.dept_id = #{deptId} OR u.dept_id IN ( SELECT t.dept_id FROM sys_dept t WHERE find_in_set(#{deptId}, ancestors) )) </if> <!-- 数据范围过滤 --> ${params.dataScope} </select>
更多推荐




所有评论(0)