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,加上部门的
    限制,就可以使得用户只能查看本部门的数据了,这就是数据权限的实现。

大佬文章