Compare commits

...

152 Commits

Author SHA1 Message Date
数据小王子
5476f543f3
!10 fix: 修复swagger definition失效,展示全部接口的问题
Merge pull request !10 from objcfeng/cf/fix/springdoc-config
2025-02-24 01:25:10 +00:00
objcfeng
c820bb4e74 fix: 修复swagger definition失效,展示全部接口的问题 2025-02-22 11:07:55 +08:00
数据小王子
d4f30e7de7 mybatis-flex升级:1.9.3 => 1.9.4 2024-07-29 09:52:08 +08:00
数据小王子
384a3f9b92
!9 新增ListenerManager用以任务调度、三方接口回调等可自由控制审计字段
Merge pull request !9 from Ice/master
2024-07-01 01:31:43 +00:00
Ice
1e9abcb3f5 add:ListenerManager,用以任务调度及三方接口回调情况下自由控制审计字段(目前会因拿不到上下文用户信息将自填充的审计字段改为null值) 2024-06-27 18:24:05 +08:00
数据小王子
ee3948d8e3 mybatis-flex升级:1.9.0 => 1.9.3 2024-06-18 11:43:03 +08:00
数据小王子
4117bd6cc5
!8 修复可能导致同步套餐失败的bug
Merge pull request !8 from xiaoduan/修复可能导致同步套餐失败的bug
2024-06-18 03:38:05 +00:00
xuhaoran
0cbeb5b2df 修复可能导致同步套餐失败的bug 2024-06-07 11:09:07 +08:00
数据小王子
335f806566 mybatis-flex升级:1.8.9 => 1.9.0 2024-05-31 09:08:21 +08:00
数据小王子
8655030c7b
!7 pagehelper多数据源自动识别对应方言的分页
Merge pull request !7 from 秦白起/Pull-Request
2024-05-15 09:10:59 +00:00
GeHui
2635d74af5 ⚙️ config(common): pagehelper多数据源自动识别对应方言的分页
1.yaml里的配置是pagehelper-spring-boot-starter
  的,对pagehelper来说没用
2.使用配置类PagehelperConfig

(cherry picked from commit 2303b18a78679a3211f1caf620eb2adbdfd3cedc)
(cherry picked from commit f066ec91bc93f5a81aa7f826d227fb1b4d3fed04)
2024-05-15 15:40:49 +08:00
数据小王子
da91600948 sa-token升级:1.37.0 -> 1.38.0 2024-05-13 16:27:42 +08:00
数据小王子
08cda7b792 mybatis-flex升级:1.8.8 => 1.8.9 2024-05-11 17:52:55 +08:00
数据小王子
a0957a76c1
!6 sql脚本注释规范,避免执行出错
Merge pull request !6 from 齐家/master
2024-05-10 01:19:32 +00:00
slp
fe227e62cd sql注释规范化,否则某些软件执行时不识别会报错 2024-05-09 16:56:22 +08:00
数据小王子
502b692fee OssClient升级 2024-04-30 15:36:52 +08:00
数据小王子
d28e5fadb5 排除定时任务 2024-04-26 16:48:48 +08:00
数据小王子
935da2f093 spring-boot升级:v3.2.4 => 3.2.5 2024-04-21 08:06:16 +08:00
数据小王子
e78c09ddbe mybatis-flex升级:1.8.7 => 1.8.8 2024-04-19 09:20:53 +08:00
数据小王子
ae9c718ddd edit_columns字段添加注释 2024-04-17 09:06:13 +08:00
数据小王子
844aa24b43
!5 代码生成页面的编辑页列数允许多列
Merge pull request !5 from 晨曦/0416
2024-04-16 11:34:16 +00:00
晨曦
d86def6036 编辑页的列数由原先1列调整为多(1-4)列 2024-04-16 18:57:41 +08:00
晨曦
108bd8e7c4 编辑页的列数由原先1列调整为多(1-4)列 2024-04-16 17:39:27 +08:00
晨曦
c8738f6eb0 编辑页的列数由原先1列调整为多(1-4)列 2024-04-16 15:21:43 +08:00
晨曦
6174ca90e1 编辑页的列数由原先1列调整为多(1-4)列 2024-04-16 15:20:55 +08:00
数据小王子
3d7fd649fd 代码生成器增加导入功能 2024-04-12 21:33:42 +08:00
数据小王子
4224ffdc64 演示模块增加导入功能 2024-04-12 21:27:02 +08:00
数据小王子
0b3f82660f 更新基类TreeEntity 2024-04-12 21:17:53 +08:00
数据小王子
8230404035 树表中增加ancestors字段 2024-04-12 21:14:12 +08:00
数据小王子
4c5bb90361 字典类型添加排序字段 2024-04-11 11:55:19 +08:00
数据小王子
15b8251601 对type、status、gender:代码生成器不再自动为HtmlType赋予默认值,让用户自己选择 2024-04-10 17:00:11 +08:00
数据小王子
0e86f9a111 mybatis-flex升级:1.8.6 => 1.8.7 2024-04-10 16:43:29 +08:00
数据小王子
5de6e70ca1 更新开发文档 2024-04-09 10:48:56 +08:00
数据小王子
ee3272918b 引入另一个任务调度框架:EasyRetry 2024-04-08 21:08:10 +08:00
数据小王子
2219f02a17
!3 针对加密和docker部署
Merge pull request !3 from Duke_yzl/master
2024-04-08 12:17:08 +00:00
yuzl6
4606755e42 复原 2024-04-08 18:39:12 +08:00
yuzl6
d057d654fa 测试生产bug 2024-04-08 17:59:03 +08:00
数据小王子
8263a252f1 分离PowerJob的sql成单独文件 2024-04-08 14:12:02 +08:00
yuzl6
2c98a64d17 Merge remote-tracking branch 'origin/master' 2024-04-08 10:51:49 +08:00
yuzl6
2c721c0a09 增加请求动态加密,docker部署(适配流水线方式推送镜像) 2024-04-08 10:51:31 +08:00
yuzl6
8555e6d684 限制内存使用大小003 2024-04-08 10:35:07 +08:00
yuzl6
70d2864488 限制内存使用大小003 2024-04-08 10:15:18 +08:00
yuzl6
6517cdd084 限制内存使用大小002 2024-04-08 09:59:29 +08:00
yuzl6
226d18b627 限制内存使用大小001 2024-04-08 09:33:03 +08:00
yuzl6
0d91f15061 限制内存使用大小001 2024-04-08 09:14:55 +08:00
yuzl6
54b134e942 限制内存使用大小 2024-04-08 09:08:29 +08:00
yuzl6
a53709481c 部署文件 2024-04-07 23:33:43 +08:00
yuzl6
7a67dce923 部署文件 2024-04-07 20:57:27 +08:00
yuzl6
048fb6abcb 部署文件 2024-04-07 00:05:57 +08:00
yuzl6
3573a71af3 部署文件 2024-04-06 23:59:03 +08:00
yuzl6
41ea876a96 部署文件 2024-04-06 23:58:27 +08:00
yuzl6
d494c551d5 部署文件 2024-04-06 20:03:08 +08:00
yuzl6
445ef5763d 部署文件 2024-04-06 17:08:40 +08:00
yuzl6
92b3efb87d 部署文件 2024-04-06 15:36:38 +08:00
yuzl6
b07033ba51 部署文件 2024-04-06 15:25:47 +08:00
yuzl6
fad4f8e19e 部署文件 2024-04-06 15:18:52 +08:00
yuzl6
0b99d54edf 修改发布环境 2024-04-06 14:53:21 +08:00
yuzl6
596cf10a1c 增加请求加密 动态获取密钥 2024-04-06 14:31:25 +08:00
数据小王子
659caa1d0e
!2 修复接口文档访问错误的BUG
Merge pull request !2 from Curtion/master
2024-04-05 03:44:48 +00:00
Curtion
4358633176 修复接口文档访问错误的BUG 2024-04-04 19:00:14 +08:00
数据小王子
9660f65dad mybatis-flex升级:1.8.5 => 1.8.6 2024-04-03 08:38:17 +08:00
数据小王子
f57b1092e4 mybatis-flex升级:1.8.2 => 1.8.5 2024-04-02 08:50:17 +08:00
数据小王子
00e3ee95c3 依赖更新:
update springboot 3.2.3 => 3.2.4
update springdoc 2.3.0 => 2.4.0
update springboot-admin 3.2.2 => 3.2.3
update redisson 3.27.0 => 3.27.2
update sms4j 3.1.1 => 3.2.0
update hutool 5.8.26 => 5.8.27
2024-04-01 08:58:13 +08:00
数据小王子
e5b182a8ef 更新ip离线数据库 2024-04-01 08:57:33 +08:00
数据小王子
458f60be07 代码生成:新增导入菜单SQL脚本 2024-03-30 17:04:20 +08:00
数据小王子
32fd0311b3 更新AWS SDK 版本到2.25.15 2024-03-25 09:07:24 +08:00
数据小王子
d60ca890a2 代码生成:完善ts前端单表生成vue页面 2024-03-25 09:02:22 +08:00
数据小王子
9d6fc4f539 代码生成器ts前端:树表增加导出按钮 2024-03-22 21:18:55 +08:00
数据小王子
f3b3bec8b0 完善代码生成器ts前端类型文件 2024-03-22 21:08:15 +08:00
数据小王子
bcd06f3d2b 数据库脚本:超级管理员的role_key由admin修改为SuperAdminRole 2024-03-12 11:15:23 +08:00
数据小王子
f8c699ebbd 数据库脚本更新:超级管理员的role_key由admin修改为SuperAdminRole 2024-03-12 11:12:50 +08:00
数据小王子
c91f9686d7 update 更新 mybatis 多包扫描配置 2024-03-06 10:41:21 +08:00
数据小王子
7fb5c26cee 依赖升级:spring-boot-admin 3.2.1 => 3.2.2 2024-03-06 10:35:12 +08:00
数据小王子
c1fb7b4484 优化 AsyncConfig 虚拟线程名称支持 2024-03-06 10:31:03 +08:00
数据小王子
918d67abef redisson支持虚拟线程 2024-03-06 10:27:43 +08:00
数据小王子
a2e1acb66c 修复 空指针null问题 2024-03-06 10:23:22 +08:00
数据小王子
050a48b134 优化 mybatis依赖设置为可选依赖 避免出现不应该注入的情况 2024-03-06 10:20:24 +08:00
数据小王子
b8ab413eab 优化RateLimiter注解使用体验 2024-03-06 10:14:00 +08:00
数据小王子
7c649bb5b2 依赖升级:mybatis-flex 1.8.1 => 1.8.2 2024-03-06 10:07:21 +08:00
数据小王子
c9cb308abf 优化 GET 方法响应体支持加密 2024-03-06 09:29:10 +08:00
数据小王子
0974e65c0a 删除观测用日志记录 2024-03-06 09:01:26 +08:00
数据小王子
fa75c7155a 新增:用户、部门、角色、岗位 下拉选接口与代码实现优化 2024-03-06 08:53:11 +08:00
数据小王子
2c180c89a9 依赖升级:mybatis-flex 1.8.0 => 1.8.1 2024-03-04 11:04:42 +08:00
数据小王子
f31983c2c7 修复 excel 表达式字典 下拉框导出格式错误 2024-03-01 15:34:44 +08:00
数据小王子
62c8e1c877 优化 OssFactory,采用双重校验锁 2024-03-01 15:29:44 +08:00
数据小王子
3bedaa7ddc 优化代码格式 2024-03-01 10:31:39 +08:00
数据小王子
e7b6dac49f 优化 登录消息 支持集群发送 2024-03-01 10:00:56 +08:00
数据小王子
d151c053a7 升级 awsS3 到2.X版本 支持异步与自动分片上传下载 2024-02-29 14:15:17 +08:00
数据小王子
fc2eab2a6d 修改登录问题 2024-02-29 14:09:15 +08:00
数据小王子
e28a2a404e 新增 正则工具类 字符串提取 字符串校验 2024-02-29 11:05:33 +08:00
数据小王子
773e257fa1 增加 SpringUtils.isVirtual 方法 2024-02-29 10:30:11 +08:00
数据小王子
651da9a92a 修复 类型判断问题 2024-02-29 10:16:58 +08:00
数据小王子
e5ca734018 修改数据库id_token字段宽度 2024-02-29 10:01:07 +08:00
数据小王子
5b131ad3fd fix 修复 用户登录查询部门缓存无法获取租户id问题 2024-02-28 14:54:56 +08:00
数据小王子
fe848f418a fix: LoginHelper类 login方法 存在 重复代码 2024-02-28 14:52:31 +08:00
数据小王子
6d484c0bda 引入caffeine本地缓存 2024-02-28 14:40:14 +08:00
数据小王子
f366e430f1 依赖升级:poi 5.2.3 => 5.2.5 2024-02-28 09:10:53 +08:00
数据小王子
7e9ba78376 Merge remote-tracking branch 'origin/master' 2024-02-27 17:32:10 +08:00
数据小王子
5a814b947c 重构登录日志 2024-02-27 17:31:16 +08:00
数据小王子
0af60f5ac5
!1 update script/sql/postgresql/postgresql-ruoyiflex-V5.X.sql.
Merge pull request !1 from yuzhihaomy/N/A
2024-02-27 09:15:56 +00:00
yuzhihaomy
6b8f3e2ed1
update script/sql/postgresql/postgresql-ruoyiflex-V5.X.sql.
793行sql少了个逗号

Signed-off-by: yuzhihaomy <17150515155@163.com>
2024-02-27 09:09:20 +00:00
数据小王子
3f829e271d 解决 token与token-session 过期时间不一致问题 2024-02-27 10:48:08 +08:00
数据小王子
8027f5c2d8 调整transmittable-thread-local依赖位置 2024-02-26 17:31:53 +08:00
数据小王子
29a620eb41 修复 部门树排序问题 2024-02-26 17:24:56 +08:00
数据小王子
885918d11f 依赖HikariCP连接池:5.0.1 => 5.1.0 2024-02-26 11:27:25 +08:00
数据小王子
6d31a7107b 依赖升级:mybatis-flex 1.7.9 => 1.8.0 2024-02-26 09:19:11 +08:00
数据小王子
29ec129069 优化代码生成提交数据提示语 2024-02-23 16:17:07 +08:00
数据小王子
3f3f3d793a 依赖升级:spring-boot 3.2.2 => 3.2.3
hutool 5.8.25 => 5.8.26
   redisson 3.26.0 => 3.27.0
2024-02-23 15:39:39 +08:00
数据小王子
2b7ba78eb1 修改版本号:V5.2.0-SNAPSHOT 2024-02-23 15:33:22 +08:00
数据小王子
baf6140159 修改版本号为5.1.0 2024-02-08 19:35:35 +08:00
数据小王子
ba11dd8927 默认租户的tenant_id由0修改为1 2024-02-07 11:57:10 +08:00
数据小王子
3734a6ef23 数据库内容修改:默认租户的tenant_id由0修改为1 2024-02-07 11:54:27 +08:00
数据小王子
a601c8c54c 修复租户套餐管理无法删除的错误 2024-02-06 14:49:45 +08:00
数据小王子
9e4171f4ad 用户服务用Relations注解查询取代Join查询 2024-02-06 14:48:25 +08:00
dataprince
254dfe383b 升级mybatis-flex依赖:1.7.8 => 1.7.9 2024-02-04 14:50:03 +08:00
dataprince
a2c31048cb 添加spring-boot-starter-validation依赖 2024-02-02 21:42:26 +08:00
dataprince
fad44cc117 升级mybatis-flex依赖:1.7.7 => 1.7.8 2024-02-02 14:46:06 +08:00
dataprince
e0e190671f 修正无法修改租户的问题 2024-02-01 21:03:12 +08:00
dataprince
5e8a8f6cd9 代码生成模块导入子表类型视图对象 2024-02-01 21:01:54 +08:00
dataprince
09157fe27f 更改子表类型 2024-01-31 19:33:33 +08:00
dataprince
1183249080 代码生成支持:主子表(element-ts版本) 2024-01-31 15:43:21 +08:00
dataprince
ee7ef99c36 增加离线ip库 2024-01-29 16:49:41 +08:00
dataprince
3ae17b0657 集成JustAuth第三方登录组件 2024-01-29 15:56:48 +08:00
dataprince
84be51d691 数据库增加sys_social表 2024-01-29 14:41:27 +08:00
dataprince
e6b877366e 修正代码生成无法修改的错误 2024-01-29 14:20:43 +08:00
dataprince
e112a7b5fe 集成sms4j短信聚合框架 2024-01-27 10:57:32 +08:00
dataprince
677e5e6168 增加ruoyi-common-mail模块 2024-01-26 09:09:09 +08:00
dataprince
da582764a7 代码生成支持乐观锁、多UI前端 2024-01-25 11:48:45 +08:00
dataprince
4ee432bfee 数据库结构更新:子表mf_goods添加乐观锁字段 2024-01-25 11:41:04 +08:00
dataprince
eda8a2d33b 升级依赖:springboot 3.2.1 => 3.2.2、springboot-admin 3.2.0 => 3.2.1 2024-01-22 14:18:18 +08:00
dataprince
1d0399b9b0 增加findbugs js305依赖,消除打包警告 2024-01-22 11:35:53 +08:00
dataprince
b0204c224c 租户管理支持乐观锁 2024-01-19 21:34:11 +08:00
dataprince
ee8f101807 updateConfigByKey支持乐观锁 2024-01-19 11:40:11 +08:00
dataprince
6a6bf19db5 文档添加插件Maven Project Version描述 2024-01-18 17:26:25 +08:00
dataprince
32ef43f198 修正R.fail忘记添加return的bug 2024-01-18 17:22:06 +08:00
dataprince
b06857e8c8 通知公告模块使用websocket推送消息 2024-01-17 20:24:48 +08:00
dataprince
74853f4d9c 增加websocket模块 2024-01-17 20:24:09 +08:00
dataprince
17eb0212fd 修正修改PostgreSQL数据库结构:由integer修改为text类型。 2024-01-17 09:48:06 +08:00
dataprince
52ea5f2e2a 个人中心修改密码使用密文传输 2024-01-16 10:17:18 +08:00
dataprince
8f1ce9a5df 完善API加解密 2024-01-16 08:49:04 +08:00
dataprince
2868e20c86 升级依赖mapstruct-plus:1.3.5——>1.3.6 2024-01-16 08:47:07 +08:00
dataprince
54d4b8bf16 升级依赖pagehelper:5.3.3——>6.1.0 2024-01-15 11:10:14 +08:00
dataprince
14740e817e 删除废弃TaskExecutorBuilder方法 2024-01-15 09:44:05 +08:00
dataprince
71d1d7bd77 启用虚拟线程后,不再需要原来的线程池功能了 2024-01-14 10:35:31 +08:00
dataprince
c39403a0d2 Revert "web容器从undertow切换到tomcat,更好地支持虚拟线程"
This reverts commit 9c8de8afbf.
2024-01-14 10:22:02 +08:00
dataprince
9c8de8afbf web容器从undertow切换到tomcat,更好地支持虚拟线程 2024-01-14 10:19:30 +08:00
dataprince
a84a3c6049 web容器从undertow切换到tomcat,更好地支持虚拟线程 2024-01-14 08:54:22 +08:00
dataprince
9f4991f32f 打印SQL配置参数化 2024-01-13 08:56:05 +08:00
dataprince
d09b65c396 yml配置文件添加swagger-ui的版本号配置 2024-01-12 16:59:12 +08:00
dataprince
9b1602649e 去掉@MapperScan配置 2024-01-12 16:39:12 +08:00
dataprince
ec9ff68567 完善文档,增加“演示模块”说明 2024-01-12 09:30:40 +08:00
dataprince
58c62ee6b4 升级springdoc依赖到V2.3.0 2024-01-11 11:08:55 +08:00
255 changed files with 15434 additions and 3647 deletions

View File

