Compare commits
152 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
5476f543f3 | ||
![]() |
c820bb4e74 | ||
![]() |
d4f30e7de7 | ||
![]() |
384a3f9b92 | ||
![]() |
1e9abcb3f5 | ||
![]() |
ee3948d8e3 | ||
![]() |
4117bd6cc5 | ||
![]() |
0cbeb5b2df | ||
![]() |
335f806566 | ||
![]() |
8655030c7b | ||
![]() |
2635d74af5 | ||
![]() |
da91600948 | ||
![]() |
08cda7b792 | ||
![]() |
a0957a76c1 | ||
![]() |
fe227e62cd | ||
![]() |
502b692fee | ||
![]() |
d28e5fadb5 | ||
![]() |
935da2f093 | ||
![]() |
e78c09ddbe | ||
![]() |
ae9c718ddd | ||
![]() |
844aa24b43 | ||
![]() |
d86def6036 | ||
![]() |
108bd8e7c4 | ||
![]() |
c8738f6eb0 | ||
![]() |
6174ca90e1 | ||
![]() |
3d7fd649fd | ||
![]() |
4224ffdc64 | ||
![]() |
0b3f82660f | ||
![]() |
8230404035 | ||
![]() |
4c5bb90361 | ||
![]() |
15b8251601 | ||
![]() |
0e86f9a111 | ||
![]() |
5de6e70ca1 | ||
![]() |
ee3272918b | ||
![]() |
2219f02a17 | ||
![]() |
4606755e42 | ||
![]() |
d057d654fa | ||
![]() |
8263a252f1 | ||
![]() |
2c98a64d17 | ||
![]() |
2c721c0a09 | ||
![]() |
8555e6d684 | ||
![]() |
70d2864488 | ||
![]() |
6517cdd084 | ||
![]() |
226d18b627 | ||
![]() |
0d91f15061 | ||
![]() |
54b134e942 | ||
![]() |
a53709481c | ||
![]() |
7a67dce923 | ||
![]() |
048fb6abcb | ||
![]() |
3573a71af3 | ||
![]() |
41ea876a96 | ||
![]() |
d494c551d5 | ||
![]() |
445ef5763d | ||
![]() |
92b3efb87d | ||
![]() |
b07033ba51 | ||
![]() |
fad4f8e19e | ||
![]() |
0b99d54edf | ||
![]() |
596cf10a1c | ||
![]() |
659caa1d0e | ||
![]() |
4358633176 | ||
![]() |
9660f65dad | ||
![]() |
f57b1092e4 | ||
![]() |
00e3ee95c3 | ||
![]() |
e5b182a8ef | ||
![]() |
458f60be07 | ||
![]() |
32fd0311b3 | ||
![]() |
d60ca890a2 | ||
![]() |
9d6fc4f539 | ||
![]() |
f3b3bec8b0 | ||
![]() |
bcd06f3d2b | ||
![]() |
f8c699ebbd | ||
![]() |
c91f9686d7 | ||
![]() |
7fb5c26cee | ||
![]() |
c1fb7b4484 | ||
![]() |
918d67abef | ||
![]() |
a2e1acb66c | ||
![]() |
050a48b134 | ||
![]() |
b8ab413eab | ||
![]() |
7c649bb5b2 | ||
![]() |
c9cb308abf | ||
![]() |
0974e65c0a | ||
![]() |
fa75c7155a | ||
![]() |
2c180c89a9 | ||
![]() |
f31983c2c7 | ||
![]() |
62c8e1c877 | ||
![]() |
3bedaa7ddc | ||
![]() |
e7b6dac49f | ||
![]() |
d151c053a7 | ||
![]() |
fc2eab2a6d | ||
![]() |
e28a2a404e | ||
![]() |
773e257fa1 | ||
![]() |
651da9a92a | ||
![]() |
e5ca734018 | ||
![]() |
5b131ad3fd | ||
![]() |
fe848f418a | ||
![]() |
6d484c0bda | ||
![]() |
f366e430f1 | ||
![]() |
7e9ba78376 | ||
![]() |
5a814b947c | ||
![]() |
0af60f5ac5 | ||
![]() |
6b8f3e2ed1 | ||
![]() |
3f829e271d | ||
![]() |
8027f5c2d8 | ||
![]() |
29a620eb41 | ||
![]() |
885918d11f | ||
![]() |
6d31a7107b | ||
![]() |
29ec129069 | ||
![]() |
3f3f3d793a | ||
![]() |
2b7ba78eb1 | ||
![]() |
baf6140159 | ||
![]() |
ba11dd8927 | ||
![]() |
3734a6ef23 | ||
![]() |
a601c8c54c | ||
![]() |
9e4171f4ad | ||
![]() |
254dfe383b | ||
![]() |
a2c31048cb | ||
![]() |
fad44cc117 | ||
![]() |
e0e190671f | ||
![]() |
5e8a8f6cd9 | ||
![]() |
09157fe27f | ||
![]() |
1183249080 | ||
![]() |
ee7ef99c36 | ||
![]() |
3ae17b0657 | ||
![]() |
84be51d691 | ||
![]() |
e6b877366e | ||
![]() |
e112a7b5fe | ||
![]() |
677e5e6168 | ||
![]() |
da582764a7 | ||
![]() |
4ee432bfee | ||
![]() |
eda8a2d33b | ||
![]() |
1d0399b9b0 | ||
![]() |
b0204c224c | ||
![]() |
ee8f101807 | ||
![]() |
6a6bf19db5 | ||
![]() |
32ef43f198 | ||
![]() |
b06857e8c8 | ||
![]() |
74853f4d9c | ||
![]() |
17eb0212fd | ||
![]() |
52ea5f2e2a | ||
![]() |
8f1ce9a5df | ||
![]() |
2868e20c86 | ||
![]() |
54d4b8bf16 | ||
![]() |
14740e817e | ||
![]() |
71d1d7bd77 | ||
![]() |
c39403a0d2 | ||
![]() |
9c8de8afbf | ||
![]() |
a84a3c6049 | ||
![]() |
9f4991f32f | ||
![]() |
d09b65c396 | ||
![]() |
9b1602649e | ||
![]() |
ec9ff68567 | ||
![]() |
58c62ee6b4 |
66
README.md
66
README.md
@ -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个前端项目:
|
||||
|
||||
### (1)ruoyiflex-elementplus-ts
|
||||
使用elementplus、typescript构建,项目地址: [ruoyiflex-elementplus-ts](https://gitee.com/dataprince/ruoyiflex-elementplus-ts)
|
||||
|
||||
### (2)ruoyiflex-antdesign-vben
|
||||
使用antdesign、vben、typescript构建,项目地址: [ruoyiflex-antdesign-vben](https://gitee.com/dataprince/ruoyiflex-antdesign-vben)
|
||||
|
||||
### (3)flex-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
BIN
doc/~$oyi-Flex-Guide.docx
Normal file
Binary file not shown.
135
pom.xml
135
pom.xml
@ -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
8
ruoyi-admin/Dockerfile
Normal 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"]
|
@ -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>
|
||||
|
||||
|
@ -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" +
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -12,6 +12,11 @@ import java.util.List;
|
||||
@Data
|
||||
public class LoginTenantVo {
|
||||
|
||||
/**
|
||||
* 租户开关
|
||||
*/
|
||||
private Boolean tenantEnabled;
|
||||
|
||||
/**
|
||||
* 租户对象列表
|
||||
*/
|
||||
|
@ -13,10 +13,19 @@ import lombok.Data;
|
||||
@AutoMapper(target = SysTenantVo.class)
|
||||
public class TenantListVo {
|
||||
|
||||
/**
|
||||
* 租户编号
|
||||
*/
|
||||
private String tenantId;
|
||||
|
||||
/**
|
||||
* 企业名称
|
||||
*/
|
||||
private String companyName;
|
||||
|
||||
/**
|
||||
* 域名
|
||||
*/
|
||||
private String domain;
|
||||
|
||||
}
|
||||
|
@ -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,17 +34,15 @@ 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));
|
||||
@ -48,17 +50,30 @@ public class UserActionListener implements SaTokenListener {
|
||||
dto.setOs(userAgent.getOs().getName());
|
||||
dto.setLoginTime(System.currentTimeMillis());
|
||||
dto.setTokenId(tokenValue);
|
||||
dto.setUserName(user.getUsername());
|
||||
dto.setDeptName(user.getDeptName());
|
||||
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()));
|
||||
}
|
||||
});
|
||||
// 记录登录日志
|
||||
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);
|
||||
} else if (userType == UserType.APP_USER) {
|
||||
// app端 自行根据业务编写
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
@ -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);
|
||||
|
||||
}
|
||||
|
@ -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)) {
|
||||
public void recordLoginInfo(Long userId, String ip) {
|
||||
SysUserVo sysUserVo = userService.selectUserById(userId);
|
||||
if (ObjectUtil.isNull(sysUserVo)) {
|
||||
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) {
|
||||
}
|
||||
}
|
||||
|
||||
SysUser sysUser = new SysUser();
|
||||
sysUser.setUserId(userId);
|
||||
sysUser.setLoginIp(ip);
|
||||
sysUser.setLoginDate(DateUtils.getNowDate());
|
||||
sysUser.setUpdateBy(userId);
|
||||
sysUser.setVersion(sysUserVo.getVersion());
|
||||
userService.updateById(sysUser);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -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,9 +43,9 @@ public class SysRegisterService
|
||||
/**
|
||||
* 注册
|
||||
*/
|
||||
public void register(RegisterBody registerBody)
|
||||
{
|
||||
public void register(RegisterBody registerBody) {
|
||||
Long tenantId = registerBody.getTenantId();
|
||||
TenantHelper.dynamic(tenantId, () -> {
|
||||
String username = registerBody.getUsername();
|
||||
String password = registerBody.getPassword();
|
||||
// 校验用户类型是否存在
|
||||
@ -62,7 +62,9 @@ public class SysRegisterService
|
||||
sysUser.setPassword(BCrypt.hashpw(password));
|
||||
sysUser.setUserType(userType);
|
||||
|
||||
if (!userService.checkUserNameUnique(sysUser)) {
|
||||
boolean unique = userService.checkUserNameUnique(sysUser);
|
||||
|
||||
if (!unique) {
|
||||
throw new UserException("user.register.save.error", username);
|
||||
}
|
||||
boolean regFlag = userService.registerUser(sysUser, tenantId);
|
||||
@ -70,6 +72,7 @@ public class SysRegisterService
|
||||
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) {
|
||||
|
@ -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,8 +89,7 @@ public class EmailAuthStrategy implements IAuthStrategy {
|
||||
}
|
||||
|
||||
private SysUserVo loadUserByEmail(Long tenantId, String email) {
|
||||
return TenantHelper.dynamic(tenantId, () -> {
|
||||
SysUserVo user =userService.selectUserByEmail(email);
|
||||
SysUserVo user = userService.selectTenantUserByEmail(tenantId, email);
|
||||
if (ObjectUtil.isNull(user)) {
|
||||
log.info("登录用户:{} 不存在.", email);
|
||||
throw new UserException("user.not.exists", email);
|
||||
@ -107,7 +98,6 @@ public class EmailAuthStrategy implements IAuthStrategy {
|
||||
throw new UserException("user.blocked", email);
|
||||
}
|
||||
return user;
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -9,7 +9,7 @@ ruoyi:
|
||||
# 实例演示开关
|
||||
demoEnabled: true
|
||||
# 文件路径 示例( Windows配置D:/ruoyi/uploadPath,Linux配置 /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,它会保留授权数据并且不会在浏览器关闭/刷新时丢失
|
||||
@ -219,13 +226,17 @@ springdoc:
|
||||
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:
|
||||
|
BIN
ruoyi-admin/src/main/resources/ip2region.xdb
Normal file
BIN
ruoyi-admin/src/main/resources/ip2region.xdb
Normal file
Binary file not shown.
@ -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" />
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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
|
||||
{
|
||||
|
||||
|
@ -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");
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行周期性或定时任务
|
||||
*/
|
||||
|
@ -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;
|
||||
|
||||
}
|
@ -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";
|
||||
|
||||
/**
|
||||
* 用户账户
|
||||
*/
|
||||
|
@ -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]$";
|
||||
|
||||
}
|
@ -41,6 +41,6 @@ public interface TenantConstants {
|
||||
/**
|
||||
* 默认租户ID
|
||||
*/
|
||||
Long DEFAULT_TENANT_ID = 0L;
|
||||
Long DEFAULT_TENANT_ID = 1L;
|
||||
|
||||
}
|
||||
|
@ -34,6 +34,16 @@ public class UserOnlineDTO implements Serializable {
|
||||
*/
|
||||
private String userName;
|
||||
|
||||
/**
|
||||
* 客户端
|
||||
*/
|
||||
private String clientKey;
|
||||
|
||||
/**
|
||||
* 设备类型
|
||||
*/
|
||||
private String deviceType;
|
||||
|
||||
/**
|
||||
* 登录IP地址
|
||||
*/
|
||||
|
@ -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;
|
||||
|
||||
}
|
@ -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;
|
||||
|
||||
}
|
@ -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;
|
||||
|
||||
}
|
@ -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;
|
||||
|
||||
}
|
@ -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);
|
||||
|
||||
}
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取参数不为空值
|
||||
*
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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 {
|
||||
|
||||
/**
|
||||
|
@ -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);
|
||||
if (ObjectUtil.isNotNull(apiEncrypt)) {
|
||||
// 响应加密标志
|
||||
encryptFlag = apiEncrypt.response();
|
||||
requestWrapper = new DecryptRequestBodyWrapper(servletRequest, headerValue);
|
||||
} else {
|
||||
// 是否有注解,有就报错,没有放行
|
||||
HandlerExceptionResolver exceptionResolver = SpringUtils.getBean(HandlerExceptionResolver.class);
|
||||
if (ObjectUtil.isNotNull(apiEncrypt)) {
|
||||
HandlerExceptionResolver exceptionResolver = SpringUtils.getBean("handlerExceptionResolver", HandlerExceptionResolver.class);
|
||||
exceptionResolver.resolveException(
|
||||
servletRequest, servletResponse, null,
|
||||
new ServiceException("没有访问权限,请联系管理员授权", HttpStatus.FORBIDDEN));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 判断是否响应加密
|
||||
if (encryptFlag) {
|
||||
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(
|
||||
|
@ -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,21 +26,19 @@ public class DecryptRequestBodyWrapper extends HttpServletRequestWrapper {
|
||||
|
||||
private final byte[] body;
|
||||
|
||||
public DecryptRequestBodyWrapper(HttpServletRequest request, String privateKey, String headerFlag) throws IOException {
|
||||
public DecryptRequestBodyWrapper(HttpServletRequest request, String privateKey) 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);
|
||||
String privateKeyValue = RedisUtils.getCacheMapValue("loginRsa", privateKey);
|
||||
|
||||
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);
|
||||
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()));
|
||||
|
@ -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{
|
||||
|
||||
}
|
||||
}
|
@ -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,则使用枚举的逻辑
|
||||
|
@ -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>
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,2 @@
|
||||
com.ruoyi.common.job.config.PowerJobConfig
|
||||
com.ruoyi.common.job.config.EasyRetryConfig
|
@ -27,11 +27,6 @@
|
||||
<artifactId>ruoyi-common-json</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.alibaba</groupId>
|
||||
<artifactId>transmittable-thread-local</artifactId>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
|
@ -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) {
|
||||
|
34
ruoyi-common/ruoyi-common-mail/pom.xml
Normal file
34
ruoyi-common/ruoyi-common-mail/pom.xml
Normal 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>
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
@ -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 <user@xxx.xx>
|
||||
* </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 + "]";
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1 @@
|
||||
com.ruoyi.common.mail.config.MailConfig
|
@ -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>
|
||||
|
@ -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,9 +55,11 @@ public class MyBatisFlexConfig implements ConfigurationCustomizer, MyBatisFlexCu
|
||||
|
||||
@Override
|
||||
public void customize(FlexConfiguration configuration) {
|
||||
//mybatis实现的打印sql到控制台,便于调试
|
||||
//mybatis实现的打印详细sql及返回结果到控制台,便于调试
|
||||
if (sqlPrint) {
|
||||
configuration.setLogImpl(StdOutImpl.class);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mybatis-Flex自定义初始化配置
|
||||
@ -57,10 +73,14 @@ public class MyBatisFlexConfig implements ConfigurationCustomizer, MyBatisFlexCu
|
||||
globalConfig.registerUpdateListener(new EntityUpdateListener(), BaseEntity.class);
|
||||
|
||||
// 开启审计功能
|
||||
AuditManager.setAuditEnable(enableAudit);
|
||||
if (sqlPrint) {
|
||||
// 开启sql打印默认会开启sql审计
|
||||
AuditManager.setAuditEnable(true);
|
||||
// AuditManager.setMessageFactory(new AuditMessageFactory());
|
||||
// 设置 SQL 审计收集器
|
||||
AuditManager.setMessageCollector(new ConsoleMessageCollector());
|
||||
//设置 SQL 审计收集器
|
||||
MessageCollector collector = new ConsoleMessageCollector();
|
||||
AuditManager.setMessageCollector(collector);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
||||
/** 子部门 */
|
||||
|
@ -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("未找到数据源,请联系管理员确认");
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
@ -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();
|
||||
|
@ -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());
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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() {
|
||||
try {
|
||||
String bucketName = properties.getBucketName();
|
||||
if (client.doesBucketExistV2(bucketName)) {
|
||||
return;
|
||||
}
|
||||
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) {
|
||||
try {
|
||||
// 尝试获取存储桶的信息
|
||||
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() + "]");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
/**
|
||||
* 删除云存储服务中指定路径下文件
|
||||
*
|
||||
* @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() + "]");
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
// 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");
|
||||
String policy = switch (policyType) {
|
||||
case WRITE -> """
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": []
|
||||
}
|
||||
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();
|
||||
""";
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -21,4 +21,9 @@ public class UploadResult {
|
||||
* 文件名
|
||||
*/
|
||||
private String filename;
|
||||
|
||||
/**
|
||||
* 已上传对象的实体标记(用来校验文件)
|
||||
*/
|
||||
private String eTag;
|
||||
}
|
||||
|
@ -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;
|
||||
|
||||
/**
|
||||
* 桶策略类型
|
||||
|
@ -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) {
|
||||
// 客户端不存在或配置不相同则重新构建
|
||||
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);
|
||||
}
|
||||
// 配置不相同则重新构建
|
||||
if (!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;
|
||||
}
|
||||
|
@ -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,31 +73,21 @@ 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();
|
||||
// 获取方法(通过方法签名来获取)
|
||||
if (StringUtils.isNotBlank(key)) {
|
||||
MethodSignature signature = (MethodSignature) point.getSignature();
|
||||
Method method = signature.getMethod();
|
||||
Class<?> targetClass = method.getDeclaringClass();
|
||||
// 判断是否是spel格式
|
||||
if (StringUtils.containsAny(key, "#")) {
|
||||
// 获取参数值
|
||||
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 {
|
||||
//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())) {
|
||||
@ -108,10 +95,7 @@ public class RateLimiterAspect {
|
||||
} else {
|
||||
expression = parser.parseExpression(key);
|
||||
}
|
||||
key = expression.getValue(context, String.class) + ":";
|
||||
} catch (Exception e) {
|
||||
throw new ServiceException("限流key解析异常!请联系管理员!");
|
||||
}
|
||||
key = expression.getValue(context, String.class);
|
||||
}
|
||||
StringBuilder stringBuffer = new StringBuilder(GlobalConstants.RATE_LIMIT_KEY);
|
||||
stringBuffer.append(ServletUtils.getRequest().getRequestURI()).append(":");
|
||||
|
@ -0,0 +1,7 @@
|
||||
{
|
||||
"com.ruoyi.common.ratelimiter.annotation.RateLimiter@key": {
|
||||
"method": {
|
||||
"parameters": true
|
||||
}
|
||||
}
|
||||
}
|
@ -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>
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
@ -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
|
||||
*
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
@ -1 +1,2 @@
|
||||
com.ruoyi.common.redis.config.RedisConfig
|
||||
com.ruoyi.common.redis.config.CacheConfig
|
||||
|
@ -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>
|
||||
|
@ -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("/**")
|
||||
|
@ -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 + "*");
|
||||
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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
});
|
||||
return (LoginUser) session.get(LOGIN_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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
33
ruoyi-common/ruoyi-common-sms/pom.xml
Normal file
33
ruoyi-common/ruoyi-common-sms/pom.xml
Normal 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>
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
@ -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:");
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1 @@
|
||||
com.ruoyi.common.sms.config.SmsAutoConfiguration
|
34
ruoyi-common/ruoyi-common-social/pom.xml
Normal file
34
ruoyi-common/ruoyi-common-social/pom.xml
Normal 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>
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
|
||||
}
|
@ -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;
|
||||
|
||||
}
|
@ -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
Loading…
Reference in New Issue
Block a user