松哥最近正在录制 TienChin 项目视频~采用 Spring Boot Vue3 技术栈,里边会涉及到各种好玩的技术,小伙伴们来和松哥一起做一个完成率超 90% 的项目,戳戳戳这里-->TienChin 项目配套视频来啦。
- 1. 权限颗粒度
- 2. 权限表
- 3. 后端权限判断
因为写了不少 Spring Security 文章的缘故,所以总是有小伙伴来问松哥:按钮级别的权限怎么实现?甚至有一些看过 vhr 的小伙伴也问这种问题,其实有的时候搞得我确实挺郁闷的,最近刚好要做 TienChin 项目,我就再把这个问题拎出来和小伙伴们仔细捋一捋。
1. 权限颗粒度
首先小伙伴们都知道权限有不同的颗粒度,在 vhr 项目中,整体上我是基于请求地址去处理权限的,这个粒度算粗还是算细呢?
有的小伙伴们可能认为这个权限粒度太粗,所谓细粒度的权限应该是基于按钮的。
如果有小伙伴们做过前后端不分的开发,应该会有这样的体会:在 Shiro 或者 Spring Security 框架中,都提供了一些标签,通过这些标签可以做到在满足某种角色或者权限的情况下,显示某个按钮;当用户不具备某种角色或者权限的时候,按钮则会自动隐藏起来。
但是大家想想,按钮的显示与隐藏不过是前端页面为了提高用户体验而作出的样式的变化而已,本质上,当你点击一个按钮的时候,还是发送了一个 HTTP 请求,那么服务端处理该请求的接口,必须要进行权限控制。既然要在接口上进行权限控制,那么跟 vhr 的区别在哪里呢?
现在流行前后端分离开发,所以 Shiro 或者 Spring Security 中的那些前端标签现在基本上都不用了,取而代之的做法是用户在登录成功之后,向服务端发送请求,获取当前登录用户的权限以及角色信息,然后根据这些权限、角色等信息,在前端自动的去判断一个菜单或者按钮应该是显示还是隐藏,这么做的目的是为了提高用户体验,避免用户点击一个没有权限的按钮。前端的显示或者隐藏仅仅只是为了提高用户体验,真正的权限控制还是要后端来做。
后端可以在接口或者业务层对权限进行处理,具体在哪里做,就要看各自的项目了。
所以,vhr 中的权限,从设计上来说,粒度并不算粗,也是细粒度的,只不过跟菜单表放在了一起,小伙伴们可能感觉有点粗。但是,菜单表是可以继续细化的,我们可以继续在菜单表中添加新的记录,新记录的 hidden 字段为 true,则菜单是隐藏的,就单纯只是细化权限而已。
如下图可以继续添加新的访问规则,只不过把 enabled 字段设置为 false 即可(这样菜单就不会显示出来了,单纯就只是权限的配置)。
所以 vhr 的权限设计是 OK 的。
当你理解了 vhr 中的权限设计,再来看 TienChin 这个项目,或者说看 RuoYi-Vue 这个脚手架,就会发现非常 easy 了。
2. 权限表
首先我们来看看资源表的定义,也就是 sys_menu
。
CREATE TABLE `sys_menu` (
`menu_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '菜单ID',
`menu_name` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '菜单名称',
`parent_id` bigint(20) DEFAULT '0' COMMENT '父菜单ID',
`order_num` int(4) DEFAULT '0' COMMENT '显示顺序',
`path` varchar(200) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '路由地址',
`component` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '组件路径',
`query` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '路由参数',
`is_frame` int(1) DEFAULT '1' COMMENT '是否为外链(0是 1否)',
`is_cache` int(1) DEFAULT '0' COMMENT '是否缓存(0缓存 1不缓存)',
`menu_type` char(1) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '菜单类型(M目录 C菜单 F按钮)',
`visible` char(1) COLLATE utf8mb4_unicode_ci DEFAULT '0' COMMENT '菜单状态(0显示 1隐藏)',
`status` char(1) COLLATE utf8mb4_unicode_ci DEFAULT '0' COMMENT '菜单状态(0正常 1停用)',
`perms` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '权限标识',
`icon` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT '#' COMMENT '菜单图标',
`create_by` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '创建者',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_by` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '更新者',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`remark` varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '备注',
PRIMARY KEY (`menu_id`)
) ENGINE=InnoDB AUTO_INCREMENT=3054 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='菜单权限表';
其实这里很多字段都和我们 vhr 项目项目很相似,我也就不重复啰嗦了,我这里主要和小伙伴们说一个字段,那就是 menu_type
。
menu_type
表示一个菜单字段的类型,一个菜单有三种类型,分别是目录(M)、菜单(C)以及按钮(F)。这里所说的目录,相当于我们在 vhr 中所说的一级菜单,菜单相当于我们在 vhr 中所说的二级菜单。
当用户从前端登录成功后,要去动态加载的菜单的时候,就查询 M 和 C 类型的数据即可,F 类型的数据不是菜单项,查询的时候直接过滤掉即可,通过 menu_type
这个字段可以轻松的过滤掉 F 类型的数据。小伙伴们想想,F 类型的数据过滤掉之后,剩下的数据不就是一级菜单和二级菜单了,那不就和 vhr 又一样了么!
最后再来说说 F 类型的,F 类型的就是按钮级别的权限了,前端每一个按钮的执行,需要哪些权限,现在就在这里定义好。
举一个简单的例子大家来看下:
当需要展示用户管理这个菜单的时候,需要 system:user:list
这个权限,当需要点击用户修改这个按钮的时候,则需要 system:user:edit
这个权限。
其他相关的表基本上和 vhr 都是一样的,用户有用户表 sys_user
,角色有角色表 sys_role
,用户和角色关联的表是 sys_user_role
,资源和角色关联的表是 sys_role_menu
。
当用户登录成功后,后端会提供一个接口,将当前用户的角色和权限统统返回给前端:
- 查询角色思路:根据用户 id,先去
sys_user_role
表中查询到角色 id,再根据角色 id 去sys_role
表中查询到对应的角色(这里为了方便大家理解这么描述,实际上一个多表联合查询即可)。 - 查询权限思路:根据用户 id,先去
sys_user_role
表中查询到角色 id,再根据角色 id 去sys_role
表中查询到对应的角色,再拿着角色 id 去sys_role_menu
表中查询到对应的menu_id
,再根据menu_id
去sys_menu
表中查询到对应的 menu 中的权限(这里为了方便大家理解这么描述,实际上一个多表联合查询即可)。
前端有了用户的权限以及角色之后,就可以自行决定是否显示某一个菜单或者是否展示某一个按钮了。
3. 后端权限判断
我先来说说这块 TienChin 项目中是怎么做的(即 RuoYi 脚手架的实现方案),再来和 vhr 进行一个对比。
在 TienChin 项目中是通过注解来控制权限的,接口的访问权限都是通过注解来标记的,例如下面这种:
代码语言:javascript复制@PreAuthorize("@ss.hasPermi('system:menu:add')")
@PostMapping
public AjaxResult add(@Validated @RequestBody SysMenu menu) {
//省略
}
/**
* 修改菜单
*/
@PreAuthorize("@ss.hasPermi('system:menu:edit')")
@PutMapping
public AjaxResult edit(@Validated @RequestBody SysMenu menu) {
//省略
}
/**
* 删除菜单
*/
@PreAuthorize("@ss.hasPermi('system:menu:remove')")
@DeleteMapping("/{menuId}")
public AjaxResult remove(@PathVariable("menuId") Long menuId) {
//省略
}
每一个接口需要什么权限,都是通过 @PreAuthorize
注解来实现的,关于这个注解的使用原理,松哥之前也有两篇文章:
- 想要控制好权限,这八个注解你必须知道!
- Spring Security 中的权限注解很神奇吗?
看懂了这两篇文章,上面这个注解就懂了,我这里不赘述。
不过上面这种写法说到底还是有一点“硬编码”,因为访问哪个接口需要哪些权限,在代码中固定了,如果接口和权限直接的关系能够保存到数据库中,那么用户就可以在自己需要的时候,随时进行灵活修改,岂不美哉!
在 vhr 项目中,松哥利用 Spring Security 中自定义 FilterInvocationSecurityMetadataSource 和 AccessDecisionManager 实现了服务端动态控制权限。这个具体的实现思路之前的文章中也和大家分享过了,传送门:Spring Security 动态权限实现方案!,这里就不赘述了。
相对来说,vhr 中的实现方案更灵活一些,因为可以配置接口和权限之间的关系。不过怎么说呢?其实像 RuoYi-Vue 这样硬编码其实也不是不可以,毕竟接口和权限之间的映射关系还是稍显“专业”一些,普通用户可能并不懂该如何配置,这个加入说系统提供了这个功能,那么更多的还是面向程序员这一类专业人员的,那么程序员到底是否需要这个功能呢?我觉得还是得具体情况具体分析。
总之,小伙伴们可以结合自己项目的实际情况,来决定接口和权限之间的映射关系是否需要动态管理,如果需要动态管理,那么可以按照 vhr 中的方案来,如果不需要动态管理,那么就按照 RuoYi-Vue 脚手架中的方式来就行了。
好啦,这就是 RuoYi-Vue 这个脚手架中关于权限的设计,现在有一个新的问题摆在面前:如何给用户设置权限的?现在整个系统的权限架构师安排的明明白白的,那么用户的权限又是从何而来的呢?这个我们下篇文章继续拆解。
松哥最近正在录制 TienChin 项目视频~采用 Spring Boot Vue3 技术栈,里边会涉及到各种好玩的技术,小伙伴们来和松哥一起做一个完成率超 90% 的项目,戳戳戳这里-->TienChin 项目配套视频来啦。