@ -1,13 +1,13 @@
<p align="center">
<img alt="logo" src="https://gitee.com/dataprince/ruoyi-flex/raw/master/image/ruoyi-flex-logo.png">
</p>
<h1 align="center" style="margin: 30px 0 30px; font-weight: bold;">Ruoyi-Flex V5.0.0</h1>
<h1 align="center" style="margin: 30px 0 30px; font-weight: bold;">Ruoyi-Flex V5.2.0-SNAPSHOT</h1>
<h4 align="center">Ruoyi-Flex是基于JDK21、Spring Boot V3.2.X+平台 前后端分离的未来8年更快的Java开发框架</h4>
## 1、平台简介
Ruoyi-Flex是一套全部开源的快速开发平台针对”分布式集群与多租户“场景全方位升级使用MIT开源许可协议毫无保留给个人及企业免费使用。基于RuoYi-Vue、RuoYi-Vue-Plus集成MyBatis-Flex、JDK21、SpringBootV3.2.X+、Lombok、Sa-Token、SpringDoc、Hutool、SpringBoot Admin、PowerJob、Vue3、Element-Plus、MinIO等优秀开源软件支持PostgreSQL、MySQL开源数据库及其衍生分布式数据库。
Ruoyi-Flex是一套全部开源的快速开发平台针对”分布式集群与多租户“场景全方位升级使用MIT开源许可协议毫无保留给个人及企业免费使用。基于RuoYi-Vue、RuoYi-Vue-Plus集成MyBatis-Flex、JDK21、SpringBootV3.2.X+、Lombok、Sa-Token、SpringDoc、Hutool、SpringBoot Admin、EasyRetry、PowerJob、Vue3、Element-Plus、AntDesign-Vben、MinIO、Flowable等优秀开源软件支持PostgreSQL、MySQL开源数据库及其衍生分布式数据库。
## 2、系统特色
Ruoyi-Flex秉承“写的更少、性能更好、出错更低、交流通畅、快速入门” 的理念,为您带来全方位的赋能与提升:
@ -28,33 +28,53 @@ Ruoyi-Flex秉承“写的更少、性能更好、出错更低、交流通畅、
### 4交流通畅
“非我族类其心必异”。Ruoyi-Flex集成了一大波国产开源软件MyBatis-Flex、Sa-Token、Hutool、PowerJob、Element-Plus等同根同源交流自然顺畅开发中遇到问题可联系作者快速得到解决。例如同一个领域的安全框架一个中国人只需半天就可学会Sa-Token干活如果是学Spring Security的话七天也不一定能学会。
### 5入门快速
### 5多端同步
Ruoyi-Flex提供“1+3”端1个后台端、3个前台端熟悉js的可使用flex-elementplus-ui前端熟悉ts的可使用ruoyiflex-elementplus-ts前端既熟悉ts又熟悉antdesign的请使用ruoyiflex-antdesign-vben前端总有一款适合您的前端供您选择
### 6入门快速
Ruoyi-Flex已集成各种开源开发框架扫平了技术障碍可直接上手干活。使用者只需要设计好数据库表结构系统能可视化生成前后端本地代码单表、树表、主子表任你选10分钟就能开发一个模块快速入门开发高效。
## 3、前端项目
Ruoyi-Flex实行前后端分离仓库本项目是java后端部分前端项目是flex-elementplus-ui前端项目地址: [flex-elementplus-ui](https://gitee.com/dataprince/flex-elementplus-ui)
Ruoyi-Flex实行前后端分离仓库本项目是java后端部分目前有3个前端项目
### 1ruoyiflex-elementplus-ts
使用elementplus、typescript构建项目地址: [ruoyiflex-elementplus-ts](https://gitee.com/dataprince/ruoyiflex-elementplus-ts)
### 2ruoyiflex-antdesign-vben
使用antdesign、vben、typescript构建项目地址: [ruoyiflex-antdesign-vben](https://gitee.com/dataprince/ruoyiflex-antdesign-vben)
### 3flex-elementplus-ui
使用elementplus、js构建项目地址: [flex-elementplus-ui](https://gitee.com/dataprince/flex-elementplus-ui)
## 4、内置功能
1. 用户管理:用户是系统操作者,该功能主要完成系统用户配置。
2. 部门管理:配置系统组织机构(公司、部门、小组),树结构展现支持数据权限。
3. 岗位管理:配置系统用户所属担任职务。
4. 菜单管理:配置系统菜单,操作权限,按钮权限标识等。
5. 角色管理:角色菜单权限分配、设置角色按机构进行数据范围权限划分。
6. 字典管理:对系统中经常使用的一些较为固定的数据进行维护。
7. 参数管理:对系统动态配置常用参数。
8. 通知公告:系统通知公告信息发布维护。
9. 操作日志:系统正常操作日志记录和查询;系统异常信息日志记录和查询。
10. 登录日志:系统登录日志记录查询包含登录异常。
11. 文件管理引入云存储服务将文件存储到MinIO、七牛、阿里、腾讯等OSS服务器上支持上传、下载。
12. 在线用户:当前系统中活跃用户状态监控。
13. 调度中心集成PowerJob全新一代分布式任务调度与计算框架。
14. 代码生成前后端代码的生成java、html、vue、js支持单表、树表、主子表减少70%以上的开发工作量。
15. 系统接口集成springdoc根据文档注释自动生成相关的api接口文档。
16. 监控中心集成Spring Boot Admin监视集群系统CPU、内存、磁盘、堆栈、在线日志、Spring相关配置等。
17. 缓存监控:对系统的缓存信息查询,命令统计等。
18. 后台数据库支持PostgreSQL、MySQL开源数据库及其衍生分布式数据库。
19. 演示模块mybatis、mybatis-flex两种格式代码的单表、树表、主子表三种类型的演示程序。
1. 租户管理:系统内租户的管理 如:租户套餐、过期时间、用户数量、企业信息等。
2. 租户套餐管理:系统内租户所能使用的套餐管理 如:套餐内所包含的菜单等。
3. 客户端管理:系统内对接的所有客户端管理 如: pc端、小程序端等支持动态授权登录方式 如: 短信登录、密码登录等 支持动态控制token时效。
4. 用户管理:用户是系统操作者,该功能主要完成系统用户配置。
5. 部门管理:配置系统组织机构(公司、部门、小组),树结构展现支持数据权限。
6. 岗位管理:配置系统用户所属担任职务。
7. 菜单管理:配置系统菜单,操作权限,按钮权限标识等。
8. 角色管理:角色菜单权限分配、设置角色按机构进行数据范围权限划分。
9. 字典管理:对系统中经常使用的一些较为固定的数据进行维护。
10. 参数管理:对系统动态配置常用参数。
11. 通知公告:系统通知公告信息发布维护。
12. 操作日志:系统正常操作日志记录和查询;系统异常信息日志记录和查询。
13. 登录日志:系统登录日志记录查询包含登录异常。
14. 文件管理引入云存储服务将文件存储到MinIO、七牛、阿里、腾讯等OSS服务器上支持上传、下载。
15. 在线用户:当前系统中活跃用户状态监控。
16. 调度中心集成PowerJob全新一代分布式任务调度与计算框架。
17. 代码生成前后端代码的生成java、html、vue、js支持单表、树表、主子表减少70%以上的开发工作量。
18. 系统接口集成springdoc根据文档注释自动生成相关的api接口文档。
19. 监控中心集成Spring Boot Admin监视集群系统CPU、内存、磁盘、堆栈、在线日志、Spring相关配置等。
20. 缓存监控:对系统的缓存信息查询,命令统计等。
21. 后台数据库支持PostgreSQL、MySQL开源数据库及其衍生分布式数据库。
22. 演示模块mybatis、mybatis-flex两种格式代码的单表、树表、主子表三种类型的演示程序。
23. 实现多租户功能。
24. 实现乐观锁功能。
25. 实现逻辑删除功能。
26. 启用JAVA21虚拟线程、分代ZGC功能。
27. 实现API接口加密功能密码使用密文传输。
## 5、演示图

Binary file not shown.

BIN
doc/~$oyi-Flex-Guide.docx Normal file

Binary file not shown.

135
pom.xml
View File

@ -13,54 +13,64 @@
<description>Ruoyi-Flex管理系统</description>
<properties>
<revision>5.0.0</revision>
<revision>5.2.0-SNAPSHOT</revision>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>21</java.version>
<spring-boot.version>3.2.1</spring-boot.version>
<mybatis-flex.version>1.7.7</mybatis-flex.version>
<satoken.version>1.37.0</satoken.version>
<maven-jar-plugin.version>3.1.1</maven-jar-plugin.version>
<HikariCP.version>5.0.1</HikariCP.version>
<spring-boot.version>3.2.5</spring-boot.version>
<mybatis-flex.version>1.9.4</mybatis-flex.version>
<satoken.version>1.38.0</satoken.version>
<HikariCP.version>5.1.0</HikariCP.version>
<bitwalker.version>1.21</bitwalker.version>
<caffeine.version>3.1.8</caffeine.version>
<kaptcha.version>2.3.3</kaptcha.version>
<pagehelper.version>5.3.3</pagehelper.version>
<pagehelper.version>6.1.0</pagehelper.version>
<fastjson.version>2.0.43</fastjson.version>
<oshi.version>6.4.8</oshi.version>
<commons.collections.version>3.2.2</commons.collections.version>
<poi.version>5.2.3</poi.version>
<poi.version>5.2.5</poi.version>
<easyexcel.version>3.3.3</easyexcel.version>
<velocity.version>2.3</velocity.version>
<jwt.version>0.9.1</jwt.version>
<servlet-api.version>6.0.0</servlet-api.version>
<guava.version>32.1.1-jre</guava.version>
<flatten-maven-plugin.version>1.5.0</flatten-maven-plugin.version>
<springdoc.version>2.3.0</springdoc.version>
<springdoc-openapi-starter-common.version>2.2.0</springdoc-openapi-starter-common.version>
<springdoc.version>2.4.0</springdoc.version>
<springdoc-openapi-starter-common.version>2.4.0</springdoc-openapi-starter-common.version>
<therapi-runtime-javadoc.version>0.15.0</therapi-runtime-javadoc.version>
<snakeyaml.version>2.2</snakeyaml.version>
<lombok.version>1.18.30</lombok.version>
<mapstruct-plus.version>1.3.5</mapstruct-plus.version>
<mapstruct-plus.version>1.3.6</mapstruct-plus.version>
<mapstruct-plus.lombok.version>0.2.0</mapstruct-plus.lombok.version>
<hutool.version>5.8.24</hutool.version>
<redisson.version>3.25.2</redisson.version>
<lock4j.version>2.2.4</lock4j.version>
<hutool.version>5.8.27</hutool.version>
<redisson.version>3.27.2</redisson.version>
<lock4j.version>2.2.7</lock4j.version>
<alibaba-ttl.version>2.14.4</alibaba-ttl.version>
<spring-boot-admin.version>3.2.0</spring-boot-admin.version>
<spring-boot-admin.version>3.2.3</spring-boot-admin.version>
<powerjob.version>4.3.6</powerjob.version>
<easyretry.version>3.2.0</easyretry.version>
<!-- 离线IP地址定位库 -->
<ip2region.version>2.7.0</ip2region.version>
<!-- OSS 配置 -->
<aws-java-sdk-s3.version>1.12.600</aws-java-sdk-s3.version>
<aws.sdk.version>2.25.15</aws.sdk.version>
<aws.crt.version>0.29.13</aws.crt.version>
<!-- 加解密依赖库 -->
<bcprov-jdk.version>1.77</bcprov-jdk.version>
<!-- SMS 配置 -->
<sms4j.version>3.2.0</sms4j.version>
<!-- findbugs消除打包警告 -->
<jsr305.version>3.0.2</jsr305.version>
<!-- 三方授权认证 -->
<justauth.version>1.16.6</justauth.version>
<!-- 插件版本 -->
<maven-jar-plugin.version>3.2.2</maven-jar-plugin.version>
<maven-war-plugin.version>3.2.2</maven-war-plugin.version>
<maven-compiler-plugin.verison>3.11.0</maven-compiler-plugin.verison>
<maven-surefire-plugin.version>3.1.2</maven-surefire-plugin.version>
<flatten-maven-plugin.version>1.3.0</flatten-maven-plugin.version>
<flatten-maven-plugin.version>1.5.0</flatten-maven-plugin.version>
<!--工作流配置-->
<flowable.version>7.0.0</flowable.version>
</properties>
<profiles>
@ -97,6 +107,13 @@
<scope>import</scope>
</dependency>
<!-- validation检验-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<!-- hutool 的依赖配置-->
<dependency>
<groupId>cn.hutool</groupId>
@ -106,6 +123,15 @@
<scope>import</scope>
</dependency>
<!-- flowable 的依赖配置-->
<dependency>
<groupId>org.flowable</groupId>
<artifactId>flowable-bom</artifactId>
<version>${flowable.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- common 的依赖配置-->
<dependency>
<groupId>com.ruoyi</groupId>
@ -161,6 +187,13 @@
<version>${satoken.version}</version>
</dependency>
<!-- caffeine缓存 -->
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>${caffeine.version}</version>
</dependency>
<!-- servlet包 -->
<dependency>
<groupId>jakarta.servlet</groupId>
@ -321,11 +354,23 @@
<version>${ip2region.version}</version>
</dependency>
<!-- OSS 配置 -->
<!-- AWS SDK for Java 2.x -->
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-java-sdk-s3</artifactId>
<version>${aws-java-sdk-s3.version}</version>
<groupId>software.amazon.awssdk</groupId>
<artifactId>s3</artifactId>
<version>${aws.sdk.version}</version>
</dependency>
<!-- 使用AWS基于 CRT 的 S3 客户端 -->
<dependency>
<groupId>software.amazon.awssdk.crt</groupId>
<artifactId>aws-crt</artifactId>
<version>${aws.crt.version}</version>
</dependency>
<!-- 基于 AWS CRT 的 S3 客户端的性能增强的 S3 传输管理器 -->
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>s3-transfer-manager</artifactId>
<version>${aws.sdk.version}</version>
</dependency>
<!-- 加解密依赖库 -->
@ -335,6 +380,20 @@
<version>${bcprov-jdk.version}</version>
</dependency>
<!-- findbugs消除打包警告 -->
<dependency>
<groupId>com.google.code.findbugs</groupId>
<artifactId>jsr305</artifactId>
<version>${jsr305.version}</version>
</dependency>
<!-- JustAuth 的依赖配置-->
<dependency>
<groupId>me.zhyd.oauth</groupId>
<artifactId>JustAuth</artifactId>
<version>${justauth.version}</version>
</dependency>
<!--Annotation Processor-->
<dependency>
<groupId>org.springframework.boot</groupId>
@ -348,6 +407,13 @@
<version>${alibaba-ttl.version}</version>
</dependency>
<!--短信sms4j-->
<dependency>
<groupId>org.dromara.sms4j</groupId>
<artifactId>sms4j-spring-boot-starter</artifactId>
<version>${sms4j.version}</version>
</dependency>
<!-- spring-boot-admin监控-->
<dependency>
<groupId>de.codecentric</groupId>
@ -379,6 +445,23 @@
<version>${revision}</version>
</dependency>
<!-- EasyRetry Client -->
<dependency>
<groupId>com.aizuda</groupId>
<artifactId>easy-retry-client-starter</artifactId>
<version>${easyretry.version}</version>
</dependency>
<dependency>
<groupId>com.aizuda</groupId>
<artifactId>easy-retry-client-core</artifactId>
<version>${easyretry.version}</version>
</dependency>
<dependency>
<groupId>com.aizuda</groupId>
<artifactId>easy-retry-client-job-core</artifactId>
<version>${easyretry.version}</version>
</dependency>
<!-- 代码生成-->
<dependency>
<groupId>com.ruoyi</groupId>
@ -393,7 +476,6 @@
<version>${revision}</version>
</dependency>
<!-- demo模块 -->
<dependency>
<groupId>com.ruoyi</groupId>
@ -401,6 +483,13 @@
<version>${revision}</version>
</dependency>
<!-- 工作流模块 -->
<dependency>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi-workflow</artifactId>
<version>${revision}</version>
</dependency>
</dependencies>
</dependencyManagement>

8
ruoyi-admin/Dockerfile Normal file
View File

@ -0,0 +1,8 @@
# 使用官方的 Java 运行时作为父镜像
FROM registry.cn-qingdao.aliyuncs.com/yuzl1/jdk:21
# 将本地文件复制到容器中
COPY target/ruoyi-admin.jar /ruoyi-admin.jar
# 运行应用
ENTRYPOINT ["java","-jar","/ruoyi-admin.jar"]

View File

@ -31,6 +31,12 @@
<artifactId>mysql-connector-j</artifactId>
</dependency>
<!-- 三方授权认证 -->
<dependency>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi-common-social</artifactId>
</dependency>
<!-- PostgreSql -->
<dependency>
<groupId>org.postgresql</groupId>
@ -72,11 +78,18 @@
<artifactId>spring-boot-admin-starter-client</artifactId>
</dependency>
<!-- powerjob 客户端 -->
<!-- <dependency>-->
<!-- <groupId>tech.powerjob</groupId>-->
<!-- <artifactId>powerjob-worker-spring-boot-starter</artifactId>-->
<!-- </dependency>-->
<dependency>
<groupId>com.aizuda</groupId>
<artifactId>easy-retry-client-starter</artifactId>
</dependency>
<dependency>
<groupId>com.aizuda</groupId>
<artifactId>easy-retry-client-core</artifactId>
</dependency>
<dependency>
<groupId>com.aizuda</groupId>
<artifactId>easy-retry-client-job-core</artifactId>
</dependency>
</dependencies>

View File

@ -2,19 +2,23 @@ package com.ruoyi;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.data.web.SpringDataWebAutoConfiguration;
import org.springframework.boot.context.metrics.buffering.BufferingApplicationStartup;
/**
* 启动程序
*
* @author ruoyi
*/
@SpringBootApplication
@SpringBootApplication(exclude = SpringDataWebAutoConfiguration.class)
public class RuoYiApplication
{
public static void main(String[] args)
{
SpringApplication.run(RuoYiApplication.class, args);
System.out.println("(♥◠‿◠)ノ゙ RuoYi-Flex启动成功 ლ(´ڡ`ლ)゙ \n" +
SpringApplication application = new SpringApplication(RuoYiApplication.class);
application.setApplicationStartup(new BufferingApplicationStartup(2048));
application.run(args);
System.out.println("(♥◠‿◠)ノ゙ RuoYi-Flex-Boot启动成功 ლ(´ڡ`ლ)゙ \n" +
" ███████ ██ ██ ██ ████████ ██ \n" +
"░██░░░░██ ░░██ ██ ░░ ░██░░░░░ ░██ \n" +
"░██ ░██ ██ ██ ██████ ░░████ ██ ░██ ░██ █████ ██ ██\n" +

View File

@ -5,20 +5,24 @@ import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil;
import com.mybatisflex.core.query.QueryWrapper;
import com.ruoyi.common.core.constant.UserConstants;
import com.ruoyi.common.core.core.domain.AjaxResult;
import com.ruoyi.common.core.core.domain.model.LoginUser;
import com.ruoyi.common.core.core.domain.model.SocialLoginBody;
import com.ruoyi.common.core.utils.*;
import com.ruoyi.common.encrypt.annotation.ApiEncrypt;
import com.ruoyi.common.json.utils.JsonUtils;
import com.ruoyi.common.security.utils.LoginHelper;
import com.ruoyi.common.tenant.helper.TenantHelper;
import com.ruoyi.common.social.config.properties.SocialLoginConfigProperties;
import com.ruoyi.common.social.config.properties.SocialProperties;
import com.ruoyi.common.social.utils.SocialUtils;
import com.ruoyi.common.websocket.dto.WebSocketMessageDto;
import com.ruoyi.common.websocket.utils.WebSocketUtils;
import com.ruoyi.system.domain.bo.SysTenantBo;
import com.ruoyi.system.domain.vo.SysClientVo;
import com.ruoyi.system.domain.vo.SysTenantVo;
import com.ruoyi.system.domain.vo.SysUserVo;
import com.ruoyi.system.service.*;
import com.ruoyi.web.domain.vo.LoginTenantVo;
import com.ruoyi.web.domain.vo.LoginVo;
import com.ruoyi.web.domain.vo.TenantListVo;
import com.ruoyi.web.service.SysRegisterService;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
@ -26,15 +30,19 @@ import lombok.extern.slf4j.Slf4j;
import com.ruoyi.common.core.core.domain.R;
import com.ruoyi.common.core.core.domain.model.LoginBody;
import com.ruoyi.common.core.core.domain.model.RegisterBody;
import com.ruoyi.system.domain.SysClient;
import com.ruoyi.web.service.IAuthStrategy;
import com.ruoyi.web.service.SysLoginService;
import me.zhyd.oauth.model.AuthResponse;
import me.zhyd.oauth.model.AuthUser;
import me.zhyd.oauth.request.AuthRequest;
import me.zhyd.oauth.utils.AuthStateUtils;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.net.URL;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import static com.ruoyi.system.domain.table.SysClientTableDef.SYS_CLIENT;
@ -46,7 +54,6 @@ import static com.ruoyi.system.domain.table.SysClientTableDef.SYS_CLIENT;
*/
@Slf4j
@SaIgnore
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/auth")
@ -57,7 +64,16 @@ public class AuthController {
@Resource
private ISysClientService clientService;
@Resource
private SysRegisterService registerService;
@Resource
private ISysConfigService configService;
@Resource
private ISysTenantService tenantService;
@Resource
private ISysSocialService socialService;
private final ScheduledExecutorService scheduledExecutorService;
private final SocialProperties socialProperties;
/**
* 登录方法
@ -74,7 +90,7 @@ public class AuthController {
String clientId = loginBody.getClientId();
String grantType = loginBody.getGrantType();
QueryWrapper query=QueryWrapper.create().from(SYS_CLIENT).where(SYS_CLIENT.CLIENT_ID.eq(clientId));
SysClient client = clientService.getOne(query);
SysClientVo client = clientService.getOneAs(query,SysClientVo.class);
// 查询不到 client client 内不包含 grantType
if (ObjectUtil.isNull(client) || !StringUtils.contains(client.getGrantType(), grantType)) {
log.info("客户端id: {} 认证类型:{} 异常!.", clientId, grantType);
@ -86,7 +102,67 @@ public class AuthController {
loginService.checkTenant(loginBody.getTenantId());
// 登录
return R.ok(IAuthStrategy.login(body, client, grantType));
LoginVo loginVo =IAuthStrategy.login(body, client, grantType);
Long userId = LoginHelper.getUserId();
scheduledExecutorService.schedule(() -> {
WebSocketMessageDto dto = new WebSocketMessageDto();
dto.setMessage("欢迎登录RuoYi-Flex多租户管理系统");
dto.setSessionKeys(List.of(userId));
WebSocketUtils.publishMessage(dto);
}, 3, TimeUnit.SECONDS);
return R.ok(loginVo);
}
/**
* 第三方登录请求
*
* @param source 登录来源
* @return 结果
*/
@GetMapping("/binding/{source}")
public R<String> authBinding(@PathVariable("source") String source) {
SocialLoginConfigProperties obj = socialProperties.getType().get(source);
if (ObjectUtil.isNull(obj)) {
return R.fail(source + "平台账号暂不支持");
}
AuthRequest authRequest = SocialUtils.getAuthRequest(source, socialProperties);
String authorizeUrl = authRequest.authorize(AuthStateUtils.createState());
return R.ok("操作成功", authorizeUrl);
}
/**
* 第三方登录回调业务处理 绑定授权
*
* @param loginBody 请求体
* @return 结果
*/
@PostMapping("/social/callback")
public R<Void> socialCallback(@RequestBody SocialLoginBody loginBody) {
// 获取第三方登录信息
AuthResponse<AuthUser> response = SocialUtils.loginAuth(
loginBody.getSource(), loginBody.getSocialCode(),
loginBody.getSocialState(), socialProperties);
AuthUser authUserData = response.getData();
// 判断授权响应是否成功
if (!response.ok()) {
return R.fail(response.getMsg());
}
loginService.socialRegister(authUserData);
return R.ok();
}
/**
* 取消授权
*
* @param socialId socialId
*/
@DeleteMapping(value = "/unlock/{socialId}")
public R<Void> unlockSocial(@PathVariable Long socialId) {
Boolean rows = socialService.deleteWithValidById(socialId);
return rows ? R.ok() : R.fail("取消授权失败");
}
/**
@ -103,12 +179,12 @@ public class AuthController {
*/
@PostMapping("/register")
public R<Void> register(@Validated @RequestBody RegisterBody user) {
//if (!configService.selectRegisterEnabled(user.getTenantId())) // TODO注册代码
if (!configService.selectRegisterEnabled(user.getTenantId()))
{
return R.fail("当前系统没有开启注册功能!");
}
// registerService.register(user);
// return R.ok();
registerService.register(user);
return R.ok();
}
/**
@ -134,6 +210,7 @@ public class AuthController {
StringUtils.equals(vo.getDomain(), host));
// 返回对象
LoginTenantVo vo = new LoginTenantVo();
vo.setTenantEnabled(true);
vo.setVoList(CollUtil.isNotEmpty(list) ? list : voList);
return R.ok(vo);
}

View File

@ -2,22 +2,35 @@ package com.ruoyi.web.controller;
import cn.dev33.satoken.annotation.SaIgnore;
import java.time.Duration;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.UUID;
import cn.hutool.captcha.AbstractCaptcha;
import cn.hutool.captcha.generator.CodeGenerator;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.RandomUtil;
import com.ruoyi.common.core.annotation.RateLimiter;
import com.ruoyi.common.core.constant.GlobalConstants;
import com.ruoyi.common.core.core.domain.AjaxResult;
import com.ruoyi.common.core.enums.LimitType;
import com.ruoyi.common.core.utils.StringUtils;
import com.ruoyi.common.core.utils.reflect.ReflectUtils;
import com.ruoyi.common.core.utils.SpringUtils;
import com.ruoyi.common.core.core.domain.R;
import com.ruoyi.common.encrypt.utils.RSAUtils;
import com.ruoyi.common.mail.config.properties.MailProperties;
import com.ruoyi.common.mail.utils.MailUtils;
import com.ruoyi.common.redis.utils.RedisUtils;
import com.ruoyi.common.web.enums.CaptchaType;
import com.ruoyi.web.domain.vo.CaptchaVo;
import jakarta.validation.constraints.NotBlank;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.sms4j.api.SmsBlend;
import org.dromara.sms4j.api.entity.SmsResponse;
import org.dromara.sms4j.core.factory.SmsFactory;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
@ -41,6 +54,7 @@ import com.ruoyi.common.web.config.properties.CaptchaProperties;
public class CaptchaController
{
private final CaptchaProperties captchaProperties;
private final MailProperties mailProperties;
/**
* 生成验证码
@ -65,6 +79,7 @@ public class CaptchaController
AbstractCaptcha captcha = SpringUtils.getBean(captchaProperties.getCategory().getClazz());
captcha.setGenerator(codeGenerator);
captcha.createCode();
// 如果是数学验证码使用SpEL表达式处理验证码结果
String code = captcha.getCode();
if (isMath) {
ExpressionParser parser = new SpelExpressionParser();
@ -77,4 +92,74 @@ public class CaptchaController
captchaVo.setImg(captcha.getImageBase64());
return R.ok(captchaVo);
}
/**
* 邮箱验证码
*
* @param email 邮箱
*/
@RateLimiter(key = "#email", time = 60, count = 1)
@GetMapping("/resource/email/code")
public R<Void> emailCode(@NotBlank(message = "{user.email.not.blank}") String email) {
if (!mailProperties.getEnabled()) {
return R.fail("当前系统没有开启邮箱功能!");
}
String key = GlobalConstants.CAPTCHA_CODE_KEY + email;
String code = RandomUtil.randomNumbers(4);
RedisUtils.setCacheObject(key, code, Duration.ofMinutes(Constants.CAPTCHA_EXPIRATION));
try {
MailUtils.sendText(email, "登录验证码", "您本次验证码为:" + code + ",有效性为" + Constants.CAPTCHA_EXPIRATION + "分钟,请尽快填写。");
} catch (Exception e) {
log.error("验证码短信发送异常 => {}", e.getMessage());
return R.fail(e.getMessage());
}
return R.ok();
}
/**
* 短信验证码
*
* @param phonenumber 用户手机号
*/
@RateLimiter(key = "#phonenumber", time = 60, count = 1)
@GetMapping("/resource/sms/code")
public R<Void> smsCode(@NotBlank(message = "{user.phonenumber.not.blank}") String phonenumber) {
String key = GlobalConstants.CAPTCHA_CODE_KEY + phonenumber;
String code = RandomUtil.randomNumbers(4);
RedisUtils.setCacheObject(key, code, Duration.ofMinutes(Constants.CAPTCHA_EXPIRATION));
// 验证码模板id 自行处理 (查数据库或写死均可)
String templateId = "";
LinkedHashMap<String, String> map = new LinkedHashMap<>(1);
map.put("code", code);
SmsBlend smsBlend = SmsFactory.getSmsBlend("config1");
SmsResponse smsResponse = smsBlend.sendMessage(phonenumber, templateId, map);
if (!smsResponse.isSuccess()) {
log.error("验证码短信发送异常 => {}", smsResponse);
return R.fail(smsResponse.getData().toString());
}
return R.ok();
}
@GetMapping("/genKeyPair")
public R genKeyPair() {
Map<String,String> map=new HashMap<>();
try {
log.info("开始生产rsa秘钥");
Map<String, Object> keyPair = RSAUtils.genKeyPair();
String publicKey = RSAUtils.getPublicKey(keyPair);
String privateKey = RSAUtils.getPrivateKey(keyPair);
log.info("privateKey"+privateKey);
String uuid="ruoyi_"+ UUID.randomUUID().toString().replace("-","");
RedisUtils.setCacheMapValue("loginRsa",uuid,privateKey);
RedisUtils.expire("loginRsa",60*60);
log.info("写入redis完成");
map.put("uuidPrivateKey",uuid);
map.put("RSA_PUBLIC_KEY",publicKey);
} catch (Exception e) {
return R.fail("生成RSA秘钥失败,"+e.getMessage());
}
return R.ok(map);
}
}

View File

@ -12,6 +12,11 @@ import java.util.List;
@Data
public class LoginTenantVo {
/**
* 租户开关
*/
private Boolean tenantEnabled;
/**
* 租户对象列表
*/

View File

@ -13,10 +13,19 @@ import lombok.Data;
@AutoMapper(target = SysTenantVo.class)
public class TenantListVo {
/**
* 租户编号
*/
private String tenantId;
/**
* 企业名称
*/
private String companyName;
/**
* 域名
*/
private String domain;
}

View File

@ -1,26 +1,30 @@
package com.ruoyi.common.security.listener;
package com.ruoyi.web.listener;
import cn.dev33.satoken.config.SaTokenConfig;
import cn.dev33.satoken.listener.SaTokenListener;
import cn.dev33.satoken.stp.SaLoginModel;
import cn.hutool.http.useragent.UserAgent;
import cn.hutool.http.useragent.UserAgentUtil;
import com.ruoyi.common.core.constant.CacheConstants;
import com.ruoyi.common.core.core.domain.dto.UserOnlineDTO;
import com.ruoyi.common.core.core.domain.model.LoginUser;
import com.ruoyi.common.core.enums.UserType;
import com.ruoyi.common.redis.utils.RedisUtils;
import com.ruoyi.common.security.utils.LoginHelper;
import com.ruoyi.common.core.utils.ip.AddressUtils;
import com.ruoyi.common.core.utils.ServletUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import com.ruoyi.common.core.constant.CacheConstants;
import com.ruoyi.common.core.constant.Constants;
import com.ruoyi.common.core.core.domain.dto.UserOnlineDTO;
import com.ruoyi.common.core.utils.MessageUtils;
import com.ruoyi.common.core.utils.ServletUtils;
import com.ruoyi.common.core.utils.SpringUtils;
import com.ruoyi.common.core.utils.ip.AddressUtils;
import com.ruoyi.common.log.event.LogininforEvent;
import com.ruoyi.common.redis.utils.RedisUtils;
import com.ruoyi.common.security.utils.LoginHelper;
import com.ruoyi.common.tenant.helper.TenantHelper;
import com.ruoyi.web.service.SysLoginService;
import org.springframework.stereotype.Component;
import java.time.Duration;
/**
* 用户行为 自定义侦听器
* 用户行为 侦听器的实现
*
* @author Lion Li
*/
@ -30,35 +34,46 @@ import java.time.Duration;
public class UserActionListener implements SaTokenListener {
private final SaTokenConfig tokenConfig;
private final SysLoginService loginService;
/**
* 每次登录时触发
*/
@Override
public void doLogin(String loginType, Object loginId, String tokenValue, SaLoginModel loginModel) {
UserType userType = UserType.getUserType(loginId.toString());
if (userType == UserType.SYS_USER) {
UserAgent userAgent = UserAgentUtil.parse(ServletUtils.getRequest().getHeader("User-Agent"));
String ip = ServletUtils.getClientIP();
LoginUser user = LoginHelper.getLoginUser();
UserOnlineDTO dto = new UserOnlineDTO();
dto.setIpaddr(ip);
dto.setLoginLocation(AddressUtils.getRealAddressByIP(ip));
dto.setBrowser(userAgent.getBrowser().getName());
dto.setOs(userAgent.getOs().getName());
dto.setLoginTime(System.currentTimeMillis());
dto.setTokenId(tokenValue);
dto.setUserName(user.getUsername());
dto.setDeptName(user.getDeptName());
UserAgent userAgent = UserAgentUtil.parse(ServletUtils.getRequest().getHeader("User-Agent"));
String ip = ServletUtils.getClientIP();
UserOnlineDTO dto = new UserOnlineDTO();
dto.setIpaddr(ip);
dto.setLoginLocation(AddressUtils.getRealAddressByIP(ip));
dto.setBrowser(userAgent.getBrowser().getName());
dto.setOs(userAgent.getOs().getName());
dto.setLoginTime(System.currentTimeMillis());
dto.setTokenId(tokenValue);
String username = (String) loginModel.getExtra(LoginHelper.USER_NAME_KEY);
Long tenantId = (Long) loginModel.getExtra(LoginHelper.TENANT_KEY);
dto.setUserName(username);
dto.setClientKey((String) loginModel.getExtra(LoginHelper.CLIENT_KEY));
dto.setDeviceType(loginModel.getDevice());
dto.setDeptName((String) loginModel.getExtra(LoginHelper.DEPT_NAME_KEY));
TenantHelper.dynamic(tenantId, () -> {
if(tokenConfig.getTimeout() == -1) {
RedisUtils.setCacheObject(CacheConstants.ONLINE_TOKEN_KEY + tokenValue, dto);
} else {
RedisUtils.setCacheObject(CacheConstants.ONLINE_TOKEN_KEY + tokenValue, dto, Duration.ofSeconds(tokenConfig.getTimeout()));
}
log.info("user doLogin, userId:{}, token:{}", loginId, tokenValue);
} else if (userType == UserType.APP_USER) {
// app端 自行根据业务编写
}
});
// 记录登录日志
LogininforEvent logininforEvent = new LogininforEvent();
logininforEvent.setTenantId(tenantId);
logininforEvent.setUsername(username);
logininforEvent.setStatus(Constants.LOGIN_SUCCESS);
logininforEvent.setMessage(MessageUtils.message("user.login.success"));
logininforEvent.setRequest(ServletUtils.getRequest());
SpringUtils.context().publishEvent(logininforEvent);
// 更新登录信息
loginService.recordLoginInfo((Long) loginModel.getExtra(LoginHelper.USER_KEY), ip);
log.info("user doLogin, userId:{}, token:{}", loginId, tokenValue);
}
/**

View File

@ -6,6 +6,7 @@ import com.ruoyi.common.core.core.domain.model.LoginBody;
import com.ruoyi.common.core.exception.ServiceException;
import com.ruoyi.common.core.utils.SpringUtils;
import com.ruoyi.system.domain.SysClient;
import com.ruoyi.system.domain.vo.SysClientVo;
import com.ruoyi.web.domain.vo.LoginVo;
/**
@ -19,8 +20,13 @@ public interface IAuthStrategy {
/**
* 登录
*
* @param body 登录对象
* @param client 授权管理视图对象
* @param grantType 授权类型
* @return 登录验证信息
*/
static LoginVo login(String body, SysClient client, String grantType) {
static LoginVo login(String body, SysClientVo client, String grantType) {
// 授权类型和客户端id
String beanName = grantType + BASE_NAME;
if (!SpringUtils.containsBean(beanName)) {
@ -32,7 +38,11 @@ public interface IAuthStrategy {
/**
* 登录
*
* @param body 登录对象
* @param client 授权管理视图对象
* @return 登录验证信息
*/
LoginVo login(String body, SysClient client);
LoginVo login(String body, SysClientVo client);
}

View File

@ -3,8 +3,8 @@ package com.ruoyi.web.service;
import cn.dev33.satoken.exception.NotLoginException;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil;
import com.mybatisflex.core.tenant.TenantManager;
import com.ruoyi.common.core.constant.*;
import com.ruoyi.common.core.core.domain.dto.RoleDTO;
import com.ruoyi.common.core.enums.LoginType;
@ -18,19 +18,17 @@ import com.ruoyi.common.security.utils.LoginHelper;
import com.ruoyi.common.tenant.exception.TenantException;
import com.ruoyi.common.tenant.helper.TenantHelper;
import com.ruoyi.system.domain.SysUser;
import com.ruoyi.system.domain.bo.SysSocialBo;
import com.ruoyi.system.domain.bo.SysUserBo;
import com.ruoyi.system.domain.vo.SysTenantVo;
import com.ruoyi.system.domain.vo.SysUserVo;
import com.ruoyi.system.service.ISysPermissionService;
import com.ruoyi.system.service.ISysTenantService;
import com.ruoyi.system.domain.vo.*;
import com.ruoyi.system.service.*;
import jakarta.annotation.Resource;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import me.zhyd.oauth.model.AuthUser;
import com.ruoyi.common.core.core.domain.model.LoginUser;
import com.ruoyi.common.core.utils.DateUtils;
import com.ruoyi.common.core.utils.MessageUtils;
import com.ruoyi.system.service.ISysUserService;
import org.springframework.stereotype.Service;
import org.springframework.beans.factory.annotation.Value;
@ -59,12 +57,72 @@ public class SysLoginService {
@Resource
private ISysPermissionService permissionService;
@Resource
private ISysSocialService sysSocialService;
@Resource
private ISysUserService userService;
@Resource
private ISysDeptService deptService;
@Resource
private ISysRoleService roleService;
@Resource
private ISysTenantService tenantService;
/**
* 绑定第三方用户
*
* @param authUserData 授权响应实体
* @return 统一响应实体
*/
public void socialRegister(AuthUser authUserData) {
String authId = authUserData.getSource() + authUserData.getUuid();
// 第三方用户信息
SysSocialBo bo = BeanUtil.toBean(authUserData, SysSocialBo.class);
BeanUtil.copyProperties(authUserData.getToken(), bo);
bo.setUserId(LoginHelper.getUserId());
bo.setAuthId(authId);
bo.setOpenId(authUserData.getUuid());
bo.setUserName(authUserData.getUsername());
bo.setNickName(authUserData.getNickname());
// 查询是否已经绑定用户
List<SysSocialVo> list = sysSocialService.selectListByAuthId(authId);
if (CollUtil.isEmpty(list)) {
// 没有绑定用户, 新增用户信息
sysSocialService.insertByBo(bo);
} else {
// 更新用户信息
bo.setSocialId(list.get(0).getSocialId());
sysSocialService.updateByBo(bo);
}
}
/**
* 退出登录
*/
public void logout() {
try {
LoginUser loginUser = LoginHelper.getLoginUser();
if (ObjectUtil.isNull(loginUser)) {
return;
}
if (LoginHelper.isSuperAdmin()) {
// 超级管理员 登出清除动态租户
TenantHelper.clearDynamic();
}
recordLogininfor(loginUser.getTenantId(), loginUser.getUsername(), Constants.LOGOUT, MessageUtils.message("user.logout.success"));
} catch (NotLoginException ignored) {
} finally {
try {
StpUtil.logout();
} catch (NotLoginException ignored) {
}
}
}
/**
* 登录校验
*/
@ -180,25 +238,24 @@ public class SysLoginService {
}
/**
* 退出登录
* 记录登录信息
*
* @param userId 用户ID
*/
public void logout() {
try {
LoginUser loginUser = LoginHelper.getLoginUser();
if (ObjectUtil.isNull(loginUser)) {
return;
}
if (LoginHelper.isSuperAdmin()) {
// 超级管理员 登出清除动态租户
TenantHelper.clearDynamic();
}
recordLogininfor(loginUser.getTenantId(), loginUser.getUsername(), Constants.LOGOUT, MessageUtils.message("user.logout.success"));
} catch (NotLoginException ignored) {
} finally {
try {
StpUtil.logout();
} catch (NotLoginException ignored) {
}
public void recordLoginInfo(Long userId, String ip) {
SysUserVo sysUserVo = userService.selectUserById(userId);
if (ObjectUtil.isNull(sysUserVo)) {
return;
}
SysUser sysUser = new SysUser();
sysUser.setUserId(userId);
sysUser.setLoginIp(ip);
sysUser.setLoginDate(DateUtils.getNowDate());
sysUser.setUpdateBy(userId);
sysUser.setVersion(sysUserVo.getVersion());
userService.updateById(sysUser);
}
}

View File

@ -8,6 +8,7 @@ import com.ruoyi.common.core.utils.ServletUtils;
import com.ruoyi.common.core.utils.SpringUtils;
import com.ruoyi.common.log.event.LogininforEvent;
import com.ruoyi.common.redis.utils.RedisUtils;
import com.ruoyi.common.tenant.helper.TenantHelper;
import com.ruoyi.common.web.config.properties.CaptchaProperties;
import com.ruoyi.system.domain.SysUser;
import com.ruoyi.system.domain.bo.SysUserBo;
@ -30,8 +31,7 @@ import org.springframework.stereotype.Service;
*/
@RequiredArgsConstructor
@Service
public class SysRegisterService
{
public class SysRegisterService {
@Resource
private ISysUserService userService;
@ -43,33 +43,36 @@ public class SysRegisterService
/**
* 注册
*/
public void register(RegisterBody registerBody)
{
public void register(RegisterBody registerBody) {
Long tenantId = registerBody.getTenantId();
String username = registerBody.getUsername();
String password = registerBody.getPassword();
// 校验用户类型是否存在
String userType = UserType.getUserType(registerBody.getUserType()).getUserType();
TenantHelper.dynamic(tenantId, () -> {
String username = registerBody.getUsername();
String password = registerBody.getPassword();
// 校验用户类型是否存在
String userType = UserType.getUserType(registerBody.getUserType()).getUserType();
boolean captchaEnabled = captchaProperties.getEnable();
// 验证码开关
if (captchaEnabled) {
validateCaptcha(tenantId, username, registerBody.getCode(), registerBody.getUuid());
}
SysUserBo sysUser = new SysUserBo();
sysUser.setUserName(username);
sysUser.setNickName(username);
sysUser.setPassword(BCrypt.hashpw(password));
sysUser.setUserType(userType);
boolean captchaEnabled = captchaProperties.getEnable();
// 验证码开关
if (captchaEnabled) {
validateCaptcha(tenantId, username, registerBody.getCode(), registerBody.getUuid());
}
SysUserBo sysUser = new SysUserBo();
sysUser.setUserName(username);
sysUser.setNickName(username);
sysUser.setPassword(BCrypt.hashpw(password));
sysUser.setUserType(userType);
if (!userService.checkUserNameUnique(sysUser)) {
throw new UserException("user.register.save.error", username);
}
boolean regFlag = userService.registerUser(sysUser, tenantId);
if (!regFlag) {
throw new UserException("user.register.error");
}
recordLogininfor(tenantId, username, Constants.REGISTER, MessageUtils.message("user.register.success"));
boolean unique = userService.checkUserNameUnique(sysUser);
if (!unique) {
throw new UserException("user.register.save.error", username);
}
boolean regFlag = userService.registerUser(sysUser, tenantId);
if (!regFlag) {
throw new UserException("user.register.error");
}
recordLogininfor(tenantId, username, Constants.REGISTER, MessageUtils.message("user.register.success"));
});
}
/**
@ -81,7 +84,7 @@ public class SysRegisterService
* @param uuid 唯一标识
*/
public void validateCaptcha(Long tenantId, String username, String code, String uuid) {
String verifyKey = GlobalConstants.CAPTCHA_CODE_KEY + StringUtils.defaultString(uuid, "");
String verifyKey = GlobalConstants.CAPTCHA_CODE_KEY + StringUtils.blankToDefault(uuid, "");
String captcha = RedisUtils.getCacheObject(verifyKey);
RedisUtils.deleteObject(verifyKey);
if (captcha == null) {

View File

@ -3,10 +3,9 @@ package com.ruoyi.web.service.impl;
import cn.dev33.satoken.stp.SaLoginModel;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.util.ObjectUtil;
import com.ruoyi.common.core.core.domain.AjaxResult;
import com.ruoyi.common.core.core.domain.model.EmailLoginBody;
import com.ruoyi.common.json.utils.JsonUtils;
import com.ruoyi.common.tenant.helper.TenantHelper;
import com.ruoyi.system.domain.vo.SysClientVo;
import com.ruoyi.system.domain.vo.SysUserVo;
import com.ruoyi.system.service.ISysUserService;
import com.ruoyi.web.domain.vo.LoginVo;
@ -15,7 +14,6 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import com.ruoyi.common.core.constant.Constants;
import com.ruoyi.common.core.constant.GlobalConstants;
import com.ruoyi.common.core.core.domain.model.LoginBody;
import com.ruoyi.common.core.core.domain.model.LoginUser;
import com.ruoyi.common.core.enums.LoginType;
import com.ruoyi.common.core.enums.UserStatus;
@ -24,12 +22,8 @@ import com.ruoyi.common.core.exception.user.UserException;
import com.ruoyi.common.core.utils.MessageUtils;
import com.ruoyi.common.core.utils.StringUtils;
import com.ruoyi.common.core.utils.ValidatorUtils;
import com.ruoyi.common.core.validate.auth.EmailGroup;
import com.ruoyi.common.redis.utils.RedisUtils;
import com.ruoyi.common.security.utils.LoginHelper;
import com.ruoyi.system.domain.SysClient;
import com.ruoyi.system.domain.SysUser;
import com.ruoyi.system.mapper.SysUserMapper;
import com.ruoyi.web.service.IAuthStrategy;
import com.ruoyi.web.service.SysLoginService;
import org.springframework.stereotype.Service;
@ -48,8 +42,9 @@ public class EmailAuthStrategy implements IAuthStrategy {
private final SysLoginService loginService;
@Resource
private ISysUserService userService;
@Override
public LoginVo login(String body, SysClient client) {
public LoginVo login(String body, SysClientVo client) {
EmailLoginBody loginBody = JsonUtils.parseObject(body, EmailLoginBody.class);
ValidatorUtils.validate(loginBody);
Long tenantId = loginBody.getTenantId();
@ -74,9 +69,6 @@ public class EmailAuthStrategy implements IAuthStrategy {
// 生成token
LoginHelper.login(loginUser, model);
// loginService.recordLogininfor(loginUser.getTenantId(), user.getUserName(), Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success"));
// loginService.recordLoginInfo(user.getUserId());
LoginVo loginVo = new LoginVo();
loginVo.setAccessToken(StpUtil.getTokenValue());
loginVo.setExpireIn(StpUtil.getTokenTimeout());
@ -97,17 +89,15 @@ public class EmailAuthStrategy implements IAuthStrategy {
}
private SysUserVo loadUserByEmail(Long tenantId, String email) {
return TenantHelper.dynamic(tenantId, () -> {
SysUserVo user =userService.selectUserByEmail(email);
if (ObjectUtil.isNull(user)) {
log.info("登录用户:{} 不存在.", email);
throw new UserException("user.not.exists", email);
} else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) {
log.info("登录用户:{} 已被停用.", email);
throw new UserException("user.blocked", email);
}
return user;
});
SysUserVo user = userService.selectTenantUserByEmail(tenantId, email);
if (ObjectUtil.isNull(user)) {
log.info("登录用户:{} 不存在.", email);
throw new UserException("user.not.exists", email);
} else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) {
log.info("登录用户:{} 已被停用.", email);
throw new UserException("user.blocked", email);
}
return user;
}

View File

@ -6,7 +6,7 @@ import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.util.ObjectUtil;
import com.ruoyi.common.core.core.domain.model.PasswordLoginBody;
import com.ruoyi.common.json.utils.JsonUtils;
import com.ruoyi.common.tenant.helper.TenantHelper;
import com.ruoyi.system.domain.vo.SysClientVo;
import com.ruoyi.system.service.ISysUserService;
import jakarta.annotation.Resource;
import lombok.RequiredArgsConstructor;
@ -25,7 +25,6 @@ import com.ruoyi.common.core.utils.ValidatorUtils;
import com.ruoyi.common.redis.utils.RedisUtils;
import com.ruoyi.common.security.utils.LoginHelper;
import com.ruoyi.common.web.config.properties.CaptchaProperties;
import com.ruoyi.system.domain.SysClient;
import com.ruoyi.system.domain.vo.SysUserVo;
import com.ruoyi.web.domain.vo.LoginVo;
import com.ruoyi.web.service.IAuthStrategy;
@ -49,7 +48,7 @@ public class PasswordAuthStrategy implements IAuthStrategy {
private ISysUserService userService;
@Override
public LoginVo login(String body, SysClient client) {
public LoginVo login(String body, SysClientVo client) {
PasswordLoginBody loginBody = JsonUtils.parseObject(body, PasswordLoginBody.class);
ValidatorUtils.validate(loginBody);
Long tenantId = loginBody.getTenantId();
@ -80,9 +79,6 @@ public class PasswordAuthStrategy implements IAuthStrategy {
// 生成token
LoginHelper.login(loginUser, model);
loginService.recordLogininfor(loginUser.getTenantId(), username, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success"));
loginService.recordLoginInfo(user.getUserId(),user.getVersion());
LoginVo loginVo = new LoginVo();
loginVo.setAccessToken(StpUtil.getTokenValue());
loginVo.setExpireIn(StpUtil.getTokenTimeout());
@ -113,8 +109,7 @@ public class PasswordAuthStrategy implements IAuthStrategy {
}
private SysUserVo loadUserByUsername(Long tenantId, String username) {
SysUserVo user = userService.selectTenantUserByUserName(username,tenantId);
SysUserVo user = userService.selectTenantUserByUserName(tenantId,username);
if (ObjectUtil.isNull(user)) {
log.info("登录用户:{} 不存在.", username);
throw new UserException("user.not.exists", username);

View File

@ -0,0 +1,103 @@
package com.ruoyi.web.service.impl;
import cn.dev33.satoken.stp.SaLoginModel;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.util.ObjectUtil;
import com.ruoyi.common.core.core.domain.model.LoginUser;
import com.ruoyi.common.core.core.domain.model.SmsLoginBody;
import com.ruoyi.system.service.ISysUserService;
import jakarta.annotation.Resource;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import com.ruoyi.common.core.constant.Constants;
import com.ruoyi.common.core.constant.GlobalConstants;
import com.ruoyi.common.core.enums.LoginType;
import com.ruoyi.common.core.enums.UserStatus;
import com.ruoyi.common.core.exception.user.CaptchaExpireException;
import com.ruoyi.common.core.exception.user.UserException;
import com.ruoyi.common.core.utils.MessageUtils;
import com.ruoyi.common.core.utils.StringUtils;
import com.ruoyi.common.core.utils.ValidatorUtils;
import com.ruoyi.common.json.utils.JsonUtils;
import com.ruoyi.common.redis.utils.RedisUtils;
import com.ruoyi.common.security.utils.LoginHelper;
import com.ruoyi.system.domain.vo.SysClientVo;
import com.ruoyi.system.domain.vo.SysUserVo;
import com.ruoyi.web.domain.vo.LoginVo;
import com.ruoyi.web.service.IAuthStrategy;
import com.ruoyi.web.service.SysLoginService;
import org.springframework.stereotype.Service;
/**
* 短信认证策略
*
* @author Michelle.Chung
*/
@Slf4j
@Service("sms" + IAuthStrategy.BASE_NAME)
@RequiredArgsConstructor
public class SmsAuthStrategy implements IAuthStrategy {
@Resource
private SysLoginService loginService;
@Resource
private ISysUserService userService;
@Override
public LoginVo login(String body, SysClientVo client) {
SmsLoginBody loginBody = JsonUtils.parseObject(body, SmsLoginBody.class);
ValidatorUtils.validate(loginBody);
Long tenantId = loginBody.getTenantId();
String phonenumber = loginBody.getPhonenumber();
String smsCode = loginBody.getSmsCode();
// 通过手机号查找用户
SysUserVo user = loadUserByPhonenumber(tenantId, phonenumber);
loginService.checkLogin(LoginType.SMS, tenantId, user.getUserName(), () -> !validateSmsCode(tenantId, phonenumber, smsCode));
// 此处可根据登录用户的数据不同 自行创建 loginUser 属性不够用继承扩展就行了
LoginUser loginUser = loginService.buildLoginUser(user);
loginUser.setClientKey(client.getClientKey());
loginUser.setDeviceType(client.getDeviceType());
SaLoginModel model = new SaLoginModel();
model.setDevice(client.getDeviceType());
// 自定义分配 不同用户体系 不同 token 授权时间 不设置默认走全局 yml 配置
// 例如: 后台用户30分钟过期 app用户1天过期
model.setTimeout(client.getTimeout());
model.setActiveTimeout(client.getActiveTimeout());
model.setExtra(LoginHelper.CLIENT_KEY, client.getClientId());
// 生成token
LoginHelper.login(loginUser, model);
LoginVo loginVo = new LoginVo();
loginVo.setAccessToken(StpUtil.getTokenValue());
loginVo.setExpireIn(StpUtil.getTokenTimeout());
loginVo.setClientId(client.getClientId());
return loginVo;
}
/**
* 校验短信验证码
*/
private boolean validateSmsCode(Long tenantId, String phonenumber, String smsCode) {
String code = RedisUtils.getCacheObject(GlobalConstants.CAPTCHA_CODE_KEY + phonenumber);
if (StringUtils.isBlank(code)) {
loginService.recordLogininfor(tenantId, phonenumber, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire"));
throw new CaptchaExpireException();
}
return code.equals(smsCode);
}
private SysUserVo loadUserByPhonenumber(Long tenantId, String phonenumber) {
SysUserVo user = userService.selectTenantUserByPhonenumber(tenantId, phonenumber);
if (ObjectUtil.isNull(user)) {
log.info("登录用户:{} 不存在.", phonenumber);
throw new UserException("user.not.exists", phonenumber);
} else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) {
log.info("登录用户:{} 已被停用.", phonenumber);
throw new UserException("user.blocked", phonenumber);
}
return user;
}
}

View File

@ -0,0 +1,117 @@
package com.ruoyi.web.service.impl;
import cn.dev33.satoken.stp.SaLoginModel;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil;
import com.ruoyi.system.service.ISysUserService;
import jakarta.annotation.Resource;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import me.zhyd.oauth.model.AuthResponse;
import me.zhyd.oauth.model.AuthUser;
import com.ruoyi.common.core.core.domain.model.LoginUser;
import com.ruoyi.common.core.core.domain.model.SocialLoginBody;
import com.ruoyi.common.core.enums.UserStatus;
import com.ruoyi.common.core.exception.ServiceException;
import com.ruoyi.common.core.exception.user.UserException;
import com.ruoyi.common.core.utils.ValidatorUtils;
import com.ruoyi.common.json.utils.JsonUtils;
import com.ruoyi.common.security.utils.LoginHelper;
import com.ruoyi.common.social.config.properties.SocialProperties;
import com.ruoyi.common.social.utils.SocialUtils;
import com.ruoyi.system.domain.vo.SysClientVo;
import com.ruoyi.system.domain.vo.SysSocialVo;
import com.ruoyi.system.domain.vo.SysUserVo;
import com.ruoyi.system.service.ISysSocialService;
import com.ruoyi.web.domain.vo.LoginVo;
import com.ruoyi.web.service.IAuthStrategy;
import com.ruoyi.web.service.SysLoginService;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;
/**
* 第三方授权策略
*
* @author thiszhc is 三三
*/
@Slf4j
@Service("social" + IAuthStrategy.BASE_NAME)
@RequiredArgsConstructor
public class SocialAuthStrategy implements IAuthStrategy {
private final SocialProperties socialProperties;
@Resource
private ISysSocialService sysSocialService;
@Resource
private ISysUserService userService;
@Resource
private SysLoginService loginService;
/**
* 登录-第三方授权登录
*
* @param body 登录信息
* @param client 客户端信息
*/
@Override
public LoginVo login(String body, SysClientVo client) {
SocialLoginBody loginBody = JsonUtils.parseObject(body, SocialLoginBody.class);
ValidatorUtils.validate(loginBody);
AuthResponse<AuthUser> response = SocialUtils.loginAuth(
loginBody.getSource(), loginBody.getSocialCode(),
loginBody.getSocialState(), socialProperties);
if (!response.ok()) {
throw new ServiceException(response.getMsg());
}
AuthUser authUserData = response.getData();
List<SysSocialVo> list = sysSocialService.selectListByAuthId(authUserData.getSource() + authUserData.getUuid());
if (CollUtil.isEmpty(list)) {
throw new ServiceException("你还没有绑定第三方账号,绑定后才可以登录!");
}
Optional<SysSocialVo> opt = list.stream().filter(x -> x.getTenantId().equals(loginBody.getTenantId())).findAny();
if (opt.isEmpty()) {
throw new ServiceException("对不起,你没有权限登录当前租户!");
}
SysSocialVo social = opt.get();
// 查找用户
SysUserVo user = loadUser(social.getTenantId(), social.getUserId());
// 此处可根据登录用户的数据不同 自行创建 loginUser 属性不够用继承扩展就行了
LoginUser loginUser = loginService.buildLoginUser(user);
loginUser.setClientKey(client.getClientKey());
loginUser.setDeviceType(client.getDeviceType());
SaLoginModel model = new SaLoginModel();
model.setDevice(client.getDeviceType());
// 自定义分配 不同用户体系 不同 token 授权时间 不设置默认走全局 yml 配置
// 例如: 后台用户30分钟过期 app用户1天过期
model.setTimeout(client.getTimeout());
model.setActiveTimeout(client.getActiveTimeout());
model.setExtra(LoginHelper.CLIENT_KEY, client.getClientId());
// 生成token
LoginHelper.login(loginUser, model);
LoginVo loginVo = new LoginVo();
loginVo.setAccessToken(StpUtil.getTokenValue());
loginVo.setExpireIn(StpUtil.getTokenTimeout());
loginVo.setClientId(client.getClientId());
return loginVo;
}
private SysUserVo loadUser(Long tenantId, Long userId) {
SysUserVo user = userService.selectTenantUserById(tenantId, userId);
if (ObjectUtil.isNull(user)) {
log.info("登录用户:{} 不存在.", "");
throw new UserException("user.not.exists", "");
} else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) {
log.info("登录用户:{} 已被停用.", "");
throw new UserException("user.blocked", "");
}
return user;
}
}

View File

@ -0,0 +1,93 @@
package com.ruoyi.web.service.impl;
import cn.dev33.satoken.stp.SaLoginModel;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.util.ObjectUtil;
import jakarta.annotation.Resource;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import com.ruoyi.common.core.core.domain.model.XcxLoginBody;
import com.ruoyi.common.core.core.domain.model.XcxLoginUser;
import com.ruoyi.common.core.enums.UserStatus;
import com.ruoyi.common.core.utils.ValidatorUtils;
import com.ruoyi.common.json.utils.JsonUtils;
import com.ruoyi.common.security.utils.LoginHelper;
import com.ruoyi.system.domain.vo.SysClientVo;
import com.ruoyi.system.domain.vo.SysUserVo;
import com.ruoyi.web.domain.vo.LoginVo;
import com.ruoyi.web.service.IAuthStrategy;
import com.ruoyi.web.service.SysLoginService;
import org.springframework.stereotype.Service;
/**
* 小程序认证策略
*
* @author Michelle.Chung
*/
@Slf4j
@Service("xcx" + IAuthStrategy.BASE_NAME)
@RequiredArgsConstructor
public class XcxAuthStrategy implements IAuthStrategy {
@Resource
private SysLoginService loginService;
@Override
public LoginVo login(String body, SysClientVo client) {
XcxLoginBody loginBody = JsonUtils.parseObject(body, XcxLoginBody.class);
ValidatorUtils.validate(loginBody);
// xcxCode 小程序调用 wx.login 授权后获取
String xcxCode = loginBody.getXcxCode();
// 多个小程序识别使用
String appid = loginBody.getAppid();
// todo 以下自行实现
// 校验 appid + appsrcret + xcxCode 调用登录凭证校验接口 获取 session_key openid
String openid = "";
// 框架登录不限制从什么表查询 只要最终构建出 LoginUser 即可
SysUserVo user = loadUserByOpenid(openid);
// 此处可根据登录用户的数据不同 自行创建 loginUser 属性不够用继承扩展就行了
XcxLoginUser loginUser = new XcxLoginUser();
loginUser.setTenantId(user.getTenantId());
loginUser.setUserId(user.getUserId());
loginUser.setUsername(user.getUserName());
loginUser.setNickname(user.getNickName());
loginUser.setUserType(user.getUserType());
loginUser.setClientKey(client.getClientKey());
loginUser.setDeviceType(client.getDeviceType());
loginUser.setOpenid(openid);
SaLoginModel model = new SaLoginModel();
model.setDevice(client.getDeviceType());
// 自定义分配 不同用户体系 不同 token 授权时间 不设置默认走全局 yml 配置
// 例如: 后台用户30分钟过期 app用户1天过期
model.setTimeout(client.getTimeout());
model.setActiveTimeout(client.getActiveTimeout());
model.setExtra(LoginHelper.CLIENT_KEY, client.getClientId());
// 生成token
LoginHelper.login(loginUser, model);
LoginVo loginVo = new LoginVo();
loginVo.setAccessToken(StpUtil.getTokenValue());
loginVo.setExpireIn(StpUtil.getTokenTimeout());
loginVo.setClientId(client.getClientId());
loginVo.setOpenid(openid);
return loginVo;
}
private SysUserVo loadUserByOpenid(String openid) {
// 使用 openid 查询绑定用户 如未绑定用户 则根据业务自行处理 例如 创建默认用户
// todo 自行实现 userService.selectUserByOpenid(openid);
SysUserVo user = new SysUserVo();
if (ObjectUtil.isNull(user)) {
log.info("登录用户:{} 不存在.", openid);
// todo 用户不存在 业务逻辑自行实现
} else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) {
log.info("登录用户:{} 已被停用.", openid);
// todo 用户已被停用 业务逻辑自行实现
}
return user;
}
}

View File

@ -18,6 +18,10 @@ spring:
# 多久检查一次连接的活性
keepaliveTime: 30000
mybatis-flex:
# sql审计
audit_enable: true
# sql打印
sql_print: true
datasource:
# 数据源-1
ds1:
@ -25,30 +29,15 @@ mybatis-flex:
# rewriteBatchedStatements=true 批处理优化 大幅提升批量插入更新删除性能(对数据库有性能损耗 使用批量操作应考虑性能问题)
type: ${spring.datasource.type}
# mysql数据库
# driver-class-name: com.mysql.cj.jdbc.Driver
# url: jdbc:mysql://localhost:3306/ruoyi-flex?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
# username: root
# password: Root@369
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/ruoyi-flex?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
username: root
password: Root@369
#postgresql数据库
driver-class-name: org.postgresql.Driver
url: jdbc:postgresql://localhost:5432/ruoyi-flex?useUnicode=true&characterEncoding=utf8&useSSL=true&autoReconnect=true&reWriteBatchedInserts=true
username: postgres
password: postgres@369
# # 数据源-2
# ds2:
# # 指定为HikariDataSource
# type: ${spring.datasource.type}
# # mysql数据库
# driver-class-name: com.mysql.cj.jdbc.Driver
# url: jdbc:mysql://localhost:3306/ruoyi-flex?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
# username: root
# password: Root@369
# #postgresql数据库
## driver-class-name: org.postgresql.Driver
## url: jdbc:postgresql://localhost:5432/ruoyi-flex?useUnicode=true&characterEncoding=utf8&useSSL=true&autoReconnect=true&reWriteBatchedInserts=true
## username: postgres
## password: postgres@369
# driver-class-name: org.postgresql.Driver
# url: jdbc:postgresql://localhost:5432/ruoyi-flex?useUnicode=true&characterEncoding=utf8&useSSL=true&autoReconnect=true&reWriteBatchedInserts=true
# username: postgres
# password: postgres@369
# redis 单机配置(单机与集群只能开启一个另一个需要注释掉)
spring.data:
@ -113,3 +102,146 @@ powerjob:
allow-lazy-connect-server: false
max-appended-wf-context-length: 4096
max-result-length: 4096
--- # easy-retry 配置
easy-retry:
enabled: false
# 需要在EasyRetry后台组管理创建对应名称的组,然后创建任务的时候选择对应的组,才能正确分派任务
group-name: "ruoyi_group"
# EasyRetry接入验证令牌 详见 script/sql/easy_retry.sql `er_group_config` 表
token: "ER_cKqBTPzCsWA3VyuCfFoccmuIEGXjr5KT"
server:
host: 127.0.0.1
port: 1788
# 详见 script/sql/easy_retry.sql `er_namespace` 表
namespace: ${spring.profiles.active}
--- # mail 邮件发送
mail:
enabled: false
host: smtp.163.com
port: 465
# 是否需要用户名密码验证
auth: true
# 发送方遵循RFC-822标准
from: xxx@163.com
# 用户名注意如果使用foxmail邮箱此处user为qq号
user: xxx@163.com
# 密码注意某些邮箱需要为SMTP服务单独设置密码详情查看相关帮助
pass: xxxxxxxxxx
# 使用 STARTTLS安全连接STARTTLS是对纯文本通信协议的扩展。
starttlsEnable: true
# 使用SSL安全连接
sslEnable: true
# SMTP超时时长单位毫秒缺省值不超时
timeout: 0
# Socket连接超时值单位毫秒缺省值不超时
connectionTimeout: 0
--- # sms 短信 支持 阿里云 腾讯云 云片 等等各式各样的短信服务商
# https://sms4j.com/doc3/ 差异配置文档地址 支持单厂商多配置,可以配置多个同时使用
sms:
# 配置源类型用于标定配置来源(interface,yaml)
config-type: yaml
# 用于标定yml中的配置是否开启短信拦截接口配置不受此限制
restricted: true
# 短信拦截限制单手机号每分钟最大发送,只对开启了拦截的配置有效
minute-max: 1
# 短信拦截限制单手机号每日最大发送量,只对开启了拦截的配置有效
account-max: 30
# 以下配置来自于 org.dromara.sms4j.provider.config.BaseConfig类中
blends:
# 唯一ID 用于发送短信寻找具体配置 随便定义别用中文即可
# 可以同时存在两个相同厂商 例如: ali1 ali2 两个不同的阿里短信账号 也可用于区分租户
config1:
# 框架定义的厂商名称标识,标定此配置是哪个厂商,详细请看厂商标识介绍部分
supplier: alibaba
# 有些称为accessKey有些称之为apiKey也有称为sdkKey或者appId。
access-key-id: 您的accessKey
# 称为accessSecret有些称之为apiSecret
access-key-secret: 您的accessKeySecret
signature: 您的短信签名
sdk-app-id: 您的sdkAppId
config2:
# 厂商标识,标定此配置是哪个厂商,详细请看厂商标识介绍部分
supplier: tencent
access-key-id: 您的accessKey
access-key-secret: 您的accessKeySecret
signature: 您的短信签名
sdk-app-id: 您的sdkAppId
--- # 三方授权
justauth:
# 前端外网访问地址
address: http://localhost:80
type:
maxkey:
# maxkey 服务器地址
# 注意 如下均配置均不需要修改 maxkey 已经内置好了数据
server-url: http://sso.maxkey.top
client-id: 876892492581044224
client-secret: x1Y5MTMwNzIwMjMxNTM4NDc3Mzche8
redirect-uri: ${justauth.address}/social-callback?source=maxkey
topiam:
# topiam 服务器地址
server-url: http://127.0.0.1:1989/api/v1/authorize/y0q************spq***********8ol
client-id: 449c4*********937************759
client-secret: ac7***********1e0************28d
redirect-uri: ${justauth.address}/social-callback?source=topiam
scopes: [openid, email, phone, profile]
qq:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: ${justauth.address}/social-callback?source=qq
union-id: false
weibo:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: ${justauth.address}/social-callback?source=weibo
gitee:
client-id: 91436b7940090**********d67eea73acbf61b6b590751a98
client-secret: 02c6fcfd70342980cd8**********c754d7a264c4e125f9ba915ac
redirect-uri: ${justauth.address}/social-callback?source=gitee
dingtalk:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: ${justauth.address}/social-callback?source=dingtalk
baidu:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: ${justauth.address}/social-callback?source=baidu
csdn:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: ${justauth.address}/social-callback?source=csdn
coding:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: ${justauth.address}/social-callback?source=coding
coding-group-name: xx
oschina:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: ${justauth.address}/social-callback?source=oschina
alipay_wallet:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: ${justauth.address}/social-callback?source=alipay_wallet
alipay-public-key: MIIB**************DAQAB
wechat_open:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: ${justauth.address}/social-callback?source=wechat_open
wechat_mp:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: ${justauth.address}/social-callback?source=wechat_mp
wechat_enterprise:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: ${justauth.address}/social-callback?source=wechat_enterprise
agent-id: 1000002
gitlab:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: ${justauth.address}/social-callback?source=gitlab

View File

@ -18,6 +18,10 @@ spring:
# 多久检查一次连接的活性
keepaliveTime: 30000
mybatis-flex:
# sql审计
audit_enable: false
# sql打印
sql_print: false
datasource:
# 数据源-1
ds1:
@ -25,30 +29,15 @@ mybatis-flex:
# rewriteBatchedStatements=true 批处理优化 大幅提升批量插入更新删除性能(对数据库有性能损耗 使用批量操作应考虑性能问题)
type: ${spring.datasource.type}
# mysql数据库
# driver-class-name: com.mysql.cj.jdbc.Driver
# url: jdbc:mysql://localhost:3306/ruoyi-flex?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
# username: root
# password: Root@369
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/ruoyi-flex?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
username: root
password: Root@369
#postgresql数据库
driver-class-name: org.postgresql.Driver
url: jdbc:postgresql://localhost:5432/ruoyi-flex?useUnicode=true&characterEncoding=utf8&useSSL=true&autoReconnect=true&reWriteBatchedInserts=true
username: postgres
password: postgres@369
# # 数据源-2
# ds2:
# # 指定为HikariDataSource
# type: ${spring.datasource.type}
# # mysql数据库
# driver-class-name: com.mysql.cj.jdbc.Driver
# url: jdbc:mysql://localhost:3306/ruoyi-flex?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
# username: root
# password: Root@369
# #postgresql数据库
## driver-class-name: org.postgresql.Driver
## url: jdbc:postgresql://localhost:5432/ruoyi-flex?useUnicode=true&characterEncoding=utf8&useSSL=true&autoReconnect=true&reWriteBatchedInserts=true
## username: postgres
## password: postgres@369
# driver-class-name: org.postgresql.Driver
# url: jdbc:postgresql://localhost:5432/ruoyi-flex?useUnicode=true&characterEncoding=utf8&useSSL=true&autoReconnect=true&reWriteBatchedInserts=true
# username: postgres
# password: postgres@369
# redis 单机配置(单机与集群只能开启一个另一个需要注释掉)
spring.data:
@ -113,3 +102,147 @@ powerjob:
allow-lazy-connect-server: false
max-appended-wf-context-length: 4096
max-result-length: 4096
--- # easy-retry 配置
easy-retry:
enabled: false
# 需要在EasyRetry后台组管理创建对应名称的组,然后创建任务的时候选择对应的组,才能正确分派任务
group-name: "ruoyi_group"
# EasyRetry接入验证令牌 详见 script/sql/easy_retry.sql `er_group_config` 表
token: "ER_cKqBTPzCsWA3VyuCfFoccmuIEGXjr5KT"
server:
host: 127.0.0.1
port: 1788
# 详见 script/sql/easy_retry.sql `er_namespace` 表
namespace: ${spring.profiles.active}
--- # mail 邮件发送
mail:
enabled: false
host: smtp.163.com
port: 465
# 是否需要用户名密码验证
auth: true
# 发送方遵循RFC-822标准
from: xxx@163.com
# 用户名注意如果使用foxmail邮箱此处user为qq号
user: xxx@163.com
# 密码注意某些邮箱需要为SMTP服务单独设置密码详情查看相关帮助
pass: xxxxxxxxxx
# 使用 STARTTLS安全连接STARTTLS是对纯文本通信协议的扩展。
starttlsEnable: true
# 使用SSL安全连接
sslEnable: true
# SMTP超时时长单位毫秒缺省值不超时
timeout: 0
# Socket连接超时值单位毫秒缺省值不超时
connectionTimeout: 0
--- # sms 短信 支持 阿里云 腾讯云 云片 等等各式各样的短信服务商
# https://sms4j.com/doc3/ 差异配置文档地址 支持单厂商多配置,可以配置多个同时使用
sms:
# 配置源类型用于标定配置来源(interface,yaml)
config-type: yaml
# 用于标定yml中的配置是否开启短信拦截接口配置不受此限制
restricted: true
# 短信拦截限制单手机号每分钟最大发送,只对开启了拦截的配置有效
minute-max: 1
# 短信拦截限制单手机号每日最大发送量,只对开启了拦截的配置有效
account-max: 30
# 以下配置来自于 org.dromara.sms4j.provider.config.BaseConfig类中
blends:
# 唯一ID 用于发送短信寻找具体配置 随便定义别用中文即可
# 可以同时存在两个相同厂商 例如: ali1 ali2 两个不同的阿里短信账号 也可用于区分租户
config1:
# 框架定义的厂商名称标识,标定此配置是哪个厂商,详细请看厂商标识介绍部分
supplier: alibaba
# 有些称为accessKey有些称之为apiKey也有称为sdkKey或者appId。
access-key-id: 您的accessKey
# 称为accessSecret有些称之为apiSecret
access-key-secret: 您的accessKeySecret
signature: 您的短信签名
sdk-app-id: 您的sdkAppId
config2:
# 厂商标识,标定此配置是哪个厂商,详细请看厂商标识介绍部分
supplier: tencent
access-key-id: 您的accessKey
access-key-secret: 您的accessKeySecret
signature: 您的短信签名
sdk-app-id: 您的sdkAppId
--- # 三方授权
justauth:
# 前端外网访问地址
address: http://localhost:80
type:
maxkey:
# maxkey 服务器地址
# 注意 如下均配置均不需要修改 maxkey 已经内置好了数据
server-url: http://sso.maxkey.top
client-id: 876892492581044224
client-secret: x1Y5MTMwNzIwMjMxNTM4NDc3Mzche8
redirect-uri: ${justauth.address}/social-callback?source=maxkey
topiam:
# topiam 服务器地址
server-url: http://127.0.0.1:1989/api/v1/authorize/y0q************spq***********8ol
client-id: 449c4*********937************759
client-secret: ac7***********1e0************28d
redirect-uri: ${justauth.address}/social-callback?source=topiam
scopes: [openid, email, phone, profile]
qq:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: ${justauth.address}/social-callback?source=qq
union-id: false
weibo:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: ${justauth.address}/social-callback?source=weibo
gitee:
client-id: 91436b7940090**********d67eea73acbf61b6b590751a98
client-secret: 02c6fcfd70342980cd8**********c754d7a264c4e125f9ba915ac
redirect-uri: ${justauth.address}/social-callback?source=gitee
dingtalk:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: ${justauth.address}/social-callback?source=dingtalk
baidu:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: ${justauth.address}/social-callback?source=baidu
csdn:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: ${justauth.address}/social-callback?source=csdn
coding:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: ${justauth.address}/social-callback?source=coding
coding-group-name: xx
oschina:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: ${justauth.address}/social-callback?source=oschina
alipay_wallet:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: ${justauth.address}/social-callback?source=alipay_wallet
alipay-public-key: MIIB**************DAQAB
wechat_open:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: ${justauth.address}/social-callback?source=wechat_open
wechat_mp:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: ${justauth.address}/social-callback?source=wechat_mp
wechat_enterprise:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: ${justauth.address}/social-callback?source=wechat_enterprise
agent-id: 1000002
gitlab:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: ${justauth.address}/social-callback?source=gitlab

View File

@ -9,7 +9,7 @@ ruoyi:
# 实例演示开关
demoEnabled: true
# 文件路径 示例( Windows配置D:/ruoyi/uploadPathLinux配置 /home/ruoyi/uploadPath
profile: D:/ruoyi/uploadPath
profile: /home/ruoyi/uploadPath
# 获取ip地址开关
addressEnabled: false
@ -33,27 +33,35 @@ server:
# 应用的访问路径
context-path: /
# tomcat web容器配置
tomcat:
# tomcat的URI编码
uri-encoding: UTF-8
# 连接数满后的排队数默认为100
accept-count: 1000
# undertow web容器配置
undertow:
# HTTP post内容的最大大小。当值为-1时默认值为大小是无限的
max-http-post-size: -1
# 以下的配置会影响buffer,这些buffer会用于服务器连接的IO操作,有点类似netty的池化内存管理
# 每块buffer的空间大小,越小的空间被利用越充分
buffer-size: 512
# 是否分配的直接内存
direct-buffers: true
threads:
# 设置IO线程数, 它主要执行非阻塞的任务,它们会负责多个连接, 默认设置每个CPU核心一个线程
io: 8
# 阻塞任务线程池, 当执行类似servlet请求阻塞操作, undertow会从这个线程池中取得线程,它的值设置取决于系统的负载
worker: 256
# undertow:
# # HTTP post内容的最大大小。当值为-1时默认值为大小是无限的
# max-http-post-size: -1
# # 以下的配置会影响buffer,这些buffer会用于服务器连接的IO操作,有点类似netty的池化内存管理
# # 每块buffer的空间大小,越小的空间被利用越充分
# buffer-size: 512
# # 是否分配的直接内存
# direct-buffers: true
# threads:
# # 设置IO线程数, 它主要执行非阻塞的任务,它们会负责多个连接, 默认设置每个CPU核心一个线程
# io: 8
# # 阻塞任务线程池, 当执行类似servlet请求阻塞操作, undertow会从这个线程池中取得线程,它的值设置取决于系统的负载
# worker: 256
# 日志配置
logging:
level:
com.ruoyi: debug
com.ruoyi: @logging.level@
org.springframework: warn
tech.powerjob.worker.background: warn
org.mybatis.spring.mapper: error
config: classpath:logback.xml
# 用户配置
@ -116,7 +124,7 @@ pagehelper:
mybatis-flex:
# 搜索指定包别名
type-aliases-package: com.ruoyi.**.domain
# 不支持多包, 如有需要可在注解配置 或 提升扫包等级com.**.**.mapper
# 多包名使用 例如 org.dromara.**.mapper,org.xxx.**.mapper
mapper-package: com.ruoyi.**.mapper
# 配置mapper的扫描找到所有的mapper.xml映射文件
mapper-locations: classpath*:mapper/**/*Mapper.xml
@ -134,7 +142,7 @@ mybatis-flex:
# 关闭日志记录 org.apache.ibatis.logging.nologging.NoLoggingImpl
# 默认日志输出 org.apache.ibatis.logging.slf4j.Slf4jImpl
#log_impl: org.apache.ibatis.logging.stdout.StdOutImpl
logImpl: org.apache.ibatis.logging.stdout.StdOutImpl
logImpl: org.apache.ibatis.logging.nologging.NoLoggingImpl
cacheEnabled: true
global-config:
# 是否控制台打印 MyBatis-Flex 的 LOGO 及版本号
@ -183,13 +191,12 @@ api-decrypt:
# SpringDoc配置
springdoc:
#需要扫描的包,可以配置多个,使用逗号分割
packages-to-scan: com.ruoyi
paths-to-exclude: #配置不包含在swagger文档中的api
- /api/test/**
- /api/mockito/data
swagger-ui:
enabled: true #开启/禁止swagger,prod可以设置为false
version: 5.11.8 #指定swagger-ui的版本号
disable-swagger-default-url: true #禁用default petstore url
path: /swagger-ui.html #swagger页面
persistAuthorization: true # 持久化认证数据,如果设置为 true它会保留授权数据并且不会在浏览器关闭/刷新时丢失
@ -212,20 +219,24 @@ springdoc:
email: 738981257@qq.com
url: https://gitee.com/dataprince/ruoyi-flex
components:
# 鉴权方式配置
# 鉴权方式配置
security-schemes:
apiKey:
type: APIKEY
in: HEADER
name: ${sa-token.token-name}
group-configs:
- group: 1.演示模块
packages-to-scan: com.ruoyi.demo
- group: 2.通用模块
- group: 1.web模块
packages-to-scan: com.ruoyi.web
- group: 2.演示模块
packages-to-scan:
- com.ruoyi.demo
- com.ruoyi.mf
- group: 3.通用模块
packages-to-scan: com.ruoyi.common
- group: 3.系统模块
- group: 4.系统模块
packages-to-scan: com.ruoyi.system
- group: 4.代码生成模块
- group: 5.代码生成模块
packages-to-scan: com.ruoyi.generator
# 防止XSS攻击
@ -237,15 +248,6 @@ xss:
# 匹配链接
urlPatterns: /system/*,/monitor/*,/tool/*,/demo/*
# 全局线程池相关配置
thread-pool:
# 是否开启线程池
enabled: false
# 队列最大长度
queueCapacity: 128
# 线程池维护线程所允许的空闲时间
keepAliveSeconds: 300
# 分布式锁 lock4j 全局配置
lock4j:
# 获取分布式锁超时时间,默认为 3000 毫秒
@ -298,6 +300,8 @@ security:
- /captchaImage
- /captcha/get
- /captcha/check
- /genKeyPair
- /job/**
--- # Actuator 监控端点的配置项
management:

Binary file not shown.

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- 日志存放路径 -->
<property name="log.path" value="/home/ruoyi/logs" />
<property name="log.path" value="./home/ruoyi/logs" />
<!-- 日志输出格式 -->
<property name="log.pattern" value="%d{HH:mm:ss.SSS} [%thread] %-5level %logger{20} - [%method,%line] - %msg%n" />
@ -11,7 +11,7 @@
<pattern>${log.pattern}</pattern>
</encoder>
</appender>
<!-- 系统日志输出 -->
<appender name="file_info" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log.path}/sys-info.log</file>
@ -34,7 +34,7 @@
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<appender name="file_error" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log.path}/sys-error.log</file>
<!-- 循环政策:基于时间创建日志文件 -->
@ -56,7 +56,7 @@
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<!-- 用户访问日志输出 -->
<appender name="sys-user" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log.path}/sys-user.log</file>
@ -70,7 +70,7 @@
<pattern>${log.pattern}</pattern>
</encoder>
</appender>
<!-- 系统模块日志级别控制 -->
<logger name="com.ruoyi" level="info" />
<!-- Spring日志级别控制 -->
@ -79,15 +79,15 @@
<root level="info">
<appender-ref ref="console" />
</root>
<!--系统操作日志-->
<root level="info">
<appender-ref ref="file_info" />
<appender-ref ref="file_error" />
</root>
<!--系统用户操作日志-->
<logger name="sys-user" level="info">
<appender-ref ref="sys-user"/>
</logger>
</configuration>
</configuration>

View File

@ -18,14 +18,18 @@
<module>ruoyi-common-job</module>
<module>ruoyi-common-json</module>
<module>ruoyi-common-log</module>
<module>ruoyi-common-mail</module>
<module>ruoyi-common-orm</module>
<module>ruoyi-common-oss</module>
<module>ruoyi-common-ratelimiter</module>
<module>ruoyi-common-redis</module>
<module>ruoyi-common-security</module>
<module>ruoyi-common-sms</module>
<module>ruoyi-common-social</module>
<module>ruoyi-common-springdoc</module>
<module>ruoyi-common-tenant</module>
<module>ruoyi-common-web</module>
<module>ruoyi-common-websocket</module>
</modules>
<artifactId>ruoyi-common</artifactId>

View File

@ -14,7 +14,7 @@
</description>
<properties>
<revision>5.0.0</revision>
<revision>5.2.0-SNAPSHOT</revision>
</properties>
<dependencyManagement>
@ -61,6 +61,13 @@
<version>${revision}</version>
</dependency>
<!-- 邮件模块 -->
<dependency>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi-common-mail</artifactId>
<version>${revision}</version>
</dependency>
<!-- 数据库映射模块 -->
<dependency>
<groupId>com.ruoyi</groupId>
@ -96,6 +103,20 @@
<version>${revision}</version>
</dependency>
<!-- 短信模块 -->
<dependency>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi-common-sms</artifactId>
<version>${revision}</version>
</dependency>
<!-- 三方授权认证模块 -->
<dependency>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi-common-social</artifactId>
<version>${revision}</version>
</dependency>
<!-- 接口模块 -->
<dependency>
<groupId>com.ruoyi</groupId>
@ -117,6 +138,13 @@
<version>${revision}</version>
</dependency>
<!-- WebSocket模块 -->
<dependency>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi-common-websocket</artifactId>
<version>${revision}</version>
</dependency>
</dependencies>
</dependencyManagement>

View File

@ -143,6 +143,16 @@
<artifactId>ip2region</artifactId>
</dependency>
<!-- findbugs消除打包警告 -->
<dependency>
<groupId>com.google.code.findbugs</groupId>
<artifactId>jsr305</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>transmittable-thread-local</artifactId>
</dependency>
</dependencies>

View File

@ -2,6 +2,7 @@ package com.ruoyi.common.core.config;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.scheduling.annotation.EnableAsync;
/**
* 程序注解配置
@ -11,7 +12,7 @@ import org.springframework.context.annotation.EnableAspectJAutoProxy;
@AutoConfiguration
// 表示通过aop框架暴露该代理对象,AopContext能够访问
@EnableAspectJAutoProxy(exposeProxy = true)
// 指定要扫描的Mapper类的包的路径
@EnableAsync(proxyTargetClass = true)
public class ApplicationConfig
{

View File

@ -5,6 +5,8 @@ import com.ruoyi.common.core.exception.ServiceException;
import com.ruoyi.common.core.utils.SpringUtils;
import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.core.task.VirtualThreadTaskExecutor;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.annotation.EnableAsync;
@ -13,10 +15,10 @@ import java.util.concurrent.Executor;
/**
* 异步配置
*
* <p>
* 如果未使用虚拟线程则生效
* @author Lion Li
*/
@EnableAsync(proxyTargetClass = true)
@AutoConfiguration
public class AsyncConfig implements AsyncConfigurer {
@ -25,6 +27,9 @@ public class AsyncConfig implements AsyncConfigurer {
*/
@Override
public Executor getAsyncExecutor() {
if(SpringUtils.isVirtual()) {
return new VirtualThreadTaskExecutor("async-");
}
return SpringUtils.getBean("scheduledExecutorService");
}

View File

@ -1,15 +1,11 @@
package com.ruoyi.common.core.config;
import com.ruoyi.common.core.config.properties.ThreadPoolProperties;
import com.ruoyi.common.core.utils.Threads;
import jakarta.annotation.PreDestroy;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.concurrent.BasicThreadFactory;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.ThreadPoolExecutor;
@ -21,7 +17,6 @@ import java.util.concurrent.ThreadPoolExecutor;
**/
@Slf4j
@AutoConfiguration
@EnableConfigurationProperties(ThreadPoolProperties.class)
public class ThreadPoolConfig
{
/**
@ -31,18 +26,6 @@ public class ThreadPoolConfig
private ScheduledExecutorService scheduledExecutorService;
@Bean(name = "threadPoolTaskExecutor")
@ConditionalOnProperty(prefix = "thread-pool", name = "enabled", havingValue = "true")
public ThreadPoolTaskExecutor threadPoolTaskExecutor(ThreadPoolProperties threadPoolProperties) {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(core);
executor.setMaxPoolSize(core * 2);
executor.setQueueCapacity(threadPoolProperties.getQueueCapacity());
executor.setKeepAliveSeconds(threadPoolProperties.getKeepAliveSeconds());
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
return executor;
}
/**
* 执行周期性或定时任务
*/

View File

@ -1,30 +0,0 @@
package com.ruoyi.common.core.config.properties;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* 线程池 配置属性
*
* @author Lion Li
*/
@Data
@ConfigurationProperties(prefix = "thread-pool")
public class ThreadPoolProperties {
/**
* 是否开启线程池
*/
private boolean enabled;
/**
* 队列最大长度
*/
private int queueCapacity;
/**
* 线程池维护线程所允许的空闲时间
*/
private int keepAliveSeconds;
}

View File

@ -35,6 +35,11 @@ public interface CacheNames {
*/
String SYS_TENANT = GlobalConstants.GLOBAL_REDIS_KEY + "sys_tenant#30d";
/**
* 客户端
*/
String SYS_CLIENT = GlobalConstants.GLOBAL_REDIS_KEY + "sys_client#30d";
/**
* 用户账户
*/

View File

@ -0,0 +1,49 @@
package com.ruoyi.common.core.constant;
import cn.hutool.core.lang.RegexPool;
/**
* 常用正则表达式字符串
* <p>
* 常用正则表达式集合更多正则见: https://any86.github.io/any-rule/
*
* @author Feng
*/
public interface RegexConstants extends RegexPool {
/**
* 字典类型必须以字母开头且只能为小写字母数字下滑线
*/
public static final String DICTIONARY_TYPE = "^[a-z][a-z0-9_]*$";
/**
* 身份证号码后6位
*/
public static final String ID_CARD_LAST_6 = "^(([0-2][1-9])|10|20|30|31)\\d{3}[0-9Xx]$";
/**
* QQ号码
*/
public static final String QQ_NUMBER = "^[1-9][0-9]\\d{4,9}$";
/**
* 邮政编码
*/
public static final String POSTAL_CODE = "^[1-9]\\d{5}$";
/**
* 注册账号
*/
public static final String ACCOUNT = "^[a-zA-Z][a-zA-Z0-9_]{4,15}$";
/**
* 密码包含至少8个字符包括大写字母小写字母数字和特殊字符
*/
public static final String PASSWORD = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[@$!%*?&])[A-Za-z\\d@$!%*?&]{8,}$";
/**
* 通用状态0表示正常1表示停用
*/
public static final String STATUS = "^[01]$";
}

View File

@ -41,6 +41,6 @@ public interface TenantConstants {
/**
* 默认租户ID
*/
Long DEFAULT_TENANT_ID = 0L;
Long DEFAULT_TENANT_ID = 1L;
}

View File

@ -34,6 +34,16 @@ public class UserOnlineDTO implements Serializable {
*/
private String userName;
/**
* 客户端
*/
private String clientKey;
/**
* 设备类型
*/
private String deviceType;
/**
* 登录IP地址
*/

View File

@ -0,0 +1,29 @@
package com.ruoyi.common.core.core.domain.model;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 短信登录对象
*
* @author Lion Li
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class SmsLoginBody extends LoginBody {
/**
* 手机号
*/
@NotBlank(message = "{user.phonenumber.not.blank}")
private String phonenumber;
/**
* 短信code
*/
@NotBlank(message = "{sms.code.not.blank}")
private String smsCode;
}

View File

@ -0,0 +1,35 @@
package com.ruoyi.common.core.core.domain.model;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 三方登录对象
*
* @author Lion Li
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class SocialLoginBody extends LoginBody {
/**
* 第三方登录平台
*/
@NotBlank(message = "{social.source.not.blank}")
private String source;
/**
* 第三方登录code
*/
@NotBlank(message = "{social.code.not.blank}")
private String socialCode;
/**
* 第三方登录socialState
*/
@NotBlank(message = "{social.state.not.blank}")
private String socialState;
}

View File

@ -0,0 +1,28 @@
package com.ruoyi.common.core.core.domain.model;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 三方登录对象
*
* @author Lion Li
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class XcxLoginBody extends LoginBody {
/**
* 小程序id(多个小程序时使用)
*/
private String appid;
/**
* 小程序code
*/
@NotBlank(message = "{xcx.code.not.blank}")
private String xcxCode;
}

View File

@ -0,0 +1,27 @@
package com.ruoyi.common.core.core.domain.model;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import java.io.Serial;
/**
* 小程序登录用户身份权限
*
* @author Lion Li
*/
@Data
@EqualsAndHashCode(callSuper = true)
@NoArgsConstructor
public class XcxLoginUser extends LoginUser {
@Serial
private static final long serialVersionUID = 1L;
/**
* openid
*/
private String openid;
}

View File

@ -0,0 +1,52 @@
package com.ruoyi.common.core.factory;
import cn.hutool.core.lang.PatternPool;
import com.ruoyi.common.core.constant.RegexConstants;
import java.util.regex.Pattern;
/**
* 正则表达式模式池工厂
* <p>初始化的时候将正则表达式加入缓存池当中</p>
* <p>提高正则表达式的性能避免重复编译相同的正则表达式</p>
*
* @author 21001
*/
public class RegexPatternPoolFactory extends PatternPool {
/**
* 字典类型必须以字母开头且只能为小写字母数字下滑线
*/
public static final Pattern DICTIONARY_TYPE = get(RegexConstants.DICTIONARY_TYPE);
/**
* 身份证号码后6位
*/
public static final Pattern ID_CARD_LAST_6 = get(RegexConstants.ID_CARD_LAST_6);
/**
* QQ号码
*/
public static final Pattern QQ_NUMBER = get(RegexConstants.QQ_NUMBER);
/**
* 邮政编码
*/
public static final Pattern POSTAL_CODE = get(RegexConstants.POSTAL_CODE);
/**
* 注册账号
*/
public static final Pattern ACCOUNT = get(RegexConstants.ACCOUNT);
/**
* 密码包含至少8个字符包括大写字母小写字母数字和特殊字符
*/
public static final Pattern PASSWORD = get(RegexConstants.PASSWORD);
/**
* 通用状态0表示正常1表示停用
*/
public static final Pattern STATUS = get(RegexConstants.STATUS);
}

View File

@ -3,7 +3,9 @@ package com.ruoyi.common.core.utils;
import cn.hutool.extra.spring.SpringUtil;
import org.springframework.aop.framework.AopContext;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.boot.autoconfigure.thread.Threading;
import org.springframework.context.ApplicationContext;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;
/**
@ -98,4 +100,8 @@ public final class SpringUtils extends SpringUtil
return StringUtils.isNotEmpty(activeProfiles) ? activeProfiles[0] : null;
}
public static boolean isVirtual() {
return Threading.VIRTUAL.isActive(getBean(Environment.class));
}
}

View File

@ -26,9 +26,21 @@ public class StringUtils extends org.apache.commons.lang3.StringUtils
{
public static final String SEPARATOR = ",";
public static final String SLASH = "/";
/** 空字符串 */
private static final String NULLSTR = "";
/**
* 获取参数不为空值
*
* @param str defaultValue 要判断的value
* @return value 返回值
*/
public static String blankToDefault(String str, String defaultValue) {
return StrUtil.blankToDefault(str, defaultValue);
}
/**
* 获取参数不为空值
*

View File

@ -0,0 +1,30 @@
package com.ruoyi.common.core.utils.regex;
import cn.hutool.core.util.ReUtil;
import com.ruoyi.common.core.constant.RegexConstants;
/**
* 正则相关工具类
*
* @author Feng
*/
public final class RegexUtils extends ReUtil {
/**
* 从输入字符串中提取匹配的部分如果没有匹配则返回默认值
*
* @param input 要提取的输入字符串
* @param regex 用于匹配的正则表达式可以使用 {@link RegexConstants} 中定义的常量
* @param defaultInput 如果没有匹配时返回的默认值
* @return 如果找到匹配的部分则返回匹配的部分否则返回默认值
*/
public static String extractFromString(String input, String regex, String defaultInput) {
try {
return ReUtil.get(regex, input, 1);
} catch (Exception e) {
return defaultInput;
}
}
}

View File

@ -0,0 +1,105 @@
package com.ruoyi.common.core.utils.regex;
import cn.hutool.core.exceptions.ValidateException;
import cn.hutool.core.lang.Validator;
import com.ruoyi.common.core.factory.RegexPatternPoolFactory;
import java.util.regex.Pattern;
/**
* 正则字段校验器
* 主要验证字段非空是否为满足指定格式等
*
* @author Feng
*/
public class RegexValidator extends Validator {
/**
* 字典类型必须以字母开头且只能为小写字母数字下滑线
*/
public static final Pattern DICTIONARY_TYPE = RegexPatternPoolFactory.DICTIONARY_TYPE;
/**
* 身份证号码后6位
*/
public static final Pattern ID_CARD_LAST_6 = RegexPatternPoolFactory.ID_CARD_LAST_6;
/**
* QQ号码
*/
public static final Pattern QQ_NUMBER = RegexPatternPoolFactory.QQ_NUMBER;
/**
* 邮政编码
*/
public static final Pattern POSTAL_CODE = RegexPatternPoolFactory.POSTAL_CODE;
/**
* 注册账号
*/
public static final Pattern ACCOUNT = RegexPatternPoolFactory.ACCOUNT;
/**
* 密码包含至少8个字符包括大写字母小写字母数字和特殊字符
*/
public static final Pattern PASSWORD = RegexPatternPoolFactory.PASSWORD;
/**
* 通用状态0表示正常1表示停用
*/
public static final Pattern STATUS = RegexPatternPoolFactory.STATUS;
/**
* 检查输入的账号是否匹配预定义的规则
*
* @param value 要验证的账号
* @return 如果账号符合规则返回 true否则返回 false
*/
public static boolean isAccount(CharSequence value) {
return isMatchRegex(ACCOUNT, value);
}
/**
* 验证输入的账号是否符合规则如果不符合则抛出 ValidateException 异常
*
* @param value 要验证的账号
* @param errorMsg 验证失败时抛出的异常消息
* @param <T> CharSequence 的子类型
* @return 如果验证通过返回输入的账号
* @throws ValidateException 如果验证失败
*/
public static <T extends CharSequence> T validateAccount(T value, String errorMsg) throws ValidateException {
if (!isAccount(value)) {
throw new ValidateException(errorMsg);
}
return value;
}
/**
* 检查输入的状态是否匹配预定义的规则
*
* @param value 要验证的状态
* @return 如果状态符合规则返回 true否则返回 false
*/
public static boolean isStatus(CharSequence value) {
return isMatchRegex(STATUS, value);
}
/**
* 验证输入的状态是否符合规则如果不符合则抛出 ValidateException 异常
*
* @param value 要验证的状态
* @param errorMsg 验证失败时抛出的异常消息
* @param <T> CharSequence 的子类型
* @return 如果验证通过返回输入的状态
* @throws ValidateException 如果验证失败
*/
public static <T extends CharSequence> T validateStatus(T value, String errorMsg) throws ValidateException {
if (!isStatus(value)) {
throw new ValidateException(errorMsg);
}
return value;
}
}

View File

@ -1,6 +1,7 @@
package com.ruoyi.common.encrypt.core;
import cn.hutool.core.util.ReflectUtil;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import com.ruoyi.common.encrypt.annotation.EncryptField;
@ -19,6 +20,7 @@ import java.util.stream.Collectors;
* @version 4.6.0
*/
@Slf4j
@NoArgsConstructor
public class EncryptorManager {
/**

View File

@ -36,8 +36,9 @@ public class CryptoFilter implements Filter {
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest servletRequest = (HttpServletRequest) request;
HttpServletResponse servletResponse = (HttpServletResponse) response;
boolean encryptFlag = false;
// 获取加密注解
ApiEncrypt apiEncrypt = this.getApiEncryptAnnotation(servletRequest);
boolean responseFlag = apiEncrypt != null && apiEncrypt.response();
ServletRequest requestWrapper = null;
ServletResponse responseWrapper = null;
EncryptResponseBodyWrapper responseBodyWrapper = null;
@ -50,33 +51,30 @@ public class CryptoFilter implements Filter {
String headerValue = servletRequest.getHeader(properties.getHeaderFlag());
if (StringUtils.isNotBlank(headerValue)) {
// 请求解密
requestWrapper = new DecryptRequestBodyWrapper(servletRequest, properties.getPrivateKey(), properties.getHeaderFlag());
// 获取加密注解
ApiEncrypt apiEncrypt = this.getApiEncryptAnnotation(servletRequest);
requestWrapper = new DecryptRequestBodyWrapper(servletRequest, headerValue);
} else {
// 是否有注解有就报错没有放行
if (ObjectUtil.isNotNull(apiEncrypt)) {
// 响应加密标志
encryptFlag = apiEncrypt.response();
} else {
// 是否有注解有就报错没有放行
HandlerExceptionResolver exceptionResolver = SpringUtils.getBean(HandlerExceptionResolver.class);
HandlerExceptionResolver exceptionResolver = SpringUtils.getBean("handlerExceptionResolver", HandlerExceptionResolver.class);
exceptionResolver.resolveException(
servletRequest, servletResponse, null,
new ServiceException("没有访问权限,请联系管理员授权", HttpStatus.FORBIDDEN));
return;
}
}
// 判断是否响应加密
if (encryptFlag) {
responseBodyWrapper = new EncryptResponseBodyWrapper(servletResponse);
responseWrapper = responseBodyWrapper;
}
}
}
// 判断是否响应加密
if (responseFlag) {
responseBodyWrapper = new EncryptResponseBodyWrapper(servletResponse);
responseWrapper = responseBodyWrapper;
}
chain.doFilter(
ObjectUtil.defaultIfNull(requestWrapper, request),
ObjectUtil.defaultIfNull(responseWrapper, response));
if (encryptFlag) {
if (responseFlag) {
servletResponse.reset();
// 对原始内容加密
String encryptContent = responseBodyWrapper.getEncryptContent(

View File

@ -2,12 +2,14 @@ package com.ruoyi.common.encrypt.filter;
import cn.hutool.core.io.IoUtil;
import com.ruoyi.common.encrypt.utils.EncryptUtils;
import com.ruoyi.common.redis.utils.RedisUtils;
import jakarta.servlet.ReadListener;
import jakarta.servlet.ServletInputStream;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletRequestWrapper;
import com.ruoyi.common.core.constant.Constants;
import org.springframework.http.MediaType;
import org.springframework.web.util.UriUtils;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
@ -24,22 +26,20 @@ public class DecryptRequestBodyWrapper extends HttpServletRequestWrapper {
private final byte[] body;
public DecryptRequestBodyWrapper(HttpServletRequest request, String privateKey, String headerFlag) throws IOException {
super(request);
// 获取 AES 密码 采用 RSA 加密
String headerRsa = request.getHeader(headerFlag);
String decryptAes = EncryptUtils.decryptByRsa(headerRsa, privateKey);
// 解密 AES 密码
String aesPassword = EncryptUtils.decryptByBase64(decryptAes);
request.setCharacterEncoding(Constants.UTF8);
byte[] readBytes = IoUtil.readBytes(request.getInputStream(), false);
String requestBody = new String(readBytes, StandardCharsets.UTF_8);
// 解密 body 采用 AES 加密
String decryptBody = EncryptUtils.decryptByAes(requestBody, aesPassword);
body = decryptBody.getBytes(StandardCharsets.UTF_8);
}
public DecryptRequestBodyWrapper(HttpServletRequest request, String privateKey) throws IOException {
super(request);
// 获取 AES 密码 采用 RSA 加密
String privateKeyValue = RedisUtils.getCacheMapValue("loginRsa", privateKey);
@Override
byte[] readBytes = IoUtil.readBytes(request.getInputStream(), false);
String requestBody = new String(readBytes, StandardCharsets.UTF_8);
// 解密 body 采用 AES 加密
String decryptBody = EncryptUtils.decryptByRsa(requestBody, privateKeyValue);
body = UriUtils.decode(decryptBody, StandardCharsets.UTF_8).getBytes(StandardCharsets.UTF_8);
}
@Override
public BufferedReader getReader() {
return new BufferedReader(new InputStreamReader(getInputStream()));
}

View File

@ -0,0 +1,363 @@
package com.ruoyi.common.encrypt.utils;
import org.apache.commons.codec.binary.Base64;
import javax.crypto.Cipher;
import java.io.ByteArrayOutputStream;
import java.security.*;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.HashMap;
import java.util.Map;
/**
* @NAME: RSAEncryptUtils
* @AUTHOR: gaoly
* @DATE: 2021/1/26 15:15
* @DES:
**/
public class RSAUtils {
/** */
/**
* 加密算法RSA
*/
public static final String KEY_ALGORITHM = "RSA";
/** */
/**
* 签名算法
*/
public static final String SIGNATURE_ALGORITHM = "MD5withRSA";
/** */
/**
* 获取公钥的key
*/
private static final String PUBLIC_KEY = "RSAPublicKey";
/** */
/**
* 获取私钥的key
*/
private static final String PRIVATE_KEY = "RSAPrivateKey";
/** */
/**
* RSA最大加密明文大小
*/
private static final int MAX_ENCRYPT_BLOCK = 117;
/** */
/**
* RSA最大解密密文大小
*/
private static final int MAX_DECRYPT_BLOCK = 128;
/** */
/**
* RSA 位数 如果采用2048 上面最大加密和最大解密则须填写: 245 256
*/
private static final int INITIALIZE_LENGTH = 1024;
/** */
/**
* <p>
* 生成密钥对(公钥和私钥)
* </p>
*
* @return
* @throws Exception
*/
public static Map<String, Object> genKeyPair() throws Exception {
KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance(KEY_ALGORITHM);
keyPairGen.initialize(INITIALIZE_LENGTH);
KeyPair keyPair = keyPairGen.generateKeyPair();
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
Map<String, Object> keyMap = new HashMap<String, Object>(2);
keyMap.put(PUBLIC_KEY, publicKey);
keyMap.put(PRIVATE_KEY, privateKey);
return keyMap;
}
/** */
/**
* <p>
* 用私钥对信息生成数字签名
* </p>
*
* @param data
* 已加密数据
* @param privateKey
* 私钥(BASE64编码)
*
* @return
* @throws Exception
*/
public static String sign(byte[] data, String privateKey) throws Exception {
byte[] keyBytes = Base64.decodeBase64(privateKey);
PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM);
PrivateKey privateK = keyFactory.generatePrivate(pkcs8KeySpec);
Signature signature = Signature.getInstance(SIGNATURE_ALGORITHM);
signature.initSign(privateK);
signature.update(data);
return Base64.encodeBase64String(signature.sign());
}
/** */
/**
* <p>
* 校验数字签名
* </p>
*
* @param data
* 已加密数据
* @param publicKey
* 公钥(BASE64编码)
* @param sign
* 数字签名
*
* @return
* @throws Exception
*
*/
public static boolean verify(byte[] data, String publicKey, String sign) throws Exception {
byte[] keyBytes = Base64.decodeBase64(publicKey);
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM);
PublicKey publicK = keyFactory.generatePublic(keySpec);
Signature signature = Signature.getInstance(SIGNATURE_ALGORITHM);
signature.initVerify(publicK);
signature.update(data);
return signature.verify(Base64.decodeBase64(sign));
}
/** */
/**
* <P>
* 私钥解密
* </p>
*
* @param encryptedData
* 已加密数据
* @param privateKey
* 私钥(BASE64编码)
* @return
* @throws Exception
*/
public static byte[] decryptByPrivateKey(byte[] encryptedData, String privateKey) throws Exception {
byte[] keyBytes = Base64.decodeBase64(privateKey);
PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM);
Key privateK = keyFactory.generatePrivate(pkcs8KeySpec);
Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm());
cipher.init(Cipher.DECRYPT_MODE, privateK);
int inputLen = encryptedData.length;
ByteArrayOutputStream out = new ByteArrayOutputStream();
int offSet = 0;
byte[] cache;
int i = 0;
// 对数据分段解密
while (inputLen - offSet > 0) {
if (inputLen - offSet > MAX_DECRYPT_BLOCK) {
cache = cipher.doFinal(encryptedData, offSet, MAX_DECRYPT_BLOCK);
} else {
cache = cipher.doFinal(encryptedData, offSet, inputLen - offSet);
}
out.write(cache, 0, cache.length);
i++;
offSet = i * MAX_DECRYPT_BLOCK;
}
byte[] decryptedData = out.toByteArray();
out.close();
return decryptedData;
}
/** */
/**
* <p>
* 公钥解密
* </p>
*
* @param encryptedData
* 已加密数据
* @param publicKey
* 公钥(BASE64编码)
* @return
* @throws Exception
*/
public static byte[] decryptByPublicKey(byte[] encryptedData, String publicKey) throws Exception {
byte[] keyBytes = Base64.decodeBase64(publicKey);
X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM);
Key publicK = keyFactory.generatePublic(x509KeySpec);
Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm());
cipher.init(Cipher.DECRYPT_MODE, publicK);
int inputLen = encryptedData.length;
ByteArrayOutputStream out = new ByteArrayOutputStream();
int offSet = 0;
byte[] cache;
int i = 0;
// 对数据分段解密
while (inputLen - offSet > 0) {
if (inputLen - offSet > MAX_DECRYPT_BLOCK) {
cache = cipher.doFinal(encryptedData, offSet, MAX_DECRYPT_BLOCK);
} else {
cache = cipher.doFinal(encryptedData, offSet, inputLen - offSet);
}
out.write(cache, 0, cache.length);
i++;
offSet = i * MAX_DECRYPT_BLOCK;
}
byte[] decryptedData = out.toByteArray();
out.close();
return decryptedData;
}
/** */
/**
* <p>
* 公钥加密
* </p>
*
* @param data
* 源数据
* @param publicKey
* 公钥(BASE64编码)
* @return
* @throws Exception
*/
public static byte[] encryptByPublicKey(byte[] data, String publicKey) throws Exception {
byte[] keyBytes = Base64.decodeBase64(publicKey);
X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM);
Key publicK = keyFactory.generatePublic(x509KeySpec);
// 对数据加密
Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm());
cipher.init(Cipher.ENCRYPT_MODE, publicK);
int inputLen = data.length;
ByteArrayOutputStream out = new ByteArrayOutputStream();
int offSet = 0;
byte[] cache;
int i = 0;
// 对数据分段加密
while (inputLen - offSet > 0) {
if (inputLen - offSet > MAX_ENCRYPT_BLOCK) {
cache = cipher.doFinal(data, offSet, MAX_ENCRYPT_BLOCK);
} else {
cache = cipher.doFinal(data, offSet, inputLen - offSet);
}
out.write(cache, 0, cache.length);
i++;
offSet = i * MAX_ENCRYPT_BLOCK;
}
byte[] encryptedData = out.toByteArray();
out.close();
return encryptedData;
}
/** */
/**
* <p>
* 私钥加密
* </p>
*
* @param data
* 源数据
* @param privateKey
* 私钥(BASE64编码)
* @return
* @throws Exception
*/
public static byte[] encryptByPrivateKey(byte[] data, String privateKey) throws Exception {
byte[] keyBytes = Base64.decodeBase64(privateKey);
PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM);
Key privateK = keyFactory.generatePrivate(pkcs8KeySpec);
Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm());
cipher.init(Cipher.ENCRYPT_MODE, privateK);
int inputLen = data.length;
ByteArrayOutputStream out = new ByteArrayOutputStream();
int offSet = 0;
byte[] cache;
int i = 0;
// 对数据分段加密
while (inputLen - offSet > 0) {
if (inputLen - offSet > MAX_ENCRYPT_BLOCK) {
cache = cipher.doFinal(data, offSet, MAX_ENCRYPT_BLOCK);
} else {
cache = cipher.doFinal(data, offSet, inputLen - offSet);
}
out.write(cache, 0, cache.length);
i++;
offSet = i * MAX_ENCRYPT_BLOCK;
}
byte[] encryptedData = out.toByteArray();
out.close();
return encryptedData;
}
/** */
/**
* <p>
* 获取私钥
* </p>
*
* @param keyMap
* 密钥对
* @return
* @throws Exception
*/
public static String getPrivateKey(Map<String, Object> keyMap) throws Exception {
Key key = (Key) keyMap.get(PRIVATE_KEY);
return Base64.encodeBase64String(key.getEncoded());
}
/** */
/**
* <p>
* 获取公钥
* </p>
*
* @param keyMap
* 密钥对
* @return
* @throws Exception
*/
public static String getPublicKey(Map<String, Object> keyMap) throws Exception {
Key key = (Key) keyMap.get(PUBLIC_KEY);
return Base64.encodeBase64String(key.getEncoded());
}
/**
* java端公钥加密
*/
public static String encryptedDataOnJava(String data, String PUBLICKEY) {
try {
data = Base64.encodeBase64String(encryptByPublicKey(data.getBytes(), PUBLICKEY));
} catch (Exception e) {
}
return data;
}
/**
* java端私钥解密
*/
public static String decryptDataOnJava(String data, String PRIVATEKEY) {
String temp = "";
try {
byte[] rs = Base64.decodeBase64(data);
temp = new String(RSAUtils.decryptByPrivateKey(rs, PRIVATEKEY),"UTF-8");
} catch (Exception e) {
}
return temp;
}
public static void main(String[] args) throws Exception{
}
}

View File

@ -11,6 +11,7 @@ import com.alibaba.excel.util.ClassUtils;
import com.alibaba.excel.write.handler.SheetWriteHandler;
import com.alibaba.excel.write.metadata.holder.WriteSheetHolder;
import com.alibaba.excel.write.metadata.holder.WriteWorkbookHolder;
import com.ruoyi.common.core.utils.StringUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.ss.util.CellRangeAddressList;
@ -99,15 +100,16 @@ public class ExcelDownHandler implements SheetWriteHandler {
ExcelDictFormat format = field.getDeclaredAnnotation(ExcelDictFormat.class);
String dictType = format.dictType();
String converterExp = format.readConverterExp();
if (StrUtil.isNotBlank(dictType)) {
if (StringUtils.isNotBlank(dictType)) {
// 如果传递了字典名则依据字典建立下拉
Collection<String> values = Optional.ofNullable(dictService.getAllDictByDictType(dictType))
.orElseThrow(() -> new ServiceException(String.format("字典 %s 不存在", dictType)))
.values();
options = new ArrayList<>(values);
} else if (StrUtil.isNotBlank(converterExp)) {
} else if (StringUtils.isNotBlank(converterExp)) {
// 如果指定了确切的值则直接解析确切的值
options = StrUtil.split(converterExp, format.separator(), true, true);
List<String> strList = StringUtils.splitList(converterExp, format.separator());
options = StreamUtils.toList(strList, s -> StringUtils.split(s, "=")[1]);
}
} else if (field.isAnnotationPresent(ExcelEnumFormat.class)) {
// 否则如果指定了@ExcelEnumFormat则使用枚举的逻辑

View File

@ -38,6 +38,20 @@
<artifactId>powerjob-official-processors</artifactId>
</dependency>
<!-- EasyRetry client -->
<dependency>
<groupId>com.aizuda</groupId>
<artifactId>easy-retry-client-starter</artifactId>
</dependency>
<dependency>
<groupId>com.aizuda</groupId>
<artifactId>easy-retry-client-core</artifactId>
</dependency>
<dependency>
<groupId>com.aizuda</groupId>
<artifactId>easy-retry-client-job-core</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>

View File

@ -0,0 +1,37 @@
package com.ruoyi.common.job.config;
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.spi.ILoggingEvent;
import com.aizuda.easy.retry.client.common.appender.EasyRetryLogbackAppender;
import com.aizuda.easy.retry.client.common.event.EasyRetryStartingEvent;
import com.aizuda.easy.retry.client.starter.EnableEasyRetry;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.EnableScheduling;
/**
* 启动定时任务
*
* @author dhb52
* @since 2024/3/12
*/
@AutoConfiguration
@ConditionalOnProperty(prefix = "easy-retry", name = "enabled", havingValue = "true")
@EnableScheduling
@EnableEasyRetry(group = "${easy-retry.group-name}")
public class EasyRetryConfig {
@EventListener(EasyRetryStartingEvent.class)
public void onStarting(EasyRetryStartingEvent event) {
LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory();
EasyRetryLogbackAppender<ILoggingEvent> ca = new EasyRetryLogbackAppender<>();
ca.setName("easy_log_appender");
ca.start();
Logger rootLogger = lc.getLogger(Logger.ROOT_LOGGER_NAME);
rootLogger.addAppender(ca);
}
}

View File

@ -0,0 +1,2 @@
com.ruoyi.common.job.config.PowerJobConfig
com.ruoyi.common.job.config.EasyRetryConfig

View File

@ -27,11 +27,6 @@
<artifactId>ruoyi-common-json</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>transmittable-thread-local</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -204,7 +204,7 @@ public class LogAspect {
public boolean isFilterObject(final Object o) {
Class<?> clazz = o.getClass();
if (clazz.isArray()) {
return clazz.getComponentType().isAssignableFrom(MultipartFile.class);
return MultipartFile.class.isAssignableFrom(clazz.getComponentType());
} else if (Collection.class.isAssignableFrom(clazz)) {
Collection collection = (Collection) o;
for (Object value : collection) {

View File

@ -0,0 +1,34 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi-common</artifactId>
<version>${revision}</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>ruoyi-common-mail</artifactId>
<description>
ruoyi-common-mail 邮件模块
</description>
<dependencies>
<dependency>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi-common-core</artifactId>
</dependency>
<dependency>
<groupId>jakarta.mail</groupId>
<artifactId>jakarta.mail-api</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.angus</groupId>
<artifactId>jakarta.mail</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,37 @@
package com.ruoyi.common.mail.config;
import com.ruoyi.common.mail.config.properties.MailProperties;
import com.ruoyi.common.mail.utils.MailAccount;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
/**
* JavaMail 配置
*
* @author Michelle.Chung
*/
@AutoConfiguration
@EnableConfigurationProperties(MailProperties.class)
public class MailConfig {
@Bean
@ConditionalOnProperty(value = "mail.enabled", havingValue = "true")
public MailAccount mailAccount(MailProperties mailProperties) {
MailAccount account = new MailAccount();
account.setHost(mailProperties.getHost());
account.setPort(mailProperties.getPort());
account.setAuth(mailProperties.getAuth());
account.setFrom(mailProperties.getFrom());
account.setUser(mailProperties.getUser());
account.setPass(mailProperties.getPass());
account.setSocketFactoryPort(mailProperties.getPort());
account.setStarttlsEnable(mailProperties.getStarttlsEnable());
account.setSslEnable(mailProperties.getSslEnable());
account.setTimeout(mailProperties.getTimeout());
account.setConnectionTimeout(mailProperties.getConnectionTimeout());
return account;
}
}

View File

@ -0,0 +1,69 @@
package com.ruoyi.common.mail.config.properties;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* JavaMail 配置属性
*
* @author Michelle.Chung
*/
@Data
@ConfigurationProperties(prefix = "mail")
public class MailProperties {
/**
* 过滤开关
*/
private Boolean enabled;
/**
* SMTP服务器域名
*/
private String host;
/**
* SMTP服务端口
*/
private Integer port;
/**
* 是否需要用户名密码验证
*/
private Boolean auth;
/**
* 用户名
*/
private String user;
/**
* 密码
*/
private String pass;
/**
* 发送方遵循RFC-822标准
*/
private String from;
/**
* 使用 STARTTLS安全连接STARTTLS是对纯文本通信协议的扩展它将纯文本连接升级为加密连接TLS或SSL 而不是使用一个单独的加密通信端口
*/
private Boolean starttlsEnable;
/**
* 使用 SSL安全连接
*/
private Boolean sslEnable;
/**
* SMTP超时时长单位毫秒缺省值不超时
*/
private Long timeout;
/**
* Socket连接超时值单位毫秒缺省值不超时
*/
private Long connectionTimeout;
}

View File

@ -0,0 +1,46 @@
package com.ruoyi.common.mail.utils;
import cn.hutool.core.io.IORuntimeException;
/**
* 全局邮件帐户依赖于邮件配置文件{@link MailAccount#MAIL_SETTING_PATHS}
*
* @author looly
*/
public enum GlobalMailAccount {
INSTANCE;
private final MailAccount mailAccount;
/**
* 构造
*/
GlobalMailAccount() {
mailAccount = createDefaultAccount();
}
/**
* 获得邮件帐户
*
* @return 邮件帐户
*/
public MailAccount getAccount() {
return this.mailAccount;
}
/**
* 创建默认帐户
*
* @return MailAccount
*/
private MailAccount createDefaultAccount() {
for (String mailSettingPath : MailAccount.MAIL_SETTING_PATHS) {
try {
return new MailAccount(mailSettingPath);
} catch (IORuntimeException ignore) {
//ignore
}
}
return null;
}
}

View File

@ -0,0 +1,108 @@
package com.ruoyi.common.mail.utils;
import cn.hutool.core.util.ArrayUtil;
import jakarta.mail.internet.AddressException;
import jakarta.mail.internet.InternetAddress;
import jakarta.mail.internet.MimeUtility;
import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* 邮件内部工具类
*
* @author looly
* @since 3.2.3
*/
public class InternalMailUtil {
/**
* 将多个字符串邮件地址转为{@link InternetAddress}列表<br>
* 单个字符串地址可以是多个地址合并的字符串
*
* @param addrStrs 地址数组
* @param charset 编码主要用于中文用户名的编码
* @return 地址数组
* @since 4.0.3
*/
public static InternetAddress[] parseAddressFromStrs(String[] addrStrs, Charset charset) {
final List<InternetAddress> resultList = new ArrayList<>(addrStrs.length);
InternetAddress[] addrs;
for (String addrStr : addrStrs) {
addrs = parseAddress(addrStr, charset);
if (ArrayUtil.isNotEmpty(addrs)) {
Collections.addAll(resultList, addrs);
}
}
return resultList.toArray(new InternetAddress[0]);
}
/**
* 解析第一个地址
*
* @param address 地址字符串
* @param charset 编码{@code null}表示使用系统属性定义的编码或系统编码
* @return 地址列表
*/
public static InternetAddress parseFirstAddress(String address, Charset charset) {
final InternetAddress[] internetAddresses = parseAddress(address, charset);
if (ArrayUtil.isEmpty(internetAddresses)) {
try {
return new InternetAddress(address);
} catch (AddressException e) {
throw new MailException(e);
}
}
return internetAddresses[0];
}
/**
* 将一个地址字符串解析为多个地址<br>
* 地址间使用" "","";"分隔
*
* @param address 地址字符串
* @param charset 编码{@code null}表示使用系统属性定义的编码或系统编码
* @return 地址列表
*/
public static InternetAddress[] parseAddress(String address, Charset charset) {
InternetAddress[] addresses;
try {
addresses = InternetAddress.parse(address);
} catch (AddressException e) {
throw new MailException(e);
}
//编码用户名
if (ArrayUtil.isNotEmpty(addresses)) {
final String charsetStr = null == charset ? null : charset.name();
for (InternetAddress internetAddress : addresses) {
try {
internetAddress.setPersonal(internetAddress.getPersonal(), charsetStr);
} catch (UnsupportedEncodingException e) {
throw new MailException(e);
}
}
}
return addresses;
}
/**
* 编码中文字符<br>
* 编码失败返回原字符串
*
* @param text 被编码的文本
* @param charset 编码
* @return 编码后的结果
*/
public static String encodeText(String text, Charset charset) {
try {
return MimeUtility.encodeText(text, charset.name(), null);
} catch (UnsupportedEncodingException e) {
// ignore
}
return text;
}
}

View File

@ -0,0 +1,483 @@
package com.ruoyi.common.mail.utils;
import cn.hutool.core.builder.Builder;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.IORuntimeException;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import jakarta.activation.DataHandler;
import jakarta.activation.DataSource;
import jakarta.activation.FileDataSource;
import jakarta.activation.FileTypeMap;
import jakarta.mail.*;
import jakarta.mail.internet.MimeBodyPart;
import jakarta.mail.internet.MimeMessage;
import jakarta.mail.internet.MimeMultipart;
import jakarta.mail.internet.MimeUtility;
import jakarta.mail.util.ByteArrayDataSource;
import java.io.*;
import java.nio.charset.Charset;
import java.util.Date;
/**
* 邮件发送客户端
*
* @author looly
* @since 3.2.0
*/
public class Mail implements Builder<MimeMessage> {
@Serial
private static final long serialVersionUID = 1L;
/**
* 邮箱帐户信息以及一些客户端配置信息
*/
private final MailAccount mailAccount;
/**
* 收件人列表
*/
private String[] tos;
/**
* 抄送人列表carbon copy
*/
private String[] ccs;
/**
* 密送人列表blind carbon copy
*/
private String[] bccs;
/**
* 回复地址(reply-to)
*/
private String[] reply;
/**
* 标题
*/
private String title;
/**
* 内容
*/
private String content;
/**
* 是否为HTML
*/
private boolean isHtml;
/**
* 正文附件和图片的混合部分
*/
private final Multipart multipart = new MimeMultipart();
/**
* 是否使用全局会话默认为false
*/
private boolean useGlobalSession = false;
/**
* debug输出位置可以自定义debug日志
*/
private PrintStream debugOutput;
/**
* 创建邮件客户端
*
* @param mailAccount 邮件帐号
* @return Mail
*/
public static Mail create(MailAccount mailAccount) {
return new Mail(mailAccount);
}
/**
* 创建邮件客户端使用全局邮件帐户
*
* @return Mail
*/
public static Mail create() {
return new Mail();
}
// --------------------------------------------------------------- Constructor start
/**
* 构造使用全局邮件帐户
*/
public Mail() {
this(GlobalMailAccount.INSTANCE.getAccount());
}
/**
* 构造
*
* @param mailAccount 邮件帐户如果为null使用默认配置文件的全局邮件配置
*/
public Mail(MailAccount mailAccount) {
mailAccount = (null != mailAccount) ? mailAccount : GlobalMailAccount.INSTANCE.getAccount();
this.mailAccount = mailAccount.defaultIfEmpty();
}
// --------------------------------------------------------------- Constructor end
// --------------------------------------------------------------- Getters and Setters start
/**
* 设置收件人
*
* @param tos 收件人列表
* @return this
* @see #setTos(String...)
*/
public Mail to(String... tos) {
return setTos(tos);
}
/**
* 设置多个收件人
*
* @param tos 收件人列表
* @return this
*/
public Mail setTos(String... tos) {
this.tos = tos;
return this;
}
/**
* 设置多个抄送人carbon copy
*
* @param ccs 抄送人列表
* @return this
* @since 4.0.3
*/
public Mail setCcs(String... ccs) {
this.ccs = ccs;
return this;
}
/**
* 设置多个密送人blind carbon copy
*
* @param bccs 密送人列表
* @return this
* @since 4.0.3
*/
public Mail setBccs(String... bccs) {
this.bccs = bccs;
return this;
}
/**
* 设置多个回复地址(reply-to)
*
* @param reply 回复地址(reply-to)列表
* @return this
* @since 4.6.0
*/
public Mail setReply(String... reply) {
this.reply = reply;
return this;
}
/**
* 设置标题
*
* @param title 标题
* @return this
*/
public Mail setTitle(String title) {
this.title = title;
return this;
}
/**
* 设置正文<br>
* 正文可以是普通文本也可以是HTML默认普通文本可以通过调用{@link #setHtml(boolean)} 设置是否为HTML
*
* @param content 正文
* @return this
*/
public Mail setContent(String content) {
this.content = content;
return this;
}
/**
* 设置是否是HTML
*
* @param isHtml 是否为HTML
* @return this
*/
public Mail setHtml(boolean isHtml) {
this.isHtml = isHtml;
return this;
}
/**
* 设置正文
*
* @param content 正文内容
* @param isHtml 是否为HTML
* @return this
*/
public Mail setContent(String content, boolean isHtml) {
setContent(content);
return setHtml(isHtml);
}
/**
* 设置文件类型附件文件可以是图片文件此时自动设置cid正文中引用图片默认cid为文件名
*
* @param files 附件文件列表
* @return this
*/
public Mail setFiles(File... files) {
if (ArrayUtil.isEmpty(files)) {
return this;
}
final DataSource[] attachments = new DataSource[files.length];
for (int i = 0; i < files.length; i++) {
attachments[i] = new FileDataSource(files[i]);
}
return setAttachments(attachments);
}
/**
* 增加附件或图片附件使用{@link DataSource} 形式表示可以使用{@link FileDataSource}包装文件表示文件附件
*
* @param attachments 附件列表
* @return this
* @since 4.0.9
*/
public Mail setAttachments(DataSource... attachments) {
if (ArrayUtil.isNotEmpty(attachments)) {
final Charset charset = this.mailAccount.getCharset();
MimeBodyPart bodyPart;
String nameEncoded;
try {
for (DataSource attachment : attachments) {
bodyPart = new MimeBodyPart();
bodyPart.setDataHandler(new DataHandler(attachment));
nameEncoded = attachment.getName();
if (this.mailAccount.isEncodefilename()) {
nameEncoded = InternalMailUtil.encodeText(nameEncoded, charset);
}
// 普通附件文件名
bodyPart.setFileName(nameEncoded);
if (StrUtil.startWith(attachment.getContentType(), "image/")) {
// 图片附件用于正文中引用图片
bodyPart.setContentID(nameEncoded);
}
this.multipart.addBodyPart(bodyPart);
}
} catch (MessagingException e) {
throw new MailException(e);
}
}
return this;
}
/**
* 增加图片图片的键对应到邮件模板中的占位字符串图片类型默认为"image/jpeg"
*
* @param cid 图片与占位符占位符格式为cid:${cid}
* @param imageStream 图片文件
* @return this
* @since 4.6.3
*/
public Mail addImage(String cid, InputStream imageStream) {
return addImage(cid, imageStream, null);
}
/**
* 增加图片图片的键对应到邮件模板中的占位字符串
*
* @param cid 图片与占位符占位符格式为cid:${cid}
* @param imageStream 图片流不关闭
* @param contentType 图片类型null赋值默认的"image/jpeg"
* @return this
* @since 4.6.3
*/
public Mail addImage(String cid, InputStream imageStream, String contentType) {
ByteArrayDataSource imgSource;
try {
imgSource = new ByteArrayDataSource(imageStream, ObjectUtil.defaultIfNull(contentType, "image/jpeg"));
} catch (IOException e) {
throw new IORuntimeException(e);
}
imgSource.setName(cid);
return setAttachments(imgSource);
}
/**
* 增加图片图片的键对应到邮件模板中的占位字符串
*
* @param cid 图片与占位符占位符格式为cid:${cid}
* @param imageFile 图片文件
* @return this
* @since 4.6.3
*/
public Mail addImage(String cid, File imageFile) {
InputStream in = null;
try {
in = FileUtil.getInputStream(imageFile);
return addImage(cid, in, FileTypeMap.getDefaultFileTypeMap().getContentType(imageFile));
} finally {
IoUtil.close(in);
}
}
/**
* 设置字符集编码
*
* @param charset 字符集编码
* @return this
* @see MailAccount#setCharset(Charset)
*/
public Mail setCharset(Charset charset) {
this.mailAccount.setCharset(charset);
return this;
}
/**
* 设置是否使用全局会话默认为true
*
* @param isUseGlobalSession 是否使用全局会话默认为true
* @return this
* @since 4.0.2
*/
public Mail setUseGlobalSession(boolean isUseGlobalSession) {
this.useGlobalSession = isUseGlobalSession;
return this;
}
/**
* 设置debug输出位置可以自定义debug日志
*
* @param debugOutput debug输出位置
* @return this
* @since 5.5.6
*/
public Mail setDebugOutput(PrintStream debugOutput) {
this.debugOutput = debugOutput;
return this;
}
// --------------------------------------------------------------- Getters and Setters end
@Override
public MimeMessage build() {
try {
return buildMsg();
} catch (MessagingException e) {
throw new MailException(e);
}
}
/**
* 发送
*
* @return message-id
* @throws MailException 邮件发送异常
*/
public String send() throws MailException {
try {
return doSend();
} catch (MessagingException e) {
if (e instanceof SendFailedException) {
// 当地址无效时显示更加详细的无效地址信息
final Address[] invalidAddresses = ((SendFailedException) e).getInvalidAddresses();
final String msg = StrUtil.format("Invalid Addresses: {}", ArrayUtil.toString(invalidAddresses));
throw new MailException(msg, e);
}
throw new MailException(e);
}
}
// --------------------------------------------------------------- Private method start
/**
* 执行发送
*
* @return message-id
* @throws MessagingException 发送异常
*/
private String doSend() throws MessagingException {
final MimeMessage mimeMessage = buildMsg();
Transport.send(mimeMessage);
return mimeMessage.getMessageID();
}
/**
* 构建消息
*
* @return {@link MimeMessage}消息
* @throws MessagingException 消息异常
*/
private MimeMessage buildMsg() throws MessagingException {
final Charset charset = this.mailAccount.getCharset();
final MimeMessage msg = new MimeMessage(getSession());
// 发件人
final String from = this.mailAccount.getFrom();
if (StrUtil.isEmpty(from)) {
// 用户未提供发送方则从Session中自动获取
msg.setFrom();
} else {
msg.setFrom(InternalMailUtil.parseFirstAddress(from, charset));
}
// 标题
msg.setSubject(this.title, (null == charset) ? null : charset.name());
// 发送时间
msg.setSentDate(new Date());
// 内容和附件
msg.setContent(buildContent(charset));
// 收件人
msg.setRecipients(MimeMessage.RecipientType.TO, InternalMailUtil.parseAddressFromStrs(this.tos, charset));
// 抄送人
if (ArrayUtil.isNotEmpty(this.ccs)) {
msg.setRecipients(MimeMessage.RecipientType.CC, InternalMailUtil.parseAddressFromStrs(this.ccs, charset));
}
// 密送人
if (ArrayUtil.isNotEmpty(this.bccs)) {
msg.setRecipients(MimeMessage.RecipientType.BCC, InternalMailUtil.parseAddressFromStrs(this.bccs, charset));
}
// 回复地址(reply-to)
if (ArrayUtil.isNotEmpty(this.reply)) {
msg.setReplyTo(InternalMailUtil.parseAddressFromStrs(this.reply, charset));
}
return msg;
}
/**
* 构建邮件信息主体
*
* @param charset 编码{@code null}则使用{@link MimeUtility#getDefaultJavaCharset()}
* @return 邮件信息主体
* @throws MessagingException 消息异常
*/
private Multipart buildContent(Charset charset) throws MessagingException {
final String charsetStr = null != charset ? charset.name() : MimeUtility.getDefaultJavaCharset();
// 正文
final MimeBodyPart body = new MimeBodyPart();
body.setContent(content, StrUtil.format("text/{}; charset={}", isHtml ? "html" : "plain", charsetStr));
this.multipart.addBodyPart(body);
return this.multipart;
}
/**
* 获取默认邮件会话<br>
* 如果为全局单例的会话则全局只允许一个邮件帐号否则每次发送邮件会新建一个新的会话
*
* @return 邮件会话 {@link Session}
*/
private Session getSession() {
final Session session = MailUtils.getSession(this.mailAccount, this.useGlobalSession);
if (null != this.debugOutput) {
session.setDebugOut(debugOutput);
}
return session;
}
// --------------------------------------------------------------- Private method end
}

View File

@ -0,0 +1,659 @@
package com.ruoyi.common.mail.utils;
import cn.hutool.core.util.CharsetUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.setting.Setting;
import java.io.Serial;
import java.io.Serializable;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
/**
* 邮件账户对象
*
* @author Luxiaolei
*/
public class MailAccount implements Serializable {
@Serial
private static final long serialVersionUID = -6937313421815719204L;
private static final String MAIL_PROTOCOL = "mail.transport.protocol";
private static final String SMTP_HOST = "mail.smtp.host";
private static final String SMTP_PORT = "mail.smtp.port";
private static final String SMTP_AUTH = "mail.smtp.auth";
private static final String SMTP_TIMEOUT = "mail.smtp.timeout";
private static final String SMTP_CONNECTION_TIMEOUT = "mail.smtp.connectiontimeout";
private static final String SMTP_WRITE_TIMEOUT = "mail.smtp.writetimeout";
// SSL
private static final String STARTTLS_ENABLE = "mail.smtp.starttls.enable";
private static final String SSL_ENABLE = "mail.smtp.ssl.enable";
private static final String SSL_PROTOCOLS = "mail.smtp.ssl.protocols";
private static final String SOCKET_FACTORY = "mail.smtp.socketFactory.class";
private static final String SOCKET_FACTORY_FALLBACK = "mail.smtp.socketFactory.fallback";
private static final String SOCKET_FACTORY_PORT = "smtp.socketFactory.port";
// System Properties
private static final String SPLIT_LONG_PARAMS = "mail.mime.splitlongparameters";
//private static final String ENCODE_FILE_NAME = "mail.mime.encodefilename";
//private static final String CHARSET = "mail.mime.charset";
// 其他
private static final String MAIL_DEBUG = "mail.debug";
public static final String[] MAIL_SETTING_PATHS = new String[]{"config/mail.setting", "config/mailAccount.setting", "mail.setting"};
/**
* SMTP服务器域名
*/
private String host;
/**
* SMTP服务端口
*/
private Integer port;
/**
* 是否需要用户名密码验证
*/
private Boolean auth;
/**
* 用户名
*/
private String user;
/**
* 密码
*/
private String pass;
/**
* 发送方遵循RFC-822标准
*/
private String from;
/**
* 是否打开调试模式调试模式会显示与邮件服务器通信过程默认不开启
*/
private boolean debug;
/**
* 编码用于编码邮件正文和发送人收件人等中文
*/
private Charset charset = CharsetUtil.CHARSET_UTF_8;
/**
* 对于超长参数是否切分为多份默认为false国内邮箱附件不支持切分的附件名
*/
private boolean splitlongparameters = false;
/**
* 对于文件名是否使用{@link #charset}编码默认为 {@code true}
*/
private boolean encodefilename = true;
/**
* 使用 STARTTLS安全连接STARTTLS是对纯文本通信协议的扩展它将纯文本连接升级为加密连接TLS或SSL 而不是使用一个单独的加密通信端口
*/
private boolean starttlsEnable = false;
/**
* 使用 SSL安全连接
*/
private Boolean sslEnable;
/**
* SSL协议多个协议用空格分隔
*/
private String sslProtocols;
/**
* 指定实现javax.net.SocketFactory接口的类的名称,这个类将被用于创建SMTP的套接字
*/
private String socketFactoryClass = "javax.net.ssl.SSLSocketFactory";
/**
* 如果设置为true,未能创建一个套接字使用指定的套接字工厂类将导致使用java.net.Socket创建的套接字类, 默认值为true
*/
private boolean socketFactoryFallback;
/**
* 指定的端口连接到在使用指定的套接字工厂如果没有设置,将使用默认端口
*/
private int socketFactoryPort = 465;
/**
* SMTP超时时长单位毫秒缺省值不超时
*/
private long timeout;
/**
* Socket连接超时值单位毫秒缺省值不超时
*/
private long connectionTimeout;
/**
* Socket写出超时值单位毫秒缺省值不超时
*/
private long writeTimeout;
/**
* 自定义的其他属性此自定义属性会覆盖默认属性
*/
private final Map<String, Object> customProperty = new HashMap<>();
// -------------------------------------------------------------- Constructor start
/**
* 构造,所有参数需自行定义或保持默认值
*/
public MailAccount() {
}
/**
* 构造
*
* @param settingPath 配置文件路径
*/
public MailAccount(String settingPath) {
this(new Setting(settingPath));
}
/**
* 构造
*
* @param setting 配置文件
*/
public MailAccount(Setting setting) {
setting.toBean(this);
}
// -------------------------------------------------------------- Constructor end
/**
* 获得SMTP服务器域名
*
* @return SMTP服务器域名
*/
public String getHost() {
return host;
}
/**
* 设置SMTP服务器域名
*
* @param host SMTP服务器域名
* @return this
*/
public MailAccount setHost(String host) {
this.host = host;
return this;
}
/**
* 获得SMTP服务端口
*
* @return SMTP服务端口
*/
public Integer getPort() {
return port;
}
/**
* 设置SMTP服务端口
*
* @param port SMTP服务端口
* @return this
*/
public MailAccount setPort(Integer port) {
this.port = port;
return this;
}
/**
* 是否需要用户名密码验证
*
* @return 是否需要用户名密码验证
*/
public Boolean isAuth() {
return auth;
}
/**
* 设置是否需要用户名密码验证
*
* @param isAuth 是否需要用户名密码验证
* @return this
*/
public MailAccount setAuth(boolean isAuth) {
this.auth = isAuth;
return this;
}
/**
* 获取用户名
*
* @return 用户名
*/
public String getUser() {
return user;
}
/**
* 设置用户名
*
* @param user 用户名
* @return this
*/
public MailAccount setUser(String user) {
this.user = user;
return this;
}
/**
* 获取密码
*
* @return 密码
*/
public String getPass() {
return pass;
}
/**
* 设置密码
*
* @param pass 密码
* @return this
*/
public MailAccount setPass(String pass) {
this.pass = pass;
return this;
}
/**
* 获取发送方遵循RFC-822标准
*
* @return 发送方遵循RFC-822标准
*/
public String getFrom() {
return from;
}
/**
* 设置发送方遵循RFC-822标准<br>
* 发件人可以是以下形式
*
* <pre>
* 1. user@xxx.xx
* 2. name &lt;user@xxx.xx&gt;
* </pre>
*
* @param from 发送方遵循RFC-822标准
* @return this
*/
public MailAccount setFrom(String from) {
this.from = from;
return this;
}
/**
* 是否打开调试模式调试模式会显示与邮件服务器通信过程默认不开启
*
* @return 是否打开调试模式调试模式会显示与邮件服务器通信过程默认不开启
* @since 4.0.2
*/
public boolean isDebug() {
return debug;
}
/**
* 设置是否打开调试模式调试模式会显示与邮件服务器通信过程默认不开启
*
* @param debug 是否打开调试模式调试模式会显示与邮件服务器通信过程默认不开启
* @return this
* @since 4.0.2
*/
public MailAccount setDebug(boolean debug) {
this.debug = debug;
return this;
}
/**
* 获取字符集编码
*
* @return 编码可能为{@code null}
*/
public Charset getCharset() {
return charset;
}
/**
* 设置字符集编码此选项不会修改全局配置若修改全局配置请设置此项为{@code null}并设置
* <pre>
* System.setProperty("mail.mime.charset", charset);
* </pre>
*
* @param charset 字符集编码{@code null} 则表示使用全局设置的默认编码全局编码为mail.mime.charset系统属性
* @return this
*/
public MailAccount setCharset(Charset charset) {
this.charset = charset;
return this;
}
/**
* 对于超长参数是否切分为多份默认为false国内邮箱附件不支持切分的附件名
*
* @return 对于超长参数是否切分为多份
*/
public boolean isSplitlongparameters() {
return splitlongparameters;
}
/**
* 设置对于超长参数是否切分为多份默认为false国内邮箱附件不支持切分的附件名<br>
* 注意此项为全局设置此项会调用
* <pre>
* System.setProperty("mail.mime.splitlongparameters", true)
* </pre>
*
* @param splitlongparameters 对于超长参数是否切分为多份
*/
public void setSplitlongparameters(boolean splitlongparameters) {
this.splitlongparameters = splitlongparameters;
}
/**
* 对于文件名是否使用{@link #charset}编码默认为 {@code true}
*
* @return 对于文件名是否使用{@link #charset}编码默认为 {@code true}
* @since 5.7.16
*/
public boolean isEncodefilename() {
return encodefilename;
}
/**
* 设置对于文件名是否使用{@link #charset}编码此选项不会修改全局配置<br>
* 如果此选项设置为{@code false}则是否编码取决于两个系统属性
* <ul>
* <li>mail.mime.encodefilename 是否编码附件文件名</li>
* <li>mail.mime.charset 编码文件名的编码</li>
* </ul>
*
* @param encodefilename 对于文件名是否使用{@link #charset}编码
* @since 5.7.16
*/
public void setEncodefilename(boolean encodefilename) {
this.encodefilename = encodefilename;
}
/**
* 是否使用 STARTTLS安全连接STARTTLS是对纯文本通信协议的扩展它将纯文本连接升级为加密连接TLS或SSL 而不是使用一个单独的加密通信端口
*
* @return 是否使用 STARTTLS安全连接
*/
public boolean isStarttlsEnable() {
return this.starttlsEnable;
}
/**
* 设置是否使用STARTTLS安全连接STARTTLS是对纯文本通信协议的扩展它将纯文本连接升级为加密连接TLS或SSL 而不是使用一个单独的加密通信端口
*
* @param startttlsEnable 是否使用STARTTLS安全连接
* @return this
*/
public MailAccount setStarttlsEnable(boolean startttlsEnable) {
this.starttlsEnable = startttlsEnable;
return this;
}
/**
* 是否使用 SSL安全连接
*
* @return 是否使用 SSL安全连接
*/
public Boolean isSslEnable() {
return this.sslEnable;
}
/**
* 设置是否使用SSL安全连接
*
* @param sslEnable 是否使用SSL安全连接
* @return this
*/
public MailAccount setSslEnable(Boolean sslEnable) {
this.sslEnable = sslEnable;
return this;
}
/**
* 获取SSL协议多个协议用空格分隔
*
* @return SSL协议多个协议用空格分隔
* @since 5.5.7
*/
public String getSslProtocols() {
return sslProtocols;
}
/**
* 设置SSL协议多个协议用空格分隔
*
* @param sslProtocols SSL协议多个协议用空格分隔
* @since 5.5.7
*/
public void setSslProtocols(String sslProtocols) {
this.sslProtocols = sslProtocols;
}
/**
* 获取指定实现javax.net.SocketFactory接口的类的名称,这个类将被用于创建SMTP的套接字
*
* @return 指定实现javax.net.SocketFactory接口的类的名称, 这个类将被用于创建SMTP的套接字
*/
public String getSocketFactoryClass() {
return socketFactoryClass;
}
/**
* 设置指定实现javax.net.SocketFactory接口的类的名称,这个类将被用于创建SMTP的套接字
*
* @param socketFactoryClass 指定实现javax.net.SocketFactory接口的类的名称,这个类将被用于创建SMTP的套接字
* @return this
*/
public MailAccount setSocketFactoryClass(String socketFactoryClass) {
this.socketFactoryClass = socketFactoryClass;
return this;
}
/**
* 如果设置为true,未能创建一个套接字使用指定的套接字工厂类将导致使用java.net.Socket创建的套接字类, 默认值为true
*
* @return 如果设置为true, 未能创建一个套接字使用指定的套接字工厂类将导致使用java.net.Socket创建的套接字类, 默认值为true
*/
public boolean isSocketFactoryFallback() {
return socketFactoryFallback;
}
/**
* 如果设置为true,未能创建一个套接字使用指定的套接字工厂类将导致使用java.net.Socket创建的套接字类, 默认值为true
*
* @param socketFactoryFallback 如果设置为true,未能创建一个套接字使用指定的套接字工厂类将导致使用java.net.Socket创建的套接字类, 默认值为true
* @return this
*/
public MailAccount setSocketFactoryFallback(boolean socketFactoryFallback) {
this.socketFactoryFallback = socketFactoryFallback;
return this;
}
/**
* 获取指定的端口连接到在使用指定的套接字工厂如果没有设置,将使用默认端口
*
* @return 指定的端口连接到在使用指定的套接字工厂如果没有设置,将使用默认端口
*/
public int getSocketFactoryPort() {
return socketFactoryPort;
}
/**
* 指定的端口连接到在使用指定的套接字工厂如果没有设置,将使用默认端口
*
* @param socketFactoryPort 指定的端口连接到在使用指定的套接字工厂如果没有设置,将使用默认端口
* @return this
*/
public MailAccount setSocketFactoryPort(int socketFactoryPort) {
this.socketFactoryPort = socketFactoryPort;
return this;
}
/**
* 设置SMTP超时时长单位毫秒缺省值不超时
*
* @param timeout SMTP超时时长单位毫秒缺省值不超时
* @return this
* @since 4.1.17
*/
public MailAccount setTimeout(long timeout) {
this.timeout = timeout;
return this;
}
/**
* 设置Socket连接超时值单位毫秒缺省值不超时
*
* @param connectionTimeout Socket连接超时值单位毫秒缺省值不超时
* @return this
* @since 4.1.17
*/
public MailAccount setConnectionTimeout(long connectionTimeout) {
this.connectionTimeout = connectionTimeout;
return this;
}
/**
* 设置Socket写出超时值单位毫秒缺省值不超时
*
* @param writeTimeout Socket写出超时值单位毫秒缺省值不超时
* @return this
* @since 5.8.3
*/
public MailAccount setWriteTimeout(long writeTimeout) {
this.writeTimeout = writeTimeout;
return this;
}
/**
* 获取自定义属性列表
*
* @return 自定义参数列表
* @since 5.6.4
*/
public Map<String, Object> getCustomProperty() {
return customProperty;
}
/**
* 设置自定义属性如mail.smtp.ssl.socketFactory
*
* @param key 属性名空白被忽略
* @param value 属性值 null被忽略
* @return this
* @since 5.6.4
*/
public MailAccount setCustomProperty(String key, Object value) {
if (StrUtil.isNotBlank(key) && ObjectUtil.isNotNull(value)) {
this.customProperty.put(key, value);
}
return this;
}
/**
* 获得SMTP相关信息
*
* @return {@link Properties}
*/
public Properties getSmtpProps() {
//全局系统参数
System.setProperty(SPLIT_LONG_PARAMS, String.valueOf(this.splitlongparameters));
final Properties p = new Properties();
p.put(MAIL_PROTOCOL, "smtp");
p.put(SMTP_HOST, this.host);
p.put(SMTP_PORT, String.valueOf(this.port));
p.put(SMTP_AUTH, String.valueOf(this.auth));
if (this.timeout > 0) {
p.put(SMTP_TIMEOUT, String.valueOf(this.timeout));
}
if (this.connectionTimeout > 0) {
p.put(SMTP_CONNECTION_TIMEOUT, String.valueOf(this.connectionTimeout));
}
// issue#2355
if (this.writeTimeout > 0) {
p.put(SMTP_WRITE_TIMEOUT, String.valueOf(this.writeTimeout));
}
p.put(MAIL_DEBUG, String.valueOf(this.debug));
if (this.starttlsEnable) {
//STARTTLS是对纯文本通信协议的扩展它将纯文本连接升级为加密连接TLS或SSL 而不是使用一个单独的加密通信端口
p.put(STARTTLS_ENABLE, "true");
if (null == this.sslEnable) {
//为了兼容旧版本当用户没有此项配置时按照starttlsEnable开启状态时对待
this.sslEnable = true;
}
}
// SSL
if (null != this.sslEnable && this.sslEnable) {
p.put(SSL_ENABLE, "true");
p.put(SOCKET_FACTORY, socketFactoryClass);
p.put(SOCKET_FACTORY_FALLBACK, String.valueOf(this.socketFactoryFallback));
p.put(SOCKET_FACTORY_PORT, String.valueOf(this.socketFactoryPort));
// issue#IZN95@Gitee在Linux下需自定义SSL协议版本
if (StrUtil.isNotBlank(this.sslProtocols)) {
p.put(SSL_PROTOCOLS, this.sslProtocols);
}
}
// 补充自定义属性允许自定属性覆盖已经设置的值
p.putAll(this.customProperty);
return p;
}
/**
* 如果某些值为null使用默认值
*
* @return this
*/
public MailAccount defaultIfEmpty() {
// 去掉发件人的姓名部分
final String fromAddress = InternalMailUtil.parseFirstAddress(this.from, this.charset).getAddress();
if (StrUtil.isBlank(this.host)) {
// 如果SMTP地址为空默认使用smtp.<发件人邮箱后缀>
this.host = StrUtil.format("smtp.{}", StrUtil.subSuf(fromAddress, fromAddress.indexOf('@') + 1));
}
if (StrUtil.isBlank(user)) {
// 如果用户名为空默认为发件人issue#I4FYVY@Gitee
//this.user = StrUtil.subPre(fromAddress, fromAddress.indexOf('@'));
this.user = fromAddress;
}
if (null == this.auth) {
// 如果密码非空白则使用认证模式
this.auth = (false == StrUtil.isBlank(this.pass));
}
if (null == this.port) {
// 端口在SSL状态下默认与socketFactoryPort一致非SSL状态下默认为25
this.port = (null != this.sslEnable && this.sslEnable) ? this.socketFactoryPort : 25;
}
if (null == this.charset) {
// 默认UTF-8编码
this.charset = CharsetUtil.CHARSET_UTF_8;
}
return this;
}
@Override
public String toString() {
return "MailAccount [host=" + host + ", port=" + port + ", auth=" + auth + ", user=" + user + ", pass=" + (StrUtil.isEmpty(this.pass) ? "" : "******") + ", from=" + from + ", startttlsEnable="
+ starttlsEnable + ", socketFactoryClass=" + socketFactoryClass + ", socketFactoryFallback=" + socketFactoryFallback + ", socketFactoryPort=" + socketFactoryPort + "]";
}
}

View File

@ -0,0 +1,40 @@
package com.ruoyi.common.mail.utils;
import cn.hutool.core.exceptions.ExceptionUtil;
import cn.hutool.core.util.StrUtil;
import java.io.Serial;
/**
* 邮件异常
*
* @author xiaoleilu
*/
public class MailException extends RuntimeException {
@Serial
private static final long serialVersionUID = 8247610319171014183L;
public MailException(Throwable e) {
super(ExceptionUtil.getMessage(e), e);
}
public MailException(String message) {
super(message);
}
public MailException(String messageTemplate, Object... params) {
super(StrUtil.format(messageTemplate, params));
}
public MailException(String message, Throwable throwable) {
super(message, throwable);
}
public MailException(String message, Throwable throwable, boolean enableSuppression, boolean writableStackTrace) {
super(message, throwable, enableSuppression, writableStackTrace);
}
public MailException(Throwable throwable, String messageTemplate, Object... params) {
super(StrUtil.format(messageTemplate, params), throwable);
}
}

View File

@ -0,0 +1,467 @@
package com.ruoyi.common.mail.utils;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.CharUtil;
import cn.hutool.core.util.StrUtil;
import jakarta.mail.Authenticator;
import jakarta.mail.Session;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import com.ruoyi.common.core.utils.SpringUtils;
import com.ruoyi.common.core.utils.StringUtils;
import java.io.File;
import java.io.InputStream;
import java.util.Collection;
import java.util.List;
import java.util.Map;
/**
* 邮件工具类
*/
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class MailUtils {
private static final MailAccount ACCOUNT = SpringUtils.getBean(MailAccount.class);
/**
* 获取邮件发送实例
*/
public static MailAccount getMailAccount() {
return ACCOUNT;
}
/**
* 获取邮件发送实例 (自定义发送人以及授权码)
*
* @param user 发送人
* @param pass 授权码
*/
public static MailAccount getMailAccount(String from, String user, String pass) {
ACCOUNT.setFrom(StringUtils.blankToDefault(from, ACCOUNT.getFrom()));
ACCOUNT.setUser(StringUtils.blankToDefault(user, ACCOUNT.getUser()));
ACCOUNT.setPass(StringUtils.blankToDefault(pass, ACCOUNT.getPass()));
return ACCOUNT;
}
/**
* 使用配置文件中设置的账户发送文本邮件发送给单个或多个收件人<br>
* 多个收件人可以使用逗号,分隔也可以通过分号;分隔
*
* @param to 收件人
* @param subject 标题
* @param content 正文
* @param files 附件列表
* @return message-id
* @since 3.2.0
*/
public static String sendText(String to, String subject, String content, File... files) {
return send(to, subject, content, false, files);
}
/**
* 使用配置文件中设置的账户发送HTML邮件发送给单个或多个收件人<br>
* 多个收件人可以使用逗号,分隔也可以通过分号;分隔
*
* @param to 收件人
* @param subject 标题
* @param content 正文
* @param files 附件列表
* @return message-id
* @since 3.2.0
*/
public static String sendHtml(String to, String subject, String content, File... files) {
return send(to, subject, content, true, files);
}
/**
* 使用配置文件中设置的账户发送邮件发送单个或多个收件人<br>
* 多个收件人可以使用逗号,分隔也可以通过分号;分隔
*
* @param to 收件人
* @param subject 标题
* @param content 正文
* @param isHtml 是否为HTML
* @param files 附件列表
* @return message-id
*/
public static String send(String to, String subject, String content, boolean isHtml, File... files) {
return send(splitAddress(to), subject, content, isHtml, files);
}
/**
* 使用配置文件中设置的账户发送邮件发送单个或多个收件人<br>
* 多个收件人抄送人密送人可以使用逗号,分隔也可以通过分号;分隔
*
* @param to 收件人可以使用逗号,分隔也可以通过分号;分隔
* @param cc 抄送人可以使用逗号,分隔也可以通过分号;分隔
* @param bcc 密送人可以使用逗号,分隔也可以通过分号;分隔
* @param subject 标题
* @param content 正文
* @param isHtml 是否为HTML
* @param files 附件列表
* @return message-id
* @since 4.0.3
*/
public static String send(String to, String cc, String bcc, String subject, String content, boolean isHtml, File... files) {
return send(splitAddress(to), splitAddress(cc), splitAddress(bcc), subject, content, isHtml, files);
}
/**
* 使用配置文件中设置的账户发送文本邮件发送给多人
*
* @param tos 收件人列表
* @param subject 标题
* @param content 正文
* @param files 附件列表
* @return message-id
*/
public static String sendText(Collection<String> tos, String subject, String content, File... files) {
return send(tos, subject, content, false, files);
}
/**
* 使用配置文件中设置的账户发送HTML邮件发送给多人
*
* @param tos 收件人列表
* @param subject 标题
* @param content 正文
* @param files 附件列表
* @return message-id
* @since 3.2.0
*/
public static String sendHtml(Collection<String> tos, String subject, String content, File... files) {
return send(tos, subject, content, true, files);
}
/**
* 使用配置文件中设置的账户发送邮件发送给多人
*
* @param tos 收件人列表
* @param subject 标题
* @param content 正文
* @param isHtml 是否为HTML
* @param files 附件列表
* @return message-id
*/
public static String send(Collection<String> tos, String subject, String content, boolean isHtml, File... files) {
return send(tos, null, null, subject, content, isHtml, files);
}
/**
* 使用配置文件中设置的账户发送邮件发送给多人
*
* @param tos 收件人列表
* @param ccs 抄送人列表可以为null或空
* @param bccs 密送人列表可以为null或空
* @param subject 标题
* @param content 正文
* @param isHtml 是否为HTML
* @param files 附件列表
* @return message-id
* @since 4.0.3
*/
public static String send(Collection<String> tos, Collection<String> ccs, Collection<String> bccs, String subject, String content, boolean isHtml, File... files) {
return send(getMailAccount(), true, tos, ccs, bccs, subject, content, null, isHtml, files);
}
// ------------------------------------------------------------------------------------------------------------------------------- Custom MailAccount
/**
* 发送邮件给多人
*
* @param mailAccount 邮件认证对象
* @param to 收件人多个收件人逗号或者分号隔开
* @param subject 标题
* @param content 正文
* @param isHtml 是否为HTML格式
* @param files 附件列表
* @return message-id
* @since 3.2.0
*/
public static String send(MailAccount mailAccount, String to, String subject, String content, boolean isHtml, File... files) {
return send(mailAccount, splitAddress(to), subject, content, isHtml, files);
}
/**
* 发送邮件给多人
*
* @param mailAccount 邮件帐户信息
* @param tos 收件人列表
* @param subject 标题
* @param content 正文
* @param isHtml 是否为HTML格式
* @param files 附件列表
* @return message-id
*/
public static String send(MailAccount mailAccount, Collection<String> tos, String subject, String content, boolean isHtml, File... files) {
return send(mailAccount, tos, null, null, subject, content, isHtml, files);
}
/**
* 发送邮件给多人
*
* @param mailAccount 邮件帐户信息
* @param tos 收件人列表
* @param ccs 抄送人列表可以为null或空
* @param bccs 密送人列表可以为null或空
* @param subject 标题
* @param content 正文
* @param isHtml 是否为HTML格式
* @param files 附件列表
* @return message-id
* @since 4.0.3
*/
public static String send(MailAccount mailAccount, Collection<String> tos, Collection<String> ccs, Collection<String> bccs, String subject, String content, boolean isHtml, File... files) {
return send(mailAccount, false, tos, ccs, bccs, subject, content, null, isHtml, files);
}
/**
* 使用配置文件中设置的账户发送HTML邮件发送给单个或多个收件人<br>
* 多个收件人可以使用逗号,分隔也可以通过分号;分隔
*
* @param to 收件人
* @param subject 标题
* @param content 正文
* @param imageMap 图片与占位符占位符格式为cid:$IMAGE_PLACEHOLDER
* @param files 附件列表
* @return message-id
* @since 3.2.0
*/
public static String sendHtml(String to, String subject, String content, Map<String, InputStream> imageMap, File... files) {
return send(to, subject, content, imageMap, true, files);
}
/**
* 使用配置文件中设置的账户发送邮件发送单个或多个收件人<br>
* 多个收件人可以使用逗号,分隔也可以通过分号;分隔
*
* @param to 收件人
* @param subject 标题
* @param content 正文
* @param imageMap 图片与占位符占位符格式为cid:$IMAGE_PLACEHOLDER
* @param isHtml 是否为HTML
* @param files 附件列表
* @return message-id
*/
public static String send(String to, String subject, String content, Map<String, InputStream> imageMap, boolean isHtml, File... files) {
return send(splitAddress(to), subject, content, imageMap, isHtml, files);
}
/**
* 使用配置文件中设置的账户发送邮件发送单个或多个收件人<br>
* 多个收件人抄送人密送人可以使用逗号,分隔也可以通过分号;分隔
*
* @param to 收件人可以使用逗号,分隔也可以通过分号;分隔
* @param cc 抄送人可以使用逗号,分隔也可以通过分号;分隔
* @param bcc 密送人可以使用逗号,分隔也可以通过分号;分隔
* @param subject 标题
* @param content 正文
* @param imageMap 图片与占位符占位符格式为cid:$IMAGE_PLACEHOLDER
* @param isHtml 是否为HTML
* @param files 附件列表
* @return message-id
* @since 4.0.3
*/
public static String send(String to, String cc, String bcc, String subject, String content, Map<String, InputStream> imageMap, boolean isHtml, File... files) {
return send(splitAddress(to), splitAddress(cc), splitAddress(bcc), subject, content, imageMap, isHtml, files);
}
/**
* 使用配置文件中设置的账户发送HTML邮件发送给多人
*
* @param tos 收件人列表
* @param subject 标题
* @param content 正文
* @param imageMap 图片与占位符占位符格式为cid:$IMAGE_PLACEHOLDER
* @param files 附件列表
* @return message-id
* @since 3.2.0
*/
public static String sendHtml(Collection<String> tos, String subject, String content, Map<String, InputStream> imageMap, File... files) {
return send(tos, subject, content, imageMap, true, files);
}
/**
* 使用配置文件中设置的账户发送邮件发送给多人
*
* @param tos 收件人列表
* @param subject 标题
* @param content 正文
* @param imageMap 图片与占位符占位符格式为cid:$IMAGE_PLACEHOLDER
* @param isHtml 是否为HTML
* @param files 附件列表
* @return message-id
*/
public static String send(Collection<String> tos, String subject, String content, Map<String, InputStream> imageMap, boolean isHtml, File... files) {
return send(tos, null, null, subject, content, imageMap, isHtml, files);
}
/**
* 使用配置文件中设置的账户发送邮件发送给多人
*
* @param tos 收件人列表
* @param ccs 抄送人列表可以为null或空
* @param bccs 密送人列表可以为null或空
* @param subject 标题
* @param content 正文
* @param imageMap 图片与占位符占位符格式为cid:$IMAGE_PLACEHOLDER
* @param isHtml 是否为HTML
* @param files 附件列表
* @return message-id
* @since 4.0.3
*/
public static String send(Collection<String> tos, Collection<String> ccs, Collection<String> bccs, String subject, String content, Map<String, InputStream> imageMap, boolean isHtml, File... files) {
return send(getMailAccount(), true, tos, ccs, bccs, subject, content, imageMap, isHtml, files);
}
// ------------------------------------------------------------------------------------------------------------------------------- Custom MailAccount
/**
* 发送邮件给多人
*
* @param mailAccount 邮件认证对象
* @param to 收件人多个收件人逗号或者分号隔开
* @param subject 标题
* @param content 正文
* @param imageMap 图片与占位符占位符格式为cid:$IMAGE_PLACEHOLDER
* @param isHtml 是否为HTML格式
* @param files 附件列表
* @return message-id
* @since 3.2.0
*/
public static String send(MailAccount mailAccount, String to, String subject, String content, Map<String, InputStream> imageMap, boolean isHtml, File... files) {
return send(mailAccount, splitAddress(to), subject, content, imageMap, isHtml, files);
}
/**
* 发送邮件给多人
*
* @param mailAccount 邮件帐户信息
* @param tos 收件人列表
* @param subject 标题
* @param content 正文
* @param imageMap 图片与占位符占位符格式为cid:$IMAGE_PLACEHOLDER
* @param isHtml 是否为HTML格式
* @param files 附件列表
* @return message-id
* @since 4.6.3
*/
public static String send(MailAccount mailAccount, Collection<String> tos, String subject, String content, Map<String, InputStream> imageMap, boolean isHtml, File... files) {
return send(mailAccount, tos, null, null, subject, content, imageMap, isHtml, files);
}
/**
* 发送邮件给多人
*
* @param mailAccount 邮件帐户信息
* @param tos 收件人列表
* @param ccs 抄送人列表可以为null或空
* @param bccs 密送人列表可以为null或空
* @param subject 标题
* @param content 正文
* @param imageMap 图片与占位符占位符格式为cid:$IMAGE_PLACEHOLDER
* @param isHtml 是否为HTML格式
* @param files 附件列表
* @return message-id
* @since 4.6.3
*/
public static String send(MailAccount mailAccount, Collection<String> tos, Collection<String> ccs, Collection<String> bccs, String subject, String content, Map<String, InputStream> imageMap,
boolean isHtml, File... files) {
return send(mailAccount, false, tos, ccs, bccs, subject, content, imageMap, isHtml, files);
}
/**
* 根据配置文件获取邮件客户端会话
*
* @param mailAccount 邮件账户配置
* @param isSingleton 是否单例全局共享会话
* @return {@link Session}
* @since 5.5.7
*/
public static Session getSession(MailAccount mailAccount, boolean isSingleton) {
Authenticator authenticator = null;
if (mailAccount.isAuth()) {
authenticator = new UserPassAuthenticator(mailAccount.getUser(), mailAccount.getPass());
}
return isSingleton ? Session.getDefaultInstance(mailAccount.getSmtpProps(), authenticator) //
: Session.getInstance(mailAccount.getSmtpProps(), authenticator);
}
// ------------------------------------------------------------------------------------------------------------------------ Private method start
/**
* 发送邮件给多人
*
* @param mailAccount 邮件帐户信息
* @param useGlobalSession 是否全局共享Session
* @param tos 收件人列表
* @param ccs 抄送人列表可以为null或空
* @param bccs 密送人列表可以为null或空
* @param subject 标题
* @param content 正文
* @param imageMap 图片与占位符占位符格式为cid:${cid}
* @param isHtml 是否为HTML格式
* @param files 附件列表
* @return message-id
* @since 4.6.3
*/
private static String send(MailAccount mailAccount, boolean useGlobalSession, Collection<String> tos, Collection<String> ccs, Collection<String> bccs, String subject, String content,
Map<String, InputStream> imageMap, boolean isHtml, File... files) {
final Mail mail = Mail.create(mailAccount).setUseGlobalSession(useGlobalSession);
// 可选抄送人
if (CollUtil.isNotEmpty(ccs)) {
mail.setCcs(ccs.toArray(new String[0]));
}
// 可选密送人
if (CollUtil.isNotEmpty(bccs)) {
mail.setBccs(bccs.toArray(new String[0]));
}
mail.setTos(tos.toArray(new String[0]));
mail.setTitle(subject);
mail.setContent(content);
mail.setHtml(isHtml);
mail.setFiles(files);
// 图片
if (MapUtil.isNotEmpty(imageMap)) {
for (Map.Entry<String, InputStream> entry : imageMap.entrySet()) {
mail.addImage(entry.getKey(), entry.getValue());
// 关闭流
IoUtil.close(entry.getValue());
}
}
return mail.send();
}
/**
* 将多个联系人转为列表分隔符为逗号或者分号
*
* @param addresses 多个联系人如果为空返回null
* @return 联系人列表
*/
private static List<String> splitAddress(String addresses) {
if (StrUtil.isBlank(addresses)) {
return null;
}
List<String> result;
if (StrUtil.contains(addresses, CharUtil.COMMA)) {
result = StrUtil.splitTrim(addresses, CharUtil.COMMA);
} else if (StrUtil.contains(addresses, ';')) {
result = StrUtil.splitTrim(addresses, ';');
} else {
result = CollUtil.newArrayList(addresses);
}
return result;
}
// ------------------------------------------------------------------------------------------------------------------------ Private method end
}

View File

@ -0,0 +1,33 @@
package com.ruoyi.common.mail.utils;
import jakarta.mail.Authenticator;
import jakarta.mail.PasswordAuthentication;
/**
* 用户名密码验证器
*
* @author looly
* @since 3.1.2
*/
public class UserPassAuthenticator extends Authenticator {
private final String user;
private final String pass;
/**
* 构造
*
* @param user 用户名
* @param pass 密码
*/
public UserPassAuthenticator(String user, String pass) {
this.user = user;
this.pass = pass;
}
@Override
protected PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication(this.user, this.pass);
}
}

View File

@ -21,6 +21,11 @@
<artifactId>ruoyi-common-core</artifactId>
</dependency>
<dependency>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi-common-excel</artifactId>
</dependency>
<dependency>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi-common-security</artifactId>

View File

@ -3,20 +3,23 @@ package com.ruoyi.common.orm.config;
import com.mybatisflex.core.FlexGlobalConfig;
import com.mybatisflex.core.audit.AuditManager;
import com.mybatisflex.core.audit.ConsoleMessageCollector;
import com.mybatisflex.core.audit.MessageCollector;
import com.mybatisflex.core.datasource.DataSourceDecipher;
import com.mybatisflex.core.mybatis.FlexConfiguration;
import com.mybatisflex.core.query.QueryColumnBehavior;
import com.mybatisflex.spring.boot.ConfigurationCustomizer;
import com.mybatisflex.spring.boot.MyBatisFlexCustomizer;
import com.ruoyi.common.orm.core.domain.BaseEntity;
import com.ruoyi.common.orm.decipher.Decipher;
import com.ruoyi.common.orm.listener.EntityInsertListener;
import com.ruoyi.common.orm.listener.EntityUpdateListener;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.logging.stdout.StdOutImpl;
import org.mybatis.spring.annotation.MapperScan;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.transaction.annotation.EnableTransactionManagement;
/**
@ -24,11 +27,22 @@ import org.springframework.context.annotation.Configuration;
*
* @author dataprince数据小王子
*/
@EnableTransactionManagement(proxyTargetClass = true)
@AutoConfiguration
@Slf4j
@Configuration
@MapperScan("${mybatis-flex.mapper-package}")
public class MyBatisFlexConfig implements ConfigurationCustomizer, MyBatisFlexCustomizer {
private static final Logger logger = LoggerFactory.getLogger("mybatis-flex-sql");
@Value("${mybatis-flex.audit_enable}")
private Boolean enableAudit = false;
@Value("${mybatis-flex.sql_print}")
private Boolean sqlPrint = false;
static {
QueryColumnBehavior.setIgnoreFunction(QueryColumnBehavior.IGNORE_BLANK);
QueryColumnBehavior.setSmartConvertInToEquals(true);
}
/**
* 数据源解密
@ -41,8 +55,10 @@ public class MyBatisFlexConfig implements ConfigurationCustomizer, MyBatisFlexCu
@Override
public void customize(FlexConfiguration configuration) {
//mybatis实现的打印sql到控制台便于调试
configuration.setLogImpl(StdOutImpl.class);
//mybatis实现的打印详细sql及返回结果到控制台便于调试
if (sqlPrint) {
configuration.setLogImpl(StdOutImpl.class);
}
}
/**
@ -57,10 +73,14 @@ public class MyBatisFlexConfig implements ConfigurationCustomizer, MyBatisFlexCu
globalConfig.registerUpdateListener(new EntityUpdateListener(), BaseEntity.class);
// 开启审计功能
AuditManager.setAuditEnable(true);
// AuditManager.setMessageFactory(new AuditMessageFactory());
// 设置 SQL 审计收集器
AuditManager.setMessageCollector(new ConsoleMessageCollector());
AuditManager.setAuditEnable(enableAudit);
if (sqlPrint) {
// 开启sql打印默认会开启sql审计
AuditManager.setAuditEnable(true);
//设置 SQL 审计收集器
MessageCollector collector = new ConsoleMessageCollector();
AuditManager.setMessageCollector(collector);
}
}
}

View File

@ -4,6 +4,8 @@ import com.github.pagehelper.PageInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.Properties;
/**
* Pagehelper分页兼用老项目
*
@ -14,6 +16,11 @@ public class PagehelperConfig {
@Bean
public PageInterceptor pageInterceptor(){
PageInterceptor pageInterceptor = new PageInterceptor();
Properties properties = new Properties();
properties.setProperty("supportMethodsArguments","true");
properties.setProperty("autoRuntimeDialect","true");
pageInterceptor.setProperties(properties);
return pageInterceptor;
}
}

View File

@ -1,6 +1,8 @@
package com.ruoyi.common.orm.core.domain;
import com.alibaba.excel.annotation.ExcelProperty;
import com.mybatisflex.annotation.Column;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.io.Serial;
@ -19,18 +21,21 @@ public class TreeEntity extends BaseEntity
@Serial
private static final long serialVersionUID = 1L;
/** 父菜单名称 */
/** 父名称 */
@Column(ignore = true)
private String parentName;
/** 父菜单ID */
/** 父亲ID */
@ExcelProperty(value = "上级编号")
@NotNull(message = "上级编号不能为空")
private Long parentId;
/** 显示顺序 */
@ExcelProperty(value = "显示顺序")
private Integer orderNum;
/** 祖级列表 */
@Column(ignore = true)
//@Column(ignore = true)
private String ancestors;
/** 子部门 */

View File

@ -1,6 +1,7 @@
package com.ruoyi.common.orm.handler;
import com.ruoyi.common.core.core.domain.R;
import com.ruoyi.common.core.utils.StringUtils;
import lombok.extern.slf4j.Slf4j;
import org.mybatis.spring.MyBatisSystemException;
import org.springframework.dao.DuplicateKeyException;
@ -35,7 +36,7 @@ public class MybatisExceptionHandler {
public R<Void> handleCannotFindDataSourceException(MyBatisSystemException e, HttpServletRequest request) {
String requestURI = request.getRequestURI();
String message = e.getMessage();
if ("CannotFindDataSourceException".contains(message)) {
if (StringUtils.contains("CannotFindDataSourceException", message)) {
log.error("请求地址'{}', 未找到数据源", requestURI);
return R.fail("未找到数据源,请联系管理员确认");
}

View File

@ -0,0 +1,183 @@
package com.ruoyi.common.orm.helper;
import lombok.extern.slf4j.Slf4j;
import java.util.function.Supplier;
/**
* 监听器管理
* <p>
* 考虑任务调度三方接口回调等可自由控制审计字段
*
* @author Ice
* @version 1.0
*/
@Slf4j
public class ListenerManager {
private ListenerManager() {}
private static ThreadLocal<Boolean> ignoreInsertListenerTl = ThreadLocal.withInitial(() -> Boolean.FALSE);
private static ThreadLocal<Boolean> ignoreUpdateListenerTl = ThreadLocal.withInitial(() -> Boolean.FALSE);
/**
* 设置 InsertListenerThreadLocal
* @param tl ThreadLocal
*/
public static synchronized void setInsertListenerTl(ThreadLocal<Boolean> tl) {
ignoreInsertListenerTl = tl;
}
/**
* 设置 UpdateListenerThreadLocal
* @param tl ThreadLocal
*/
public static synchronized void setUpdateListenerTl(ThreadLocal<Boolean> tl) {
ignoreUpdateListenerTl = tl;
}
/**
* 是否执行 InsertListener
* @return 是否执行
*/
public static boolean isDoInsertListener() {
return !ignoreInsertListenerTl.get();
}
/**
* 是否执行 UpdateListener
* @return 是否执行
*/
public static boolean isDoUpdateListener() {
return !ignoreUpdateListenerTl.get();
}
/**
* 忽略 Listener
*/
public static <T> T withoutListener(Supplier<T> supplier) {
try {
ignoreListener();
return supplier.get();
} finally {
restoreListener();
}
}
/**
* 忽略 Listener
*/
public static void withoutListener(Runnable runnable) {
try {
ignoreListener();
runnable.run();
} finally {
restoreListener();
}
}
/**
* 忽略 Listener
*/
public static void ignoreListener() {
ignoreInsertListenerTl.set(Boolean.TRUE);
ignoreUpdateListenerTl.set(Boolean.TRUE);
}
/**
* 恢复 Listener
*/
public static void restoreListener() {
ignoreInsertListenerTl.remove();
ignoreUpdateListenerTl.remove();
}
/**
* 忽略 InsertListener
*/
public static <T> T withoutInsertListener(Supplier<T> supplier) {
try {
ignoreInsertListener();
return supplier.get();
} finally {
restoreInsertListener();
}
}
/**
* 忽略 InsertListener
*/
public static void withoutInsertListener(Runnable runnable) {
try {
ignoreInsertListener();
runnable.run();
} finally {
restoreInsertListener();
}
}
/**
* 忽略 InsertListener
*/
public static void ignoreInsertListener() {
ignoreInsertListenerTl.set(Boolean.TRUE);
}
/**
* 恢复 InsertListener
*/
public static void restoreInsertListener() {
ignoreInsertListenerTl.remove();
}
/**
* 忽略 UpdateListener
*/
public static <T> T withoutUpdateListener(Supplier<T> supplier) {
try {
ignoreUpdateListener();
return supplier.get();
} finally {
restoreUpdateListener();
}
}
/**
* 忽略 UpdateListener
*/
public static void withoutUpdateListener(Runnable runnable) {
try {
ignoreUpdateListener();
runnable.run();
} finally {
restoreUpdateListener();
}
}
/**
* 忽略 UpdateListener
*/
public static void ignoreUpdateListener() {
ignoreUpdateListenerTl.set(Boolean.TRUE);
}
/**
* 恢复 UpdateListener
*/
public static void restoreUpdateListener() {
ignoreUpdateListenerTl.remove();
}
}

View File

@ -5,6 +5,7 @@ import cn.hutool.http.HttpStatus;
import com.mybatisflex.annotation.InsertListener;
import com.ruoyi.common.core.exception.ServiceException;
import com.ruoyi.common.orm.core.domain.BaseEntity;
import com.ruoyi.common.orm.helper.ListenerManager;
import com.ruoyi.common.security.utils.LoginHelper;
import java.util.Date;
@ -19,7 +20,7 @@ public class EntityInsertListener implements InsertListener {
@Override
public void onInsert(Object entity) {
try {
if (ObjectUtil.isNotNull(entity) && (entity instanceof BaseEntity)) {
if (ListenerManager.isDoInsertListener() && ObjectUtil.isNotNull(entity) && (entity instanceof BaseEntity)) {
BaseEntity baseEntity = (BaseEntity) entity;
Long loginUserId = LoginHelper.getUserId();

View File

@ -5,6 +5,7 @@ import cn.hutool.http.HttpStatus;
import com.mybatisflex.annotation.UpdateListener;
import com.ruoyi.common.core.exception.ServiceException;
import com.ruoyi.common.orm.core.domain.BaseEntity;
import com.ruoyi.common.orm.helper.ListenerManager;
import com.ruoyi.common.security.utils.LoginHelper;
import java.util.Date;
@ -18,7 +19,7 @@ public class EntityUpdateListener implements UpdateListener {
@Override
public void onUpdate(Object entity) {
try {
if (ObjectUtil.isNotNull(entity) && (entity instanceof BaseEntity)) {
if (ListenerManager.isDoUpdateListener() && ObjectUtil.isNotNull(entity) && (entity instanceof BaseEntity)) {
BaseEntity baseEntity = (BaseEntity) entity;
baseEntity.setUpdateBy(LoginHelper.getUserId());
baseEntity.setUpdateTime(new Date());

View File

@ -31,9 +31,44 @@
<artifactId>ruoyi-common-redis</artifactId>
</dependency>
<!-- AWS SDK for Java 2.x -->
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-java-sdk-s3</artifactId>
<groupId>software.amazon.awssdk</groupId>
<artifactId>s3</artifactId>
<exclusions>
<!-- 将基于 Netty 的 HTTP 客户端从类路径中移除 -->
<exclusion>
<groupId>software.amazon.awssdk</groupId>
<artifactId>netty-nio-client</artifactId>
</exclusion>
<!-- 将基于 CRT 的 HTTP 客户端从类路径中移除 -->
<exclusion>
<groupId>software.amazon.awssdk</groupId>
<artifactId>aws-crt-client</artifactId>
</exclusion>
<!-- 将基于 Apache 的 HTTP 客户端从类路径中移除 -->
<exclusion>
<groupId>software.amazon.awssdk</groupId>
<artifactId>apache-client</artifactId>
</exclusion>
<!-- 将配置基于 URL 连接的 HTTP 客户端从类路径中移除 -->
<exclusion>
<groupId>software.amazon.awssdk</groupId>
<artifactId>url-connection-client</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- 使用AWS基于 CRT 的 S3 客户端 -->
<dependency>
<groupId>software.amazon.awssdk.crt</groupId>
<artifactId>aws-crt</artifactId>
</dependency>
<!-- 基于 AWS CRT 的 S3 客户端的性能增强的 S3 传输管理器 -->
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>s3-transfer-manager</artifactId>
</dependency>
</dependencies>

View File

@ -2,73 +2,117 @@ package com.ruoyi.common.oss.core;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.IdUtil;
import com.amazonaws.ClientConfiguration;
import com.amazonaws.HttpMethod;
import com.amazonaws.Protocol;
import com.amazonaws.auth.AWSCredentials;
import com.amazonaws.auth.AWSCredentialsProvider;
import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.client.builder.AwsClientBuilder;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.AmazonS3Client;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
import com.amazonaws.services.s3.model.*;
import com.ruoyi.common.core.constant.Constants;
import com.ruoyi.common.core.utils.DateUtils;
import com.ruoyi.common.core.utils.StringUtils;
import com.ruoyi.common.core.utils.file.FileUtils;
import com.ruoyi.common.oss.constant.OssConstant;
import com.ruoyi.common.oss.entity.UploadResult;
import com.ruoyi.common.oss.enumd.AccessPolicyType;
import com.ruoyi.common.oss.enumd.PolicyType;
import com.ruoyi.common.oss.exception.OssException;
import com.ruoyi.common.oss.properties.OssProperties;
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
import software.amazon.awssdk.core.async.AsyncRequestBody;
import software.amazon.awssdk.core.async.BlockingInputStreamAsyncRequestBody;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.s3.S3AsyncClient;
import software.amazon.awssdk.services.s3.S3Configuration;
import software.amazon.awssdk.services.s3.model.NoSuchBucketException;
import software.amazon.awssdk.services.s3.model.S3Exception;
import software.amazon.awssdk.services.s3.presigner.S3Presigner;
import software.amazon.awssdk.transfer.s3.S3TransferManager;
import software.amazon.awssdk.transfer.s3.model.*;
import software.amazon.awssdk.transfer.s3.progress.LoggingTransferListener;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URL;
import java.util.Date;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;
/**
* S3 存储协议 所有兼容S3协议的云厂商均支持
* 阿里云 腾讯云 七牛云 minio
*
* @author Lion Li
* @author AprilWind
*/
public class OssClient {
/**
* 服务商
*/
private final String configKey;
/**
* 配置属性
*/
private final OssProperties properties;
private final AmazonS3 client;
/**
* Amazon S3 异步客户端
*/
private final S3AsyncClient client;
/**
* 用于管理 S3 数据传输的高级工具
*/
private final S3TransferManager transferManager;
/**
* AWS S3 预签名 URL 的生成器
*/
private final S3Presigner presigner;
/**
* 构造方法
*
* @param configKey 配置键
* @param ossProperties Oss配置属性
*/
public OssClient(String configKey, OssProperties ossProperties) {
this.configKey = configKey;
this.properties = ossProperties;
try {
AwsClientBuilder.EndpointConfiguration endpointConfig =
new AwsClientBuilder.EndpointConfiguration(properties.getEndpoint(), properties.getRegion());
// 创建 AWS 认证信息
StaticCredentialsProvider credentialsProvider = StaticCredentialsProvider.create(
AwsBasicCredentials.create(properties.getAccessKey(), properties.getSecretKey()));
AWSCredentials credentials = new BasicAWSCredentials(properties.getAccessKey(), properties.getSecretKey());
AWSCredentialsProvider credentialsProvider = new AWSStaticCredentialsProvider(credentials);
ClientConfiguration clientConfig = new ClientConfiguration();
if (OssConstant.IS_HTTPS.equals(properties.getIsHttps())) {
clientConfig.setProtocol(Protocol.HTTPS);
} else {
clientConfig.setProtocol(Protocol.HTTP);
}
AmazonS3ClientBuilder build = AmazonS3Client.builder()
.withEndpointConfiguration(endpointConfig)
.withClientConfiguration(clientConfig)
.withCredentials(credentialsProvider)
.disableChunkedEncoding();
if (!StringUtils.containsAny(properties.getEndpoint(), OssConstant.CLOUD_SERVICE)) {
// minio 使用https限制使用域名访问 需要此配置 站点填域名
build.enablePathStyleAccess();
}
this.client = build.build();
//MinIO 使用 HTTPS 限制使用域名访问站点填域名需要启用路径样式访问
boolean isStyle = !StringUtils.containsAny(properties.getEndpoint(), OssConstant.CLOUD_SERVICE);
//创建AWS基于 CRT S3 客户端
this.client = S3AsyncClient.crtBuilder()
.credentialsProvider(credentialsProvider)
.endpointOverride(URI.create(getEndpoint()))
.region(of())
.targetThroughputInGbps(20.0)
.minimumPartSizeInBytes(10 * 1025 * 1024L)
.checksumValidationEnabled(false)
.forcePathStyle(isStyle)
.build();
//AWS基于 CRT S3 AsyncClient 实例用作 S3 传输管理器的底层客户端
this.transferManager = S3TransferManager.builder().s3Client(this.client).build();
// 创建 S3 配置对象
S3Configuration config = S3Configuration.builder().chunkedEncodingEnabled(false)
.pathStyleAccessEnabled(isStyle).build();
// 创建 预签名 URL 的生成器 实例用于生成 S3 预签名 URL
this.presigner = S3Presigner.builder()
.region(of())
.credentialsProvider(credentialsProvider)
.endpointOverride(URI.create(getDomain()))
.serviceConfiguration(config)
.build();
// 创建存储桶
createBucket();
} catch (Exception e) {
if (e instanceof OssException) {
@ -78,126 +122,158 @@ public class OssClient {
}
}
/**
* 同步创建存储桶
* 如果存储桶不存在会进行创建如果存储桶存在不执行任何操作
*
* @throws OssException 当创建存储桶时发生异常时抛出
*/
public void createBucket() {
String bucketName = properties.getBucketName();
try {
String bucketName = properties.getBucketName();
if (client.doesBucketExistV2(bucketName)) {
return;
// 尝试获取存储桶的信息
client.headBucket(
x -> x.bucket(bucketName)
.build())
.join();
} catch (Exception ex) {
if (ex.getCause() instanceof NoSuchBucketException) {
try {
// 存储桶不存在尝试创建存储桶
client.createBucket(
x -> x.bucket(bucketName))
.join();
// 设置存储桶的访问策略Bucket Policy
client.putBucketPolicy(
x -> x.bucket(bucketName)
.policy(getPolicy(bucketName, getAccessPolicy().getPolicyType())))
.join();
} catch (S3Exception e) {
// 存储桶创建或策略设置失败
throw new OssException("创建Bucket失败, 请核对配置信息:[" + e.getMessage() + "]");
}
} else {
throw new OssException("判断Bucket是否存在失败请核对配置信息:[" + ex.getMessage() + "]");
}
CreateBucketRequest createBucketRequest = new CreateBucketRequest(bucketName);
AccessPolicyType accessPolicy = getAccessPolicy();
createBucketRequest.setCannedAcl(accessPolicy.getAcl());
client.createBucket(createBucketRequest);
client.setBucketPolicy(bucketName, getPolicy(bucketName, accessPolicy.getPolicyType()));
} catch (Exception e) {
throw new OssException("创建Bucket失败, 请核对配置信息:[" + e.getMessage() + "]");
}
}
public UploadResult upload(byte[] data, String path, String contentType) {
return upload(new ByteArrayInputStream(data), path, contentType);
/**
* 上传文件到 Amazon S3并返回上传结果
*
* @param filePath 本地文件路径
* @param key Amazon S3 中的对象键
* @param md5Digest 本地文件的 MD5 哈希值可选
* @return UploadResult 包含上传后的文件信息
* @throws OssException 如果上传失败抛出自定义异常
*/
public UploadResult upload(Path filePath, String key, String md5Digest) {
try {
// 构建上传请求对象
FileUpload fileUpload = transferManager.uploadFile(
x -> x.putObjectRequest(
y -> y.bucket(properties.getBucketName())
.key(key)
.contentMD5(StringUtils.isNotEmpty(md5Digest) ? md5Digest : null)
.build())
.addTransferListener(LoggingTransferListener.create())
.source(filePath).build());
// 等待上传完成并获取上传结果
CompletedFileUpload uploadResult = fileUpload.completionFuture().join();
String eTag = uploadResult.response().eTag();
// 提取上传结果中的 ETag并构建一个自定义的 UploadResult 对象
return UploadResult.builder().url(getUrl() + StringUtils.SLASH + key).filename(key).eTag(eTag).build();
} catch (Exception e) {
// 捕获异常并抛出自定义异常
throw new OssException("上传文件失败,请检查配置信息:[" + e.getMessage() + "]");
} finally {
// 无论上传是否成功最终都会删除临时文件
FileUtils.del(filePath);
}
}
public UploadResult upload(InputStream inputStream, String path, String contentType) {
/**
* 上传 InputStream Amazon S3
*
* @param inputStream 要上传的输入流
* @param key Amazon S3 中的对象键
* @param length 输入流的长度
* @return UploadResult 包含上传后的文件信息
* @throws OssException 如果上传失败抛出自定义异常
*/
public UploadResult upload(InputStream inputStream, String key, Long length) {
// 如果输入流不是 ByteArrayInputStream则将其读取为字节数组再创建 ByteArrayInputStream
if (!(inputStream instanceof ByteArrayInputStream)) {
inputStream = new ByteArrayInputStream(IoUtil.readBytes(inputStream));
}
try {
ObjectMetadata metadata = new ObjectMetadata();
metadata.setContentType(contentType);
metadata.setContentLength(inputStream.available());
PutObjectRequest putObjectRequest = new PutObjectRequest(properties.getBucketName(), path, inputStream, metadata);
// 设置上传对象的 Acl 为公共读
putObjectRequest.setCannedAcl(getAccessPolicy().getAcl());
client.putObject(putObjectRequest);
// 创建异步请求体length如果为空会报错
BlockingInputStreamAsyncRequestBody body = AsyncRequestBody.forBlockingInputStream(length);
// 使用 transferManager 进行上传
Upload upload = transferManager.upload(
x -> x.requestBody(body)
.putObjectRequest(
y -> y.bucket(properties.getBucketName())
.key(key)
.build())
.build());
// 将输入流写入请求体
body.writeInputStream(inputStream);
// 等待文件上传操作完成
CompletedUpload uploadResult = upload.completionFuture().join();
String eTag = uploadResult.response().eTag();
// 提取上传结果中的 ETag并构建一个自定义的 UploadResult 对象
return UploadResult.builder().url(getUrl() + StringUtils.SLASH + key).filename(key).eTag(eTag).build();
} catch (Exception e) {
throw new OssException("上传文件失败,请检查配置信息:[" + e.getMessage() + "]");
}
return UploadResult.builder().url(getUrl() + "/" + path).filename(path).build();
}
public UploadResult upload(File file, String path) {
try {
PutObjectRequest putObjectRequest = new PutObjectRequest(properties.getBucketName(), path, file);
// 设置上传对象的 Acl 为公共读
putObjectRequest.setCannedAcl(getAccessPolicy().getAcl());
client.putObject(putObjectRequest);
} catch (Exception e) {
throw new OssException("上传文件失败,请检查配置信息:[" + e.getMessage() + "]");
}
return UploadResult.builder().url(getUrl() + "/" + path).filename(path).build();
}
public void delete(String path) {
path = path.replace(getUrl() + "/", "");
try {
client.deleteObject(properties.getBucketName(), path);
} catch (Exception e) {
throw new OssException("删除文件失败,请检查配置信息:[" + e.getMessage() + "]");
}
}
public UploadResult uploadSuffix(byte[] data, String suffix, String contentType) {
return upload(data, getPath(properties.getPrefix(), suffix), contentType);
}
public UploadResult uploadSuffix(InputStream inputStream, String suffix, String contentType) {
return upload(inputStream, getPath(properties.getPrefix(), suffix), contentType);
}
public UploadResult uploadSuffix(File file, String suffix) {
return upload(file, getPath(properties.getPrefix(), suffix));
}
/**
* 获取文件元数据
* 下载文件从 Amazon S3 到临时目录
*
* @param path 完整文件路径
* @param path 文件在 Amazon S3 中的对象键
* @return 下载后的文件在本地的临时路径
* @throws OssException 如果下载失败抛出自定义异常
*/
public ObjectMetadata getObjectMetadata(String path) {
path = path.replace(getUrl() + "/", "");
S3Object object = client.getObject(properties.getBucketName(), path);
return object.getObjectMetadata();
public Path fileDownload(String path) {
// 构建临时文件
Path tempFilePath = FileUtils.createTempFile().toPath();
// 使用 S3TransferManager 下载文件
FileDownload downloadFile = transferManager.downloadFile(
x -> x.getObjectRequest(
y -> y.bucket(properties.getBucketName())
.key(removeBaseUrl(path))
.build())
.addTransferListener(LoggingTransferListener.create())
.destination(tempFilePath)
.build());
// 等待文件下载操作完成
downloadFile.completionFuture().join();
return tempFilePath;
}
public InputStream getObjectContent(String path) {
path = path.replace(getUrl() + "/", "");
S3Object object = client.getObject(properties.getBucketName(), path);
return object.getObjectContent();
}
public String getUrl() {
String domain = properties.getDomain();
String endpoint = properties.getEndpoint();
String header = OssConstant.IS_HTTPS.equals(properties.getIsHttps()) ? "https://" : "http://";
// 云服务商直接返回
if (StringUtils.containsAny(endpoint, OssConstant.CLOUD_SERVICE)) {
if (StringUtils.isNotBlank(domain)) {
return header + domain;
}
return header + properties.getBucketName() + "." + endpoint;
/**
* 删除云存储服务中指定路径下文件
*
* @param path 指定路径
*/
public void delete(String path) {
try {
client.deleteObject(
x -> x.bucket(properties.getBucketName())
.key(removeBaseUrl(path))
.build());
} catch (Exception e) {
throw new OssException("删除文件失败,请检查配置信息:[" + e.getMessage() + "]");
}
// minio 单独处理
if (StringUtils.isNotBlank(domain)) {
return header + domain + "/" + properties.getBucketName();
}
return header + endpoint + "/" + properties.getBucketName();
}
public String getPath(String prefix, String suffix) {
// 生成uuid
String uuid = IdUtil.fastSimpleUUID();
// 文件路径
String path = DateUtils.datePath() + "/" + uuid;
if (StringUtils.isNotBlank(prefix)) {
path = prefix + "/" + path;
}
return path + suffix;
}
public String getConfigKey() {
return configKey;
}
/**
@ -207,14 +283,189 @@ public class OssClient {
* @param second 授权时间
*/
public String getPrivateUrl(String objectKey, Integer second) {
GeneratePresignedUrlRequest generatePresignedUrlRequest =
new GeneratePresignedUrlRequest(properties.getBucketName(), objectKey)
.withMethod(HttpMethod.GET)
.withExpiration(new Date(System.currentTimeMillis() + 1000L * second));
URL url = client.generatePresignedUrl(generatePresignedUrlRequest);
// 使用 AWS S3 预签名 URL 的生成器 获取对象的预签名 URL
URL url = presigner.presignGetObject(
x -> x.signatureDuration(Duration.ofSeconds(second))
.getObjectRequest(
y -> y.bucket(properties.getBucketName())
.key(objectKey)
.build())
.build())
.url();
return url.toString();
}
/**
* 上传 byte[] 数据到 Amazon S3使用指定的后缀构造对象键
*
* @param data 要上传的 byte[] 数据
* @param suffix 对象键的后缀
* @return UploadResult 包含上传后的文件信息
* @throws OssException 如果上传失败抛出自定义异常
*/
public UploadResult uploadSuffix(byte[] data, String suffix) {
return upload(new ByteArrayInputStream(data), getPath(properties.getPrefix(), suffix), Long.valueOf(data.length));
}
/**
* 上传 InputStream Amazon S3使用指定的后缀构造对象键
*
* @param inputStream 要上传的输入流
* @param suffix 对象键的后缀
* @param length 输入流的长度
* @return UploadResult 包含上传后的文件信息
* @throws OssException 如果上传失败抛出自定义异常
*/
public UploadResult uploadSuffix(InputStream inputStream, String suffix, Long length) {
return upload(inputStream, getPath(properties.getPrefix(), suffix), length);
}
/**
* 上传文件到 Amazon S3使用指定的后缀构造对象键
*
* @param file 要上传的文件
* @param suffix 对象键的后缀
* @return UploadResult 包含上传后的文件信息
* @throws OssException 如果上传失败抛出自定义异常
*/
public UploadResult uploadSuffix(File file, String suffix) {
return upload(file.toPath(), getPath(properties.getPrefix(), suffix), null);
}
/**
* 获取文件输入流
*
* @param path 完整文件路径
* @return 输入流
*/
public InputStream getObjectContent(String path) throws IOException {
// 下载文件到临时目录
Path tempFilePath = fileDownload(path);
// 创建输入流
InputStream inputStream = Files.newInputStream(tempFilePath);
// 删除临时文件
FileUtils.del(tempFilePath);
// 返回对象内容的输入流
return inputStream;
}
/**
* 获取 S3 客户端的终端点 URL
*
* @return 终端点 URL
*/
public String getEndpoint() {
// 根据配置文件中的是否使用 HTTPS设置协议头部
String header = getIsHttps();
// 拼接协议头部和终端点得到完整的终端点 URL
return header + properties.getEndpoint();
}
/**
* 获取 S3 客户端的终端点 URL自定义域名
*
* @return 终端点 URL
*/
public String getDomain() {
// 从配置中获取域名终端点是否使用 HTTPS 等信息
String domain = properties.getDomain();
String endpoint = properties.getEndpoint();
String header = getIsHttps();
// 如果是云服务商直接返回域名或终端点
if (StringUtils.containsAny(endpoint, OssConstant.CLOUD_SERVICE)) {
return StringUtils.isNotEmpty(domain) ? header + domain : header + endpoint;
}
// 如果是 MinIO处理域名并返回
if (StringUtils.isNotEmpty(domain)) {
return domain.startsWith(Constants.HTTPS) || domain.startsWith(Constants.HTTP) ? domain : header + domain;
}
// 返回终端点
return header + endpoint;
}
/**
* 根据传入的 region 参数返回相应的 AWS 区域
* 如果 region 参数非空使用 Region.of 方法创建并返回对应的 AWS 区域对象
* 如果 region 参数为空返回一个默认的 AWS 区域例如us-east-1作为广泛支持的区域
*
* @return 对应的 AWS 区域对象或者默认的广泛支持的区域us-east-1
*/
public Region of() {
//AWS 区域字符串
String region = properties.getRegion();
// 如果 region 参数非空使用 Region.of 方法创建对应的 AWS 区域对象否则返回默认区域
return StringUtils.isNotEmpty(region) ? Region.of(region) : Region.US_EAST_1;
}
/**
* 获取云存储服务的URL
*
* @return 文件路径
*/
public String getUrl() {
String domain = properties.getDomain();
String endpoint = properties.getEndpoint();
String header = getIsHttps();
// 云服务商直接返回
if (StringUtils.containsAny(endpoint, OssConstant.CLOUD_SERVICE)) {
return header + (StringUtils.isNotEmpty(domain) ? domain : properties.getBucketName() + "." + endpoint);
}
// MinIO 单独处理
if (StringUtils.isNotEmpty(domain)) {
// 如果 domain "https://" "http://" 开头
return (domain.startsWith(Constants.HTTPS) || domain.startsWith(Constants.HTTP)) ?
domain + StringUtils.SLASH + properties.getBucketName() : header + domain + StringUtils.SLASH + properties.getBucketName();
}
return header + endpoint + StringUtils.SLASH + properties.getBucketName();
}
/**
* 生成一个符合特定规则的唯一的文件路径通过使用日期UUID前缀和后缀等元素的组合确保了文件路径的独一无二性
*
* @param prefix 前缀
* @param suffix 后缀
* @return 文件路径
*/
public String getPath(String prefix, String suffix) {
// 生成uuid
String uuid = IdUtil.fastSimpleUUID();
// 生成日期路径
String datePath = DateUtils.datePath();
// 拼接路径
String path = StringUtils.isNotEmpty(prefix) ?
prefix + StringUtils.SLASH + datePath + StringUtils.SLASH + uuid : datePath + StringUtils.SLASH + uuid;
return path + suffix;
}
/**
* 移除路径中的基础URL部分得到相对路径
*
* @param path 完整的路径包括基础URL和相对路径
* @return 去除基础URL后的相对路径
*/
public String removeBaseUrl(String path) {
return path.replace(getUrl() + StringUtils.SLASH, "");
}
/**
* 服务商
*/
public String getConfigKey() {
return configKey;
}
/**
* 获取是否使用 HTTPS 的配置并返回相应的协议头部
*
* @return 协议头部根据是否使用 HTTPS 返回 "https://" "http://"
*/
public String getIsHttps() {
return OssConstant.IS_HTTPS.equals(properties.getIsHttps()) ? Constants.HTTPS : Constants.HTTP;
}
/**
* 检查配置是否相同
*/
@ -231,32 +482,77 @@ public class OssClient {
return AccessPolicyType.getByType(properties.getAccessPolicy());
}
/**
* 生成 AWS S3 存储桶访问策略
*
* @param bucketName 存储桶
* @param policyType 桶策略类型
* @return 符合 AWS S3 存储桶访问策略格式的字符串
*/
private static String getPolicy(String bucketName, PolicyType policyType) {
StringBuilder builder = new StringBuilder();
builder.append("{\n\"Statement\": [\n{\n\"Action\": [\n");
builder.append(switch (policyType) {
case WRITE -> "\"s3:GetBucketLocation\",\n\"s3:ListBucketMultipartUploads\"\n";
case READ_WRITE -> "\"s3:GetBucketLocation\",\n\"s3:ListBucket\",\n\"s3:ListBucketMultipartUploads\"\n";
default -> "\"s3:GetBucketLocation\"\n";
});
builder.append("],\n\"Effect\": \"Allow\",\n\"Principal\": \"*\",\n\"Resource\": \"arn:aws:s3:::");
builder.append(bucketName);
builder.append("\"\n},\n");
if (policyType == PolicyType.READ) {
builder.append("{\n\"Action\": [\n\"s3:ListBucket\"\n],\n\"Effect\": \"Deny\",\n\"Principal\": \"*\",\n\"Resource\": \"arn:aws:s3:::");
builder.append(bucketName);
builder.append("\"\n},\n");
}
builder.append("{\n\"Action\": ");
builder.append(switch (policyType) {
case WRITE -> "[\n\"s3:AbortMultipartUpload\",\n\"s3:DeleteObject\",\n\"s3:ListMultipartUploadParts\",\n\"s3:PutObject\"\n],\n";
case READ_WRITE -> "[\n\"s3:AbortMultipartUpload\",\n\"s3:DeleteObject\",\n\"s3:GetObject\",\n\"s3:ListMultipartUploadParts\",\n\"s3:PutObject\"\n],\n";
default -> "\"s3:GetObject\",\n";
});
builder.append("\"Effect\": \"Allow\",\n\"Principal\": \"*\",\n\"Resource\": \"arn:aws:s3:::");
builder.append(bucketName);
builder.append("/*\"\n}\n],\n\"Version\": \"2012-10-17\"\n}\n");
return builder.toString();
String policy = switch (policyType) {
case WRITE -> """
{
"Version": "2012-10-17",
"Statement": []
}
""";
case READ_WRITE -> """
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": "*",
"Action": [
"s3:GetBucketLocation",
"s3:ListBucket",
"s3:ListBucketMultipartUploads"
],
"Resource": "arn:aws:s3:::bucketName"
},
{
"Effect": "Allow",
"Principal": "*",
"Action": [
"s3:AbortMultipartUpload",
"s3:DeleteObject",
"s3:GetObject",
"s3:ListMultipartUploadParts",
"s3:PutObject"
],
"Resource": "arn:aws:s3:::bucketName/*"
}
]
}
""";
case READ -> """
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": "*",
"Action": ["s3:GetBucketLocation"],
"Resource": "arn:aws:s3:::bucketName"
},
{
"Effect": "Deny",
"Principal": "*",
"Action": ["s3:ListBucket"],
"Resource": "arn:aws:s3:::bucketName"
},
{
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::bucketName/*"
}
]
}
""";
};
return policy.replaceAll("bucketName", bucketName);
}
}

View File

@ -21,4 +21,9 @@ public class UploadResult {
* 文件名
*/
private String filename;
/**
* 已上传对象的实体标记用来校验文件
*/
private String eTag;
}

View File

@ -1,8 +1,9 @@
package com.ruoyi.common.oss.enumd;
import com.amazonaws.services.s3.model.CannedAccessControlList;
import lombok.AllArgsConstructor;
import lombok.Getter;
import software.amazon.awssdk.services.s3.model.BucketCannedACL;
import software.amazon.awssdk.services.s3.model.ObjectCannedACL;
/**
* 桶访问策略配置
@ -16,27 +17,32 @@ public enum AccessPolicyType {
/**
* private
*/
PRIVATE("0", CannedAccessControlList.Private, PolicyType.WRITE),
PRIVATE("0", BucketCannedACL.PRIVATE, ObjectCannedACL.PRIVATE, PolicyType.WRITE),
/**
* public
*/
PUBLIC("1", CannedAccessControlList.PublicRead, PolicyType.READ),
PUBLIC("1", BucketCannedACL.PUBLIC_READ_WRITE, ObjectCannedACL.PUBLIC_READ_WRITE, PolicyType.READ_WRITE),
/**
* custom
*/
CUSTOM("2",CannedAccessControlList.PublicRead, PolicyType.READ);
CUSTOM("2", BucketCannedACL.PUBLIC_READ, ObjectCannedACL.PUBLIC_READ, PolicyType.READ);
/**
* 权限类型
* 权限类型数据库值
*/
private final String type;
/**
* 权限类型
*/
private final BucketCannedACL bucketCannedACL;
/**
* 文件对象 权限类型
*/
private final CannedAccessControlList acl;
private final ObjectCannedACL objectCannedACL;
/**
* 桶策略类型

View File

@ -13,6 +13,7 @@ import lombok.extern.slf4j.Slf4j;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReentrantLock;
/**
* 文件上传Factory
@ -23,6 +24,7 @@ import java.util.concurrent.ConcurrentHashMap;
public class OssFactory {
private static final Map<String, OssClient> CLIENT_CACHE = new ConcurrentHashMap<>();
private static final ReentrantLock LOCK = new ReentrantLock();
/**
* 获取默认实例
@ -39,7 +41,7 @@ public class OssFactory {
/**
* 根据类型获取实例
*/
public static synchronized OssClient instance(String configKey) {
public static OssClient instance(String configKey) {
String json = CacheUtils.get(CacheNames.SYS_OSS_CONFIG, configKey);
if (json == null) {
throw new OssException("系统异常, '" + configKey + "'配置信息不存在!");
@ -48,16 +50,19 @@ public class OssFactory {
// 使用租户标识避免多个租户相同key实例覆盖
String key = properties.getTenantId() + ":" + configKey;
OssClient client = CLIENT_CACHE.get(key);
if (client == null) {
CLIENT_CACHE.put(key, new OssClient(configKey, properties));
log.info("创建OSS实例 key => {}", configKey);
return CLIENT_CACHE.get(key);
}
// 配置不相同则重新构建
if (!client.checkPropertiesSame(properties)) {
CLIENT_CACHE.put(key, new OssClient(configKey, properties));
log.info("重载OSS实例 key => {}", configKey);
return CLIENT_CACHE.get(key);
// 客户端不存在或配置不相同则重新构建
if (client == null || !client.checkPropertiesSame(properties)) {
LOCK.lock();
try {
client = CLIENT_CACHE.get(key);
if (client == null || !client.checkPropertiesSame(properties)) {
CLIENT_CACHE.put(key, new OssClient(configKey, properties));
log.info("创建OSS实例 key => {}", configKey);
return CLIENT_CACHE.get(key);
}
} finally {
LOCK.unlock();
}
}
return client;
}

View File

@ -1,29 +1,29 @@
package com.ruoyi.common.ratelimiter.aspectj;
import cn.hutool.core.util.ArrayUtil;
import com.ruoyi.common.core.constant.GlobalConstants;
import com.ruoyi.common.core.exception.ServiceException;
import com.ruoyi.common.core.utils.MessageUtils;
import com.ruoyi.common.core.utils.ServletUtils;
import com.ruoyi.common.core.utils.StringUtils;
import com.ruoyi.common.ratelimiter.annotation.RateLimiter;
import com.ruoyi.common.ratelimiter.enums.LimitType;
import com.ruoyi.common.redis.utils.RedisUtils;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.reflect.MethodSignature;
import com.ruoyi.common.core.constant.GlobalConstants;
import com.ruoyi.common.core.exception.ServiceException;
import com.ruoyi.common.core.utils.MessageUtils;
import com.ruoyi.common.core.utils.ServletUtils;
import com.ruoyi.common.core.utils.SpringUtils;
import com.ruoyi.common.core.utils.StringUtils;
import com.ruoyi.common.ratelimiter.annotation.RateLimiter;
import com.ruoyi.common.ratelimiter.enums.LimitType;
import com.ruoyi.common.redis.utils.RedisUtils;
import org.redisson.api.RateType;
import org.springframework.context.expression.BeanFactoryResolver;
import org.springframework.context.expression.MethodBasedEvaluationContext;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.ParserContext;
import org.springframework.expression.common.TemplateParserContext;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import java.lang.reflect.Method;
@ -44,21 +44,18 @@ public class RateLimiterAspect {
* 定义spel解析模版
*/
private final ParserContext parserContext = new TemplateParserContext();
/**
* 定义spel上下文对象进行解析
*/
private final EvaluationContext context = new StandardEvaluationContext();
/**
* 方法参数解析器
*/
private final ParameterNameDiscoverer pnd = new DefaultParameterNameDiscoverer();
@Before("@annotation(rateLimiter)")
public void doBefore(JoinPoint point, RateLimiter rateLimiter) throws Throwable {
public void doBefore(JoinPoint point, RateLimiter rateLimiter) {
int time = rateLimiter.time();
int count = rateLimiter.count();
String combineKey = getCombineKey(rateLimiter, point);
try {
String combineKey = getCombineKey(rateLimiter, point);
RateType rateType = RateType.OVERALL;
if (rateLimiter.limitType() == LimitType.CLUSTER) {
rateType = RateType.PER_CLIENT;
@ -76,42 +73,29 @@ public class RateLimiterAspect {
if (e instanceof ServiceException) {
throw e;
} else {
throw new RuntimeException("服务器限流异常,请稍候再试");
throw new RuntimeException("服务器限流异常,请稍候再试", e);
}
}
}
public String getCombineKey(RateLimiter rateLimiter, JoinPoint point) {
private String getCombineKey(RateLimiter rateLimiter, JoinPoint point) {
String key = rateLimiter.key();
// 获取方法(通过方法签名来获取)
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
Class<?> targetClass = method.getDeclaringClass();
// 判断是否是spel格式
if (StringUtils.containsAny(key, "#")) {
// 获取参数值
if (StringUtils.isNotBlank(key)) {
MethodSignature signature = (MethodSignature) point.getSignature();
Method targetMethod = signature.getMethod();
Object[] args = point.getArgs();
// 获取方法上参数的名称
String[] parameterNames = pnd.getParameterNames(method);
if (ArrayUtil.isEmpty(parameterNames)) {
throw new ServiceException("限流key解析异常!请联系管理员!");
}
for (int i = 0; i < parameterNames.length; i++) {
context.setVariable(parameterNames[i], args[i]);
}
// 解析返回给key
try {
Expression expression;
if (StringUtils.startsWith(key, parserContext.getExpressionPrefix())
&& StringUtils.endsWith(key, parserContext.getExpressionSuffix())) {
expression = parser.parseExpression(key, parserContext);
} else {
expression = parser.parseExpression(key);
}
key = expression.getValue(context, String.class) + ":";
} catch (Exception e) {
throw new ServiceException("限流key解析异常!请联系管理员!");
//noinspection DataFlowIssue
MethodBasedEvaluationContext context =
new MethodBasedEvaluationContext(null, targetMethod, args, pnd);
context.setBeanResolver(new BeanFactoryResolver(SpringUtils.getBeanFactory()));
Expression expression;
if (StringUtils.startsWith(key, parserContext.getExpressionPrefix())
&& StringUtils.endsWith(key, parserContext.getExpressionSuffix())) {
expression = parser.parseExpression(key, parserContext);
} else {
expression = parser.parseExpression(key);
}
key = expression.getValue(context, String.class);
}
StringBuilder stringBuffer = new StringBuilder(GlobalConstants.RATE_LIMIT_KEY);
stringBuffer.append(ServletUtils.getRequest().getRequestURI()).append(":");

View File

@ -0,0 +1,7 @@
{
"com.ruoyi.common.ratelimiter.annotation.RateLimiter@key": {
"method": {
"parameters": true
}
}
}

View File

@ -46,6 +46,12 @@
<artifactId>spring-boot-configuration-processor</artifactId>
</dependency>
<!-- caffeine缓存 -->
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,45 @@
package com.ruoyi.common.redis.config;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.ruoyi.common.redis.manager.FlexSpringCacheManager;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import java.util.concurrent.TimeUnit;
/**
* 缓存配置
*
* @author Lion Li
*/
@AutoConfiguration
@EnableCaching
public class CacheConfig {
/**
* caffeine 本地缓存处理器
*/
@Bean
public Cache<Object, Object> caffeine() {
return Caffeine.newBuilder()
// 设置最后一次写入或访问后经过固定时间过期
.expireAfterWrite(30, TimeUnit.SECONDS)
// 初始的缓存空间大小
.initialCapacity(100)
// 缓存的最大条数
.maximumSize(1000)
.build();
}
/**
* 自定义缓存管理器 整合spring-cache
*/
@Bean
public CacheManager cacheManager() {
return new FlexSpringCacheManager();
}
}

View File

@ -5,8 +5,9 @@ import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import com.ruoyi.common.core.utils.SpringUtils;
import lombok.extern.slf4j.Slf4j;
import com.ruoyi.common.redis.handler.KeyPrefixHandler;
import com.ruoyi.common.redis.manager.FlexSpringCacheManager;
import com.ruoyi.common.redis.config.properties.RedissonProperties;
import jakarta.annotation.Resource;
import org.redisson.client.codec.StringCodec;
@ -16,22 +17,18 @@ import org.redisson.spring.starter.RedissonAutoConfigurationCustomizer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.task.VirtualThreadTaskExecutor;
/**
* redis配置
*
* @author Lion Li
*/
@Slf4j
@AutoConfiguration
@EnableCaching
@EnableConfigurationProperties(RedissonProperties.class)
public class RedisConfig {
private static final Logger log = LoggerFactory.getLogger(RedisConfig.class);
@Autowired
private RedissonProperties redissonProperties;
@Resource
@ -52,6 +49,9 @@ public class RedisConfig {
// 缓存 Lua 脚本 减少网络传输(redisson 大部分的功能都是基于 Lua 脚本实现)
.setUseScriptCache(true)
.setCodec(codec);
if (SpringUtils.isVirtual()) {
config.setNettyExecutor(new VirtualThreadTaskExecutor("redisson-"));
}
RedissonProperties.SingleServerConfig singleServerConfig = redissonProperties.getSingleServerConfig();
if (ObjectUtil.isNotNull(singleServerConfig)) {
// 使用单机模式
@ -86,14 +86,6 @@ public class RedisConfig {
};
}
/**
* 自定义缓存管理器 整合spring-cache
*/
@Bean
public CacheManager cacheManager() {
return new FlexSpringCacheManager();
}
/**
* redis集群配置 yml
*

View File

@ -0,0 +1,88 @@
package com.ruoyi.common.redis.manager;
import com.ruoyi.common.core.utils.SpringUtils;
import org.springframework.cache.Cache;
import java.util.concurrent.Callable;
/**
* Cache 装饰器模式(用于扩展 Caffeine 一级缓存)
*
* @author LionLi
*/
public class CaffeineCacheDecorator implements Cache {
private static final com.github.benmanes.caffeine.cache.Cache<Object, Object>
CAFFEINE = SpringUtils.getBean("caffeine");
private final Cache cache;
public CaffeineCacheDecorator(Cache cache) {
this.cache = cache;
}
@Override
public String getName() {
return cache.getName();
}
@Override
public Object getNativeCache() {
return cache.getNativeCache();
}
public String getUniqueKey(Object key) {
return cache.getName() + ":" + key;
}
@Override
public ValueWrapper get(Object key) {
Object o = CAFFEINE.get(getUniqueKey(key), k -> cache.get(key));
return (ValueWrapper) o;
}
@SuppressWarnings("unchecked")
public <T> T get(Object key, Class<T> type) {
Object o = CAFFEINE.get(getUniqueKey(key), k -> cache.get(key, type));
return (T) o;
}
@Override
public void put(Object key, Object value) {
cache.put(key, value);
}
public ValueWrapper putIfAbsent(Object key, Object value) {
return cache.putIfAbsent(key, value);
}
@Override
public void evict(Object key) {
evictIfPresent(key);
}
public boolean evictIfPresent(Object key) {
boolean b = cache.evictIfPresent(key);
if (b) {
CAFFEINE.invalidate(getUniqueKey(key));
}
return b;
}
@Override
public void clear() {
cache.clear();
}
public boolean invalidate() {
return cache.invalidate();
}
@SuppressWarnings("unchecked")
@Override
public <T> T get(Object key, Callable<T> valueLoader) {
Object o = CAFFEINE.get(getUniqueKey(key), k -> cache.get(key, valueLoader));
return (T) o;
}
}

View File

@ -33,7 +33,7 @@ import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
/**
* A {@link org.springframework.cache.CacheManager} implementation
* A {@link CacheManager} implementation
* backed by Redisson instance.
* <p>
* 修改 RedissonSpringCacheManager 源码
@ -156,7 +156,7 @@ public class FlexSpringCacheManager implements CacheManager {
private Cache createMap(String name, CacheConfig config) {
RMap<Object, Object> map = RedisUtils.getClient().getMap(name);
Cache cache = new RedissonCache(map, allowNullValues);
Cache cache = new CaffeineCacheDecorator(new RedissonCache(map, allowNullValues));
if (transactionAware) {
cache = new TransactionAwareCacheDecorator(cache);
}
@ -170,7 +170,7 @@ public class FlexSpringCacheManager implements CacheManager {
private Cache createMapCache(String name, CacheConfig config) {
RMapCache<Object, Object> map = RedisUtils.getClient().getMapCache(name);
Cache cache = new RedissonCache(map, config, allowNullValues);
Cache cache = new CaffeineCacheDecorator(new RedissonCache(map, config, allowNullValues));
if (transactionAware) {
cache = new TransactionAwareCacheDecorator(cache);
}

View File

@ -1 +1,2 @@
com.ruoyi.common.redis.config.RedisConfig
com.ruoyi.common.redis.config.CacheConfig

View File

@ -41,6 +41,12 @@
<artifactId>sa-token-jwt</artifactId>
</dependency>
<!-- caffeine缓存 -->
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -49,7 +49,16 @@ public class SecurityConfig implements WebMvcConfigurer {
// 检查是否登录 是否有token
StpUtil.checkLogin();
//TODO :以后完善多平台登录校验clientID功能
// 检查 header param 里的 clientid token 里的是否一致
String headerCid = ServletUtils.getRequest().getHeader(LoginHelper.CLIENT_KEY);
String paramCid = ServletUtils.getParameter(LoginHelper.CLIENT_KEY);
String clientId = StpUtil.getExtra(LoginHelper.CLIENT_KEY).toString();
if (!StringUtils.equalsAny(clientId, headerCid, paramCid)) {
// token 无效
throw NotLoginException.newInstance(StpUtil.getLoginType(),
"-100", "客户端ID与Token不匹配",
StpUtil.getTokenValue());
}
});
})).addPathPatterns("/**")

View File

@ -2,12 +2,15 @@ package com.ruoyi.common.security.core.dao;
import cn.dev33.satoken.dao.SaTokenDao;
import cn.dev33.satoken.util.SaFoxUtil;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.ruoyi.common.redis.utils.RedisUtils;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* Sa-Token持久层接口(使用框架自带RedisUtils实现 协议统一)
@ -16,12 +19,22 @@ import java.util.List;
*/
public class FlexSaTokenDao implements SaTokenDao {
private static final Cache<String, Object> CAFFEINE = Caffeine.newBuilder()
// 设置最后一次写入或访问后经过固定时间过期
.expireAfterWrite(5, TimeUnit.SECONDS)
// 初始的缓存空间大小
.initialCapacity(100)
// 缓存的最大条数
.maximumSize(1000)
.build();
/**
* 获取Value如无返空
*/
@Override
public String get(String key) {
return RedisUtils.getCacheObject(key);
Object o = CAFFEINE.get(key, k -> RedisUtils.getCacheObject(key));
return (String) o;
}
/**
@ -38,6 +51,7 @@ public class FlexSaTokenDao implements SaTokenDao {
} else {
RedisUtils.setCacheObject(key, value, Duration.ofSeconds(timeout));
}
CAFFEINE.put(key, value);
}
/**
@ -47,6 +61,7 @@ public class FlexSaTokenDao implements SaTokenDao {
public void update(String key, String value) {
if (RedisUtils.hasKey(key)) {
RedisUtils.setCacheObject(key, value, true);
CAFFEINE.put(key, value);
}
}
@ -81,7 +96,8 @@ public class FlexSaTokenDao implements SaTokenDao {
*/
@Override
public Object getObject(String key) {
return RedisUtils.getCacheObject(key);
Object o = CAFFEINE.get(key, k -> RedisUtils.getCacheObject(key));
return o;
}
/**
@ -98,6 +114,7 @@ public class FlexSaTokenDao implements SaTokenDao {
} else {
RedisUtils.setCacheObject(key, object, Duration.ofSeconds(timeout));
}
CAFFEINE.put(key, object);
}
/**
@ -107,6 +124,7 @@ public class FlexSaTokenDao implements SaTokenDao {
public void updateObject(String key, Object object) {
if (RedisUtils.hasKey(key)) {
RedisUtils.setCacheObject(key, object, true);
CAFFEINE.put(key, object);
}
}
@ -139,10 +157,14 @@ public class FlexSaTokenDao implements SaTokenDao {
/**
* 搜索数据
*/
@SuppressWarnings("unchecked")
@Override
public List<String> searchData(String prefix, String keyword, int start, int size, boolean sortType) {
Collection<String> keys = RedisUtils.keys(prefix + "*" + keyword + "*");
List<String> list = new ArrayList<>(keys);
return SaFoxUtil.searchList(list, start, size, sortType);
String keyStr = prefix + "*" + keyword + "*";
return (List<String>) CAFFEINE.get(keyStr, k -> {
Collection<String> keys = RedisUtils.keys(keyStr);
List<String> list = new ArrayList<>(keys);
return SaFoxUtil.searchList(list, start, size, sortType);
});
}
}

View File

@ -15,7 +15,6 @@ import com.ruoyi.common.core.core.domain.model.LoginUser;
import com.ruoyi.common.core.enums.UserType;
import java.util.Set;
import java.util.function.Supplier;
/**
* 登录鉴权助手
@ -35,9 +34,10 @@ public class LoginHelper {
public static final String LOGIN_USER_KEY = "loginUser";
public static final String TENANT_KEY = "tenantId";
public static final String USER_KEY = "userId";
public static final String USER_NAME_KEY = "userName";
public static final String DEPT_KEY = "deptId";
public static final String DEPT_NAME_KEY = "deptName";
public static final String CLIENT_KEY = "clientid";
public static final String TENANT_ADMIN_KEY = "isTenantAdmin";
/**
* 登录系统 基于 设备类型
@ -57,7 +57,10 @@ public class LoginHelper {
StpUtil.login(loginUser.getLoginId(),
model.setExtra(TENANT_KEY, loginUser.getTenantId())
.setExtra(USER_KEY, loginUser.getUserId())
.setExtra(DEPT_KEY, loginUser.getDeptId()));
.setExtra(USER_NAME_KEY, loginUser.getUsername())
.setExtra(DEPT_KEY, loginUser.getDeptId())
.setExtra(DEPT_NAME_KEY, loginUser.getDeptName())
);
StpUtil.getTokenSession().set(LOGIN_USER_KEY, loginUser);
}
@ -65,13 +68,11 @@ public class LoginHelper {
* 获取用户(多级缓存)
*/
public static LoginUser getLoginUser() {
return (LoginUser) getStorageIfAbsentSet(LOGIN_USER_KEY, () -> {
SaSession session = StpUtil.getTokenSession();
if (ObjectUtil.isNull(session)) {
return null;
}
return session.get(LOGIN_USER_KEY);
});
SaSession session = StpUtil.getTokenSession();
if (ObjectUtil.isNull(session)) {
return null;
}
return (LoginUser) session.get(LOGIN_USER_KEY);
}
/**
@ -89,7 +90,7 @@ public class LoginHelper {
* 获取用户id
*/
public static Long getUserId() {
return Convert.toLong(getExtra(USER_KEY));
return Convert.toLong(getExtra(USER_KEY));
}
/**
@ -106,8 +107,18 @@ public class LoginHelper {
return Convert.toLong(getExtra(DEPT_KEY));
}
/**
* 获取当前 Token 的扩展信息
*
* @param key 键值
* @return 对应的扩展数据
*/
private static Object getExtra(String key) {
return getStorageIfAbsentSet(key, () -> StpUtil.getExtra(key));
try {
return StpUtil.getExtra(key);
} catch (Exception e) {
return null;
}
}
/**
@ -135,12 +146,17 @@ public class LoginHelper {
return UserConstants.SUPER_ADMIN_ID.equals(userId);
}
/**
* 是否为超级管理员
*
* @return 结果
*/
public static boolean isSuperAdmin() {
return isSuperAdmin(getUserId());
}
/**
* 是否为超级管理员
* 是否为租户管理员
*
* @param rolePermission 角色权限标识组
* @return 结果
@ -150,27 +166,16 @@ public class LoginHelper {
}
public static boolean isTenantAdmin() {
Object value = getStorageIfAbsentSet(TENANT_ADMIN_KEY, () -> {
return isTenantAdmin(getLoginUser().getRolePermission());
});
return Convert.toBool(value);
return Convert.toBool(isTenantAdmin(getLoginUser().getRolePermission()));
}
/**
* 检查当前用户是否已登录
*
* @return 结果
*/
public static boolean isLogin() {
return getLoginUser() != null;
}
public static Object getStorageIfAbsentSet(String key, Supplier<Object> handle) {
try {
Object obj = SaHolder.getStorage().get(key);
if (ObjectUtil.isNull(obj)) {
obj = handle.get();
SaHolder.getStorage().set(key, obj);
}
return obj;
} catch (Exception e) {
return null;
}
}
}

View File

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi-common</artifactId>
<version>${revision}</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>ruoyi-common-sms</artifactId>
<description>
ruoyi-common-sms 短信模块
</description>
<dependencies>
<dependency>
<groupId>org.dromara.sms4j</groupId>
<artifactId>sms4j-spring-boot-starter</artifactId>
</dependency>
<!-- RuoYi Common Redis-->
<dependency>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi-common-redis</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,24 @@
package com.ruoyi.common.sms.config;
import com.ruoyi.common.sms.core.dao.FlexSmsDao;
import org.dromara.sms4j.api.dao.SmsDao;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;
/**
* 短信配置类
*
* @author Feng
*/
@AutoConfiguration(after = {RedisAutoConfiguration.class})
public class SmsAutoConfiguration {
@Primary
@Bean
public SmsDao smsDao() {
return new FlexSmsDao();
}
}

View File

@ -0,0 +1,72 @@
package com.ruoyi.common.sms.core.dao;
import com.ruoyi.common.core.constant.GlobalConstants;
import com.ruoyi.common.redis.utils.RedisUtils;
import org.dromara.sms4j.api.dao.SmsDao;
import java.time.Duration;
/**
* SmsDao缓存配置 (使用框架自带RedisUtils实现 协议统一)
* <p>主要用于短信重试和拦截的缓存
*
* @author Feng
*/
public class FlexSmsDao implements SmsDao {
/**
* 存储
*
* @param key
* @param value
* @param cacheTime 缓存时间单位)
*/
@Override
public void set(String key, Object value, long cacheTime) {
RedisUtils.setCacheObject(GlobalConstants.GLOBAL_REDIS_KEY + key, value, Duration.ofSeconds(cacheTime));
}
/**
* 存储
*
* @param key
* @param value
*/
@Override
public void set(String key, Object value) {
RedisUtils.setCacheObject(GlobalConstants.GLOBAL_REDIS_KEY + key, value, true);
}
/**
* 读取
*
* @param key
* @return
*/
@Override
public Object get(String key) {
return RedisUtils.getCacheObject(GlobalConstants.GLOBAL_REDIS_KEY + key);
}
/**
* remove
* <p> 根据key移除缓存
*
* @param key 缓存键
* @return 被删除的value
* @author :Wind
*/
@Override
public Object remove(String key) {
return RedisUtils.deleteObject(GlobalConstants.GLOBAL_REDIS_KEY + key);
}
/**
* 清空
*/
@Override
public void clean() {
RedisUtils.deleteObject(GlobalConstants.GLOBAL_REDIS_KEY + "sms:");
}
}

View File

@ -0,0 +1 @@
com.ruoyi.common.sms.config.SmsAutoConfiguration

View File

@ -0,0 +1,34 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi-common</artifactId>
<version>${revision}</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>ruoyi-common-social</artifactId>
<description>
ruoyi-common-social 三方授权认证
</description>
<dependencies>
<dependency>
<groupId>me.zhyd.oauth</groupId>
<artifactId>JustAuth</artifactId>
</dependency>
<dependency>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi-common-json</artifactId>
</dependency>
<dependency>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi-common-redis</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,23 @@
package com.ruoyi.common.social.config;
import me.zhyd.oauth.cache.AuthStateCache;
import com.ruoyi.common.social.config.properties.SocialProperties;
import com.ruoyi.common.social.utils.AuthRedisStateCache;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
/**
* Social 配置属性
* @author thiszhc
*/
@AutoConfiguration
@EnableConfigurationProperties(SocialProperties.class)
public class SocialAutoConfiguration {
@Bean
public AuthStateCache authStateCache() {
return new AuthRedisStateCache();
}
}

View File

@ -0,0 +1,75 @@
package com.ruoyi.common.social.config.properties;
import lombok.Data;
import java.util.List;
/**
* 社交登录配置
*
* @author thiszhc
*/
@Data
public class SocialLoginConfigProperties {
/**
* 应用 ID
*/
private String clientId;
/**
* 应用密钥
*/
private String clientSecret;
/**
* 回调地址
*/
private String redirectUri;
/**
* 是否获取unionId
*/
private boolean unionId;
/**
* Coding 企业名称
*/
private String codingGroupName;
/**
* 支付宝公钥
*/
private String alipayPublicKey;
/**
* 企业微信应用ID
*/
private String agentId;
/**
* stackoverflow api key
*/
private String stackOverflowKey;
/**
* 设备ID
*/
private String deviceId;
/**
* 客户端系统类型
*/
private String clientOsType;
/**
* maxkey 服务器地址
*/
private String serverUrl;
/**
* 请求范围
*/
private List<String> scopes;
}

View File

@ -0,0 +1,24 @@
package com.ruoyi.common.social.config.properties;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.Map;
/**
* Social 配置属性
*
* @author thiszhc
*/
@Data
@Component
@ConfigurationProperties(prefix = "justauth")
public class SocialProperties {
/**
* 授权类型
*/
private Map<String, SocialLoginConfigProperties> type;
}

View File

@ -0,0 +1,80 @@
package com.ruoyi.common.social.maxkey;
import cn.hutool.core.lang.Dict;
import me.zhyd.oauth.cache.AuthStateCache;
import me.zhyd.oauth.config.AuthConfig;
import me.zhyd.oauth.exception.AuthException;
import me.zhyd.oauth.model.AuthCallback;
import me.zhyd.oauth.model.AuthToken;
import me.zhyd.oauth.model.AuthUser;
import me.zhyd.oauth.request.AuthDefaultRequest;
import com.ruoyi.common.core.utils.SpringUtils;
import com.ruoyi.common.json.utils.JsonUtils;
/**
* @author 长春叭哥 2023年03月26日
*/
public class AuthMaxKeyRequest extends AuthDefaultRequest {
public static final String SERVER_URL = SpringUtils.getProperty("justauth.type.maxkey.server-url");
/**
* 设定归属域
*/
public AuthMaxKeyRequest(AuthConfig config) {
super(config, AuthMaxKeySource.MAXKEY);
}
public AuthMaxKeyRequest(AuthConfig config, AuthStateCache authStateCache) {
super(config, AuthMaxKeySource.MAXKEY, authStateCache);
}
@Override
protected AuthToken getAccessToken(AuthCallback authCallback) {
String body = doPostAuthorizationCode(authCallback.getCode());
Dict object = JsonUtils.parseMap(body);
// oauth/token 验证异常
if (object.containsKey("error")) {
throw new AuthException(object.getStr("error_description"));
}
// user 验证异常
if (object.containsKey("message")) {
throw new AuthException(object.getStr("message"));
}
return AuthToken.builder()
.accessToken(object.getStr("access_token"))
.refreshToken(object.getStr("refresh_token"))
.idToken(object.getStr("id_token"))
.tokenType(object.getStr("token_type"))
.scope(object.getStr("scope"))
.build();
}
@Override
protected AuthUser getUserInfo(AuthToken authToken) {
String body = doGetUserInfo(authToken);
Dict object = JsonUtils.parseMap(body);
// oauth/token 验证异常
if (object.containsKey("error")) {
throw new AuthException(object.getStr("error_description"));
}
// user 验证异常
if (object.containsKey("message")) {
throw new AuthException(object.getStr("message"));
}
return AuthUser.builder()
.uuid(object.getStr("userId"))
.username(object.getStr("username"))
.nickname(object.getStr("displayName"))
.avatar(object.getStr("avatar_url"))
.blog(object.getStr("web_url"))
.company(object.getStr("organization"))
.location(object.getStr("location"))
.email(object.getStr("email"))
.remark(object.getStr("bio"))
.token(authToken)
.source(source.toString())
.build();
}
}

Some files were not shown because too many files have changed in this diff Show More