代码生成器
源码分析
代码生成器是提高开发效率的重要工具,它主要分为两个部分:
第一部分涉及将业务表结构导入到系统中,在这里,开发者可以预览、编辑、删除和同步业务表结构,实现对业务表的全面管理。
第二部分是在选择了特定的表之后,点击生成按钮,系统将根据表结构生成相应的前后端代码,并提供下载。
表结构说明
若依提供了两张核心表来存储导入的业务表信息:
gen_table
:存储业务表的基本信息 ,它对应于配置代码基本信息和生成信息的页面
gen_table_column
:存储业务表的字段信息 它对应于配置代码字段信息的页面。
这两张表是一对多的关系,一张业务表可以有多个字段的信息,所以在字段信息表中有个外键table_id指向
目录结构
1)后端代码
2)前端代码
查询数据库列表
当管理员在界面上点击导入按钮时,会弹出一个对话框,此时,前端需要向后端发送请求,查询数据库并返回到前端,展示当前项目库中所有待导入的业务表。
此功能涉及前端相关的代码位于views/tool/index.vue
这个视图组件中,负责实现导入业务表的用户界面和交互逻辑。
1 | /** 打开导入表弹窗 */ |
后端处理逻辑则在代码生成模块的GenController
中,负责接收前端的请求,处理业务逻辑,并返回查询结果。
1 | /** |
具体的执行的流程如下图:
导入表结构
当管理员对话框中选中需要导入的业务表,点击确定按钮,此时,前端需要向后端发送请求,保存业务表的基本信息和字段信息
此功能涉及前端相关的代码位于views/tool/importTable.vue
这个视图组件中,负责实现导入业务表的用户界面和交互逻辑。
1 | /** 导入按钮操作 */ |
后端处理逻辑则在代码生成模块的GenController
中,负责接收前端的请求,处理业务逻辑,保存业务表的基本信息和字段信息
1 | /** |
具体的执行的流程如下图:
生成代码
首先管理员,选中需要下载的业务表,并点击生成按钮来触发代码生成并下载的过程。
前端随后向后端发送请求,这个请求会告知服务器需要生成代码的业务表。
负责实现这一功能的前端代码位于views/tool/index.vue
这个视图组件中,负责实现生成业务表的用户界面和交互逻辑。
1 | /** 生成代码操作 */ |
后端的逻辑处理则在代码生成模块的GenController
中,这里是处理前端请求、执行代码生成逻辑,将生成的代码字节流通过HTTP响应返回给客户端。
1 | /** |
具体的执行的流程如下图:
问题分析
我们已经对代码生成器的工作原理有了一定的了解,接下来我们解决一些项目中使用的问题,比如:
每次生成代码都需要修改作者,去除实体类前缀过于繁琐,现在我们可以修改generator.yml
配置文件来调整为自己项目的
1 | # 代码生成 |
我们还想在若依代码生成的基础上继续进行增强
实体类支持Lombok
1
2
3
4
5
6
7
8
9
10
11
public class Order extends BaseEntity {
private Long id;
private String orderNo;
// 没有get、set、toString方法了
}Controller类支持Swagger
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class OrderController extends BaseController{
public TableDataInfo list(...){
return success(...);
}
public AjaxResult getInfo(...) {
return success(...);
}
}
要实现这些增强功能,我们需要掌握Velocity模板引擎的使用。Velocity允许我们定制和优化代码生成模板。
在下一个小节中,我们将开始学习Velocity模板引擎,这将帮助我们更好地理解和改造代码生成器的模板。
Velocity模版引擎
介绍
Velocity是一个基于Java的模板引擎,可以通过特定的语法获取在java对象的数据 , 填充到模板中,从而实现界面和java代码的分离 !
常见的应用场景:
- Web内容生成 : 生成动态Web页面。
- 代码生成 : 生成Java源代码、SQL脚本、XML配置文件等。
- 网页静态化 : 生成静态网页。
入门
需求:根据下面html模板,完成对数据的填充
1 |
|
要求:加油少年,这几个字,需要动态填充进来
加油同学!!
加油女孩!!
加油朋友!!
准备模板
1 |
|
上述代码中的 加油少年 修改为了 ${message} 这是一个动态变量(占位符),方便动态填充数据
数据填充
编写java代码实现数据填充,并生成文件
1 | package com.dkd.test; |
效果测试
在指定的目录中生成index.html文件
打开之后的效果:
基础语法
变量
Velocity中的变量有两类
- 在模板中定义变量:
#set
开头,比如#set($name = "velocity")
- 获取变量的的值:
$name
或者${name}
下面是案例,基于刚才的入门案例模板改进
##
双#号 是vm的注释
1 | <!DOCTYPE html> |
对象的定义获取
在ruoyi-generator模块下新增一个区域的实体类
1 | package com.dkd.test; |
准备模型数据
1 | package com.dkd.test; |
动态模板展示数据
1 | <!DOCTYPE html> |
循环
循环的语法:#foreach(...) ... #end
1 | ##定义一个集合 |
准备模型数据
1 | // 创建区域对象 |
修改父工程的pom.xml文件,把jdk版本升级为11
1 | <java.version>11</java.version> |
动态模板展示数据
1 | ## 遍历区域 |
if判断
判断的语法:#if(condition) ... #elseif(condition) ... #else ... #end
1 | ##定义变量 |
其他的判断条件:
1 | ## 对象obj不为空才会执行里面的逻辑 |
在条件判断中,velocity支持常见的关系操作符,比如:&&(与), ||(或), !(非)
模板阅读
我们不需要使用velocity去开发新的模板,若依已经提供好了,在它基础上进行调整即可
下面这个是关于实体类的模板
1 | package ${packageName}.domain; |
Lombok集成
目前,我们已经基本熟悉了velocity的作用和一些语法,那接下来,我们就通过这些知识来去改造若依框架的代码生成部分
导入坐标(已完成)
在dkd-common
模块的pom.xml
中添加lombok坐标
1 | <!-- lombok工具--> |
修改模板
在dkd-generator
模块的domain.java.vm
模板中添加lombok注解
1 | package ${packageName}.domain; |
生成后的效果
修改完成之后,重启项目,找到代码生成的功能,通过代码预览可以查看实体类的代码:
- 正常添加了关于lombok的注解
- 删除了set 、 get 、toString 等方法
可以把生成后的代码,拷贝到项目中,如果订单管理能够正常访问和操作,就算修改成功了,后期再次生成的代码,全部都支持lombok
Swagger集成
修改模板
在dkd-generator
模块的 controller.java.vm
模板中添加Swagger注解
1 | package ${packageName}.controller; |
生成后的效果
修改完成之后,重启项目,找到代码生成的功能,通过代码预览可以查看Controller类的代码:
可以把生成后的代码,拷贝到项目中,如果订单管理能够正常访问和操作,且系统接口显示工单管理就算修改成功了,后期再次生成的代码,全部都支持Swagger
RBAC权限控制
SpringSecurity介绍
Spring Security是一个功能强大的Java安全框架,它提供了全面的安全认证和授权的支持。
与RBAC模型结合使用时,Spring Security能够实现灵活的权限控制。
我们来看下它的二大核心概念,认证和授权。让我们用一个简单的例子来领会它们的意义。
1)认证(Authentication)想象一下,小智同学去图书馆借书。门口的图书管理员会要求小智出示借书证。这个借书证上有小智的照片和姓名,管理员通过它来确认小智的身份。这个过程,就是一个认证的实例。在Spring Security的世界里,认证就像用户登录时提交的用户名和密码,系统通过这些信息来验证“你是谁”。
Spring Security不仅支持传统的用户名和密码认证,还支持OAuth2、JWT等现代认证方式。
2)授权(Authorization) 现在,假设小智同学已经踏入了图书馆的大门,但当小智想进入只有持特定权限才能进入的参考书籍区时,管理员会再次检查小智的借书证,看是否有相应的权限标记。如果有,那么小智就能进入这个区域;如果没有,那么小智只能遗憾地止步。这就是授权的精髓。在Spring Security中,授权是确认用户在通过认证之后,是否有权限执行某些操作或访问特定资源。
SpringSecurity配置
Spring Security的配置类是实现安全控制的核心部分
开启Spring Security各种功能,以确保Web应用程序的安全性,包括认证、授权、会话管理、过滤器添加等。
1 | package com.dkd.framework.config; |
用户登录流程
管理员在登录页面,输入用户名和密码以及验证码后,点击登录按钮,向后端发送请求,后端通过springSecurity认证管理器进行登录校验
此功能涉及前端相关的代码位于views/login.vue
这个视图组件中,负责实现用户登录界面和交互逻辑。
后端处理逻辑则在dkd-admin
模块的SysLoginController中,负责接收前端的请求,处理登录逻辑,并返回token令牌
前端
点击
login.vue
中的登录按钮调用
login.vue
中的handleLogin
方法调用
store/mondles/user.js
中的login
方法,将返回结果存入useUserStore对象中(用于管理用户相关的状态和操作)调用
api/login.js
中的login
方法调用
utils/request.js
中的service
实例基于axios发送ajax请求(.env.development文件设置了统一请求路径前缀)
后端
SysLoginController
在ruoyi-admin
模块中com.ruoyi.web.controller.system.SysLoginController
类的login
方法接收前端登录请求
SysLoginService
在ruoyi-framework
模块中com.ruoyi.framework.web.service.SysLoginService
类的login
方法处理登录逻辑
- 验证码校验
- 登录前置校验
- SS认证管理器用户校验,调用执行UserDetailsServiceImpl.loadUserByUsername
- 认证通过后,创建登录用户对象LoginUser包括用户ID、部门ID、用户信息和用户权限信息
- 登录成功,记录日志
- 修改用户表更新登录信息
- 生成token
具体的执行的流程如下图:
获取用户角色和权限
超级管理员登录帝可得系统后,可以查看区域管理所有的权限按钮,实现增删改查操作
而财务人员登录帝可得系统后,仅可以查看区域列表,其他权限没有
1)首先创建一个财务人员的角色,权限字符串 Accountant,仅设置区域管理查询权限
2)再创建一个用户,昵称为黑马彭于晏,用户名hmpyy,密码admin123 角色为财务人员
前端
- 在全局
permission.js
中的router.beforeEach
方法用于在用户导航到不同路由之前进行一些预处理 - 调用
store/mondles/user.js
中的getInfo
方法,将返回结果存入useUserStore对象中(用于管理用户相关的状态和操作) - 调用
api/login.js
中的getInfo
方法
后端
SysLoginController
在ruoyi-admin
模块中com.ruoyi.web.controller.system.SysLoginController
类的getInfo
方法接收前端获取用户信息请求
SysPermissionService
在ruoyi-framework
模块中com.ruoyi.framework.web.service.SysPermissionService
类
- getRolePermission查询该用户角色集合
- getMenuPermission查询该用户权限(菜单)集合
具体的执行的流程如下图:
页面权限
前端封装了一个指令权限,能简单快速的实现按钮级别的权限判断。
使用权限字符串 v-hasPermi:@/directive/permission/hasPermi.js
1 | // 单个 |
使用角色字符串 v-hasRole@/directive/permission/hasRole.js
1 | // 单个 |
获取动态菜单路由
在上个小节我们创建了“黑马彭于晏”这个用户,并为他分配了特定的角色权限。这意味着,当他登录帝可得系统时,看到的侧边栏菜单将根据他的角色权限而有所不同。而超级管理员是可以查看所有菜单的
实现此功能的前端代码位于src/permission.js
文件。它在登录成功后,会在跳转到新路由之前,去查询当前用户有权访问的动态菜单的路由列表。
后端处理逻辑则在dkd-admin
模块的SysLoginController中,它负责接收前端发来的请求,处理查询,并构建起一个完整的菜单树结构,然后返回给前端。
前端
- 在全局
permission.js
中的router.beforeEach
方法用于在用户导航到不同路由之前进行一些预处理 - 调用
store/mondles/permission.js
中的generateRoutes
方法,将返回结果存入usePermissionStore对象中 - 调用
api/menu.js
中的getRouters
方法
后端
SysLoginController
在ruoyi-admin
模块中com.ruoyi.web.controller.system.SysLoginController
类的getRouters
方法接收前端获取路由信息请求
ISysMenuService
在ruoyi-system
模块中com.ruoyi.web.system.service.ISysMenuService
类
- selectMenuTreeByUserId根据用户ID查询菜单树信息(递归生成父子菜单)
- buildMenus构建前端路由所需要的菜单路由格式RouterVo
具体的执行的流程如下图:
路由菜单加载
- 用户登录成功后,通过路由
router/index.js
跳转到首页并加载layout布局组件 - 在
layout/index.vue
中加载sidbar侧边栏 - 在
layout/components/Sidebar/index.vue
中遍历动态路由菜单在页面显示 - 用户点击菜单后会根据路由的path跳转到对应的视图组件在
显示
权限注解
源码分析
在若依框架中,权限的验证最核心的是使用的Spring Security的提供的权限注解@PreAuthorize
@PreAuthorize 是 Spring Security 框架中提供的一个安全注解,用于实现基于注解的访问控制。它允许开发者在方法级别上声明特定的安全约束,以确保只有满足指定条件的用户才能调用该方法
当 @PreAuthorize 注解被应用于某个方法时,Spring Security 在该方法执行前会先对当前认证的用户进行权限检查。如果检查通过,方法调用得以继续;否则,框架会抛出相应的权限异常(如 AccessDeniedException),阻止方法执行。
若依框架中的权限控制代码,如下:
1 |
|
@PreAuthorize是Spring Security框架的权限注解,在执行方法前执行
@ss.hasPermi(‘manage:order:list’)
- 其中的ss是指的一个spring管理的bean
- 位置:ruoyi-framework模块中的com.ruoyi.framework.web.service.PermissionService
- hasPermi 是PermissionService类中的一个方法,判断是否拥有该权限
- manage:order:list为方法的参数
- 其中的ss是指的一个spring管理的bean
注意:在SecurityConfig类中添加
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
@PreAuthorize才能生效
权限控制流程:
权限方法
@PreAuthorize
注解用于配置接口要求用户拥有某些权限才可访问,它拥有如下方法
方法 | 参数 | 描述 |
---|---|---|
hasPermi | String | 验证用户是否具备某权限 |
lacksPermi | String | 验证用户是否不具备某权限,与 hasPermi逻辑相反 |
hasAnyPermi | String | 验证用户是否具有以下任意一个权限 |
hasRole | String | 判断用户是否拥有某个角色 |
lacksRole | String | 验证用户是否不具备某角色,与 hasRole逻辑相反 |
hasAnyRoles | String | 验证用户是否具有以下任意一个角色,多个逗号分隔 |
使用示例
1)数据权限示例。
1 | // 符合system:user:list权限要求 |
编程式判断是否有资源权限
1 | if (SecurityUtils.hasPermi("sys:user:edit")) |
2)角色权限示例。
1 | // 属于user角色 |
编程式判断是否有角色权限
1 | if (SecurityUtils.hasRole("admin")) |
权限提示:超级管理员拥有所有权限,不受权限约束。
公开接口
如果有些接口是不需要验证权限可以公开访问的,这个时候就需要我们给接口放行。
使用注解方式,只需要在Controller
的类或方法上加入@Anonymous
该注解即可
1 | // @PreAuthorize("@ss.xxxx('....')") 注释或删除掉原有的权限注解 |
异步任务管理器
介绍
若依框架的前后端分离版本中,异步任务管理器扮演着重要角色,主要用于处理一些不需要即时返回结果的后台任务,从而提高应用程序的整体性能
1 | // 多线程执行任务AsyncManager.me().execute(AsyncFactory.createTimerTask()); |
场景:记录日志
那么,为什么使用异步任务管理器能够提高性能呢?让我们通过一个实际的例子来说明。
用户在浏览器中输入用户名、密码和验证码,然后发送登录请求。
在sysLoginService业务层代码中实现登录逻辑,核心业务是登录认证,包括验证码校验、用户名查询和密码校验。
如果校验失败,我们会记录日志并返回提示信息;如果校验成功,我们同样记录日志并返回token。
假设登录认证核心业务代码执行耗时50ms,记录日志也需要耗时50ms,那么整个登录请求耗时多久呢?没错就是100ms。
现在,如果产品经理根据客户的要求,增加了一个功能:在后台系统登录成功后给用户发送一条提示短信。这意味着我们需要在业务层增加发送短信的代码,而这同样需要耗时50ms。这样一来,整个登录请求的耗时就变成了150ms,用户的等待时间增加了,系统性能也随之下降。
此外,我们的登录业务还涉及到事务控制,以保证业务的一致性。
如果登录认证业务成功,但记录日志时出现异常,导致事务回滚,这样的设计显然是不合理的。这就是级联失败的现象。
其实对于用户只关心登录是否成功,而对于记录日志和发送短信这些非核心业务,他们并不关心是否立即完成。
因此,我们可以通过异步任务管理器的多线程来处理这些任务。我们来看下
在用户发送登录请求时,sysLoginService
业务层只需关注登录认证的核心业务代码。
记录日志和发送短信的任务可以交给异步管理器来处理。
这样,用户登录的耗时只需要50ms,无需等待异步任务的完成,从而显著提高了性能。
更重要的是,登录认证作为一个独立的事务运行在主线程中,而记录日志则在另一个线程中进行。这样,即使记录日志失败,也不会影响登录认证的事务,实现了故障隔离。
通过使用异步任务管理器,我们可以将那些不需要立即返回结果的非核心业务代码交给它来执行,从而优化系统性能,提高用户体验。
源码
若依异步任务管理器是一个单例对象使用了线程池+异步工厂(产生任务用)
- com.dkd.framework.manager.AsyncManager 异步任务管理器
- com.dkd.framework.manager.factory.AsyncFactory 异步线程工厂
1、 AsyncManager.me()获取AsyncManager对象
2、调用execute方法,执行TimerTask任务(记录登录日志),它实现了runnable接口,由线程Thread去执行
3、execute方法内部调用ScheduledExecutorService异步操作任务调度线程池的schedule方法用于延迟10毫秒执行一个任务
具体的流程图如下:
操作日志
介绍
在日常编程中,记录日志是我们的得力助手,尤其在处理关键业务时,它能帮助我们追踪和审查操作过程。
然而,手动记录日志常常是繁琐且容易出错的。我们需要收集操作内容、参数等信息,这不仅增加了工作量,还可能导致代码重复,影响业务逻辑的清晰度。
那么,有没有什么办法可以让我们更优雅地处理这个问题呢?答案是肯定的,那就是使用注解
在需要被记录日志的controller
方法上添加@Log
注解,使用方法如下:
1 |
|
注解参数说明:
参数 | 类型 | 默认值 | 描述 |
---|---|---|---|
title | String | 空 | 操作模块 |
businessType | BusinessType | OTHER | 操作功能(OTHER 其他、INSERT 新增、UPDATE 修改、DELETE 删除、GRANT 授权、EXPORT 导出、IMPORT 导入、FORCE 强退、GENCODE 生成代码、CLEAN 清空数据) |
operatorType | OperatorType | MANAGE | 操作人类别(OTHER 其他、MANAGE 后台用户、MOBILE 手机端用户) |
isSaveRequestData | boolean | true | 是否保存请求的参数 |
isSaveResponseData | boolean | true | 是否保存响应的参数 |
excludeParamNames | String[] | {} | 排除指定的请求参数 |
源码
若依操作日志使用了自定义注解+AOP切面+异步任务管理器
com.ruoyi.common.annotation.Log 自定义注解
com.ruoyi.framework.aspectj.LogAspect 在一个aop切面类
- 通过实现AOP切面编程,对目标方法进行拦截(标注Log注解的方法),实现了操作日志的自动记录
- 异步任务管理器来将任务(记录操作日志到数据库)交给线程池来完成
定时任务
源码分析
在实际项目开发中Web应用有一类不可缺少的,那就是定时任务。 定时任务的场景可以说非常广泛,比如某些视频网站,购买会员后,每天会给会员送成长值,每月会给会员送一些电影券; 比如在保证最终一致性的场景中,往往利用定时任务调度进行一些比对工作;比如一些定时需要生成的报表、邮件;比如一些需要定时清理数据的任务等。 所以我们提供方便友好的web界面,实现动态管理任务,可以达到动态控制定时任务启动、暂停、重启、删除、添加、修改等操作,极大地方便了开发过程。
表结构说明
sys_job
表:这是核心的定时任务表,用于存储定时任务的配置信息,如任务名称、任务组、执行的类全名、执行的参数、cron表达式等
sys_job_log
表:用于记录定时任务的执行日志,包括任务的开始执行时间、结束执行时间、执行结果等
目录结构
1)后端代码
- ScheduleUtils 定时任务工具类
- 抽象quartz调用类AbstractQuartzJob(实现Job接口)
- 定时任务处理类(禁止并发执行) 继承抽象类
- 定时任务处理类(允许并发执行) 继承抽象类
- 任务执行工具类 JobInvokeUtil
- cron表达式工具类
2)前端代码
Quartz体系结构
Quartz核心API
API | 描述 |
---|---|
Job | - 实际要执行的任务类 - 必须实现Quartz的 Job 接口。 |
JobDetail | - 代表一个Job 实例- 通过 JobBuilder 类创建。 |
JobBuilder | - 用于声明一个任务实例 - 可以定义关于该任务的详情,如任务名、组名等。 |
Trigger | - 触发器,用来触发并执行Job 实例的机制。 |
SimpleTrigger | - 用于简单重复执行作业的触发器 - 例如:每隔一定时间执行一次。 |
CronTrigger | - 使用Cron表达式定义执行计划的触发器 - 适用于定义复杂的执行时间。 |
TriggerBuilder | - 用于创建触发器Trigger 实例的构建器。 |
Scheduler | - Quartz中的核心组件 - 负责启动、停止、暂停和恢复任务。 |
定时任务执行
项目在启动时,初始化定时任务
流程图:
添加定时任务
刚才我们分析了在项目启动时,从数据库中查询任务配置列表,然后创建定时任务,并根据状态判断是否执行任务调度,那如果是新添加的定时任务该如何处理呢?为了解答这个问题,我们来对这部分的源码进行分析
我们已经对若依前端的代码交互模式比较熟悉了,所以本次我们关注后端部分,入口在定时任务模块的sysJobController中
1 | /** |
代码流程图:
定时任务状态修改
刚才我们分析新增定时任务的源码时,发现了任务在初始化时是处于暂停状态的。
如果要启动任务,可以在页面进行任务状态的开关控制,所以接下来我们对此功能的源码进行分析
入口在定时任务模块的sysJobController中
1 | /** |
代码流程图:
集群模式
介绍
首先我们来聊下为什么需要quartz集群
在单机模式下,默认所有的jobDetail
和trigger
都存储在内存中。这样做的好处是读取速度快,但缺点也很明显:一旦服务器故障,所有的任务数据就会丢失,这就是所谓的单点故障问题。
还有如果在一个高峰时段,比如上午9点,需要触发500个任务,这将给服务器带来巨大的负载压力。这不仅影响性能,还可能引发服务中断。
缺点:单点故障、负载压力大
为了解决这些问题,我们可以部署多个服务器节点,将任务信息存储到数据库中。这样,多个节点就可以通过共享数据库来协调任务的执行,形成Quartz集群模式。
这种方式不仅解决了单点故障问题,还能通过负载均衡提升效率。
集群模式的优势
- 高可用性:即使某个节点出现问题,其他节点仍然可以正常运行。
- 负载均衡:将任务分散到不同的节点执行,避免单个节点过载。
通常在生产环境中,我们会部署多台服务器,所以采用集群模式不会产生额外的成本。
quartz集群所需数据库表
表名 | 用途 |
---|---|
qrtz_triggers |
存储触发器的基本信息,如触发器名称、组、类型等。 |
qrtz_cron_triggers |
存储Cron触发器的额外信息,如Cron表达式。 |
qrtz_simple_triggers |
存储简单触发器的额外信息,如重复次数和间隔。 |
qrtz_blob_triggers |
存储BLOB类型触发器的额外信息,如持久化的数据。 |
qrtz_simprop_triggers |
存储具有单一触发器属性的触发器的详细信息。 |
qrtz_job_details |
存储作业详细信息,如作业名称、组、描述、作业类名等。 |
qrtz_scheduler_state |
存储调度器的状态信息,如当前主节点信息等。 |
qrtz_locks |
存储锁信息,用于控制并发和防止资源冲突。 |
qrtz_paused_trigger_grps |
存储被暂停的触发器组的信息。 |
qrtz_fired_triggers |
存储已触发的触发器的详细信息,包括执行历史。 |
qrtz_calendars |
存储日历信息,定义工作日和非工作日,用于调度时间约束。 |
实现
导入sql
将若依提供的quartz.sql
导入到数据库中
开启配置
打开dkd-quartz
模块中ScheduleConfig
配置类注释
节点复制
首先修改当前SpringBoot的启动类的名称
我们再添加(复制)一个SpringBoot的启动配置
-Dserver.port=8081
观察数据库
重启项目即可,观察数据库,已存入jobDetail和trigger,多个服务器节点可以实现共享
源码
1 | package com.dkd.quartz.config; |
数据权限
介绍
在我们开始深入学习数据权限控制之前,让我们先来思考一个实际开发中经常遇到的关键问题:如何确保用户只能访问他们被授权查看的数据?
这个问题的答案就是数据权限控制。
想象一下,我们有一个大型集团公司,旗下有多个子公司和部门。每个部门,比如市场部或财务部,都有自己的敏感数据。我们肯定不希望一个部门的员工能够访问另一个部门的敏感信息,对吧?
这时,可能会有同学问,为什么我们如此重视数据权限控制呢?原因很简单:保护敏感信息:它像一把锁,防止敏感数据泄露给未授权的人员,确保信息安全。
数据权限的场景:
- 部门级权限:比如,市场部的员工应该只能访问到销售部的数据,确保他们只能触及自己部门的信息。
- 公司级权限:子公司的经理可能需要有更广阔的视野,他们需要查看整个子公司的数据,以做出战略决策。
- 跨部门权限:而对于公司的高级领导或特定角色,他们可能需要有更全面的数据访问权限,以便跨部门或跨公司地进行管理和决策。
通过数据权限控制,我们不仅保护了公司的数据安全,还确保了数据的合理利用和流程的顺畅。
演示
在系统中,权限的分配和控制主要依赖于角色。每个角色可以被赋予不同的菜单权限和数据权限,用户则通过他们的角色来继承这些权限,进而决定他们能访问哪些系统资源。
目前,系统支持以下五种数据权限类型:
- 全部数据权限:无限制访问所有数据,相当于拥有最高权限的通行证。
- 自定数据权限:用户可以根据自己的需求设定访问特定数据的规则。
- 部门数据权限:只能访问自己所在部门的数据,限制在本部门范围内。
- 部门及以下数据权限:可以访问自己部门及下属部门的数据,适用于管理层级。
- 仅本人数据权限:只能访问和操作自己的数据,保障个人隐私和数据隔离。
提示:默认系统管理员
admin
拥有所有数据权限(userId=1)
,默认角色拥有所有数据权限(如不需要数据权限不用设置数据权限操作)
源码
1、若依系统的数据权限设计主要通过用户、角色、部门表建立关系,实现对数据的访问控制:
2、在需要数据权限控制方法上添加@DataScope
注解,其中d
和u
用来表示表的别名
部门数据权限注解
1 |
|
部门及用户权限注解
1 |
|
参数 | 类型 | 默认值 | 描述 |
---|---|---|---|
deptAlias | String | 空 | 部门表的别名 |
userAlias | String | 空 | 用户表的别名 |
3、在mybatis
查询底部标签添加数据范围过滤
1 | <select id="select" parameterType="..." resultMap="...Result"> |
其作用就是相当于在一个 select 语句后面拼接一个 and 条件语句,来实现查询限制,例如下面
1 | -- 用户管理(未过滤数据权限的情况): |
1 | -- 用户管理(已过滤数据权限的情况): |
结果很明显,我们多了如下语句。通过角色部门表(sys_role_dept)
完成了自定义类型的数据权限过滤
1 | and u.dept_id in ( |
若依数据权限底层使用了自定义注解+AOP切面+SQL拼接
- com.dkd.common.annotation.DataScope 自定义注解
com.ruoyi.framework.aspectj.DataScopeAspect:切面类
- 通过实现AOP编程,对目标方法进行拦截(标注DataScope 注解的方法),实现了构建数据范围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
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
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204package com.dkd.framework.aspectj;
import com.dkd.common.annotation.DataScope;
import com.dkd.common.core.domain.BaseEntity;
import com.dkd.common.core.domain.entity.SysRole;
import com.dkd.common.core.domain.entity.SysUser;
import com.dkd.common.core.domain.model.LoginUser;
import com.dkd.common.core.text.Convert;
import com.dkd.common.utils.SecurityUtils;
import com.dkd.common.utils.StringUtils;
import com.dkd.framework.security.context.PermissionContextHolder;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
/**
* 数据过滤处理
*
* @author ruoyi
*/
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";
public void doBefore(JoinPoint point, DataScope controllerDataScope) throws Throwable
{
// 清理数据范围(权限)过滤条件(params.dataScope)防止sql注入
clearDataScope(point);
// 设置数据范围(权限)过滤条件
handleDataScope(point, controllerDataScope);
}
protected void handleDataScope(final JoinPoint joinPoint, DataScope controllerDataScope)
{
// 获取当前的登录用户
LoginUser loginUser = SecurityUtils.getLoginUser();
if (StringUtils.isNotNull(loginUser))
{
// 获取用户基本信息
SysUser currentUser = loginUser.getUser();
// 如果是超级管理员,则不过滤数据
if (StringUtils.isNotNull(currentUser) && !currentUser.isAdmin())
{
// 获取目标方法的权限字符串 例如:用户列表(system:user:list)
String permission = StringUtils.defaultIfEmpty(controllerDataScope.permission(), PermissionContextHolder.getContext());
// 设置数据范围(权限)过滤条件,根据当前用户、部门别名、用户别名和权限标识对切点对象进行过滤处理
dataScopeFilter(joinPoint, currentUser, controllerDataScope.deptAlias(),
controllerDataScope.userAlias(), permission);
}
}
}
/**
* 数据范围过滤
*
* @param joinPoint 切点
* @param user 用户
* @param deptAlias 部门别名
* @param userAlias 用户别名
* @param permission 权限字符
*/
public static void dataScopeFilter(JoinPoint joinPoint, SysUser user, String deptAlias, String userAlias, String permission)
{
// 构建SQL字符串,用于拼接数据范围条件
StringBuilder sqlString = new StringBuilder();
// 用于存储已经添加的数据范围类型,避免重复添加
List<String> conditions = new ArrayList<String>();
// 遍历用户的所有角色
for (SysRole role : user.getRoles())
{
// 获取当前角色的数据范围条件类型 1~5
String dataScope = role.getDataScope();
// 如果数据范围类型是自定义类型且已添加,则跳过本次循环
if (!DATA_SCOPE_CUSTOM.equals(dataScope) && conditions.contains(dataScope))
{
continue;
}
// 如果当前角色权限列表不包含目标方法的权限字符串(system:user:list),则跳过本次循环
if (StringUtils.isNotEmpty(permission) && StringUtils.isNotEmpty(role.getPermissions())
&& !StringUtils.containsAny(role.getPermissions(), Convert.toStrArray(permission)))
{
continue;
}
// 如果角色的数据范围类型是全部数据,则清空SQL字符串并添加数据范围类型,结束循环
if (DATA_SCOPE_ALL.equals(dataScope))
{
sqlString = new StringBuilder();
conditions.add(dataScope);
break;
}
// 如果角色的数据范围类型是自定义数据
else if (DATA_SCOPE_CUSTOM.equals(dataScope))
{
// 拼接SQL条件,限制部门ID在角色所关联的部门范围内
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))
{
// 拼接SQL条件,限制部门ID等于用户所在部门ID
sqlString.append(StringUtils.format(" OR {}.dept_id = {} ", deptAlias, user.getDeptId()));
}
// 如果角色的数据范围类型是本部门及子部门数据
else if (DATA_SCOPE_DEPT_AND_CHILD.equals(dataScope))
{
// 拼接SQL条件,限制部门ID等于用户所在部门ID或在用户所在部门的子孙部门中
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))
{
// 如果用户表别名不为空,拼接SQL条件限制用户ID等于当前用户ID
if (StringUtils.isNotBlank(userAlias))
{
sqlString.append(StringUtils.format(" OR {}.user_id = {} ", userAlias, user.getUserId()));
}
else
{ // 否则,拼接SQL条件限制部门ID为0,即不查询任何数据
sqlString.append(StringUtils.format(" OR {}.dept_id = 0 ", deptAlias));
}
}
// 添加当前角色的数据范围类型条件
conditions.add(dataScope);
}
// 如果数据范围类型集合(即多角色情况下,所有角色都不包含传递过来目标方法的权限字符),则添加一个条件使SQL查询不返回任何数据
if (StringUtils.isEmpty(conditions))
{
sqlString.append(StringUtils.format(" OR {}.dept_id = 0 ", deptAlias));
}
// 如果SQL字符串不为空,则将构造好的数据范围条件添加到方法参数对象中,用于后续的SQL查询
if (StringUtils.isNotBlank(sqlString.toString()))
{
// 获取切点方法的第一个参数
Object params = joinPoint.getArgs()[0];
// 检查参数是否非空且为BaseEntity类型
if (StringUtils.isNotNull(params) && params instanceof BaseEntity)
{
// 将参数转换为BaseEntity类型
BaseEntity baseEntity = (BaseEntity) params;
// 向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];
// 检查参数不为null且是否为BaseEntity类型
if (StringUtils.isNotNull(params) && params instanceof BaseEntity)
{
// 将参数转为BaseEntity类型
BaseEntity baseEntity = (BaseEntity) params;
// 将数据权限过滤条件设置为空
baseEntity.getParams().put(DATA_SCOPE, "");
}
}
}
提示:仅实体继承
BaseEntity
才会进行处理,SQL
语句会存放到BaseEntity
对象中的params
属性中,然后在xml
中通过${params.dataScope}
获取拼接后的语句。
改造
需求
我们有一个系统登录日志,里面记录了所有用户的登录信息。
但是,并不是所有人都应该看到所有的日志数据。所以,我们需要根据用户的角色来控制他们能查看的数据范围。
添加权限注解
在dkd-system
模块的com.dkd.system.service.impl.SysLogininforServiceImpl
在服务层的方法上使用 @DataScope
注解
1 | /** |
添加表字段
如果sys_logininfo
业务表需要实现数据权限,需要有dept_id
和user_id
这两个字段。
1 | -- prompt:你是一名软件工程师,请为sys_logininfor表添加dept_id和user_id二个字段和注释,类型为bigint,生成sql语句 |
添加实体属性
在dkd-system
模块的com.dkd.system.domain.SysLogininfor
实体类中,需要有deptId
和userId
这两个属性。
1 | package com.dkd.system.domain; |
修改映射文件
在dkd-system
模块的com.dkd.system.domain.SysLogininforMapper.xml
映射文件中,通过动态拼接 SQL,实现数据范围的过滤
1 | <insert id="insertLogininfor" parameterType="SysLogininfor"> |
异步工厂调整
在dkd-framework
模块的com.dkd.framework.manager.factory.AsyncFactory
异步工厂创建登录日志任务时,需要有deptId
和userId
这两个属性。
1 | /** |