上一篇地址:【清晨平台记录五】代码实现平台后台用户模块三(主要实现登录账号信息、操作权限内容)
本篇代码地址:Gitee 地址 注意是 project06 分支哦
接口文档地址:Apifox工具编写的接口文档
目录
5.用户模块—研发思路
5.2子模块研发
5.2.7操作权限、数据权限
1.操作权限
操作权限从设计的角度来说,就是用户使用系统时,能够展示并使用哪些导航栏、页面、按钮。
有两种形式:
一种是后端返回用户权限集合后,用户操作时,由前端判断当前操作权限是否包含在用户权限集合中,后端不做判断处理。
一种是后端返回用户权限集合后,用户操作时,前端直接调用相应后端接口,由后端判断当前操作权限是否包含在用户权限集合中,前端不做判断处理。
第一种有一个问题,用户正常使用时不会出现问题,但是如果有用户了解平台的接口调用规则,直接调用接口(通过Apifox等),就可以直接无权限访问!这是不可以的,所以建议后端一定要做操作权限判断!
而我们使用 springSecurity 工具框架,他就为我们提供了这样的工具,我们直接使用就可以,灵活又方便~~~
我们主要是用 springSecurity 的权限注解,自定义调用权限校验的方式 :@PreAuthorize("@serviceBeanName.hasPermi('system:user:list')")
这个注解是添加到接口方法上的,当接口被访问时,会先调用 bean 名字为 serviceBeanName 的 .hasPermi(参数) 的方法,方法的返回值是 boolean ,如果是 true 则表示权限校验通过,如果是 false 则表示权限校验失败 。
也就是说,我们需要提前获取到当前用户都有哪些权限(即权限标识)集合,然后写一个验证权限的service业务类,需要将当前访问接口需要的权限作为入参传进来,然后判断入参在不在权限集合里面,若在则返回true,不择则返回false,最后再将这个注解添加到需要验证权限的接口方法上面~
开始码代码:
//我们在 admin 模块中写表现层
1.实体类
登录用户类:com.qingchen.common.core.domain.model.LoginUser implements UserDetails
添加一个 set 集合permissions ,里面保存当前账号所有启用的操作权限的标识集合
2.业务层
加载用户信息类ServiceImpl:com.qingchen.framework.web.service.UserDetailsServiceImpl implements UserDetailsService
我们需要在用户登录的时候给登录用户类里的权限集合添加数据
校验权限类service:com.qingchen.framework.web.service.PermissionService
我们在接口上加的注解中调用的bean方法就是这个类里面的
3.配置类
security配置类:com.qingchen.framework.config.SecurityConfig extends WebSecurityConfigurerAdapter
如果使用security的表达式控制方法权限,就需要加上 @EnableGlobalMethodSecurity(prePostEnabled = true)注解
4.表现层(给需要授权的接口方法加上,对应的权限注解,这里举个栗子)
用户表现类:com.qingchen.controller.system.SysUserController extends BaseController
// 我们给新增和修改方法,加上注解,注意这里的权限标识'system:user:list',需要和数据库 sys_menu 表里的标识对应!
// @PreAuthorize("@permissionService.hasPermi('system:user:list')")
测试之前,我们需要搭建一下测试数据~
首先需要完整的 sys_menu 表数据,然后创建角色,并给角色选择性绑定菜单权限,然后给用户绑定角色。
可以测试没有这个权限的用户访问结果,和有这个权限的用户访问结果来对比:
这是没有权限时的访问
2.数据权限
操作权限从设计的角度来说,就是用户使用系统时,如果打开了一个列表的页面,那么会根据他的对于这个列表的数据范围获取到的相应的创建的数据。
对于设计而言,每个角色对应一个菜单权限和数据权限,数据权限只针对于当前角色里面的菜单权限。如果一个用户有多个角色,角色里面的菜单权限有重复的,那么这些重复的菜单的数据权限按照并集处理(也就是按照最大的范围获取)。
数据权限范围有大到小为:1.全部,2.自定义用户组,3.当前用户组,4.当前用户组及其子用户组,5.仅自己。
举个例子:
下图中,如果用户打开了菜单管理页面,它能够查询到的数据范围是全部创建的(全部 > 仅自己),如果他打开用户管理页面,他能够查询到的数据范围是本用户组创建的(本用户组 > 仅自己),以此类推。
也就是说,获取某个页面的列表时,先根据当前访问的菜单列表权限,从拥有这个菜单权限的角色集合中判断一个最大角色范围,按照这个最大的角色范围去获取数据。
也就是说,当我们获取需要区分数据范围的列表数据时,我们先给菜单类型的数据一个标识,当我们访问这个菜单对应的列表查询的业务层时,先根据这个菜单标识,查询用户绑定的角色中,哪些角色有这个菜单权限,然后从这些角色中获取一个范围最大的数据范围,根据这个范围去获取数据~
注意,我们需要通过菜单标识,获取一个能确认唯一一个菜单类型的菜单,这里我们选择用 perm 字段来获取,为什么不用 menu_id 呢?因为菜单可能会删除,删除后代码里的判断就无效啦,并且某个菜单的权限逻辑是不会轻易改变的,所以他的标识也不会轻易改变!并且菜单的权限标识也应该唯一的,不能够重复,否则就定位不到了。
(所以我们记得在新增、修改菜单时,判断菜单权限标识是否唯一,并且,在判断操作权限的hasPermi(String permission)方法基础上,在再增加一个可以判断多权限的方法 hasPermi(String[] permission) )
我们使用切面编程来实现数据权限,这样就不会影响到方法内部的逻辑了,我们想要给某个方法加上这个逻辑,就直接定位到这个方法就行了。定位方法我们用注解的方式,这样直接给方法加上注解就行啦!
再一个,我们拼接了mybatis 动态查询的语句后,需要传给 mapper ,我们就需要从切面类回传给service,可以通过反向代理拿到业务方法的入参,然后将动态语句传给入参,这样业务方法就能够获取到动态语句啦!这样我们就需要给业务方法添加一个入参,专门存放动态语句,这样就可能就需要给所有需要判断数据权限的方法都加上入参,破坏性太大!
通常情况,我们查询时都会传当前类型的实体类,那么我们就可以将动态接收的语句存到每个实体类里面,也就是实体基类!
开始码代码:
//我们在 admin 模块中写表现层
1.注解类
数据范围注解:com.qingchen.common.annotation.DataScope
我们传一个菜单类型的菜单权限标识,来确定菜单的唯一性
2.切面类
数据范围注解切面类:com.qingchen.framework.aspect.DataScopeAspect
根据切点传过来的菜单权限标识,确定数据范围,然后if判断加上对应的动态mybatis语句,并将动态语句存到入参实体类的基类的字段里面
3.实体类
实体基类:com.qingchen.common.core.domain.BaseEntity implements Serializable
添加一个专门存放动态语句的字段,在 get 方法里加个判断,若是 null 则 new 一个对象出来,防止 NullPointerException 错误
4.业务层
角色service:com.qingchen.system.service.impl.SysRoleServiceImpl implements SysRoleService
因为我们要查询当前菜单在用户的角色集合中的哪些角色存在,并对比出数据范围优先级最大的范围
菜单service:com.qingchen.system.service.impl.SysMenuServiceImpl implements SysMenuService
这里要在菜单新增编辑的时候判断权限标识的唯一性,
校验权限service:com.qingchen.framework.web.service.PermissionService
这里要再添加一个判断多权限的方法
5.持久层(给需要判断数据权限的查询语句加上必要的关联表,这里拿查询用户举个栗子)
角色mapper:com.qingchen.system.mapper.SysRoleMapper
mapper/system/SysRoleMapper.xml
添加新增加的SQL语句,修改用户组select语句
用户mapper:mapper/system/SysUserMapper.xml
给相关的查询语句添加必要的关联表<select id="selectUserList"
6.业务层
用户组service:com.qingchen.system.service.impl.SysUserServiceImpl implements SysUserService
给查询用户列表的方法加上注解:@DataScope( perm = "system:user:list")
准备测试数据:
首先我们需要 6 个角色,其中 5 个角色都有 “system:usergroup:list”的菜单权限,并且数据范围是从全部到仅自己,角色名用1-5表示;最后一个角色不包含 “system:usergroup:list”的菜单权限,数据范围随机(例如全部),角色名用 6 表示
然后,我们创建 6 个用户,并且将用户的创建人分别设置为自己。然后再创建 5 个用户组,并给他们分别绑定用户。就像下面的结构:
- 用户组1 绑定用户:yonghu1
- 用户组11 绑定用户:yonghu11 、yonghu110
- 用户组111 绑定用户y:yonghu111
- 用户组12 绑定用户:yonghu12
- 用户组2 绑定用户:yonghu2
最后,我们给角色 2 绑定 用户组1、用户组12、用户组111 三个用户组。
【以上数据都是同一个用户名下的哦~】
注意哦,登录后先通过 getInfo() 接口获取用户信息,拿到用户权限标识,否则直接调用获取用户列表是会提示没权限的!
而且测完一项记得重新登陆获取token,否则redis里的角色信息是不会变的!
我们使用 yonghu11 账号进行获取用户列表测试,按照下面的测试:
目标测试 | 用户 | 角色 | 预期(描述能查到哪个用户创建的用户) |
全部 | yonghu11(用户组11) | 1,2,3,5,6 | 能查询所有的 |
自定义 | yonghu11 | 2,4,5,6 | 能查询到yonghu 1、12、111 |
本用户组 | yonghu11 | 3,5,6 | 只能查到 yonghu 11、110 |
本用户组及其子级 | yonghu11 | 4,5,6 | 能查到yonghu 11、110 |
仅自己 | yonghu11 | 5,6 | 只能查到 yonghu 11 |
测试每一项前,只需要修改用户关联的角色就可以,例如测试本用户组时,设置 yonghu11 关联的角色为 上方说明的 3,5,6 (也就是数据库角色表里名为 用户查看(3本用户组) 、用户查看(5仅自己)、菜单管理6的角色),那么数据范围最终定位的就是 3 本用户组。
完成~
后续的模块~~
5.2.8列表分页
5.2.9角色模块添加操作用户的逻辑