“vue-element-admin项目中的页面权限是写死预设的,而很多公司的需求是每个页面的权限是动态配置的。如:你可以在后台通过一个 tree 控件或者其它展现形式给每一个页面动态配置权限,之后将这份路由表存储到后端。当用户登录后得到 roles,前端根据roles 去向后端请求可访问的路由表,从而动态生成可访问页面,之后就是 router.addRoutes 动态挂载到 router 上。多了一步将后端返回路由表和本地的组件映射到一起。就是说:将角色对应的路由表存在数据库中,前端通过用户角色去从数据库中取得路由表。”

参考:

环境部署 | 文档

若依前后端分离版,通俗易懂,快速上手 | bilibili

下载启动

Ui是前端部分,其他是后端部分,启动类在admin中。common,framework,generator,quartz,system服务于admin。

用idea或者vscode单独打开Ui文件夹。

配置数据库mysql,:创建数据库 ry并导入数据脚本 ry_2021xxxx.sqlquartz.sql

后端:启动类 com.ruoyi.RuoYiApplication.java

前端:按照readme运行命令

若依框架入门(前后端分离版本) | CSDN

权限管理

点击登陆后,浏览器还发送了两个请求,再看看这两个是什么东西,代码就是这样一步一步去看的。

如何找到这两个方法(前端):

  • getInfo,定义在store/modules/user.js中。

    任意页面跳转的时候都会去调用getInfo方法,这属于全局路由跳转,在全局(src文件夹)下permission.js里

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
//src/permission.js
router.beforeEach((to, from, next) => { //router.beforeEach:全局路由管理器。前端每一次页面跳转都会进入到这个方法中。
NProgress.start()
if (getToken()) {
to.meta.title && store.dispatch('settings/setTitle', to.meta.title)
/* has token*/
if (to.path === '/login') {
next({ path: '/' })
NProgress.done()
} else {
if (store.getters.roles.length === 0) {
isRelogin.show = true
// 判断当前用户是否已拉取完user_info信息
store.dispatch('GetInfo').then(() => {
isRelogin.show = false
store.dispatch('GenerateRoutes').then(accessRoutes => {
// 根据roles权限生成可访问的路由表
router.addRoutes(accessRoutes) // 动态添加可访问路由表
next({ ...to, replace: true }) // hack方法 确保addRoutes已完成
})
}).catch(err => {
store.dispatch('LogOut').then(() => {
Message.error(err)
next({ path: '/' })
})
//...
})
}
}
}
}

在后端中如何找到对应部分:右键根文件夹,Find in files,搜索”getInfo”。
img
进入到该方法,打个断点,刷新浏览器,看看是不是这个:
img

::*”(通配符?):所有权限
在数据库中,表Sys_user_role记录了user_id和role_id的关系。

  • generateRoutes,定义在store/permission.js,其中的getRouters来自文件夹api,请求路径/getRouters
    根据前端的请求路径,查看后端代码,追踪到其.mapper文件,可以看到SQL语句(控制台也会输出sql语句,加断点调试),复制,在navicat中执行,可以看到结果集
    img

    img

    查询到23条记录,为什么只显示4个:父节点4个。那如何实现这种层级嵌套,数据库层面如何设计这种级联关系:Sys_menu中,字段parent_id记录自己的父表的menu_id。逻辑是按照Parent_id的值从menu_id中寻找父menu。还有,如果找不到,比如parent_id为0,则此menu为一级menu。避免再用一个字段记录自己是哪一层级。

侧边栏

路由和侧边栏如何绑定起来(点击不同按钮,跳到不同页面):在数据库中设定。

前端的getRouters方法不仅获取到路由表,还有每个路由的component信息。

img

比如“用户管理”页面的组件是src/system/user/index,前端会运行index.vue,然后渲染到浏览器上。

菜单与操作权限

数据库中表sys_menu记录了菜单与权限的信息,菜单如用户管理、角色管理等,权限如用户查询、用户新增等。注意表中的属性列perms,用户拥有哪些权限用perms中的值表示,比如某一角色拥有 system:user:query,表示此角色具有用户查询权限。

比如项目中,当用户跳转到了用户管理页面,前端会向后端请求用户列表等数据,后端会先判断该用户是否有用户查询的权限,再去查询数据:

1
2
3
4
5
6
7
8
9
//SysUserController.java
@PreAuthorize("@ss.hasPermi('system:user:list')") //是否有访问这个list的权限,操作级权限
@GetMapping("/list")
public TableDataInfo list(SysUser user)
{
startPage(); //设置分页
List<SysUser> list = userService.selectUserList(user); //查询数据
return getDataTable(list); //组成table
}

数据权限

在业务中,用户必须绑定一个角色,而角色又必须将自身绑定到部门,角色绑定了哪些部门,就决定着隶属于该角色的用户能对哪些部门数据进行增删改。那么,怎么实现让用户只能遵循其绑定角色所指定的部门,来进行数据范围控制呢?一般情况下,假如我们对一张表要进行查询或更新的话,需要在sql 语句中,where 条件语法后面 加上 dept.id = {currentDeptId} 来进行过滤。在若依框架中,我们只需要在 Service 层的方法上加入 @DataScope注解,并分别通过deptAlias 和userAlias 属性,分别指出部门表和用户表在 sql语句中的别名是什么。利用此注解,就不需要去手动在 sql 语句后面过滤条件了。

数据权限在表sys_role,属性列data_scope中设置。项目目前支持以下几种权限:全部数据权限、自定数据权限、本部门数据权限、本部门及以下数据权限、仅本人数据权限,分别用字符串“1”,“2”,“3”,“4”,“5”表示。

比如项目中查询用户列表的方法 SysUserController::list,它会调用 SysUserServiceImpl::selectUserList

1
2
3
4
5
6
7
//SysUserServiceImpl::selectUserList
@Override
@DataScope(deptAlias = "d", userAlias = "u") //给表设置别名??
public List<SysUser> selectUserList(SysUser user)
{
return userMapper.selectUserList(user);
}

“在数据查询的方法上添加注解@DataScope。”如果某角色的data_scope的值为“3“(本部门数据权限),而某一方法涉及部门相关的数据,则就需要在该方法的注解上提供部门信息,比如@DataScope(deptAlias = “d”, userAlias = “u”),其中d对应最终执行的sql语句中的某个表。如selectUserList方法最终执行的sql语句为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<!--SysUserMapper.xml  -->
<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>

方法 selectUserList的注解 @DataScope(deptAlias = "d", userAlias = "u")中,d就是表sys_dept。注解@DataScope会生成一些sql语句,替换上面的${params.dataScope}(在已经获取数据的基础上再筛选出用户所在部门的数据)。

  • 实体类(parameterType的那个类,而不是resultMap)需要继承BaseEntity,因为生成的SQL语句会存放到BaseEntity对象中的params属性中,然后在xml中通过${params.dataScope}获取拼接后的语句。

@DataScope的逻辑实现代码在com.ruoyi.framework.aspectj.DataScopeAspect.dataScopeFilter。根据业务修改dataScopeFilter中的逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
//ruoyi.framework.aspectj.DataScopeAspect.dataScopeFilter.java
public static void dataScopeFilter(JoinPoint joinPoint, SysUser user, String deptAlias, String userAlias, String permission){
StringBuilder sqlString = new StringBuilder();
List<String> conditions = new ArrayList<String>();

for (SysRole role : user.getRoles())

{
String dataScope = role.getDataScope();
if (!DATA_SCOPE_CUSTOM.equals(dataScope) && conditions.contains(dataScope)) //一个人可以有多个角色,可能有很多个dataScope。只执行一次1,3,4,5,执行多次2。
//为什么执行多次2,而执行一次3:3是通过用户所在部门查,用户只有一个部门。2是通过角色获取部门,一个用户可以有多个角色,这些角色绑定的部门不同(一个角色可以绑定多个部门),所以应该遍历这些角色。”对于特殊的领导,可能需要跨部门的数据“
{
continue; //逻辑优先级:!、&&、||
}
if (StringUtils.isNotEmpty(permission) && StringUtils.isNotEmpty(role.getPermissions())
&& !StringUtils.containsAny(role.getPermissions(), Convert.toStrArray(permission)))
{
continue;
}

if (DATA_SCOPE_ALL.equals(dataScope))
{
sqlString = new StringBuilder();
break;
}
else if (DATA_SCOPE_CUSTOM.equals(dataScope)) //
{
sqlString.append(StringUtils.format(
" OR {}.dept_id IN ( SELECT dept_id FROM sys_role_dept WHERE role_id = {} ) ", deptAlias,
role.getRoleId()));
}
else if (DATA_SCOPE_DEPT.equals(dataScope))
{
sqlString.append(StringUtils.format(" OR {}.dept_id = {} ", deptAlias, user.getDeptId()));
}
else if (DATA_SCOPE_DEPT_AND_CHILD.equals(dataScope))
{
sqlString.append(StringUtils.format(
" OR {}.dept_id IN ( SELECT dept_id FROM sys_dept WHERE dept_id = {} or find_in_set( {} , ancestors ) )",
deptAlias, user.getDeptId(), user.getDeptId()));
}
else if (DATA_SCOPE_SELF.equals(dataScope))
{
if (StringUtils.isNotBlank(userAlias))
{
sqlString.append(StringUtils.format(" OR {}.user_id = {} ", userAlias, user.getUserId()));
}
else
{
// 数据权限为仅本人且没有userAlias别名不查询任何数据
sqlString.append(StringUtils.format(" OR {}.dept_id = 0 ", deptAlias));
}
}
conditions.add(dataScope);
}

if (StringUtils.isNotBlank(sqlString.toString()))
{
Object params = joinPoint.getArgs()[0];
if (StringUtils.isNotNull(params) && params instanceof BaseEntity)
{
BaseEntity baseEntity = (BaseEntity) params;
baseEntity.getParams().put(DATA_SCOPE, " AND (" + sqlString.substring(4) + ")");
}
}
}