2023-4-15更新
距离写这篇博客一年多时间过去了,我再一次学习这部分内容,我在想明明之前学过,但为什么还是不懂
就像我学了很多遍设计模式还是不会一样,学习某个未知的知识,我感觉只有亲身体会,亲身思考才记得住
不然始终是雾里看花,不得精髓
回归正题,我们公司项目有个非常奇怪的地方,我发现项目中部门竟然拥有了和角色一样的功能,可以选择菜单权限
这又把我搞蒙了,在我印象中,用户关联角色,角色关联菜单,部门关联菜单是什么意思啊,
问了同事,同事说项目中的有一个部门想要看到和其他部门不同的菜单页,所以部门也配置了菜单权限
后来想想还是觉得不对,部门怎么能和菜单权限直接关联而不是通过角色呢,下面说说我的理解
一个用户的权限受到哪些因素影响?
一个用户的权限不仅取决于该用户拥有的角色,还取决于该用户处于的部门,这里的用户拥有的权限就是指能看到的菜单页面
用户权限一部分取决于部门的权限,有些需求说让不同的部门看到不同的菜单页面,则表示,部门也需要绑定角色
部门拥有角色则表示该部门下的用户都默认拥有该角色
所以我们在查询一个用户的权限时,不仅要查该用户拥有哪些角色,还要查询该用户的部门有哪些角色,
这些角色的并集才是这个用户所拥有的权限。
权限的分类
我们知道,权限分为菜单权限,操作权限(按钮权限),数据权限,其中菜单权限和操作权限其实是一样的
放在同一张数据库表中,请看下面若依菜单权限表的设计:
我们可以看到,按钮和菜单的权限数据是放同一张数据表中,并且还需要perms字段作为权限标识
perms字段是用来干嘛的?若依中是这么设计操作权限管理的,
假设开发人员在写用户的编辑接口时,会在接口上写一个注解@RequiresPermissions(“system:user:edit”)
配合AOP的方式实现操作权限管理,用户请求接口后,AOP拦截带有这些注解的接口,然后获取该注解上的权限标识
再与用户拥有的权限标识比对,如果用户具有该标识则放行,否则不允许通过。
最后,数据权限其实就是部门的要求,不同部门查询不同的数据,其本质就是查询sql时新增一个departId=”xxx”
若依中通过注解实现的,具体细节可以看本文最下方。
近况
很久没有写博客了,确实有些懈怠了,接下来的日子我会持续学习若依系统的精华,好好准备毕设内容
2022年的主要任务是搞毕设吧,虽然还有很多不会的知识点,多线程或者刷面试题等等,但是我个人太菜
如果不抓紧时间搞毕设,那毕业都成问题了,时不我待,好好努力!
最近学习了一下若依中的权限管理,之前没有了解过这方面的内容,但学习了几天后,感觉大概有了一个认识了吧
我的博客目标一直很简单,记录学习的东西,未来总有用得到时候,以后可以作为借鉴,查漏补缺。
OK,咱们言归正传,看看本篇博客我们具体学习一下哪些内容
权限管理的概念及分类
权限管理,指的是对系统的资源做一些限制,限制哪些用户可以访问,哪些用户不能访问,
资源是什么?哪些东西需要管理,需要限制,哪些东西就是资源,实际开发中,我们需要管理页面访问
API接口调用,数据,这些都是资源。
前端页面为什么是一种资源?
在实际场景中,我们可能需要使得普通用户可以查看哪些页面,管理员能查看哪些页面,
这就需要我们对前端页面做一个管控,做一些限制,使得普通用户和管理员查看不同的页面,
我们知道,往往一个页面对应一个菜单项,当我们点击某个菜单选项时,就会跳转相应页面,那么我们只需要
限制普通用户能看到哪些菜单项,管理员能看到哪些菜单项即可,这如何做到呢?每个菜单选项对应不同的组件路由
我们只需要在用户登录后,查到该用户拥有哪些菜单,将菜单的数据(这些菜单的路由信息)发送给前端
前端将拿到的菜单数据显示即可达到不同用户对应不同的菜单数据,即拥有不同的页面,从而对页面(菜单)进行限制
达到我们的目标
API接口为什么是一种资源
在实际场景中,我们可能需要使得普通用户可以点击哪些按钮,管理员能点击哪些按钮
用户可不可以点击某个按钮,就表示用户能不能看到某个按钮,就表示用户有没有权限点击这个按钮
一个按钮对应一次请求,就表示用户有没有调用这个接口的权限,接口就是API,API接口也是一种资源
我们也需要对接口进行一些限制,以达到某些用户能请求该接口,某些用户没有权限请求该接口
数据是资源是显而易见的,有时候我们需要某一部分普通用户可以查看哪些数据,另一部分普通用户可以查看哪些数据
这就需要用户和用户之间是存在差别的,比如,一个部门下的用户不能查看另一个部门的数据
如何做到呢?部门,分公司等等这是用户组的概念,我们对用户分组就能做到不同用户查看不同的数据。
我们通常把对页面资源的限制,叫做页面权限,很简单,就是有这个权限的用户才能访问这个页面,
没这个权限的用户就无法访问,它是以整个页面为维度,而判断用户有无权限就是判断用户有无相应的菜单项
我们通常把对API接口资源的限制,叫做操作权限,属于按钮级别权限,你有无操作该接口的权限,就是你有无调用该接口的权限
我们通常把对数据资源的限制,叫做数据权限,上面两种判断你有没有权限,统称为功能权限,
而数据权限则是你有多少权限,限制用户查询全部数据,只能查询属于自己的数据。
综上所述,权限管理就是对资源进行管理,判断用户有无访问该资源的权限,权限管理分为三类:页面权限,操作权限,数据权限。
权限管理设计及其流程
权限管理设计
权限管理有很多著名的设计模型,其中最著名,使用最广泛的当属RBAC模型(Role Based Access Control)
基于角色的访问控制,请看下图:
上图中就是一个典型的RBAC模型,在RBAC模型中,用户表示我们注册的用户,查看订单,删除订单等
这些都表示资源,这里的举例就是API接口资源,这里限制的是API接口的权限,即操作权限。
本来我们以为,某个用户拥有操作权限,其他用户拥有哪些操作权限,用户与操作权限是绑定在一起的,
但是这样有很多弊端,例如如果我每新增一个用户,我就需要为这个用户添加很多操作权限,不现实。
还有很多其他方面的弊端,为此在RBAC模型中新增角色的概念,将所有的权限全部绑定到角色中,
不管你是操作权限,还是页面权限,还是功能权限,全部绑定到不同的角色中,我新增的用户只需要绑定
某个或某几个角色即可拥有角色的权限,这就是RBAC模型。
通过上面描述,我们知道,用户与角色其实是多对多的关系,例如某个人即使开发又是测试,这就相当于
拥有两个角色,而角色与权限也是多对多的关系,一个角色拥有多个权限,一个权限被多个角色共有。
根据上面的描述,我们可以看下对应的数据库的表设计:
可以看到,因为多对多的关系,所以都有中间表关联,另外我们可以看到这里的资源表的资源存储的是路径path
其实就是页面菜单的路由路径,也就是页面权限,可以想象资源还可以是API接口路径,即操作权限
实际开发中菜单的路由信息(页面权限)和API接口的路径信息(操作权限)都放在同一个表中,统一成菜单表,菜单表既包含菜单的路由信息,
也包含API接口的路径信息,若依微服务中就是这样实现,来看看若依微服务中,菜单表的设计吧
上面菜单表的perms字段就是属于操作权限,操作是每一个请求,每一个请求对应一个url,而这里的权限标识
相当于请求的url,也是唯一的。
另外我们如何实现数据权限呢?数据权限的本质是不同的用户看到不同的数据,如何区分不同的用户,
现实生活中往往是不同部门的人看到不同部门的数据,所以这里的资源表也可以是部门表,这样就能
区分不同的用户属于不同的部门,从而实现数据权限,简而言之,新增一个部门表与角色绑定,
角色再与用户绑定,这样就能区分不同的用户,再通过判断部门的不同限制不同的用户查看不同部门的数据。
像不同用户属于不同的部门,其实就是用户组的概念,按组来分配角色,按组来分配权限,
其实还有权限组的概念,权限组是一类API接口的聚合,比如用户操作权限组就可以包含对用户数据的增删查改操作
使用权限组的时候,角色不是和api去绑定,而是去和权限组绑定;
另外,有些权限系统中,角色与角色之间还有继承,互斥的关系,权限设计水太深,我只能了解个大概。
权限管理流程
现在我们已经知道了权限管理的设计模型,通过RBAC可以做到权限管理,那具体是如何实现呢?
我们常常提到认证和授权,认证指的是用户账号密码正不正确,用户合不合法,而授权指的是检查用户权限够不够
每一个用户都会被绑定角色,每个角色都有不同的权限,授权是为用户绑定角色,授予权限,也是当用户请求时
检查用户是否有相应的权限。
实际开发中,当用户登录成功后,前端会请求用户的数据,前端携带token到后端,后端根据token查到用户数据
根据用户数据中用户id,查询该用户所拥有的角色数据,根据角色查询菜单权限数据,菜单权限数据存放在一张表中
与角色表多对多关联,菜单权限数据中有该角色拥有的菜单数据,这些菜单数据就是前端需要的路由路径,
控制用户拥有哪些前端页面,属于页面权限,菜单权限数据表中还有该角色拥有的权限数据,权限数据则表示
该用户可以调用哪些API接口,属于操作权限,当然角色不仅关联菜单权限表,还可以是部门表,表明当前角色
属于哪些部门,也是多对多关联,属于数据权限了。
回归正题,用户登录成功后请求后端,拿到属于该用户的权限数据,菜单数据,前端拿到后,会根据菜单数据渲染
用户页面,不同菜单数据表示不同用户显示不同的页面,会根据用户权限数据,显示或者隐藏某些按钮,
当用户再一次请求后端后,后端首先检查是否登录,然后查询当前用户权限数据,判断该用户是否有权限调用该接口
以上便是大概的权限管理流程。
若依项目中的权限管理
若依项目中通过注解判断用户是否登录,是否拥有相应的权限,是否拥有某些角色,这些注解标注在控制器方法上
如果是,才能执行控制器方法,如果不是则不能执行控制器方法,会抛出无权限异常等信息返回给前端。
下面我们将跟踪请求的执行流程,分析权限注解,AOP,数据库表结构,看看若依项目中权限管理是怎样的
权限管理的第一步是用户已经完成登录,若依项目中,用户登录后,前端会发起一个请求,拿到该用户的
菜单数据,权限数据,用户数据信息。
这个请求的url:http://localhost/dev-api/system/user/getInfo
这个请求接收到的数据如下:{ "msg":"操作成功", "code":200, "permissions":[ "system:user:resetPwd", "system:post:list", "system:menu:query", "system:dept:remove", "system:menu:list", "tool:gen:edit", 这些属于该用户所拥有的权限标识,表示该用户拥有的权限 ], "roles":[ "common" 这是该用户拥有的角色 ], "user":{ 用户数据,略..., "dept":{ 该用户的部门数据,略... }, "roles":[ 该用户的角色数据,略... ], } }
这些用户数据,权限数据,菜单数据只需通过表的关联查询就能查出来,对了,你可能有疑问上面中也没有
菜单数据,前端怎么渲染菜单选项呢?其实这里本可以一次性查出来,但若依中并没有放在一起查询。若依中,查完上面的数据中,
又发了一个请求:http://localhost/dev-api/system/menu/getRouters
请求后数据如下:{ "msg":"操作成功", "code":200, "data":[ { "name":"System", "path":"/system", "hidden":false, "redirect":"noRedirect", "component":"Layout", "alwaysShow":true, "meta":{ "title":"系统管理", "icon":"system", "noCache":false, "link":null }, "children":[ { "name":"User", "path":"user", "hidden":false, "component":"system/user/index", "meta":{ "title":"用户管理", "icon":"user", "noCache":false, "link":null } }, 略 ] }, 略 ] }
根据上面的数据,渲染该用户的页面,当然我们的重点是权限管理。
上面我们通过
/user/getInfo
请求,我们拿到了该用户下的用户信息和权限信息,我们接着看看该请求的控制器方法@GetMapping("getInfo") public AjaxResult getInfo() { //根据token拿到用户数据 Long userId = SecurityUtils.getUserId(); // 角色集合,根据用户id拿到角色 Set<String> roles = permissionService.getRolePermission(userId); // 权限集合,根据用户id拿到该用户拥有的权限标识,即权限信息 Set<String> permissions = permissionService.getMenuPermission(userId); AjaxResult ajax = AjaxResult.success(); ajax.put("user", userService.selectUserById(userId)); ajax.put("roles", roles); ajax.put("permissions", permissions); return ajax; }
上面的代码很简单,就是简单查询数据库数据而已,下面来看看数据库的表结构吧
因为是基于RBAC模型,所有有三张表,用户表,角色表,菜单权限表,他们两两多对多关联
若依系统中表结构如下:用户表
CREATE TABLE `sys_user` ( `user_id` bigint NOT NULL AUTO_INCREMENT COMMENT '用户ID', `dept_id` bigint DEFAULT NULL COMMENT '部门ID', `user_name` varchar(30) NOT NULL COMMENT '用户账号', `nick_name` varchar(30) NOT NULL COMMENT '用户昵称', `user_type` varchar(2) DEFAULT '00' COMMENT '用户类型(00系统用户)', `email` varchar(50) DEFAULT '' COMMENT '用户邮箱', `phonenumber` varchar(11) DEFAULT '' COMMENT '手机号码', `sex` char(1) DEFAULT '0' COMMENT '用户性别(0男 1女 2未知)', `avatar` varchar(100) DEFAULT '' COMMENT '头像地址', `password` varchar(100) DEFAULT '' COMMENT '密码', `status` char(1) DEFAULT '0' COMMENT '帐号状态(0正常 1停用)', `del_flag` char(1) DEFAULT '0' COMMENT '删除标志(0代表存在 2代表删除)', `login_ip` varchar(128) DEFAULT '' COMMENT '最后登录IP', `login_date` datetime 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 '更新时间', `remark` varchar(500) DEFAULT NULL COMMENT '备注', PRIMARY KEY (`user_id`) ) ENGINE=InnoDB AUTO_INCREMENT=100 DEFAULT CHARSET=utf8 COMMENT='用户信息表'
角色表
CREATE TABLE `sys_role` ( `role_id` bigint NOT NULL AUTO_INCREMENT COMMENT '角色ID', `role_name` varchar(30) NOT NULL COMMENT '角色名称', `role_key` varchar(100) NOT NULL COMMENT '角色权限字符串', `role_sort` int NOT NULL COMMENT '显示顺序', `data_scope` char(1) DEFAULT '1' COMMENT '数据范围(1:全部数据权限 2:自定数据权限 3:本部门数据权限 4:本部门及以下数据权限)', `menu_check_strictly` tinyint(1) DEFAULT '1' COMMENT '菜单树选择项是否关联显示', `dept_check_strictly` tinyint(1) DEFAULT '1' COMMENT '部门树选择项是否关联显示', `status` char(1) NOT NULL COMMENT '角色状态(0正常 1停用)', `del_flag` char(1) DEFAULT '0' COMMENT '删除标志(0代表存在 2代表删除)', `create_by` varchar(64) DEFAULT '' COMMENT '创建者', `create_time` datetime DEFAULT NULL COMMENT '创建时间', `update_by` varchar(64) DEFAULT '' COMMENT '更新者', `update_time` datetime DEFAULT NULL COMMENT '更新时间', `remark` varchar(500) DEFAULT NULL COMMENT '备注', PRIMARY KEY (`role_id`) ) ENGINE=InnoDB AUTO_INCREMENT=100 DEFAULT CHARSET=utf8 COMMENT='角色信息表'
用户角色表,中间表
CREATE TABLE `sys_user_role` ( `user_id` bigint NOT NULL COMMENT '用户ID', `role_id` bigint NOT NULL COMMENT '角色ID', PRIMARY KEY (`user_id`,`role_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='用户和角色关联表'
菜单权限表
CREATE TABLE `sys_menu` ( `menu_id` bigint NOT NULL AUTO_INCREMENT COMMENT '菜单ID', `menu_name` varchar(50) NOT NULL COMMENT '菜单名称', `parent_id` bigint DEFAULT '0' COMMENT '父菜单ID', `order_num` int DEFAULT '0' COMMENT '显示顺序', `path` varchar(200) DEFAULT '' COMMENT '路由地址', `component` varchar(255) DEFAULT NULL COMMENT '组件路径', `query` varchar(255) DEFAULT NULL COMMENT '路由参数', `is_frame` int DEFAULT '1' COMMENT '是否为外链(0是 1否)', `is_cache` int DEFAULT '0' COMMENT '是否缓存(0缓存 1不缓存)', `menu_type` char(1) DEFAULT '' COMMENT '菜单类型(M目录 C菜单 F按钮)', `visible` char(1) DEFAULT '0' COMMENT '菜单状态(0显示 1隐藏)', `status` char(1) DEFAULT '0' COMMENT '菜单状态(0正常 1停用)', `perms` varchar(100) DEFAULT NULL COMMENT '权限标识', `icon` varchar(100) DEFAULT '#' 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 '更新时间', `remark` varchar(500) DEFAULT '' COMMENT '备注', PRIMARY KEY (`menu_id`) ) ENGINE=InnoDB AUTO_INCREMENT=2000 DEFAULT CHARSET=utf8 COMMENT='菜单权限表'
菜单权限角色表,中间表
CREATE TABLE `sys_role_menu` ( `role_id` bigint NOT NULL COMMENT '角色ID', `menu_id` bigint NOT NULL COMMENT '菜单ID', PRIMARY KEY (`role_id`,`menu_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='角色和菜单关联表'
通过上面表结构,我们自然能通过用户id,查到用户数据,用户的权限数据,用户权限数据也就是权限标识
就是菜单权限表perms字段中保存,这个权限标识的格式,在上面返回的数据中也展示了部分。直到目前为止,我仍然没有讲到权限管理哦,只是把权限管理的表结构贴了出来,好,那么继续
权限管理就是当用户请求后,判断用户有没有登录成功,判断该请求属不属于权限管理的请求,
判断用户有没有该请求的权限标识。具体做法是,创建一个拦截器,每个请求来了都拦截一下,在拦截器中,查询被权限管理的所有请求路径
查询该用户拥有的权限路径,如果当前请求不在被权限管理的范围内,则放行,如果在权限管理范围内,
则表示需要接受权限检查,再判断当前用户有没有该请求的权限,下面是大佬博客中的拦截器public class AuthInterceptor extends HandlerInterceptorAdapter { @Autowired private ResourceService resourceService; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 如果是静态资源,直接放行 if (!(handler instanceof HandlerMethod)) { return true; } // 获取请求的最佳匹配路径,这里的意思就是我之前数据演示的/API/user/test/{id}路径参数 // 如果用uri判断的话就是/API/user/test/100,就和路径参数匹配不上了,所以要用这种方式获得 String pattern = (String)request.getAttribute( HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE); // 将请求方式(GET、POST等)和请求路径用 : 拼接起来,等下好进行判断。最终拼成字符串的就像这样:DELETE:/API/user String path = request.getMethod() + ":" + pattern; // 拿到所有权限路径 和 当前用户拥有的权限路径 Set<String> allPaths = resourceService.getAllPaths(); Set<String> userPaths = resourceService.getPathsByUserId(UserContext.getCurrentUserId()); // 第一个判断:所有权限路径中包含该接口,才代表该接口需要权限处理,所以这是先决条件, // 第二个判断:判断该接口是不是属于当前用户的权限范围,如果不是,则代表该接口用户没有权限 if (allPaths.contains(path) && !userPaths.contains(path)) { throw new ApiException(ResultCode.FORBIDDEN); } // 有权限就放行 return true; } }
but,若依项目中没有这么做,若依项目中通过权限注解实现权限管理,如果控制器方法标注了该注解
则表示该方法需要权限验证,反之则无需权限处理,这就表示不需要判断该请求属不属于权限管理范围内了。
因为没有标识权限注解的方法不会被AOP拦截到,就代表无需权限管理,只需要判断该用户有没有该请求的权限就行OK,让我们来看看权限注解是怎样的吧,当然不止有权限注解,还有角色注解,登录注解和一个枚举类
我们重点关注下处理注解的AOP实现即可RequiresPermissions,权限注解
/** * 权限认证:必须具有指定权限才能进入该方法 */ @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.METHOD, ElementType.TYPE }) public @interface RequiresPermissions { /** * 需要校验的权限码 */ String[] value() default {}; /** * 验证模式:AND | OR,默认AND */ Logical logical() default Logical.AND; }
RequiresRoles,角色注解
/** * 角色认证:必须具有指定角色标识才能进入该方法 */ @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.METHOD, ElementType.TYPE }) public @interface RequiresRoles { /** * 需要校验的角色标识 */ String[] value() default {}; /** * 验证逻辑:AND | OR,默认AND */ Logical logical() default Logical.AND; }
RequiresLogin,登录注解
/** * 登录认证:只有登录之后才能进入该方法 */ @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.METHOD, ElementType.TYPE }) public @interface RequiresLogin { }
Logical,枚举类
/** * 权限注解的验证模式 */ public enum Logical { /** * 必须具有所有的元素 */ AND, /** * 只需具有其中一个元素 */ OR }
PreAuthorizeAspect,上面三个注解的AOP切面
package com.ruoyi.common.security.aspect; /** * 基于 Spring Aop 的注解鉴权 */ @Aspect @Component public class PreAuthorizeAspect { /** * 构建 */ public PreAuthorizeAspect() { } /** * 定义AOP签名 (切入所有使用鉴权注解的方法) */ public static final String POINTCUT_SIGN = " @annotation(com.ruoyi.common.security.annotation.RequiresLogin) || " + "@annotation(com.ruoyi.common.security.annotation.RequiresPermissions) || " + "@annotation(com.ruoyi.common.security.annotation.RequiresRoles)"; /** * 声明AOP签名 */ @Pointcut(POINTCUT_SIGN) public void pointcut() { } /** * 环绕切入 * * @param joinPoint 切面对象 * @return 底层方法执行后的返回值 * @throws Throwable 底层方法抛出的异常 */ @Around("pointcut()") public Object around(ProceedingJoinPoint joinPoint) throws Throwable { // 注解鉴权 MethodSignature signature = (MethodSignature) joinPoint.getSignature(); checkMethodAnnotation(signature.getMethod()); try { // 执行原有逻辑 Object obj = joinPoint.proceed(); return obj; } catch (Throwable e) { throw e; } } /** * 对一个Method对象进行注解检查 */ public void checkMethodAnnotation(Method method) { // 校验 @RequiresLogin 注解 RequiresLogin requiresLogin = method.getAnnotation(RequiresLogin.class); if (requiresLogin != null) { AuthUtil.checkLogin(); } // 校验 @RequiresRoles 注解 RequiresRoles requiresRoles = method.getAnnotation(RequiresRoles.class); if (requiresRoles != null) { AuthUtil.checkRole(requiresRoles); } // 校验 @RequiresPermissions 注解 RequiresPermissions requiresPermissions = method.getAnnotation(RequiresPermissions.class); if (requiresPermissions != null) { AuthUtil.checkPermi(requiresPermissions); } } }
我们看看上面类的checkMethodAnnotation方法中,是如何校验@RequiresPermissions 注解的
进入AuthUtil.checkPermi(requiresPermissions);
,如下:/** * 根据注解传入参数鉴权, 如果验证未通过,则抛出异常: NotPermissionException * * @param requiresPermissions 权限注解 */ public static void checkPermi(RequiresPermissions requiresPermissions) { authLogic.checkPermi(requiresPermissions); }
进入
authLogic.checkPermi(requiresPermissions);
,如下:/** * 根据注解(@RequiresPermissions)鉴权, 如果验证未通过,则抛出异常: NotPermissionException * * @param requiresPermissions 注解对象 */ public void checkPermi(RequiresPermissions requiresPermissions) { if (requiresPermissions.logical() == Logical.AND) { checkPermiAnd(requiresPermissions.value()); } else { checkPermiOr(requiresPermissions.value()); } }
这里通过requiresPermissions注解的logical,选择与或逻辑,如果你权限注解中有多个权限,判断是
需要同时满足还是只需满足一个权限即可,我们随便进个,就进与逻辑吧
进入checkPermiAnd(requiresPermissions.value());
,如下:/** * 验证用户是否含有指定权限,必须全部拥有 * * @param permissions 权限列表 */ public void checkPermiAnd(String... permissions) { Set<String> permissionList = getPermiList(); for (String permission : permissions) { if (!hasPermi(permissionList, permission)) { throw new NotPermissionException(permission); } } }
上面就是权限判断的逻辑了,getPermiList方法获取该用户拥有的全部权限,而参数permissions
则是标注在控制器方法上的权限注解的value值,下面看看getPermiList方法中的内容吧/** * 获取当前账号的权限列表 * * @return 权限列表 */ public Set<String> getPermiList() { try { LoginUser loginUser = getLoginUser(); return loginUser.getPermissions(); } catch (Exception e) { return new HashSet<>(); } }
就是简单拿到该用户的权限数据而已
上面我们看了权限管理的主要逻辑,接下来我们看看权限注解@RequiresPermissions如何使用
这个权限注解,和登录注解,角色注解都标注在控制器方法上即可,下面是一个例子:/** * 获取用户列表 */ @RequiresPermissions("system:user:list") @GetMapping("/list") public TableDataInfo list(SysUser user) { startPage(); List<SysUser> list = userService.selectUserList(user); return getDataTable(list); }
注解如果只有一个值,则这个值会赋值该注解的value属性上,上面权限注解表示,如果要调用这个方法
则需要用户有system:user:list
权限,然后因为控制器方法标注了这个注解,在执行该方法前进入
该注解的AOP切面,即上面的PreAuthorizeAspect类,接着执行权限判断逻辑了。
以上便是若依项目中的权限管理的设计与流程
若依项目中的数据权限
但是还没完呢,上面的权限管理只是包含页面权限和操作权限的设计,而数据权限还没有讲到,
页面权限和操作权限属于功能权限,你没有权限我就不能让你执行控制器方法,而数据权限则是需要对数据
进行过滤,你只能看到你能看到的数据,我们通过新增部门表即可实现,不同的部门的用户,查看各自的数据
那么若依中,数据权限是如何设计的呢?还是通过注解,@DataScope,只要将该注解标注在控制器方法上
并且在mybatis原来的sql语句后添加一个${params.dataScope}
,即可进行数据限制,具体实现是动态的拼接
过滤的sql语句,然后保存在params对象的dataScope属性上。下面看看具体实现:
DataScope注解
/** * 数据权限过滤注解 */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface DataScope { /** * 部门表的别名 */ public String deptAlias() default ""; /** * 用户表的别名 */ public String userAlias() default ""; }
DataScopeAspect,AOP切面
package com.ruoyi.common.datascope.aspect; /** * 数据过滤处理 */ @Aspect @Component public class DataScopeAspect { //全部数据权限 public static final String DATA_SCOPE_ALL = "1"; //自定数据权限 public static final String DATA_SCOPE_CUSTOM = "2"; //部门数据权限 public static final String DATA_SCOPE_DEPT = "3"; //部门及以下数据权限 public static final String DATA_SCOPE_DEPT_AND_CHILD = "4"; //仅本人数据权限 public static final String DATA_SCOPE_SELF = "5"; //数据权限过滤关键字 public static final String DATA_SCOPE = "dataScope"; @Before("@annotation(controllerDataScope)") public void doBefore(JoinPoint point, DataScope controllerDataScope) throws Throwable { clearDataScope(point); handleDataScope(point, controllerDataScope); } protected void handleDataScope(final JoinPoint joinPoint, DataScope controllerDataScope) { // 获取当前的用户 LoginUser loginUser = SecurityUtils.getLoginUser(); if (StringUtils.isNotNull(loginUser)) { SysUser currentUser = loginUser.getSysUser(); // 如果是超级管理员,则不过滤数据 if (StringUtils.isNotNull(currentUser) && !currentUser.isAdmin()) { dataScopeFilter(joinPoint, currentUser, controllerDataScope.deptAlias(), controllerDataScope.userAlias()); } } } /** * 数据范围过滤 * * @param joinPoint 切点 * @param user 用户 * @param deptAlias 部门别名 * @param userAlias 用户别名 */ public static void dataScopeFilter(JoinPoint joinPoint, SysUser user, String deptAlias, String userAlias) { StringBuilder sqlString = new StringBuilder(); for (SysRole role : user.getRoles()) { String dataScope = role.getDataScope(); 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(" OR 1=0 "); } } } 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) + ")"); } } } /** * 拼接权限sql前先清空params.dataScope参数防止注入 */ private void clearDataScope(final JoinPoint joinPoint) { Object params = joinPoint.getArgs()[0]; if (StringUtils.isNotNull(params) && params instanceof BaseEntity) { BaseEntity baseEntity = (BaseEntity) params; baseEntity.getParams().put(DATA_SCOPE, ""); } } }
部门表及部门角色表的结构
部门表
CREATE TABLE `sys_dept` ( `dept_id` bigint NOT NULL AUTO_INCREMENT COMMENT '部门id', `parent_id` bigint DEFAULT '0' COMMENT '父部门id', `ancestors` varchar(50) DEFAULT '' COMMENT '祖级列表', `dept_name` varchar(30) DEFAULT '' COMMENT '部门名称', `order_num` int DEFAULT '0' COMMENT '显示顺序', `leader` varchar(20) DEFAULT NULL COMMENT '负责人', `phone` varchar(11) DEFAULT NULL COMMENT '联系电话', `email` varchar(50) DEFAULT NULL COMMENT '邮箱', `status` char(1) DEFAULT '0' COMMENT '部门状态(0正常 1停用)', `del_flag` char(1) DEFAULT '0' COMMENT '删除标志(0代表存在 2代表删除)', `create_by` varchar(64) DEFAULT '' COMMENT '创建者', `create_time` datetime DEFAULT NULL COMMENT '创建时间', `update_by` varchar(64) DEFAULT '' COMMENT '更新者', `update_time` datetime DEFAULT NULL COMMENT '更新时间', PRIMARY KEY (`dept_id`) ) ENGINE=InnoDB AUTO_INCREMENT=200 DEFAULT CHARSET=utf8 COMMENT='部门表'
部门与角色关联表
CREATE TABLE `sys_role_dept` ( `role_id` bigint NOT NULL COMMENT '角色ID', `dept_id` bigint NOT NULL COMMENT '部门ID', PRIMARY KEY (`role_id`,`dept_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='角色和部门关联表'
数据权限注解具体使用
//在控制器方法上标注数据权限注解 @DataScope(deptAlias = "d", userAlias = "u") public List<...> select(...) { return mapper.select(...); } //在mybatis查询底部标签添加数据范围过滤 <select id="select" parameterType="..." resultMap="...Result"> <include refid="select...Vo"/> <!-- 数据范围过滤 --> ${params.dataScope} </select>
上面是使用方式,使用数据范围过滤效果是:
没有使用数据权限的sql:select u.user_id, u.dept_id, u.login_name, u.user_name, u.email , u.phonenumber, u.password, u.sex, u.avatar, u.salt , u.status, u.del_flag, u.login_ip, u.login_date, u.create_by , u.create_time, u.remark, d.dept_name from sys_user u left join sys_dept d on u.dept_id = d.dept_id where u.del_flag = '0'
使用了数据权限的sql:
select u.user_id, u.dept_id, u.login_name, u.user_name, u.email , u.phonenumber, u.password, u.sex, u.avatar, u.salt , u.status, u.del_flag, u.login_ip, u.login_date, u.create_by , u.create_time, u.remark, d.dept_name from sys_user u left join sys_dept d on u.dept_id = d.dept_id where u.del_flag = '0' and u.dept_id in ( select dept_id from sys_role_dept where role_id = 2 )
可以看到通过对象参数,动态增加了sql语句,其实还有其他的实现方式,只要能动态拼接sql,加上部门的
限制,就可以使得用户只能查看本部门的数据了,这就是数据权限的实现。
大佬文章