Compare commits

..

97 Commits

Author SHA1 Message Date
64b3c9e42e 解决积木报表springboot3 找不到类base64utils #3834和SqlServer兼容问题 2025-06-05 15:45:31 +08:00
8eb81493ce ai流程脚本节点执行报错 2025-05-28 11:28:55 +08:00
044fc47586 ai ⼯作流使⽤知识库报错"白名单未通过" 2025-05-27 17:58:02 +08:00
bbe18c582c 访问Swagger接口不带doc.html后缀,会丢失项目前缀/jeecg-boot/导致测试接口,返回下载文件 2025-05-26 19:17:00 +08:00
b894125b53 【升级到springboot 3.4.5】
Merge remote-tracking branch 'origin/springboot3' into springboot3_sas

# Conflicts:
#	jeecg-boot/jeecg-boot-base-core/pom.xml
#	jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/util/encryption/AesEncryptUtil.java
#	jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/shiro/ShiroConfig.java
#	jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/shiro/ShiroRealm.java
#	jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/application-prod.yml
2025-05-22 11:25:00 +08:00
2e236703b2 升级spring-boot-starter-parent版本至2.7.18 2025-05-19 18:04:16 +08:00
a771d24a57 升级knife4j-spring-boot-starter版本至4.5.0 2025-05-19 16:26:30 +08:00
69f5d12de7 缺少的token排除请求 2025-05-19 16:26:13 +08:00
5b5999e786 shiro排除配置改成security 2025-05-19 16:25:49 +08:00
fc3fe39d95 升级mybatis为springboot3版本 2025-05-19 15:28:07 +08:00
48e23aafab 【v3.8.0合并】Merge remote-tracking branch 'origin/springboot3' into springboot3_sas
# Conflicts:
#	jeecg-boot/jeecg-boot-base-core/pom.xml
#	jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/util/encryption/AesEncryptUtil.java
#	jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/firewall/interceptor/LowCodeModeInterceptor.java
#	jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/shiro/ShiroConfig.java
#	jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/controller/SysDataSourceController.java
#	jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/controller/SysDepartPermissionController.java
#	jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/controller/SysDepartRoleController.java
#	jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/controller/SysRoleIndexController.java
#	jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/controller/SysTableWhiteListController.java
#	jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/service/impl/SysTenantPackServiceImpl.java
#	jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/resources/jeecg/code-template-online/default/tree/java/${bussiPackage}/${entityPackage}/controller/${entityName}Controller.javai
#	jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/application-dev.yml
#	jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/application-prod.yml
#	jeecg-boot/jeecg-module-system/jeecg-system-start/src/test/java/org/jeecg/modules/system/test/InsertDemoTest.java
#	jeecg-boot/pom.xml
#	jeecgboot-vue3/pnpm-lock.yaml
2025-05-19 13:52:08 +08:00
657b84d3cf Merge pull request #8007 from EightMonth/springboot3_sas
修复 CVE-2023-6378
2025-03-25 16:02:45 +08:00
2021bf39f8 修复 CVE-2023-6378 2025-03-25 16:00:14 +08:00
fdeb37c3d0 Merge pull request #8005 from EightMonth/springboot3_sas
jeewx-api修改成weixin4j
2025-03-25 14:19:33 +08:00
f9123208e1 jeewx-api修改成weixin4j 2025-03-25 14:15:05 +08:00
accb8f2f9f Merge pull request #7950 from EightMonth/springboot3_sas
解决严重bug,War包方式部署,服务启动报错
2025-03-12 15:28:44 +08:00
c643994546 解决严重bug,War包方式部署,服务启动报错 2025-03-12 14:16:50 +08:00
6934a0adee Merge pull request #7875 from EightMonth/springboot3_sas
修复 #7613
2025-03-03 17:05:12 +08:00
93e32a7177 Merge branch 'springboot3' into springboot3_sas 2025-03-03 14:30:40 +08:00
c9f5bb4409 【3.7.3 版本合并升级】
Merge remote-tracking branch 'origin/springboot3' into springboot3_sas

# Conflicts:
#	jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/system/util/JwtUtil.java
#	jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/shiro/ShiroConfig.java
#	jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/shiro/ShiroRealm.java
#	jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/service/impl/SysTenantPackServiceImpl.java
#	jeecgboot-vue3/package.json
2025-02-24 18:24:51 +08:00
10b68858d6 Merge pull request #7796 from EightMonth/springboot3_sas
固定vue-router版本号
2025-02-11 14:49:17 +08:00
da72e8f9c5 Update package.json 2025-02-11 14:27:40 +08:00
73e86686dc 固定vue-router版本号 2025-02-11 09:25:59 +08:00
f43d0d486b Merge pull request #7378 from EightMonth/springboot3_sas
分布式事务demo修复
2024-11-20 17:42:55 +08:00
65bde3331b 修复#7459,修复sas分支 @ignoreauth注解无效 2024-11-20 11:08:53 +08:00
b60942aa86 分布式事务demo修复 2024-10-24 10:48:00 +08:00
197b267e71 分布式事务demo修复 2024-10-24 09:19:49 +08:00
79f7134bd5 【springboot3分支 issues/7353】The bean 'dataSource', defined in class path resource #7353 2024-10-19 19:30:05 +08:00
6d432bc186 Merge pull request #7332 from EightMonth/springboot3_sas
修复redis序列化认证信息问题
2024-10-11 16:33:11 +08:00
415307eb9f 修复redis序列化认证信息问题 2024-10-11 16:27:28 +08:00
48e20b2af5 Merge pull request #7329 from EightMonth/springboot3_sas
Revert "修复redis序列化认证信息问题"
2024-10-11 15:03:12 +08:00
b7924b9ca8 Revert "修复redis序列化认证信息问题"
This reverts commit 4aa88189ed.
2024-10-11 14:59:11 +08:00
a10a2e0a9d Merge pull request #7327 from EightMonth/springboot3_sas
修复redis序列化认证信息问题
2024-10-11 13:44:54 +08:00
4aa88189ed 修复redis序列化认证信息问题 2024-10-11 11:43:05 +08:00
fdb05443c2 【版本合并】springboot3 sas升级到3.7.1 2024-10-10 22:48:10 +08:00
65d737db6d Merge remote-tracking branch 'origin/springboot3_sas' into springboot3_sas 2024-08-21 15:22:11 +08:00
f04f7f9abf Merge pull request #6899 from EightMonth/springboot3_sas
缩短token长度,适配主分支前端页面登录
2024-08-21 15:14:01 +08:00
935e118d15 sas分支前端同步master分支 2024-08-21 12:46:58 +08:00
e218367332 登出及强退逻辑完善 2024-08-21 11:22:13 +08:00
3a3f3cf367 升级积木报表到v1.8.0 2024-08-20 16:41:14 +08:00
0e762b4157 缩短token长度,适配主分支前端页面登录 2024-07-15 17:11:05 +08:00
f4712baa39 Merge pull request #6879 from EightMonth/springboot3_sas
修复访问仪表盘401
2024-07-10 16:23:48 +08:00
7d8b653d6e 修复访问仪表盘401 2024-07-10 16:22:10 +08:00
cf7f3f94be Merge pull request #6878 from EightMonth/springboot3_sas
修复访问仪表盘401问题
2024-07-10 16:03:19 +08:00
49f63b92ac 修复访问仪表盘401问题 2024-07-10 16:01:57 +08:00
5670a15b20 Merge remote-tracking branch 'origin/springboot3' into springboot3_sas 2024-07-08 12:28:17 +08:00
9e9ef20b7c 改法不兼容,暂时先还原 2024-07-03 14:35:44 +08:00
0c034031d1 Merge branch 'springboot3_sas' of https://github.com/jeecgboot/jeecg-boot into springboot3_sas 2024-07-03 12:23:13 +08:00
491a038b5a 增加仪表盘的请求排除 2024-07-03 12:22:43 +08:00
8a4fcb0023 Merge remote-tracking branch 'origin/springboot3' into springboot3_sas 2024-07-03 12:02:32 +08:00
e93dcc1a7e Merge pull request #6660 from EightMonth/springboot3_sas
修复redis反序列化时间问题
2024-06-27 18:33:16 +08:00
383cbf250f 修复redis反序列化时间问题 2024-06-27 18:09:54 +08:00
9fe1450ac9 Merge pull request #6646 from EightMonth/springboot3_sas
sas实现免登录注解
2024-06-25 17:38:48 +08:00
88b9b12998 sas实现免登录注解 2024-06-25 17:32:57 +08:00
9e25566271 【重要更新】vue3前端为了适配springboot3 sas分支(Spring Authorization Server)特定修改 2024-06-23 11:25:55 +08:00
8e54e06978 Merge remote-tracking branch 'origin/springboot3' into springboot3_sas 2024-06-23 11:23:34 +08:00
e5c082ae13 版本合并前端和后端放到一起 2024-06-23 10:35:53 +08:00
96ab98ac3e Merge remote-tracking branch 'origin/springboot3' into springboot3_sas
# Conflicts:
#	jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/exception/JeecgCaptchaException.java
#	jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/shiro/JwtToken.java
#	jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/shiro/ShiroConfig.java
#	jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/shiro/ShiroRealm.java
#	jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/shiro/filters/CustomShiroFilterFactoryBean.java
#	jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/shiro/filters/JwtFilter.java
#	jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/shiro/filters/ResourceCheckFilter.java
#	jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/vo/Shiro.java
2024-06-23 10:29:34 +08:00
1632c241ee 修改nacos命名空间springboot3的id也是springboot3,方便理解 2024-06-22 23:23:55 +08:00
e9d05b0e75 访问积木报表提示token无权限,原因TokenUtils.getLoginUser方法报错 2024-06-22 22:59:34 +08:00
6ade7e22f8 积木报表扩展实现类放开注释 2024-06-22 20:06:13 +08:00
43d47c08cb 【jeecgboot 3.7.0里程碑版本发布——合并springboot3sas分支】
Merge remote-tracking branch 'origin/springboot3' into springboot3_sas

# Conflicts:
#	.gitignore
#	db/tables_nacos.sql
#	jeecg-boot-base-core/pom.xml
#	jeecg-boot-base-core/src/main/java/org/jeecg/common/aspect/AutoLogAspect.java
#	jeecg-boot-base-core/src/main/java/org/jeecg/common/exception/JeecgBootExceptionHandler.java
#	jeecg-boot-base-core/src/main/java/org/jeecg/config/DruidWallConfigRegister.java
#	jeecg-boot-base-core/src/main/java/org/jeecg/config/shiro/ShiroConfig.java
#	jeecg-boot-base-core/src/main/java/org/jeecg/config/shiro/ShiroRealm.java
#	jeecg-boot-base-core/src/main/java/org/jeecg/config/shiro/filters/JwtFilter.java
#	jeecg-boot-base-core/src/main/java/org/jeecg/modules/base/service/impl/BaseCommonServiceImpl.java
#	jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/controller/LoginController.java
#	jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/controller/SysAnnouncementController.java
#	jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/controller/SysDepartController.java
#	jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/controller/SysDepartRoleController.java
#	jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/controller/SysDictController.java
#	jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/controller/SysPermissionController.java
#	jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/controller/SysTableWhiteListController.java
#	jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/controller/SysTenantController.java
#	jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/controller/SysUserController.java
#	jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/service/impl/SysAnnouncementServiceImpl.java
#	jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/service/impl/SysDepartServiceImpl.java
#	jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/service/impl/SysTenantPackServiceImpl.java
#	jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/service/impl/SysTenantServiceImpl.java
#	jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/service/impl/SysUserDepartServiceImpl.java
#	jeecg-module-system/jeecg-system-start/src/main/resources/application-dev.yml
#	jeecg-module-system/jeecg-system-start/src/main/resources/application-prod.yml
#	jeecg-module-system/jeecg-system-start/src/main/resources/application-test.yml
#	pom.xml
2024-06-22 19:55:35 +08:00
e616c5d8fe Merge pull request #6243 from EightMonth/springboot3_sas
sas兼容shiro处理
2024-06-20 16:09:29 +08:00
cddf23c787 sas 兼容shiro处理 2024-05-31 14:29:21 +08:00
70a37309dd sas兼容shiro处理 2024-05-22 18:08:31 +08:00
48555b5219 Merge pull request #6201 from EightMonth/springboot3_sas
增加bug修复注释
2024-04-30 14:25:10 +08:00
06d58f202f 增加bug修复注释 2024-04-30 14:07:59 +08:00
628870af9b Merge pull request #6199 from EightMonth/springboot3_sas
修复#6168\#6169\websocket连接问题
2024-04-30 14:02:22 +08:00
b46a6438e6 修复#6168\#6169\websocket连接问题 2024-04-30 11:47:51 +08:00
5488f99723 Merge pull request #6194 from EightMonth/springboot3_sas
修复#6150,同时修复online表单无法加载问题
2024-04-29 17:34:06 +08:00
6bc1fe8d21 修复#6150,同时修复online表单无法加载问题 2024-04-29 17:27:25 +08:00
7cac16320c Merge pull request #6182 from EightMonth/springboot3_sas
修复Online同步数据库
2024-04-26 12:59:43 +08:00
24dbd1db39 修复Online同步数据库 2024-04-26 12:53:22 +08:00
46b026b989 Merge pull request #6092 from EightMonth/springboot3_sas
升级druid v1.2.22版本兼容处理
2024-04-08 15:26:41 +08:00
94c45f5e0f 升级druid v1.2.22版本兼容处理 2024-04-08 14:11:39 +08:00
8950e19d4e Merge pull request #6037 from EightMonth/springboot3_sas
修复 #5936
2024-03-25 15:00:06 +08:00
99eb88f71c 修复 #5936 2024-03-25 14:44:23 +08:00
824d7839d8 Merge pull request #6014 from EightMonth/springboot3_sas
支持手动生成token
2024-03-22 16:18:29 +08:00
c88f9d95d4 支持手动生成token 2024-03-19 17:16:12 +08:00
beb0bc2f64 Merge pull request #5995 from EightMonth/springboot3_sas
移除权限不足异常堆栈,权限加载加入缓存
2024-03-15 17:06:05 +08:00
f741db874c 移除权限不足异常堆栈,权限加载加入缓存 2024-03-15 13:55:58 +08:00
d684c09392 Merge pull request #5965 from EightMonth/springboot3_sas
新增token校验、客户端便捷工具类、修复登录缺乏租户信息、强退功能失效
2024-03-12 14:27:11 +08:00
364be22dd0 新增token校验、客户端便捷工具类、修复登录缺乏租户信息、强退功能失效 2024-03-08 16:30:23 +08:00
20efa3bf9a Merge pull request #5934 from EightMonth/springboot3_sas
修正springboot3 sas默认配置
2024-03-01 16:23:13 +08:00
c7977dda3d 添加nacos sql自动创建nacos库 2024-03-01 16:17:30 +08:00
c27c5a9a9b 梳理服务配置信息 2024-03-01 15:49:28 +08:00
0ab280f812 添加springboot3的配置变更 2024-03-01 14:59:40 +08:00
c3066dac17 修改认证异常类拦截 2024-03-01 14:51:16 +08:00
b650d512b3 修复 #5903 ,完善在微服务的sas认证 2024-03-01 14:51:09 +08:00
925ec9447d Merge pull request #5819 from EightMonth/springboot3_sas
打通三方登录&移除shiro
2024-01-17 13:55:58 +08:00
411a73c1bf 移除shiro 2024-01-16 19:49:42 +08:00
84077e6e24 移除shiro 2024-01-16 19:49:15 +08:00
184cf97304 打通三方登录 2024-01-16 19:09:56 +08:00
5f425b49b2 Merge pull request #5761 from EightMonth/springboot3_sas
升级 spring authorization server
2024-01-12 09:40:48 +08:00
3ac8ee304a 完全替换shiro权限注解,新增手机登录、APP登录 2024-01-12 09:26:30 +08:00
0faac01bb7 sas升级脚本 2024-01-04 11:27:33 +08:00
74d88a8fcc springboot sas升级 2024-01-04 11:27:23 +08:00
616 changed files with 9075 additions and 102535 deletions

1
.gitignore vendored
View File

@ -13,3 +13,4 @@ os_del.cmd
os_del_doc.cmd
.svn
derby.log
*.log

View File

@ -16,19 +16,6 @@ JeecgBoot平台的AIGC功能模块是一套类似`Dify`的`AIGC应用开发
[![](https://jeecgos.oss-cn-beijing.aliyuncs.com/files/jeecg_aivideo.png)](https://www.bilibili.com/video/BV1zmd7YFE4w)
##### 功能大模块
- AI应用开发平台
- AI知识库系统
- AI大模型管理
- AI流程编排
- AI对话支持图片
- AI对话助手(智能问答)
- AI建表Online表单
- AI写文章CMS
- AI表单字段建议表单设计器
#### Dify `VS` JEECG AI
@ -57,10 +44,10 @@ JeecgBoot平台的AIGC功能模块是一套类似`Dify`的`AIGC应用开发
### 技术文档
### 安装向量库 pgvector
- https://help.jeecg.com/aigc/config
- [AIGC开发文档](https://help.jeecg.com/aigc)
- [安装向量库 pgvector](https://help.jeecg.com/aigc/config)
@ -86,7 +73,7 @@ JeecgBoot平台的AIGC功能模块是一套类似`Dify`的`AIGC应用开发
## 技术交流
- 开发文档https://help.jeecg.com/aigc
- QQ群964611995、716488839(满)
- QQ群716488839
## 功能列表

View File

@ -7,12 +7,12 @@
JEECG BOOT AI Low Code Platform
===============
Current version: 3.8.2 (Release date: 2025-08-04)
Current version: 3.8.0 (Release date: 2025-04-18)
[![AUR](https://img.shields.io/badge/license-Apache%20License%202.0-blue.svg)](https://github.com/zhangdaiscott/jeecg-boot/blob/master/LICENSE)
[![](https://img.shields.io/badge/Author-guojusoft-orange.svg)](http://www.jeecg.com)
[![](https://img.shields.io/badge/version-3.8.2-brightgreen.svg)](https://github.com/zhangdaiscott/jeecg-boot)
[![](https://img.shields.io/badge/version-3.8.0-brightgreen.svg)](https://github.com/zhangdaiscott/jeecg-boot)
[![GitHub stars](https://img.shields.io/github/stars/zhangdaiscott/jeecg-boot.svg?style=social&label=Stars)](https://github.com/zhangdaiscott/jeecg-boot)
[![GitHub forks](https://img.shields.io/github/forks/zhangdaiscott/jeecg-boot.svg?style=social&label=Fork)](https://github.com/zhangdaiscott/jeecg-boot)
@ -69,13 +69,9 @@ Jeecg-Boot AI low code platform can be applied in the development of any J2EE pr
Starts the project
-----------------------------------
> Default account password admin/123456
- [IDEA Quick start](https://help.jeecg.com/java/setup/idea/startup)
- [Docker Quick start](https://help.jeecg.com/java/docker/quick)
- [Development Environment setup](https://help.jeecg.com/java/setup/tools)
- [IDEA Quick start(single model)](https://help.jeecg.com/java/setup/idea/startup)
- [Docker Quick start(single model)](https://help.jeecg.com/java/docker/quick)
- [IDEA Quick start(microservices model)](https://help.jeecg.com/java/springcloud/switchcloud/monomer)
- [Docker Quick start(microservices model)](https://help.jeecg.com/java/docker/quickcloud)
Technical documentation
@ -85,7 +81,7 @@ Technical documentation
- Demo [OnlineDemo](http://boot3.jeecg.com) | [APP](http://jeecg.com/appIndex)
- Doc [DocumentCenter](http://help.jeecg.com) | [AI Config](https://help.jeecg.com/java/ai/aichat)
- Newbie guide [Quick start](http://www.jeecg.com/doc/quickstart) | [Q&A ](http://www.jeecg.com/doc/qa) | [1 minute experience](https://my.oschina.net/jeecg/blog/3083313)
- QQ group 964611995、⑩716488839(满)、⑨808791225(满)
- QQ group ⑩716488839、⑨808791225

View File

@ -1,124 +0,0 @@
JeecgBoot低代码平台(商业版介绍)
===============
项目介绍
-----------------------------------
<h3 align="center">企业级AI低代码平台</h3>
JeecgBoot是一款集成AI应用的基于BPM流程的低代码平台旨在帮助企业快速实现低代码开发和构建个性化AI应用前后端分离架构Ant Design&Vue3SpringBootSpringCloud AlibabaMybatis-plusShiro。强大的代码生成器让前后端代码一键生成无需写任何代码 引领AI低代码开发模式: AI生成->OnlineCoding-> 代码生成-> 手工MERGE 帮助Java项目解决80%的重复工作让开发更多关注业务提高效率、节省成本同时又不失灵活性低代码能力Online表单、表单设计、流程设计、Online报表、大屏/仪表盘设计、报表设计; AI应用平台功能AI知识库问答、AI模型管理、AI流程编排、AI聊天等支持含ChatGPT、DeepSeek、Ollama等多种AI大模型
JeecgBoot 提供了一系列 `低代码能力`,实现`真正的零代码`在线开发Online表单开发、Online报表、复杂报表设计、打印设计、在线图表设计、仪表盘设计、大屏设计、移动图表能力、表单设计器、在线设计流程、流程自动化配置、插件能力可插拔
`AI赋能低代码:` 目前提供了AI应用、AI模型管理、AI流程编排、AI对话助手AI建表、AI写文章、AI知识库问答、AI字段建议等功能;支持各种AI大模型ChatGPT、DeepSeek、Ollama、智普、千问等.
`JEECG宗旨是:` 简单功能由OnlineCoding配置实现做到`零代码开发`复杂功能由代码生成器生成进行手工Merge 实现`低代码开发`,既保证了`智能`又兼顾`灵活`;实现了低代码开发的同时又支持灵活编码,解决了当前低代码产品普遍不灵活的弊端!
`JEECG业务流程:` 采用工作流来实现、扩展出任务接口,供开发编写业务逻辑,表单提供多种解决方案: 表单设计器、online配置表单、编码表单。同时实现了流程与表单的分离设计松耦合、并支持任务节点灵活配置既保证了公司流程的保密性又减少了开发人员的工作量。
#### JeecgBoot商业版与同类产品区别
-----------------------------------
- 灵活性jeecgboot基于开源技术栈设计初考虑到可插拔性和集成灵活性确保平台的智能性与灵活性避免因平台过于庞大而导致的扩展困难。
- 流程管理:支持一个表单挂接多个流程,同时一个流程可以连接多个表单,增强了流程的灵活性和复杂性管理。
- 符合中国国情的流程针对中国市场的特定需求jeecgboot能够实现各种符合中国国情的业务流程。
- 强大的表单设计器jeecgboot的表单设计器与敲敲云共享具备高质量和智能化的特点能够满足零代码应用的需求业内同类产品中不多见。
- 报表功能:自主研发的报表工具,拥有独立知识产权,功能上比业内老牌产品如帆软更智能,操作简便。
- BI产品整合提供大屏、仪表盘、门户等功能完美解决这些需求并支持移动面板的设计与渲染。
- 自主研发的模块jeecgboot的所有模块均为自主研发具有独立的知识产权。
- 颗粒度和功能细致在功能细致度和颗粒度上jeecgboot远超同类产品尤其在零代码能力方面表现突出。
- 零代码应用管理最新版支持与敲敲云的零代码应用管理能力的集成使得jeecgboot既具备低代码又具备零代码的应用能力业内独一无二。
- 强大的代码生成器作为开源代码生成器的先锋jeecgboot在代码生成的智能化和在线低代码与代码生成的结合方面优势明显。
- 精细化权限管理提供行级和列级的数据权限控制满足企业在ERP和OA领域对权限管理的严格需求。
- 多平台支持的APP目前采用uniapp3实现支持小程序、H5、App及鸿蒙、鸿蒙Next、Electron桌面应用等多种终端。
> 综上所述jeecgboot不仅在功能上具备丰富性和灵活性还在技术架构、权限管理和用户体验等方面展现出明显的优势是一个综合性能强大的低代码平台。
商业版演示
-----------------------------------
JeecgBoot vs 敲敲云
> - JeecgBoot是低代码产品拥有系列低代码能力比如流程设计、表单设计、大屏设计代码生成器适合半开发模式开发+低代码结合),也可以集成零代码应用管理模块.
> - 敲敲云是零代码产品完全不写代码通过配置搭建业务系统其在jeecgboot基础上研发而成删除了online、代码生成、OA等需要编码功能只保留应用管理功能和聊天、日程、文件三个OA组件.
- JeecgBoot低代码 https://boot3.jeecg.com
- 敲敲云零代码https://app.qiaoqiaoyun.com
- APP演示(多端): http://jeecg.com/appIndex
### 流程视频介绍
[![](https://jeecgos.oss-cn-beijing.aliyuncs.com/files/flow_video.png)](https://www.bilibili.com/video/BV1Nk4y1o7Qc)
### 商业版功能简述
> 详细的功能介绍,[请联系官方](https://jeecg.com/vip)
```
│─更多商业功能
│ ├─流程设计器
│ ├─简流设计器(类钉钉版)
│ ├─门户设计NEW
│ ├─表单设计器
│ ├─大屏设计器
│ └─我的任务
│ └─历史流程
│ └─历史流程
│ └─流程实例管理
│ └─流程监听管理
│ └─流程表达式
│ └─我发起的流程
│ └─我的抄送
│ └─流程委派、抄送、跳转
│ └─OA办公组件
│ └─零代码应用管理(无需编码,在线搭建应用系统)
│ ├─积木报表企业版含jimureport、jimubi
│ ├─AI流程设计器源码
│ ├─Online全模块功能和源码
│ ├─AI写文章CMS
│ ├─AI表单字段建议表单设计器
│ ├─OA办公协同组件
│ ├─在线聊天功能
│ ├─设计表单移动适配
│ ├─设计表单支持外部填报
│ ├─设计表单AI字段建议
│ ├─设计表单视图功能(支持多种类型含日历、表格、看板、甘特图)
│ └─。。。
```
##### 流程设计
![](https://oscimg.oschina.net/oscnet/up-981ce174e4fbb48c8a2ce4ccfd7372e2994.png)
![](https://oscimg.oschina.net/oscnet/up-1dc0d052149ec675f3e4fad632b82b48add.png)
![](https://oscimg.oschina.net/oscnet/up-de31bc2f9d9b8332c554b0954cc73d79593.png)
![输入图片说明](https://static.oschina.net/uploads/img/201907/05165142_yyQ7.png "在这里输入图片标题")
![输入图片说明](https://static.oschina.net/uploads/img/201904/14160917_9Ftz.png "在这里输入图片标题")
![输入图片说明](https://static.oschina.net/uploads/img/201904/14160633_u59G.png "在这里输入图片标题")
##### 表单设计器
![](https://oscimg.oschina.net/oscnet/up-5f8cb657615714b02190b355e59f60c5937.png)
![](https://oscimg.oschina.net/oscnet/up-d9659b2f324e33218476ec98c9b400e6508.png)
![](https://oscimg.oschina.net/oscnet/up-4868615395272d3206dbb960ade02dbc291.png)

453
README.md
View File

@ -2,13 +2,12 @@
JeecgBoot AI低代码平台
===============
当前最新版本: 3.8.2发布日期2025-08-04
当前最新版本: 3.8.0发布日期2025-04-18
[![AUR](https://img.shields.io/badge/license-Apache%20License%202.0-blue.svg)](https://github.com/jeecgboot/JeecgBoot/blob/master/LICENSE)
[![](https://img.shields.io/badge/Author-北京国炬软件-orange.svg)](https://jeecg.com)
[![](https://img.shields.io/badge/blog-技术博客-orange.svg)](https://jeecg.blog.csdn.net)
[![](https://img.shields.io/badge/version-3.8.2-brightgreen.svg)](https://github.com/jeecgboot/JeecgBoot)
[![](https://img.shields.io/badge/Author-北京国炬软件-orange.svg)](http://guojusoft.com)
[![](https://img.shields.io/badge/version-3.8.0-brightgreen.svg)](https://github.com/jeecgboot/JeecgBoot)
[![GitHub stars](https://img.shields.io/github/stars/zhangdaiscott/jeecg-boot.svg?style=social&label=Stars)](https://github.com/jeecgboot/JeecgBoot)
[![GitHub forks](https://img.shields.io/github/forks/zhangdaiscott/jeecg-boot.svg?style=social&label=Fork)](https://github.com/jeecgboot/JeecgBoot)
@ -17,171 +16,177 @@ JeecgBoot AI低代码平台
项目介绍
-----------------------------------
<h3 align="center">企业级AI低代码平台</h3>
<h3 align="center">Java AI Low Code Platform</h3>
JeecgBoot 是一款基于BPM流程和代码生成的AI低代码平台助力企业快速实现低代码开发和构建AI应用。
采用前后端分离架构Ant Design&Vue3SpringBoot3SpringCloud AlibabaMybatis-plus,强大代码生成器实现前后端一键生成,无需手写代码。
平台引领AI低代码开发模式AI生成→在线编码→代码生成→手工合并解决Java项目80%重复工作,提升效率,节省成本,兼顾灵活性。
具备强大且颗粒化的权限控制支持按钮权限和数据权限设置满足大型业务系统需求。功能涵盖在线表单、表单设计、流程设计、门户设计、报表与大屏设计、OA办公、AI应用、AI知识库、大模型管理、AI流程编排、AI聊天支持ChatGPT、DeepSeek、Ollama等多种AI大模型。
JeecgBoot是一款基于AIGC和低代码引擎的AI低代码平台旨在帮助开发者快速实现低代码开发和构建、部署个性化的 AI 应用。
前后端分离架构Ant Design&Vue3SpringBootSpringCloud AlibabaMybatis-plusShiro,强大代码生成器前后端代码一键生成,无需写任何代码!
成套AI大模型功能: AI模型管理、AI应用、知识库、AI流程编排、AI对话助手等
引领AI低代码开发模式: AIGC生成->OnlineCoding-> 代码生成-> 手工MERGE 帮助Java项目解决80%的重复工作,让开发更多关注业务,快速提高效率 节省成本,同时又不失灵活性!
`AI赋能报表:` 积木报表是一款自主研发的强大开源企业级Web报表与大屏工具。它通过零编码的拖拽式操作赋能用户如同搭积木般轻松构建各类复杂报表和数据大屏全面满足企业数据可视化与分析需求助力企业级数据产品的高效打造与应用。
`AI赋能低代码:` 提供完善成熟的AI应用平台涵盖AI应用管理、AI模型管理、智能对话助手、知识库问答、流程编排与设计器、AI建表等多项功能。平台兼容多种主流大模型包括ChatGPT、DeepSeek、Ollama、智普、千问等助力企业高效构建智能化应用推动低代码开发与AI深度融合。
JeecgBoot 提供了一系列 `低代码能力`,实现`真正的零代码`在线开发Online表单开发、Online报表、复杂报表设计、打印设计、在线图表设计、仪表盘设计、大屏设计、移动图表能力、表单设计器、在线设计流程、流程自动化配置、插件能力可插拔
`JEECG宗旨是:` JEECG旨在通过OnlineCoding平台实现简单功能的零代码快速搭建同时针对复杂功能采用代码生成器生成代码并手工合并打造智能且灵活的低代码开发模式有效解决了当前低代码产品普遍缺乏灵活性的问题提升开发效率的同时兼顾系统的扩展性和定制化能力。
`AI赋能低代码:` 目前提供了AI应用、AI模型管理、AI流程编排、AI对话助手AI建表、AI写文章、AI知识库问答、AI字段建议等功能;支持各种AI大模型ChatGPT、DeepSeek、Ollama、智普、千问等.
`JEECG业务流程:` JEECG业务流程采用BPM工作流引擎实现业务审批扩展任务接口供开发人员编写业务逻辑表单提供表单设计器、在线配置表单和编码表单等多种解决方案。通过流程与表单的分离设计松耦合及任务节点的灵活配置既保障了企业流程的安全性与保密性又大幅降低了开发人员的工作量。
`JEECG宗旨是:` 简单功能由OnlineCoding配置实现做到`零代码开发`复杂功能由代码生成器生成进行手工Merge 实现`低代码开发`,既保证了`智能`又兼顾`灵活`;实现了低代码开发的同时又支持灵活编码,解决了当前低代码产品普遍不灵活的弊端!
`JEECG业务流程:` 采用工作流来实现、扩展出任务接口,供开发编写业务逻辑,表单提供多种解决方案: 表单设计器、online配置表单、编码表单。同时实现了流程与表单的分离设计松耦合、并支持任务节点灵活配置既保证了公司流程的保密性又减少了开发人员的工作量。
### 视频介绍
[![](https://jeecgos.oss-cn-beijing.aliyuncs.com/files/flow_video.png)](https://www.bilibili.com/video/BV1Nk4y1o7Qc)
适用项目
-----------------------------------
JeecgBoot低代码平台兼容所有J2EE项目开发支持信创国产化,特别适用于SAAS、企业信息管理系统MIS、内部办公系统OA、企业资源计划系统ERP、客户关系管理系统CRM及AI知识库等场景。其半智能手工Merge开发式,可显著提升70%以上的开发效率极大降低开发成本。同时JeecgBoot还是一款全栈式AI开发平台助力企业快速构建和部署个性化AI应用。
JeecgBoot AI低代码平台,可以应用在任何J2EE项目开发,支持信创国产化。尤其适合SAAS项目、企业信息管理系统MIS、内部办公系统OA、企业资源计划系统ERP、客户关系管理系统CRM等,其半智能手工Merge开发式,可显著提高开发效率70%以上,极大降低开发成本
又是一个全栈式 AI 开发平台,快速帮助企业构建和部署个性化的 AI 应用。
**信创兼容说明**
信创国产化
-----------------------------------
JeecgBoot 是一个开源低代码开发平台,支持全信创环境。它兼容多种国产操作系统和数据库,包括:
- 操作系统:国产麒麟、银河麒麟等国产系统几乎都是基于 Linux 内核,因此它们具有良好的兼容性。
- 数据库达梦、人大金仓、TiDB
- 数据库达梦、人大金仓、TiDB , [转库文档](https://my.oschina.net/jeecg/blog/4905722)
- 中间件:东方通 TongWeb、TongRDS宝兰德 AppServer、CacheDB, [信创配置文档](https://help.jeecg.com/java/tongweb-deploy/)
通过这些适配JeecgBoot 为使用国产软件和硬件的用户提供了高效的开发解决方案。
版本说明
项目说明
-----------------------------------
|下载 | JDK17 + SpringBoot3.3 + Shiro |JDK17 + SpringBoot3.3+ SpringAuthorizationServer | JDK17/JDK8 + SpringBoot2.7 |
|------|----------------------------------------------------|--------------------------------------------|--------------------------------------------|
| Github | [`springboot3`](https://github.com/jeecgboot/JeecgBoot/tree/springboot3) | [`springboot3_sas`](https://github.com/jeecgboot/JeecgBoot/tree/springboot3_sas) 分支 |[`master`](https://github.com/jeecgboot/JeecgBoot) 分支|
| Gitee | [`springboot3`](https://gitee.com/jeecg/JeecgBoot/tree/springboot3/) | [`springboot3_sas`](https://gitee.com/jeecg/JeecgBoot/tree/springboot3_sas) 分支 |[`master`](https://gitee.com/jeecg/JeecgBoot) 分支 |
| 项目名 | 说明 |
|--------------------|------------------------|
| `jeecg-boot` | 后端源码JAVASpringBoot微服务架构 |
| `jeecgboot-vue3` | 前端源码VUE3vue3+vite6+ts最新技术栈 |
| `JeecgUniapp` | [配套APP框架](https://github.com/jeecgboot/JeecgUniapp) 适配多个终端支持APP、小程序、H5 |
- `jeecg-boot` 是后端JAVA源码项目Springboot3+SpringCloudAlibaba支持单体和微服务切换.
- `jeecgboot-vue3` 是前端VUE3源码项目vue3+vite6+ts最新技术栈.
- `JeecgUniapp` 是[配套APP框架](https://github.com/jeecgboot/JeecgUniapp) 适配多个终端支持APP、小程序、H5、鸿蒙、鸿蒙Next.
- 参考 [文档](https://help.jeecg.com/ui/2dev/mini) 可以删除不需要的demo制作一个精简版本
启动项目
-----------------------------------
> 默认账号密码: admin/123456
- [开发环境搭建](https://help.jeecg.com/java/setup/tools)
- [IDEA启动前后端(单体模式)](https://help.jeecg.com/java/setup/idea/startup)
- [Docker一键启动(单体模式)](https://help.jeecg.com/java/docker/quick)
- [IDEA启动前后端(微服务方式)](https://help.jeecg.com/java/springcloud/switchcloud/monomer)
- [Docker一键启动(微服务方式)](https://help.jeecg.com/java/docker/quickcloud)
技术文档
-----------------------------------
- 官方网站: [http://www.jeecg.com](http://www.jeecg.com)
- 在线演示: [平台演示](https://boot3.jeecg.com) | [APP演示](https://jeecg.com/appIndex)
- 入门指南 [快速入门](http://www.jeecg.com/doc/quickstart) | [代码生成使用](https://help.jeecg.com/java/codegen/online) | [开发文档](https://help.jeecg.com) | [AI应用手册](https://help.jeecg.com/aigc) | [视频教程](http://jeecg.com/doc/video)
- 技术支持: [反馈问题](https://github.com/jeecgboot/JeecgBoot/issues/new?template=bug_report.md) | [低代码体验一分钟](https://jeecg.blog.csdn.net/article/details/106079007)
- QQ交流群 964611995、⑩716488839(满)、⑨808791225(满)、其他(满)
- 在线演示 [平台演示](http://boot3.jeecg.com) | [APP演示](http://jeecg.com/appIndex) | [体验低代码](https://jeecg.blog.csdn.net/article/details/106079007) | [体验零代码](https://app.qiaoqiaoyun.com/myapps/index)
- 开发文档 [文档中心](https://help.jeecg.com) | [AIGC大模块](https://help.jeecg.com/aigc)
- 新手指南: [快速入门](http://www.jeecg.com/doc/quickstart) | [入门视频](http://jeecg.com/doc/video) | [如何反馈问题](https://github.com/jeecgboot/JeecgBoot/issues/new?template=bug_report.md)
- QQ交流群 ⑩716488839、⑨808791225(满)、其他(满)
AI 应用平台介绍
启动项目
-----------------------------------
一个全栈式 AI 开发平台,旨在帮助开发者快速构建和部署个性化的 AI 应用。
- [IDEA启动前后端项目](https://help.jeecg.com/java/setup/idea/startup)
- [Docker一键启动前后端](https://help.jeecg.com/java/docker/quick)
JeecgBoot平台提供了一套完善的AI应用管理系统模块是一套类似`Dify``AIGC应用开发平台`+`知识库问答`是一款基于LLM大语言模型AI应用平台和 RAG 的知识库问答系统。
其直观的界面结合了 AI 流程编排、RAG 管道、知识库管理、模型管理、对接向量库、实时运行可观察等让您可以快速从原型到生产拥有AI服务能力。
- [详细专题介绍,请点击查看](README-AI.md)
- AI视频介绍
AIGC应用平台介绍
-----------------------------------
JeecgBoot 平台的AIGC功能模块是一套类似`Dify``AIGC应用开发平台`+`知识库问答`是一款基于LLM大语言模型AI应用平台和 RAG 的知识库问答系统。
其直观的界面结合了 AI 流程编排、RAG 管道、知识库管理、模型管理、对接向量库、实时运行可观察等让您可以快速从原型到生产拥有AI服务能力。
> JDK说明AI流程编排引擎暂时不支持jdk21所以目前只能使用jdk8或者jdk17启动项目。
- [AIGC专题介绍页](README-AI.md)
- [AIGC开发文档](https://help.jeecg.com/aigc)
- [配置向量库PGVector](https://help.jeecg.com/aigc/config)
##### AI视频介绍
[![](https://jeecgos.oss-cn-beijing.aliyuncs.com/files/jeecg_aivideo.png)](https://www.bilibili.com/video/BV1zmd7YFE4w)
为什么选择JeecgBoot?
-----------------------------------
- 1.采用最新主流前后分离框架Spring Boot3 + MyBatis + Ant Design4 + Vue3容易上手代码生成器依赖性低灵活的扩展能力可快速实现二次开发。
- 2.前端大版本换代,最新版采用 Vue3.0 + TypeScript + Vite6 + Ant Design Vue4 等新技术方案。
- 3.支持微服务Spring Cloud AlibabaNacos、Gateway、Sentinel、Skywalking提供简易机制支持单体和微服务自由切换这样可以满足各类项目需求
- 4.开发效率高支持在线建表和AI建表提供强大代码生成器单表、树列表、一对多、一对一等数据模型增删改查功能一键生成菜单配置直接使用。
- 5.代码生成器提供强大模板机制,支持自定义模板,目前提供四套风格模板(单表两套、树模型一套、一对多三套)。
- 6.提供强大的报表和大屏可视化工具,支持丰富的数据源连接,能够通过拖拉拽方式快速制作报表、大屏和门户设计;支持多种图表类型:柱形图、折线图、散点图、饼图、环形图、面积图、漏斗图、进度图、仪表盘、雷达图、地图等。
- 7.低代码能力在线表单无需编码通过在线配置表单实现表单的增删改查支持单表、树、一对多、一对一等模型实现人人皆可编码在线配置零代码开发、所见即所得支持23种类控件。
- 8.低代码能力:在线报表、在线图表(无需编码,通过在线配置方式,实现数据报表和图形报表,可以快速抽取数据,减轻开发压力,实现人人皆可编码)
- 9.Online支持在线增强开发提供在线代码编辑器支持代码高亮、代码提示等功能支持多种语言Java、SQL、JavaScript等
- 10.封装完善的用户、角色、菜单、组织机构、数据字典、在线定时任务等基础功能,支持访问授权、按钮权限、数据权限等功能
- 11.前端UI提供丰富的组件库支持各种常用组件如表格、树形控件、下拉框、日期选择器等满足各种复杂的业务需求 [UI组件库文档](https://help.jeecg.com/category/ui%E7%BB%84%E4%BB%B6%E5%BA%93)。
- 12.提供APP配套框架一份多代码多终端适配一份代码多终端适配小程序、H5、安卓、iOS、鸿蒙Next。
- 13.新版APP框架采用Uniapp、Vue3.0、Vite、Wot-design-uni、TypeScript等最新技术栈包括二次封装组件、路由拦截、请求拦截等功能。实现了与JeecgBoot完美对接目前已经实现登录、用户信息、通讯录、公告、移动首页、九宫格、聊天、Online表单、仪表盘等功能提供了丰富的组件。
- 14.提供了一套成熟的AI应用平台功能从AI模型、知识库到AI应用搭建助力企业快速落地AI服务加速智能化升级。
- 15.AI能力目前JeecgBoot支持AI大模型chatgpt和deepseek现在最新版默认使用deepseek速度更快质量更高。目前提供了AI对话助手、AI知识库、AI应用、AI建表、AI报表等功能。
- 16.提供新行编辑表格JVXETable轻松满足各种复杂ERP布局拥有更高的性能、更灵活的扩展、更强大的功能。
- 17.平台首页风格,提供多种组合模式,支持自定义风格;支持门户设计,支持自定义首页。
- 18.常用共通封装各种工具类定时任务、短信接口、邮件发送、Excel导入导出等基本满足80%项目需求。
- 19.简易Excel导入导出支持单表导出和一对多表模式导出生成的代码自带导入导出功能。
- 20.集成智能报表工具报表打印、图像报表和数据导出非常方便可极其方便地生成PDF、Excel、Word等报表。
- 21.采用前后分离技术页面UI风格精美针对常用组件做了封装时间、行表格控件、截取显示控件、报表组件、编辑器等。
- 22.查询过滤器查询功能自动生成后台动态拼SQL追加查询条件支持多种匹配方式全匹配/模糊查询/包含查询/不匹配查询)。
- 23.数据权限(精细化数据权限控制,控制到行级、列表级、表单字段级,实现不同人看不同数据,不同人对同一个页面操作不同字段)。
- 24.接口安全机制可细化控制接口授权非常简便实现不同客户端只看自己数据等控制也提供了基于AK和SK认证鉴权的OpenAPI功能。
- 25.活跃的社区支持;近年来,随着网络威胁的日益增加,团队在安全和漏洞管理方面积累了丰富的经验,能够为企业提供全面的安全解决方案。
- 26.权限控制采用RBACRole-Based Access Control基于角色的访问控制
- 27.页面校验自动生成(必须输入、数字校验、金额校验、时间空间等)。
- 28.支持SaaS服务模式提供SaaS多租户架构方案。
- 29.分布式文件服务集成MinIO、阿里OSS等优秀的第三方提供便捷的文件上传与管理同时也支持本地存储。
- 30.主流数据库兼容一套代码完全兼容MySQL、PostgreSQL、Oracle、SQL Server、MariaDB、达梦、人大金仓等主流数据库。
- 31.集成工作流Flowable并实现了只需在页面配置流程转向可极大简化BPM工作流的开发用BPM的流程设计器画出了流程走向一个工作流基本就完成了只需写很少量的Java代码。
- 32.低代码能力在线流程设计采用开源Flowable流程引擎实现在线画流程、自定义表单、表单挂靠、业务流转。
- 33.多数据源:极其简易的使用方式,在线配置数据源配置,便捷地从其他数据抓取数据。
- 34.提供单点登录CAS集成方案项目中已经提供完善的对接代码。
- 35.低代码能力表单设计器支持用户自定义表单布局支持单表、一对多表单支持select、radio、checkbox、textarea、date、popup、列表、宏等控件。
- 36.专业接口对接机制统一采用RESTful接口方式集成Swagger-UI在线接口文档JWT token安全验证方便客户端对接。
- 37.高级组合查询功能,在线配置支持主子表关联查询,可保存查询历史。
- 38.提供各种系统监控实时跟踪系统运行情况监控Redis、Tomcat、JVM、服务器信息、请求追踪、SQL监控
- 39.消息中心支持短信、邮件、微信推送等集成WebSocket消息通知机制。
- 40.支持多语言,提供国际化方案。
- 41.数据变更记录日志,可记录数据每次变更内容,通过版本对比功能查看历史变化。
- 42.提供简单易用的打印插件支持谷歌、火狐、IE11+等各种浏览器。
- 43.后端采用Maven分模块开发方式前端支持菜单动态路由。
- 44.提供丰富的示例代码,涵盖了常用的业务场景,便于学习和参考。
##### 在线体验
- JeecgBoot演示 https://boot3.jeecg.com
- 敲敲云在线搭建AI知识库https://app.qiaoqiaoyun.com
##### Dify `VS` JEECG AI
> JEECG AI与Dify相比在多个方面展现出显著的优势特别是在文档处理、格式和图片保持方面。以下是一些具体的优点
> - Markdown文档库导入
> JEECG AI允许用户直接导入整个Markdown文档库这不仅保留markdown格式还支持图片的导入确保文档内容的完整性和可视化效果
> - 对话回复格式美观:
> 在对话过程中JEECG AI能够保持回复内容的原格式也不丢失图片使得输出的文章更加美观不会出现格式错乱的情况还支持图片的渲染
> - PDF文档导入与格式转换
> JEECG AI在处理PDF文档时能够更好地保持原始格式和图片确保转换后的内容与原始文档一致。这个功能在许多AI产品中表现不佳而JEECG AI在这方面做出了显著的优化
##### 功能大模块
- AI应用开发平台
- AI知识库系统
- AI大模型管理
- AI流程编排
- AI对话支持图片
- AI对话助手(智能问答)
- AI建表Online表单
- AI写文章CMS
- AI表单字段建议表单设计器
##### AI大模型支持
| AI大模型 | 支持 |
| --- | --- |
| DeepSeek | √ |
| ChatGTP | √ |
| Qwq | √ |
| 智库 | √ |
| Ollama本地模型 | √ |
| 等等。。 | √ |
技术架构:
-----------------------------------
#### 前端
- 前端环境要求Node.js要求`Node 20+` 版本以上、pnpm 要求`9+` 版本以上
- 依赖管理node、npm、pnpm
- 前端IDE建议IDEA、WebStorm、Vscode
- 采用 Vue3.0+TypeScript+Vite6+Ant-Design-Vue4等新技术方案包括二次封装组件、utils、hooks、动态菜单、权限校验、按钮级别权限控制等功能
- 最新技术栈Vue3.0 + TypeScript + Vite6 + ant-design-vue4 + pinia + echarts + unocss + vxe-table + qiankun + es6
#### 后端
- IDE建议 IDEA (必须安装lombok插件 )
- 语言Java 默认jdk17(jdk21)
- 语言Java 默认jdk17(支持jdk8、jdk21)
- 依赖管理Maven
- 基础框架Spring Boot 3.5.5
- 微服务框架: Spring Cloud Alibaba 2023.0.3.3
- 持久层框架MybatisPlus 3.5.12
- 报表工具: JimuReport 2.1.3
- 安全框架Apache Shiro 2.0.4Jwt 4.5.0
- 基础框架Spring Boot 2.7.18
- 微服务框架: Spring Cloud Alibaba 2021.0.6.2
- 持久层框架MybatisPlus 3.5.3.2
- 报表工具: JimuReport 1.9.5
- 安全框架Apache Shiro 1.13.0Jwt 4.5.0
- 微服务技术栈Spring Cloud Alibaba、Nacos、Gateway、Sentinel、Skywalking
- 数据库连接池阿里巴巴Druid 1.2.24
- AI大模型支持 `ChatGPT` `DeepSeek` `千问`等各种常规模式
- 数据库连接池阿里巴巴Druid 1.1.24
- AI大模型支持 `ChatGPT` `DeepSeek`切换
- 日志打印logback
- 缓存Redis
- 其他autopoi, fastjsonpoiSwagger-uiquartz, lombok简化代码等。
- 默认提供MySQL5.7+数据库脚本
- 默认数据库脚本:MySQL5.7+
- [其他数据库,需要自己转](https://my.oschina.net/jeecg/blog/4905722)
#### 数据库支持
#### 前端
- 前端IDE建议WebStorm、Vscode
- 采用 Vue3.0+TypeScript+Vite6+Ant-Design-Vue等新技术方案包括二次封装组件、utils、hooks、动态菜单、权限校验、按钮级别权限控制等功能
- 最新技术栈Vue3.0 + TypeScript + Vite6 + ant-design-vue4 + pinia + echarts + unocss + vxe-table + qiankun + es6
- 依赖管理node、npm、pnpm
#### 前端环境要求
* 本地环境安装 `Node.js 、npm 、pnpm`
* pnpm 要求`9+` 版本以上
* Node.js 版本建议`v20.15.0`,要求`Node 20+` 版本以上
` ( 因为Vite6 需要 Node.js 18 / 20+ )`
#### 平台支持数据库
> jeecgboot平台支持以下数据库默认我们只提供mysql脚本其他数据库可以参考[转库文档](https://my.oschina.net/jeecg/blog/4905722)自己转。
@ -196,12 +201,12 @@ JeecgBoot平台提供了一套完善的AI应用管理系统模块是一套类
| 人大金仓 | √ |
| TiDB | √ |
| kingbase8 | √ |
## 微服务解决方案
- 1、服务注册和发现 Nacos √
- 2、统一配置中心 Nacos √
- 3、路由网关 gateway(三种加载方式) √
@ -219,23 +224,62 @@ JeecgBoot平台提供了一套完善的AI应用管理系统模块是一套类
- 15、CAS 单点登录 √
- 16、路由限流 √
#### 微服务方式启动
- [单体快速切换微服务](https://help.jeecg.com/java/springcloud/switchcloud/monomer)
- [Docker一键启动微服务前后端](https://help.jeecg.com/java/docker/quickcloud)
#### 微服务架构图
![微服务架构图](https://jeecgos.oss-cn-beijing.aliyuncs.com/files/jeecgboot_springcloud2022.png "在这里输入图片标题")
开源版与企业版区别?
为什么选择JeecgBoot?
-----------------------------------
- JeecgBoot开源版采用 [Apache-2.0 license](LICENSE) 协议附加补充条款:允许商用使用,不会造成侵权行为,允许基于本平台软件开展业务系统开发(但在任何情况下,您不得使用本软件开发可能被认为与本软件竞争的软件).
- 商业版与开源版主要区别在于商业版提供了技术支持 和 更多的企业级功能(例如Online图表、流程监控、流程设计、流程审批、表单设计器、表单视图、积木报表企业版、OA办公、商业APP、零代码应用、Online模块源码等功能). [更多商业功能介绍,点击查看](README-Enterprise.md)
- JeecgBoot未来发展方向是零代码平台的建设也就是团队的另外一款产品 [敲敲云零代码](https://www.qiaoqiaoyun.com) 无需编码即可通过拖拽快速搭建企业级应用与JeecgBoot低代码平台形成互补满足从简单业务到复杂系统的全场景开发需求目前已经开源[欢迎下载](https://qiaoqiaoyun.com/downloadCode)
* 1.采用最新主流前后分离框架Springboot+Mybatis+antd+vue3容易上手; 代码生成器依赖性低,灵活的扩展能力,可快速实现二次开发;
* 2.支持微服务SpringCloud Alibaba(Nacos、Gateway、Sentinel、Skywalking),提供切换机制支持单体和微服务自由切换
* 3.开发效率高,采用代码生成器单表、树列表、一对多、一对一等数据模型增删改查功能一键生成菜单配置直接使用引入AI能力支持自动建表等功能
* 4.代码生成器提供强大模板机制,支持自定义模板,目前提供四套风格模板(单表两套、树模型一套、一对多三套)
* 5.代码生成器非常智能在线业务建模、在线配置、所见即所得支持23种类控件一键生成前后端代码大幅度提升开发效率不再为重复工作发愁。
* 6.AI能力目前JeecgBoot支持AI大模型chatgpt和deepseek现在最新版默认使用deepseek速度更快质量更高。目前提供了AI对话助手、AI建表、AI报表等功能。
* 6.低代码能力Online在线表单无需编码通过在线配置表单实现表单的增删改查支持单表、树、一对多、一对一等模型实现人人皆可编码
* 7.低代码能力Online在线报表、Online在线图表无需编码通过在线配置方式实现数据报表和图形报表可以快速抽取数据减轻开发压力实现人人皆可编码
* 9.封装完善的用户、角色、菜单、组织机构、数据字典、在线定时任务等基础功能,支持访问授权、按钮权限、数据权限等功能
* 10.常用共通封装,各种工具类(定时任务,短信接口,邮件发送,Excel导入导出等),基本满足80%项目需求
* 11.简易Excel导入导出支持单表导出和一对多表模式导出生成的代码自带导入导出功能
* 12.集成简易报表工具图像报表和数据导出非常方便可极其方便的生成图形报表、pdf、excel、word等报表
* 13.采用前后分离技术页面UI风格精美针对常用组件做了封装时间、行表格控件、截取显示控件、报表组件编辑器等等
* 14.查询过滤器查询功能自动生成后台动态拼SQL追加查询条件支持多种匹配方式全匹配/模糊查询/包含查询/不匹配查询);
* 15.数据权限(精细化数据权限控制,控制到行级,列表级,表单字段级,实现不同人看不同数据,不同人对同一个页面操作不同字段
* 16.页面校验自动生成(必须输入、数字校验、金额校验、时间空间等);
* 17.支持SAAS服务模式提供SaaS多租户架构方案。
* 18.分布式文件服务集成minio、阿里OSS等优秀的第三方提供便捷的文件上传与管理同时也支持本地存储。
* 19.主流数据库兼容一套代码完全兼容Mysql、Postgresql、Oracle、Sqlserver、MariaDB、达梦、人大金仓等主流数据库。
* 20.集成工作流flowable并实现了只需在页面配置流程转向可极大的简化bpm工作流的开发用bpm的流程设计器画出了流程走向一个工作流基本就完成了只需写很少量的java代码
* 21.低代码能力在线流程设计采用开源flowable流程引擎实现在线画流程,自定义表单,表单挂靠,业务流转
* 22.多数据源:及其简易的使用方式,在线配置数据源配置,便捷的从其他数据抓取数据;
* 23.提供单点登录CAS集成方案项目中已经提供完善的对接代码
* 24.低代码能力表单设计器支持用户自定义表单布局支持单表一对多表单、支持select、radio、checkbox、textarea、date、popup、列表、宏等控件
* 25.专业接口对接机制统一采用restful接口方式集成swagger-ui在线接口文档Jwt token安全验证方便客户端对接
* 26.接口安全机制,可细化控制接口授权,非常简便实现不同客户端只看自己数据等控制
* 27.高级组合查询功能,在线配置支持主子表关联查询,可保存查询历史
* 28.提供各种系统监控,实时跟踪系统运行情况(监控 Redis、Tomcat、jvm、服务器信息、请求追踪、SQL监控
* 29.消息中心(支持短信、邮件、微信推送等等)
* 30.集成Websocket消息通知机制
* 31.移动自适应效果优秀提供APP发布方案
* 32.支持多语言,提供国际化方案;
* 33.数据变更记录日志,可记录数据每次变更内容,通过版本对比功能查看历史变化
* 34.平台UI强大实现了移动自适应
* 35.平台首页风格,提供多种组合模式,支持自定义风格
* 36.提供简单易用的打印插件支持谷歌、火狐、IE11+ 等各种浏览器
* 37.示例代码丰富,提供很多学习案例参考
* 38.采用maven分模块开发方式
* 39.支持菜单动态路由
* 40.权限控制采用 RBACRole-Based Access Control基于角色的访问控制
* 41.提供新行编辑表格JVXETable轻松满足各种复杂ERP布局拥有更高的性能、更灵活的扩展、更强大的功能
* 42.提供仪表盘设计器,类大屏设计支持移动端,免费的数据可视化设计工具,支持丰富的数据源连接,能够通过拖拉拽方式快速制作图表和门户设计;目前支持多种图表类型:柱形图、折线图、散点图、饼图、环形图、面积图、漏斗图、进度图、仪表盘、雷达图、地图等等;
### Jeecg Boot 产品功能蓝图
@ -243,19 +287,47 @@ JeecgBoot平台提供了一套完善的AI应用管理系统模块是一套类
#### 系统功能架构图
![](https://oscimg.oschina.net/oscnet/up-1569487b95a07dbc3599fb1349a2e3aaae1.png)
### 分支说明
> 主干master更稳定如果你对最新技术栈无要求建议采用主干
#### springboot3分支
- 源码地址https://github.com/jeecgboot/JeecgBoot/tree/springboot3
- 架构说明升级Spring Boot3 & JDK 17 + Undertow + springdoc + fastjson2
#### springboot3_sas分支
- 源码地址https://github.com/jeecgboot/JeecgBoot/tree/springboot3_sas
- 架构说明在springboot3分支基础上采用SpringAuthorizationServer替换Shiro
### 开源版功能清单
### 功能模块
```
├─AI开发
│ ├─支持AI大模型ChatGPT和DeepSeek
│ ├─AI对话助手
│ ├─AI建表
│ ├─AI写文章
│ ├─AI流程编排
│ ├─AI知识库问答系统
│ ├─AI应用开发平台
│ ├─AI聊天窗口支持嵌入第三方
├─Online在线开发(低代码)
│ ├─Online在线表单
│ ├─Online代码生成器
│ ├─Online在线报表
│ ├─仪表盘设计器
│ ├─系统编码规则
│ ├─系统校验规则
├─积木报表设计器
│ ├─打印设计器
│ ├─数据报表设计
│ ├─图形报表设计支持echart
├─系统管理
│ ├─用户管理
│ ├─角色管理
│ ├─菜单管理
│ ├─首页配置
│ ├─权限设置(支持按钮权限、数据权限)
│ ├─表单权限(控制字段禁用、隐藏)
│ ├─部门管理
@ -266,36 +338,7 @@ JeecgBoot平台提供了一套完善的AI应用管理系统模块是一套类
│ └─职务管理
│ └─通讯录
│ ├─多数据源管理
├─白名单管理
│ ├─第三方配置(对接钉钉和企业微信)
│ └─多租户管理(租户管理、租户角色、我的租户、租户默认套餐管理)
├─Online在线开发(低代码)
│ ├─Online在线表单
│ ├─Online代码生成器
│ ├─Online在线报表
│ ├─仪表盘设计器
│ ├─系统编码规则
│ ├─系统校验规则
│ ├─APP版本管理
├─AI应用平台
│ ├─AI知识库问答系统
│ ├─AI大模型管理
│ ├─AI流程编排
│ ├─AI流程设计器
│ ├─AI对话支持图片
│ ├─AI对话助手(智能问答)
│ ├─AI建表Online表单
│ ├─AI聊天窗口支持嵌入第三方
│ ├─AI聊天窗口支持移动端
│ ├─支持常见大模型ChatGPT和DeepSeek、ollama等等
│ ├─AI OCR示例
├─数据可视化
│ ├─报表设计器(支持打印设计)
│ ├─大屏设和仪表盘设计
├─OpenAPI基于AK和SK认证鉴权
│ ├─接口管理
│ ├─接口授权
│ ├─接口文档
└─多租户管理(租户管理、租户角色、我的租户)
├─消息中心
│ ├─消息管理
│ ├─模板管理
@ -307,12 +350,8 @@ JeecgBoot平台提供了一套完善的AI应用管理系统模块是一套类
│ ├─高级查询器(弹窗自动组合查询条件)
│ ├─Excel导入导出工具集成支持单表一对多 导入导出)
│ ├─平台移动自适应支持
│ ├─提供新版uniapp3的代码生成器模板
├─系统监控
│ ├─Gateway路由网关
│ ├─基于AK和SK认证鉴权OpenAPI功能
│ ├─定时任务
│ ├─数据源管理
│ ├─性能扫描监控
│ │ ├─监控 Redis
│ │ ├─Tomcat
@ -320,11 +359,13 @@ JeecgBoot平台提供了一套完善的AI应用管理系统模块是一套类
│ │ ├─服务器信息
│ │ ├─请求追踪
│ │ ├─磁盘监控
│ ├─定时任务
│ ├─系统日志
│ ├─消息中心(支持短信、邮件、微信推送等等)
│ ├─数据日志(记录数据快照,可对比快照,查看数据变更情况)
│ ├─系统通知
│ ├─SQL监控
│ ├─在线用户
│ ├─swagger-ui(在线接口文档)
│─报表示例
│ ├─曲线图
│ └─饼状图
@ -389,16 +430,46 @@ JeecgBoot平台提供了一套完善的AI应用管理系统模块是一套类
│ ├─提供单点登录CAS集成方案
│ ├─提供APP发布方案
│ ├─集成Websocket消息通知机制
├─支持electron桌面应用打包(支持windows、linux、macOS三大平台)
│ ├─docker容器支持
│ ├─提供移动APP框架及源码Uniapp3版本支持H5、小程序、APP、鸿蒙Next
│ ├─提供移动APP低代码设计(Online表单、仪表盘)
─更多商业功能
│ ├─流程设计器
│ ├─表单设计器
│ ├─大屏设计器
│ └─我的任务
│ └─历史流程
│ └─历史流程
│ └─流程实例管理
│ └─流程监听管理
│ └─流程表达式
│ └─我发起的流程
│ └─我的抄送
│ └─流程委派、抄送、跳转
│ └─OA办公组件
│ └─。。。
```
### 系统效果
##### AI功能
AI聊天助手
![](https://oscimg.oschina.net/oscnet//65298d5710b4e6039a5f802b5f8505c5.png)
AI建表
![](https://oscimg.oschina.net/oscnet/up-381423599f219a67def45dfd9a99df8ef3f.png)
![](https://oscimg.oschina.net/oscnet/up-1508c2b0708c365605f68893044ee11f20d.png)
AI写文章
![](https://oscimg.oschina.net/oscnet/up-e3ee5b1fe497308805aa5e324b72994af79.png)
##### PC端
![](https://oscimg.oschina.net/oscnet/up-000530d95df337b43089ac77e562494f454.png)
@ -418,22 +489,6 @@ JeecgBoot平台提供了一套完善的AI应用管理系统模块是一套类
![](https://oscimg.oschina.net/oscnet/up-16c07e000278329b69b228ae3189814b8e9.png)
##### AI功能
AI聊天助手
![](https://oscimg.oschina.net/oscnet//65298d5710b4e6039a5f802b5f8505c5.png)
AI建表
![](https://oscimg.oschina.net/oscnet/up-381423599f219a67def45dfd9a99df8ef3f.png)
![](https://oscimg.oschina.net/oscnet/up-1508c2b0708c365605f68893044ee11f20d.png)
AI写文章
![](https://oscimg.oschina.net/oscnet/up-e3ee5b1fe497308805aa5e324b72994af79.png)
##### 仪表盘设计器
@ -500,6 +555,28 @@ AI写文章
![](https://oscimg.oschina.net/oscnet/up-6b81781b43086819049c4421206810667c5.png)
##### 流程设计
![](https://oscimg.oschina.net/oscnet/up-981ce174e4fbb48c8a2ce4ccfd7372e2994.png)
![](https://oscimg.oschina.net/oscnet/up-1dc0d052149ec675f3e4fad632b82b48add.png)
![](https://oscimg.oschina.net/oscnet/up-de31bc2f9d9b8332c554b0954cc73d79593.png)
![输入图片说明](https://static.oschina.net/uploads/img/201907/05165142_yyQ7.png "在这里输入图片标题")
![输入图片说明](https://static.oschina.net/uploads/img/201904/14160917_9Ftz.png "在这里输入图片标题")
![输入图片说明](https://static.oschina.net/uploads/img/201904/14160633_u59G.png "在这里输入图片标题")
##### 表单设计器
![](https://oscimg.oschina.net/oscnet/up-5f8cb657615714b02190b355e59f60c5937.png)
![](https://oscimg.oschina.net/oscnet/up-d9659b2f324e33218476ec98c9b400e6508.png)
![](https://oscimg.oschina.net/oscnet/up-4868615395272d3206dbb960ade02dbc291.png)

View File

@ -1,216 +0,0 @@
import os
import subprocess
import re
import sys
from typing import Tuple, Optional
def run_command(cmd: str) -> Tuple[int, str]:
"""执行命令并返回退出码和输出"""
try:
result = subprocess.run(cmd, shell=True, check=False,
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
text=True)
return result.returncode, result.stdout.strip()
except Exception as e:
return -1, str(e)
def check_java() -> bool:
"""检查JDK 17+是否安装"""
print("\n检查JDK 17+...")
rc, output = run_command("java -version 2>&1")
if rc != 0:
print("❌ 未检测到Java请安装JDK 17+")
return False
version_pattern = r'"(\d+)(?:\.\d+)*(?:_\d+)?'
match = re.search(version_pattern, output)
if not match:
print("❌ 无法解析Java版本")
return False
version = int(match.group(1))
if version >= 17:
print(f"✅ JDK版本 {version} (满足17+要求)")
return True
else:
print(f"❌ JDK版本 {version} (需要17+)")
return False
def check_maven() -> bool:
"""检查Maven是否安装"""
print("\n检查Maven...")
rc, output = run_command("mvn -v")
if rc == 0:
print("✅ Maven已安装")
return True
else:
print("❌ Maven未安装")
return False
def check_node() -> bool:
"""检查Node.js 20+是否安装"""
print("\n检查Node.js 20+...")
rc, output = run_command("node -v")
if rc != 0:
print("❌ Node.js未安装")
return False
version_pattern = r'v(\d+)\.\d+\.\d+'
match = re.search(version_pattern, output)
if not match:
print("❌ 无法解析Node.js版本")
return False
version = int(match.group(1))
if version >= 20:
print(f"✅ Node.js版本 {version} (满足20+要求)")
return True
else:
print(f"❌ Node.js版本 {version} (需要20+)")
return False
def check_pnpm() -> bool:
"""检查PNPM 9+是否安装"""
print("\n检查PNPM 9+...")
rc, output = run_command("pnpm -v")
if rc != 0:
print("❌ PNPM未安装")
return False
try:
# 处理可能的版本号格式v9.0.0 或 9.0.0 或 9
version_str = output.strip().lstrip('v').split('.')[0]
version = int(version_str)
if version >= 9:
print(f"✅ PNPM版本 {output.strip()} (满足9+要求)")
return True
else:
print(f"❌ PNPM版本 {output.strip()} (需要9+)")
return False
except (ValueError, IndexError):
print(f"❌ 无法解析PNPM版本: {output.strip()}")
return False
def check_redis_connection() -> bool:
"""检查Redis连接"""
print("\n检查Redis连接...")
print("⚠️ 请确保已配置Redis连接信息并在jeecg-boot项目中正确配置")
print("⚠️ 此检查需要根据实际项目配置进行验证")
print("⚠️ 配置文件位置: jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/application-dev.yml")
return True
def check_mysql_connection() -> bool:
"""检查MySQL连接"""
print("\n检查MySQL连接...")
print("⚠️ 请确保已配置MySQL连接信息并在jeecg-boot项目中正确配置")
print("⚠️ 此检查需要根据实际项目配置进行验证")
print("⚠️ 配置文件位置: jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/application-dev.yml")
return True
def print_mysql_config():
"""打印MySQL配置并提示需要修改的位置"""
print("\nMySQL配置参考 (请检查以下配置是否正确):")
print("""
spring.datasource.dynamic.datasource:
master:
url: jdbc:mysql://127.0.0.1:3306/jeecg-boot?characterEncoding=UTF-8&useUnicode=true&useSSL=false&tinyInt1isBit=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai
username: root # ← 可能需要修改
password: root # ← 可能需要修改
driver-class-name: com.mysql.cj.jdbc.Driver
""")
def check_ai_vector_db() -> bool:
"""检查AI向量库(pgvector)配置"""
print("\n检查AI知识库向量库配置...")
print("⚠️ 如果需要使用AI知识库功能请配置pgvector向量库")
print("⚠️ 配置文件位置: jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/application-dev.yml")
print("\n配置参考:")
print("""
jeecg.ai-rag:
embed-store:
host: 127.0.0.1 # ← 可能需要修改
port: 5432 # ← 可能需要修改
database: postgres # ← 可能需要修改
user: postgres # ← 可能需要修改
password: postgres # ← 可能需要修改
table: embeddings # ← 可能需要修改
""")
print("⚠️ 注意: 请确保已安装PostgreSQL并添加pgvector扩展docker安装参考https://help.jeecg.com/aigc/config")
return True
def check_ai_config() -> bool:
"""检查AI账号配置"""
print("\n检查AI功能配置...")
print("⚠️ 如果需要使用AI聊天功能请配置AI账号信息")
print("⚠️ 配置文件位置: jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/application-dev.yml")
print("\n配置参考:")
print("""
jeecg:
# AI集成
ai-chat:
enabled: true # ← 启用AI功能
model: deepseek-chat # ← 模型名称
apiKey: ?? # ← 必须修改为您的API Key
apiHost: https://api.deepseek.com/v1 # ← API地址
timeout: 60 # ← 超时时间(秒)
""")
print("⚠️ 注意: 请确保已获取有效的API Key并正确配置AI账号注册获取参考 https://help.jeecg.com/java/deepSeekSupport")
return True
def print_redis_config():
"""打印Redis配置并提示需要修改的位置"""
print("\nRedis配置参考 (请检查以下配置是否正确):")
print("""
spring.redis:
database: 0
host: 127.0.0.1 # ← 可能需要修改
port: 6379 # ← 可能需要修改
password: '' # ← 如果需要密码请修改
""")
def main():
print("="*50)
print("JeecgBoot 运行环境检查脚本")
print("="*50)
all_checks_passed = True
# 检查各项依赖
if not check_java():
all_checks_passed = False
if not check_maven():
all_checks_passed = False
if not check_node():
all_checks_passed = False
if not check_pnpm():
all_checks_passed = False
# 数据库提示
print("="*50)
check_redis_connection()
print_redis_config()
print("="*50)
check_mysql_connection()
print_mysql_config()
print("="*50)
check_ai_config()
print("="*50)
check_ai_vector_db()
print("\n" + "="*50)
if all_checks_passed:
print("✅ 所有基础环境检查通过")
print("⚠️ 注意: 请确保Redis和MySQL、AI账号、向量库pgvector 已正确配置并连接成功")
else:
print("❌ 部分环境检查未通过,请根据上述提示解决问题")
print("="*50)
if __name__ == "__main__":
main()
input("\n按回车键退出...") # 等待用户输入

View File

@ -18,33 +18,20 @@ services:
--max_allowed_packet=128M
--default-authentication-plugin=caching_sha2_password
ports:
- 13306:3306
- 3306:3306
networks:
- jeecg-boot
jeecg-boot-redis:
image: registry.cn-hangzhou.aliyuncs.com/jeecgdocker/redis:5.0
# ports:
# - 6379:6379
ports:
- 6379:6379
restart: always
hostname: jeecg-boot-redis
container_name: jeecg-boot-redis
networks:
- jeecg-boot
jeecg-boot-pgvector:
image: registry.cn-hangzhou.aliyuncs.com/jeecgdocker/pgvector
container_name: jeecg-boot-pgvector
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: vector_db
ports:
- 5432:5432
restart: always
networks:
- jeecg-boot
jeecg-boot-nacos:
restart: always
build:
@ -134,7 +121,6 @@ services:
jeecg-vue:
build:
context: ./jeecgboot-vue3
dockerfile: Dockerfile.cloud
container_name: jeecgboot-vue3-nginx
image: jeecgboot-vue3
depends_on:

View File

@ -18,33 +18,20 @@ services:
--max_allowed_packet=128M
--default-authentication-plugin=caching_sha2_password
ports:
- 13306:3306
- 3306:3306
networks:
- jeecg-boot
jeecg-boot-redis:
image: registry.cn-hangzhou.aliyuncs.com/jeecgdocker/redis:5.0
# ports:
# - 3792:6379
ports:
- 6379:6379
restart: always
hostname: jeecg-boot-redis
container_name: jeecg-boot-redis
networks:
- jeecg-boot
jeecg-boot-pgvector:
image: registry.cn-hangzhou.aliyuncs.com/jeecgdocker/pgvector
container_name: jeecg-boot-pgvector
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: vector_db
ports:
- 5432:5432
restart: always
networks:
- jeecg-boot
jeecg-boot-system:
build:
context: ./jeecg-boot/jeecg-module-system/jeecg-system-start

View File

@ -13,3 +13,4 @@ os_del.cmd
os_del_doc.cmd
.svn
derby.log
*.log

View File

@ -2,12 +2,12 @@
JeecgBoot 低代码开发平台
===============
当前最新版本: 3.8.2发布日期2025-08-04
当前最新版本: 3.8.0发布日期2025-05-16
[![AUR](https://img.shields.io/badge/license-Apache%20License%202.0-blue.svg)](https://github.com/zhangdaiscott/jeecg-boot/blob/master/LICENSE)
[![](https://img.shields.io/badge/Author-北京国炬软件-orange.svg)](http://jeecg.com/aboutusIndex)
[![](https://img.shields.io/badge/version-3.8.2-brightgreen.svg)](https://github.com/zhangdaiscott/jeecg-boot)
[![](https://img.shields.io/badge/version-3.8.0-brightgreen.svg)](https://github.com/zhangdaiscott/jeecg-boot)
[![GitHub stars](https://img.shields.io/github/stars/zhangdaiscott/jeecg-boot.svg?style=social&label=Stars)](https://github.com/zhangdaiscott/jeecg-boot)
[![GitHub forks](https://img.shields.io/github/forks/zhangdaiscott/jeecg-boot.svg?style=social&label=Fork)](https://github.com/zhangdaiscott/jeecg-boot)
@ -35,7 +35,7 @@ JeecgBoot 是一款基于代码生成器的`低代码开发平台`!前后端
- 官方网站: [http://www.jeecg.com](http://www.jeecg.com)
- 新手指南: [快速入门](http://www.jeecg.com/doc/quickstart)
- QQ交流群 964611995、⑩716488839(满)、⑨808791225(满)、其他(满)
- QQ交流群 ⑩716488839、⑨808791225、其他(满)
- 在线演示 [在线演示](http://boot3.jeecg.com) | [APP演示](http://jeecg.com/appIndex)
> 演示系统的登录账号密码,请点击 [获取账号密码](http://jeecg.com/doc/demo) 获取

File diff suppressed because one or more lines are too long

View File

@ -1,6 +1,6 @@
CREATE database if NOT EXISTS `nacos` default character set utf8mb4 collate utf8mb4_general_ci;
use `nacos`;
/*
Navicat Premium Data Transfer
@ -14,7 +14,7 @@ use `nacos`;
Target Server Version : 50738 (5.7.38)
File Encoding : 65001
Date: 22/07/2025 10:02:59
Date: 22/05/2025 10:58:10
*/
SET NAMES utf8mb4;
@ -49,8 +49,8 @@ CREATE TABLE `config_info` (
-- ----------------------------
-- Records of config_info
-- ----------------------------
INSERT INTO `config_info` VALUES (1, 'jeecg-dev.yaml', 'DEFAULT_GROUP', 'spring:\n datasource:\n druid:\n stat-view-servlet:\n enabled: true\n loginUsername: admin\n loginPassword: 123456\n allow:\n web-stat-filter:\n enabled: true\n dynamic:\n druid:\n initial-size: 5\n min-idle: 5\n maxActive: 20\n maxWait: 60000\n timeBetweenEvictionRunsMillis: 60000\n minEvictableIdleTimeMillis: 300000\n validationQuery: SELECT 1 FROM DUAL\n testWhileIdle: true\n testOnBorrow: false\n testOnReturn: false\n poolPreparedStatements: true\n maxPoolPreparedStatementPerConnectionSize: 20\n filters: stat,wall,slf4j\n wall:\n selectWhereAlwayTrueCheck: false\n stat:\n merge-sql: true\n slow-sql-millis: 5000\n datasource:\n master:\n url: jdbc:mysql://jeecg-boot-mysql:3306/jeecg-boot?characterEncoding=UTF-8&useUnicode=true&useSSL=false&tinyInt1isBit=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai\n username: root\n password: root\n driver-class-name: com.mysql.cj.jdbc.Driver\n redis:\n database: 0\n host: jeecg-boot-redis\n password:\n port: 6379\n rabbitmq:\n host: jeecg-boot-rabbitmq\n username: guest\n password: guest\n port: 5672\n publisher-confirms: true\n publisher-returns: true\n virtual-host: /\n listener:\n simple:\n acknowledge-mode: manual\n concurrency: 1\n max-concurrency: 1\n retry:\n enabled: true\n flyway:\n enabled: false\n locations: classpath:flyway/sql/mysql\nminidao:\n base-package: org.jeecg.modules.jmreport.*,org.jeecg.modules.drag.*\njeecg:\n firewall:\n dataSourceSafe: false\n lowCodeMode: dev\n signatureSecret: dd05f1c54d63749eda95f9fa6d49v442a\n signUrls: /sys/dict/getDictItems/*,/sys/dict/loadDict/*,/sys/dict/loadDictOrderByValue/*,/sys/dict/loadDictItem/*,/sys/dict/loadTreeData,/sys/api/queryTableDictItemsByCode,/sys/api/queryFilterTableDictInfo,/sys/api/queryTableDictByKeys,/sys/api/translateDictFromTable,/sys/api/translateDictFromTableByKeys,/sys/sendChangePwdSms,/sys/user/sendChangePhoneSms,/sys/sms,/desform/api/sendVerifyCode\n uploadType: local\n domainUrl:\n pc: http://localhost:3100\n app: http://localhost:8051\n path:\n upload: /opt/upFiles\n webapp: /opt/webapp\n shiro:\n excludeUrls: /test/jeecgDemo/demo3,/test/jeecgDemo/redisDemo/**,/category/**,/visual/**,/map/**,/jmreport/bigscreen2/**\n oss:\n endpoint: oss-cn-beijing.aliyuncs.com\n accessKey: ??\n secretKey: ??\n bucketName: jeecgdev\n staticDomain: ??\n file-view-domain: 127.0.0.1:8012\n minio:\n minio_url: http://minio.jeecg.com\n minio_name: ??\n minio_pass: ??\n bucketName: otatest\n jmreport:\n saasMode:\n firewall:\n dataSourceSafe: false\n lowCodeMode: dev\n wps:\n domain: https://wwo.wps.cn/office/\n appid: ??\n appsecret: ??\n xxljob:\n enabled: true\n adminAddresses: http://jeecg-boot-xxljob:9080/xxl-job-admin\n appname: ${spring.application.name}\n accessToken: \'\'\n logPath: logs/jeecg/job/jobhandler/\n logRetentionDays: 30\n redisson:\n address: jeecg-boot-redis:6379\n password:\n type: STANDALONE\n enabled: true\n ai-chat:\n enabled: false\n apiKey: \"\"\n apiHost: \"https://api.openai.com\"\n timeout: 60\n ai-rag:\n embed-store:\n host: 127.0.0.1\n port: 5432\n database: postgres\n user: postgres\n password: postgres\n table: embeddings\nlogging:\n level:\n org.jeecg.modules.system.mapper : info\ncas:\n prefixUrl: http://localhost:8888/cas\nknife4j:\n production: false\n basic:\n enable: false\n username: jeecg\n password: jeecg1314\njustauth:\n enabled: true\n type:\n GITHUB:\n client-id: ??\n client-secret: ??\n redirect-uri: http://sso.test.com:8080/jeecg-boot/thirdLogin/github/callback\n WECHAT_ENTERPRISE:\n client-id: ??\n client-secret: ??\n redirect-uri: http://sso.test.com:8080/jeecg-boot/thirdLogin/wechat_enterprise/callback\n agent-id: ??\n DINGTALK:\n client-id: ??\n client-secret: ??\n redirect-uri: http://sso.test.com:8080/jeecg-boot/thirdLogin/dingtalk/callback\n cache:\n type: default\n prefix: \'demo::\'\n timeout: 1h\nthird-app:\n enabled: false\n type:\n WECHAT_ENTERPRISE:\n enabled: false\n client-id: ??\n client-secret: ??\n agent-id: ??\n DINGTALK:\n enabled: false\n client-id: ??\n client-secret: ??\n agent-id: ??', '68112d529219e88a44245402ccf54676', '2021-03-03 13:01:11', '2025-05-28 07:47:53', NULL, '0:0:0:0:0:0:0:1', '', '', '', '', '', 'yaml', '', '');
INSERT INTO `config_info` VALUES (2, 'jeecg.yaml', 'DEFAULT_GROUP', 'server:\n undertow:\n decode-url: true\n worker-threads: 16\n buffers:\n websocket: 8192\n io: 16384\n error:\n include-exception: true\n include-stacktrace: ALWAYS\n include-message: ALWAYS\n compression:\n enabled: true\n min-response-size: 1024\n mime-types: application/javascript,application/json,application/xml,text/html,text/xml,text/plain,text/css,image/*\nmanagement:\n health:\n mail:\n enabled: false\n endpoints:\n web:\n exposure:\n include: \"*\"\n health:\n sensitive: true\n endpoint:\n health:\n show-details: ALWAYS\nspring:\n servlet:\n multipart:\n max-file-size: 10MB\n max-request-size: 10MB\n mail:\n host: smtp.163.com\n username: jeecgos@163.com\n password: ??\n properties:\n mail:\n smtp:\n auth: true\n starttls:\n enable: true\n required: true\n quartz:\n job-store-type: jdbc\n initialize-schema: embedded\n auto-startup: false\n startup-delay: 1s\n overwrite-existing-jobs: true\n properties:\n org:\n quartz:\n scheduler:\n instanceName: MyScheduler\n instanceId: AUTO\n jobStore:\n class: org.springframework.scheduling.quartz.LocalDataSourceJobStore\n driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate\n tablePrefix: QRTZ_\n isClustered: true\n misfireThreshold: 12000\n clusterCheckinInterval: 15000\n threadPool:\n class: org.quartz.simpl.SimpleThreadPool\n threadCount: 10\n threadPriority: 5\n threadsInheritContextClassLoaderOfInitializingThread: true\n jackson:\n date-format: yyyy-MM-dd HH:mm:ss\n time-zone: GMT+8\n aop:\n proxy-target-class: true\n activiti:\n check-process-definitions: false\n async-executor-activate: false\n job-executor-activate: false\n jpa:\n open-in-view: false\n freemarker:\n suffix: .ftl\n content-type: text/html\n charset: UTF-8\n cache: false\n prefer-file-system-access: false\n template-loader-path:\n - classpath:/templates\n mvc:\n static-path-pattern: /**\n pathmatch:\n matching-strategy: ant_path_matcher\n resource:\n static-locations: classpath:/static/,classpath:/public/\n autoconfigure:\n exclude:\n - com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure\n - org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration\nmybatis-plus:\n mapper-locations: classpath*:org/jeecg/**/xml/*Mapper.xml\n global-config:\n banner: false\n db-config:\n id-type: ASSIGN_ID\n table-underline: true\n configuration:\n call-setters-on-nulls: true', '70bdbfdeb2651673aea0d06ed40b4ac1', '2021-03-03 13:01:42', '2025-07-22 02:00:34', NULL, '0:0:0:0:0:0:0:1', '', '', '', '', '', 'yaml', '', '');
INSERT INTO `config_info` VALUES (1, 'jeecg-dev.yaml', 'DEFAULT_GROUP', 'spring:\n datasource:\n druid:\n stat-view-servlet:\n enabled: true\n loginUsername: admin\n loginPassword: 123456\n allow:\n web-stat-filter:\n enabled: true\n dynamic:\n druid:\n initial-size: 5\n min-idle: 5\n maxActive: 20\n maxWait: 60000\n timeBetweenEvictionRunsMillis: 60000\n minEvictableIdleTimeMillis: 300000\n validationQuery: SELECT 1 FROM DUAL\n testWhileIdle: true\n testOnBorrow: false\n testOnReturn: false\n poolPreparedStatements: true\n maxPoolPreparedStatementPerConnectionSize: 20\n filters: stat,wall,slf4j\n wall:\n selectWhereAlwayTrueCheck: false\n stat:\n merge-sql: true\n slow-sql-millis: 5000\n datasource:\n master:\n url: jdbc:mysql://jeecg-boot-mysql:3306/jeecg-boot?characterEncoding=UTF-8&useUnicode=true&useSSL=false&tinyInt1isBit=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai\n username: root\n password: root\n driver-class-name: com.mysql.cj.jdbc.Driver\n redis:\n database: 0\n host: jeecg-boot-redis\n password:\n port: 6379\n rabbitmq:\n host: jeecg-boot-rabbitmq\n username: guest\n password: guest\n port: 5672\n publisher-confirms: true\n publisher-returns: true\n virtual-host: /\n listener:\n simple:\n acknowledge-mode: manual\n concurrency: 1\n max-concurrency: 1\n retry:\n enabled: true\n flyway:\n enabled: false\n locations: classpath:flyway/sql/mysql\nminidao:\n base-package: org.jeecg.modules.jmreport.*,org.jeecg.modules.drag.*\njeecg:\n firewall:\n dataSourceSafe: false\n lowCodeMode: dev\n signatureSecret: dd05f1c54d63749eda95f9fa6d49v442a\n signUrls: /sys/dict/getDictItems/*,/sys/dict/loadDict/*,/sys/dict/loadDictOrderByValue/*,/sys/dict/loadDictItem/*,/sys/dict/loadTreeData,/sys/api/queryTableDictItemsByCode,/sys/api/queryFilterTableDictInfo,/sys/api/queryTableDictByKeys,/sys/api/translateDictFromTable,/sys/api/translateDictFromTableByKeys,/sys/sendChangePwdSms,/sys/user/sendChangePhoneSms,/sys/sms,/desform/api/sendVerifyCode\n uploadType: local\n domainUrl:\n pc: http://localhost:3100\n app: http://localhost:8051\n path:\n upload: /opt/upFiles\n webapp: /opt/webapp\n shiro:\n excludeUrls: /test/jeecgDemo/demo3,/test/jeecgDemo/redisDemo/**,/category/**,/visual/**,/map/**,/jmreport/bigscreen2/**\n oss:\n endpoint: oss-cn-beijing.aliyuncs.com\n accessKey: ??\n secretKey: ??\n bucketName: jeecgdev\n staticDomain: ??\n minio:\n minio_url: http://minio.jeecg.com\n minio_name: ??\n minio_pass: ??\n bucketName: otatest\n jmreport:\n saasMode:\n firewall:\n dataSourceSafe: false\n lowCodeMode: dev\n wps:\n domain: https://wwo.wps.cn/office/\n appid: ??\n appsecret: ??\n xxljob:\n enabled: true\n adminAddresses: http://jeecg-boot-xxljob:9080/xxl-job-admin\n appname: ${spring.application.name}\n accessToken: \'\'\n logPath: logs/jeecg/job/jobhandler/\n logRetentionDays: 30\n redisson:\n address: jeecg-boot-redis:6379\n password:\n type: STANDALONE\n enabled: true\n ai-chat:\n enabled: false\n apiKey: \"\"\n apiHost: \"https://api.openai.com\"\n timeout: 60\n ai-rag:\n embed-store:\n host: 127.0.0.1\n port: 5432\n database: postgres\n user: postgres\n password: postgres\n table: embeddings\nlogging:\n level:\n org.jeecg.modules.system.mapper : info\ncas:\n prefixUrl: http://localhost:8888/cas\nknife4j:\n production: false\n basic:\n enable: false\n username: jeecg\n password: jeecg1314\njustauth:\n enabled: true\n type:\n GITHUB:\n client-id: ??\n client-secret: ??\n redirect-uri: http://sso.test.com:8080/jeecg-boot/thirdLogin/github/callback\n WECHAT_ENTERPRISE:\n client-id: ??\n client-secret: ??\n redirect-uri: http://sso.test.com:8080/jeecg-boot/thirdLogin/wechat_enterprise/callback\n agent-id: ??\n DINGTALK:\n client-id: ??\n client-secret: ??\n redirect-uri: http://sso.test.com:8080/jeecg-boot/thirdLogin/dingtalk/callback\n cache:\n type: default\n prefix: \'demo::\'\n timeout: 1h\nthird-app:\n enabled: false\n type:\n WECHAT_ENTERPRISE:\n enabled: false\n client-id: ??\n client-secret: ??\n agent-id: ??\n DINGTALK:\n enabled: false\n client-id: ??\n client-secret: ??\n agent-id: ??', 'f1d8102a50c7b1f59458e8f9a0112012', '2021-03-03 13:01:11', '2025-05-19 12:08:56', NULL, '0:0:0:0:0:0:0:1', '', '', '', '', '', 'yaml', '', '');
INSERT INTO `config_info` VALUES (2, 'jeecg.yaml', 'DEFAULT_GROUP', 'server:\n undertow:\n worker-threads: 16\n buffers:\n websocket: 8192\n io: 16384\n error:\n include-exception: true\n include-stacktrace: ALWAYS\n include-message: ALWAYS\n compression:\n enabled: true\n min-response-size: 1024\n mime-types: application/javascript,application/json,application/xml,text/html,text/xml,text/plain,text/css,image/*\nmanagement:\n health:\n mail:\n enabled: false\n endpoints:\n web:\n exposure:\n include: \"*\"\n health:\n sensitive: true\n endpoint:\n health:\n show-details: ALWAYS\nspring:\n servlet:\n multipart:\n max-file-size: 10MB\n max-request-size: 10MB\n mail:\n host: smtp.163.com\n username: jeecgos@163.com\n password: ??\n properties:\n mail:\n smtp:\n auth: true\n starttls:\n enable: true\n required: true\n quartz:\n job-store-type: jdbc\n initialize-schema: embedded\n auto-startup: false\n startup-delay: 1s\n overwrite-existing-jobs: true\n properties:\n org:\n quartz:\n scheduler:\n instanceName: MyScheduler\n instanceId: AUTO\n jobStore:\n class: org.springframework.scheduling.quartz.LocalDataSourceJobStore\n driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate\n tablePrefix: QRTZ_\n isClustered: true\n misfireThreshold: 12000\n clusterCheckinInterval: 15000\n threadPool:\n class: org.quartz.simpl.SimpleThreadPool\n threadCount: 10\n threadPriority: 5\n threadsInheritContextClassLoaderOfInitializingThread: true\n jackson:\n date-format: yyyy-MM-dd HH:mm:ss\n time-zone: GMT+8\n aop:\n proxy-target-class: true\n activiti:\n check-process-definitions: false\n async-executor-activate: false\n job-executor-activate: false\n jpa:\n open-in-view: false\n freemarker:\n suffix: .ftl\n content-type: text/html\n charset: UTF-8\n cache: false\n prefer-file-system-access: false\n template-loader-path:\n - classpath:/templates\n mvc:\n static-path-pattern: /**\n pathmatch:\n matching-strategy: ant_path_matcher\n resource:\n static-locations: classpath:/static/,classpath:/public/\n autoconfigure:\n exclude:\n - com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure\n - org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration\nmybatis-plus:\n mapper-locations: classpath*:org/jeecg/**/xml/*Mapper.xml\n global-config:\n banner: false\n db-config:\n id-type: ASSIGN_ID\n table-underline: true\n configuration:\n call-setters-on-nulls: true', '20596e678c211d4322ead0000c0ffdbc', '2021-03-03 13:01:42', '2025-05-19 09:51:57', NULL, '0:0:0:0:0:0:0:1', '', '', '', '', '', 'yaml', '', '');
INSERT INTO `config_info` VALUES (3, 'jeecg-gateway-router.json', 'DEFAULT_GROUP', '[{\n \"id\": \"jeecg-system\",\n \"order\": 0,\n \"predicates\": [{\n \"name\": \"Path\",\n \"args\": {\n \"_genkey_0\": \"/sys/**\",\n \"_genkey_1\": \"/jmreport/**\",\n \"_genkey_3\": \"/online/**\",\n \"_genkey_4\": \"/generic/**\",\n \"_genkey_5\": \"/drag/**\",\n \"_genkey_6\": \"/actuator/**\",\n \"_genkey_7\": \"/airag/**\",\n \"_genkey_8\": \"/jimubi/**\",\n \"_genkey_9\": \"/openapi/**\"\n }\n }],\n \"filters\": [],\n \"uri\": \"lb://jeecg-system\"\n}, {\n \"id\": \"jeecg-demo\",\n \"order\": 1,\n \"predicates\": [{\n \"name\": \"Path\",\n \"args\": {\n \"_genkey_0\": \"/mock/**\",\n \"_genkey_1\": \"/test/**\",\n \"_genkey_2\": \"/bigscreen/template1/**\",\n \"_genkey_3\": \"/bigscreen/template2/**\"\n }\n }],\n \"filters\": [],\n \"uri\": \"lb://jeecg-demo\"\n}, {\n \"id\": \"jeecg-system-websocket\",\n \"order\": 2,\n \"predicates\": [{\n \"name\": \"Path\",\n \"args\": {\n \"_genkey_0\": \"/websocket/**\",\n \"_genkey_1\": \"/newsWebsocket/**\"\n }\n }],\n \"filters\": [],\n \"uri\": \"lb:ws://jeecg-system\"\n}, {\n \"id\": \"jeecg-demo-websocket\",\n \"order\": 3,\n \"predicates\": [{\n \"name\": \"Path\",\n \"args\": {\n \"_genkey_0\": \"/vxeSocket/**\"\n }\n }],\n \"filters\": [],\n \"uri\": \"lb:ws://jeecg-demo\"\n}]', '856da7f7ff7931c6b1932e89d87b83ba', '2021-03-03 13:02:14', '2025-05-21 05:41:09', NULL, '0:0:0:0:0:0:0:1', '', '', '', '', '', 'json', '', '');
INSERT INTO `config_info` VALUES (11, 'jeecg-sharding.yaml', 'DEFAULT_GROUP', 'spring:\n shardingsphere:\n datasource:\n names: ds0\n ds0:\n driverClassName: com.mysql.cj.jdbc.Driver\n url: jdbc:mysql://jeecg-boot-mysql:3306/jeecg-boot?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai\n username: root\n password: root\n type: com.alibaba.druid.pool.DruidDataSource\n props:\n sql-show: true\n rules:\n sharding:\n binding-tables: sys_log\n key-generators:\n snowflake:\n type: SNOWFLAKE\n props:\n worker-id: 123\n sharding-algorithms:\n table-classbased:\n props:\n strategy: standard\n algorithmClassName: org.jeecg.modules.test.sharding.algorithm.StandardModTableShardAlgorithm\n type: CLASS_BASED\n tables:\n sys_log:\n actual-data-nodes: ds0.sys_log$->{0..1}\n table-strategy:\n standard:\n sharding-algorithm-name: table-classbased\n sharding-column: log_type', 'a93fa455c32cd37ca84631d2bbe13005', '2022-04-13 03:12:28', '2022-08-07 13:13:57', 'nacos', '0:0:0:0:0:0:0:1', '', '', '', '', '', 'yaml', '', NULL);
INSERT INTO `config_info` VALUES (20, 'jeecg-gateway-dev.yaml', 'DEFAULT_GROUP', 'jeecg:\n route:\n config:\n #type:database nacos yml\n data-type: database\n data-id: jeecg-gateway-router\nspring:\n redis:\n database: 0\n host: jeecg-boot-redis\n port: 6379\n password:\nknife4j:\n production: false', '98e211c54b43a73f7189d92f1c77f815', '2022-08-04 16:36:11', '2024-06-11 16:13:15', NULL, '0:0:0:0:0:0:0:1', '', '', '', '', '', 'yaml', '', '');
@ -68,7 +68,7 @@ INSERT INTO `config_info` VALUES (40, 'jeecg-sharding.yaml', 'DEFAULT_GROUP', 's
INSERT INTO `config_info` VALUES (41, 'jeecg-gateway-dev.yaml', 'DEFAULT_GROUP', 'jeecg:\n route:\n config:\n #type:database nacos yml\n data-type: database\n data-id: jeecg-gateway-router\nspring:\n data:\n redis:\n database: 0\n host: jeecg-boot-redis\n port: 6379\n password:\nknife4j:\n production: false', '19d7cd93eeb85a582c8a6942d499c7f7', '2024-02-29 09:10:12', '2024-06-11 16:26:12', NULL, '0:0:0:0:0:0:0:1', '', 'efc4e412-b1a1-498f-ba01-b31807649a9a', '', '', '', 'yaml', '', '');
INSERT INTO `config_info` VALUES (42, 'jeecg-sharding-multi.yaml', 'DEFAULT_GROUP', 'spring:\n shardingsphere:\n datasource:\n names: ds0,ds1\n ds0:\n driverClassName: com.mysql.cj.jdbc.Driver\n url: jdbc:mysql://jeecg-boot-mysql:3306/jeecg-boot?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai\n type: com.alibaba.druid.pool.DruidDataSource\n username: root\n password: root\n ds1:\n driverClassName: com.mysql.cj.jdbc.Driver\n url: jdbc:mysql://jeecg-boot-mysql:3306/jeecg-boot2?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai\n type: com.alibaba.druid.pool.DruidDataSource\n username: root\n password: root\n props:\n sql-show: true\n rules:\n replica-query:\n load-balancers:\n round-robin:\n type: ROUND_ROBIN\n props:\n default: 0\n data-sources:\n prds:\n primary-data-source-name: ds0\n replica-data-source-names: ds1\n load-balancer-name: round_robin\n sharding:\n binding-tables:\n - sys_log\n key-generators:\n snowflake:\n type: SNOWFLAKE\n props:\n worker-id: 123\n sharding-algorithms:\n table-classbased:\n props:\n strategy: standard\n algorithmClassName: org.jeecg.modules.test.sharding.algorithm.StandardModTableShardAlgorithm\n type: CLASS_BASED\n database-inline:\n type: INLINE\n props:\n algorithm-expression: ds$->{operate_type % 2}\n tables:\n sys_log:\n actual-data-nodes: ds$->{0..1}.sys_log$->{0..1}\n database-strategy:\n standard:\n sharding-column: operate_type\n sharding-algorithm-name: database-inline\n table-strategy:\n standard:\n sharding-algorithm-name: table-classbased\n sharding-column: log_type', '0fc2b030ca8c0008f148c84ecbd2a8c7', '2024-02-29 09:10:12', '2024-02-29 09:10:12', NULL, '0:0:0:0:0:0:0:1', '', 'efc4e412-b1a1-498f-ba01-b31807649a9a', '', NULL, NULL, 'yaml', NULL, '');
INSERT INTO `config_info` VALUES (43, 'jeecg-dev.yaml', 'DEFAULT_GROUP', 'spring:\n datasource:\n druid:\n stat-view-servlet:\n enabled: true\n loginUsername: admin\n loginPassword: 123456\n allow:\n web-stat-filter:\n enabled: true\n dynamic:\n druid:\n initial-size: 5\n min-idle: 5\n maxActive: 20\n maxWait: 60000\n timeBetweenEvictionRunsMillis: 60000\n minEvictableIdleTimeMillis: 300000\n validationQuery: SELECT 1 FROM DUAL\n testWhileIdle: true\n testOnBorrow: false\n testOnReturn: false\n poolPreparedStatements: true\n maxPoolPreparedStatementPerConnectionSize: 20\n filters: stat,wall,slf4j\n wall:\n selectWhereAlwayTrueCheck: false\n stat:\n merge-sql: true\n slow-sql-millis: 5000\n datasource:\n master:\n url: jdbc:mysql://jeecg-boot-mysql:3306/jeecg-boot?characterEncoding=UTF-8&useUnicode=true&useSSL=false&tinyInt1isBit=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai\n username: root\n password: root\n driver-class-name: com.mysql.cj.jdbc.Driver\n data:\n redis:\n database: 0\n host: jeecg-boot-redis\n password:\n port: 6379\n rabbitmq:\n host: jeecg-boot-rabbitmq\n username: guest\n password: guest\n port: 5672\n publisher-confirms: true\n publisher-returns: true\n virtual-host: /\n listener:\n simple:\n acknowledge-mode: manual\n concurrency: 1\n max-concurrency: 1\n retry:\n enabled: true\n flyway:\n enabled: false\n locations: classpath:flyway/sql/mysql\n clean-disabled: true\nminidao:\n base-package: org.jeecg.modules.jmreport.*,org.jeecg.modules.drag.*\njeecg:\n firewall:\n dataSourceSafe: false\n lowCodeMode: dev\n signatureSecret: dd05f1c54d63749eda95f9fa6d49v442a\n signUrls: /sys/dict/getDictItems/*,/sys/dict/loadDict/*,/sys/dict/loadDictOrderByValue/*,/sys/dict/loadDictItem/*,/sys/dict/loadTreeData,/sys/api/queryTableDictItemsByCode,/sys/api/queryFilterTableDictInfo,/sys/api/queryTableDictByKeys,/sys/api/translateDictFromTable,/sys/api/translateDictFromTableByKeys\n uploadType: local\n domainUrl:\n pc: http://localhost:3100\n app: http://localhost:8051\n path:\n upload: /opt/upFiles\n webapp: /opt/webapp\n shiro:\n excludeUrls: /test/jeecgDemo/demo3,/test/jeecgDemo/redisDemo/**,/category/**,/visual/**,/map/**,/jmreport/bigscreen2/**\n oss:\n endpoint: oss-cn-beijing.aliyuncs.com\n accessKey: ??\n secretKey: ??\n bucketName: jeecgdev\n staticDomain: ?? \n file-view-domain: 127.0.0.1:8012\n minio:\n minio_url: http://minio.jeecg.com\n minio_name: ??\n minio_pass: ??\n bucketName: otatest\n jmreport:\n saasMode:\n firewall:\n dataSourceSafe: false\n lowCodeMode: dev\n wps:\n domain: https://wwo.wps.cn/office/\n appid: ??\n appsecret: ??\n xxljob:\n enabled: false\n adminAddresses: http://jeecg-boot-xxljob:9080/xxl-job-admin\n appname: ${spring.application.name}\n accessToken: \'\'\n logPath: logs/jeecg/job/jobhandler/\n logRetentionDays: 30\n redisson:\n address: jeecg-boot-redis:6379\n password:\n type: STANDALONE\n enabled: true\n ai-chat:\n enabled: false\n apiKey: \"\"\n apiHost: \"https://api.openai.com\"\n timeout: 60\n ai-rag:\n embed-store:\n host: 127.0.0.1\n port: 5432\n database: postgres\n user: postgres\n password: postgres\n table: embeddings\nlogging:\n level:\n org.jeecg.modules.system.mapper : info\ncas:\n prefixUrl: http://localhost:8888/cas\nknife4j:\n production: false\n basic:\n enable: false\n username: jeecg\n password: jeecg1314\njustauth:\n enabled: true\n type:\n GITHUB:\n client-id: ??\n client-secret: ??\n redirect-uri: http://sso.test.com:8080/jeecg-boot/thirdLogin/github/callback\n WECHAT_ENTERPRISE:\n client-id: ??\n client-secret: ??\n redirect-uri: http://sso.test.com:8080/jeecg-boot/thirdLogin/wechat_enterprise/callback\n agent-id: ??\n DINGTALK:\n client-id: ??\n client-secret: ??\n redirect-uri: http://sso.test.com:8080/jeecg-boot/thirdLogin/dingtalk/callback\n cache:\n type: default\n prefix: \'demo::\'\n timeout: 1h\nthird-app:\n enabled: false\n type:\n WECHAT_ENTERPRISE:\n enabled: false\n client-id: ??\n client-secret: ??\n agent-id: ??\n DINGTALK:\n enabled: false\n client-id: ??\n client-secret: ??\n agent-id: ??', 'dff588d1a47f42650e691b6621a401a8', '2024-06-22 15:11:57', '2025-05-19 09:48:01', NULL, '0:0:0:0:0:0:0:1', '', 'springboot3', '', '', '', 'yaml', '', '');
INSERT INTO `config_info` VALUES (44, 'jeecg.yaml', 'DEFAULT_GROUP', 'server:\n undertow:\n # max-http-post-size: 10MB\n decode-url: true\n worker-threads: 16\n buffers:\n websocket: 8192\n io: 16384\n error:\n include-exception: true\n include-stacktrace: ALWAYS\n include-message: ALWAYS\n compression:\n enabled: true\n min-response-size: 1024\n mime-types: application/javascript,application/json,application/xml,text/html,text/xml,text/plain,text/css,image/*\nmanagement:\n health:\n mail:\n enabled: false\n endpoints:\n web:\n exposure:\n include: metrics,httpexchanges,jeecghttptrace\n # include: \"*\"\n health:\n sensitive: true\n endpoint:\n health:\n show-details: ALWAYS\nspring:\n servlet:\n multipart:\n max-file-size: 10MB\n max-request-size: 10MB\n mail:\n host: smtp.163.com\n username: jeecgos@163.com\n password: ??\n properties:\n mail:\n smtp:\n auth: true\n starttls:\n enable: true\n required: true\n quartz:\n job-store-type: jdbc\n initialize-schema: embedded\n auto-startup: false\n startup-delay: 1s\n overwrite-existing-jobs: true\n properties:\n org:\n quartz:\n scheduler:\n instanceName: MyScheduler\n instanceId: AUTO\n jobStore:\n class: org.springframework.scheduling.quartz.LocalDataSourceJobStore\n driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate\n tablePrefix: QRTZ_\n isClustered: true\n misfireThreshold: 12000\n clusterCheckinInterval: 15000\n threadPool:\n class: org.quartz.simpl.SimpleThreadPool\n threadCount: 10\n threadPriority: 5\n threadsInheritContextClassLoaderOfInitializingThread: true\n jackson:\n date-format: yyyy-MM-dd HH:mm:ss\n time-zone: GMT+8\n aop:\n proxy-target-class: true\n activiti:\n check-process-definitions: false\n async-executor-activate: false\n job-executor-activate: false\n jpa:\n open-in-view: false\n freemarker:\n suffix: .ftl\n content-type: text/html\n charset: UTF-8\n cache: false\n prefer-file-system-access: false\n template-loader-path:\n - classpath:/templates\n mvc:\n static-path-pattern: /**\n pathmatch:\n matching-strategy: ant_path_matcher\n resource:\n static-locations: classpath:/static/,classpath:/public/\n autoconfigure:\n exclude:\n - com.alibaba.druid.spring.boot3.autoconfigure.DruidDataSourceAutoConfigure\n - org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration\nmybatis-plus:\n mapper-locations: classpath*:org/jeecg/**/xml/*Mapper.xml\n global-config:\n banner: false\n db-config:\n id-type: ASSIGN_ID\n table-underline: true\n configuration:\n call-setters-on-nulls: true', 'ee6593a5dbd94d90e841a466e1eee018', '2024-06-22 15:11:57', '2025-07-22 02:00:50', NULL, '0:0:0:0:0:0:0:1', '', 'springboot3', '', '', '', 'yaml', '', '');
INSERT INTO `config_info` VALUES (44, 'jeecg.yaml', 'DEFAULT_GROUP', 'server:\n undertow:\n # max-http-post-size: 10MB\n worker-threads: 16\n buffers:\n websocket: 8192\n io: 16384\n error:\n include-exception: true\n include-stacktrace: ALWAYS\n include-message: ALWAYS\n compression:\n enabled: true\n min-response-size: 1024\n mime-types: application/javascript,application/json,application/xml,text/html,text/xml,text/plain,text/css,image/*\nmanagement:\n health:\n mail:\n enabled: false\n endpoints:\n web:\n exposure:\n include: metrics,httpexchanges,jeecghttptrace\n # include: \"*\"\n health:\n sensitive: true\n endpoint:\n health:\n show-details: ALWAYS\nspring:\n servlet:\n multipart:\n max-file-size: 10MB\n max-request-size: 10MB\n mail:\n host: smtp.163.com\n username: jeecgos@163.com\n password: ??\n properties:\n mail:\n smtp:\n auth: true\n starttls:\n enable: true\n required: true\n quartz:\n job-store-type: jdbc\n initialize-schema: embedded\n auto-startup: false\n startup-delay: 1s\n overwrite-existing-jobs: true\n properties:\n org:\n quartz:\n scheduler:\n instanceName: MyScheduler\n instanceId: AUTO\n jobStore:\n class: org.springframework.scheduling.quartz.LocalDataSourceJobStore\n driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate\n tablePrefix: QRTZ_\n isClustered: true\n misfireThreshold: 12000\n clusterCheckinInterval: 15000\n threadPool:\n class: org.quartz.simpl.SimpleThreadPool\n threadCount: 10\n threadPriority: 5\n threadsInheritContextClassLoaderOfInitializingThread: true\n jackson:\n date-format: yyyy-MM-dd HH:mm:ss\n time-zone: GMT+8\n aop:\n proxy-target-class: true\n activiti:\n check-process-definitions: false\n async-executor-activate: false\n job-executor-activate: false\n jpa:\n open-in-view: false\n freemarker:\n suffix: .ftl\n content-type: text/html\n charset: UTF-8\n cache: false\n prefer-file-system-access: false\n template-loader-path:\n - classpath:/templates\n mvc:\n static-path-pattern: /**\n pathmatch:\n matching-strategy: ant_path_matcher\n resource:\n static-locations: classpath:/static/,classpath:/public/\n autoconfigure:\n exclude:\n - com.alibaba.druid.spring.boot3.autoconfigure.DruidDataSourceAutoConfigure\n - org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration\nmybatis-plus:\n mapper-locations: classpath*:org/jeecg/**/xml/*Mapper.xml\n global-config:\n banner: false\n db-config:\n id-type: ASSIGN_ID\n table-underline: true\n configuration:\n call-setters-on-nulls: true', '3a9678386fcd1b29874f358115b209d3', '2024-06-22 15:11:57', '2025-05-15 12:13:38', NULL, '0:0:0:0:0:0:0:1', '', 'springboot3', '', '', '', 'yaml', '', '');
INSERT INTO `config_info` VALUES (45, 'jeecg-gateway-router.json', 'DEFAULT_GROUP', '[{\n \"id\": \"jeecg-system\",\n \"order\": 0,\n \"predicates\": [{\n \"name\": \"Path\",\n \"args\": {\n \"_genkey_0\": \"/sys/**\",\n \"_genkey_1\": \"/jmreport/**\",\n \"_genkey_3\": \"/online/**\",\n \"_genkey_4\": \"/generic/**\",\n \"_genkey_5\": \"/oauth2/**\",\n \"_genkey_6\": \"/drag/**\",\n \"_genkey_7\": \"/actuator/**\"\n }\n }],\n \"filters\": [],\n \"uri\": \"lb://jeecg-system\"\n}, {\n \"id\": \"jeecg-demo\",\n \"order\": 1,\n \"predicates\": [{\n \"name\": \"Path\",\n \"args\": {\n \"_genkey_0\": \"/mock/**\",\n \"_genkey_1\": \"/test/**\",\n \"_genkey_2\": \"/bigscreen/template1/**\",\n \"_genkey_3\": \"/bigscreen/template2/**\"\n }\n }],\n \"filters\": [],\n \"uri\": \"lb://jeecg-demo\"\n}, {\n \"id\": \"jeecg-system-websocket\",\n \"order\": 2,\n \"predicates\": [{\n \"name\": \"Path\",\n \"args\": {\n \"_genkey_0\": \"/websocket/**\",\n \"_genkey_1\": \"/newsWebsocket/**\"\n }\n }],\n \"filters\": [],\n \"uri\": \"lb:ws://jeecg-system\"\n}, {\n \"id\": \"jeecg-demo-websocket\",\n \"order\": 3,\n \"predicates\": [{\n \"name\": \"Path\",\n \"args\": {\n \"_genkey_0\": \"/vxeSocket/**\"\n }\n }],\n \"filters\": [],\n \"uri\": \"lb:ws://jeecg-demo\"\n}]', '9794beb09d30bc6b835f2ee870781587', '2024-06-22 15:11:57', '2024-06-22 15:11:57', NULL, '0:0:0:0:0:0:0:1', '', 'springboot3', '', NULL, NULL, 'json', NULL, '');
INSERT INTO `config_info` VALUES (46, 'jeecg-sharding.yaml', 'DEFAULT_GROUP', 'spring:\n shardingsphere:\n datasource:\n names: ds0\n ds0:\n driverClassName: com.mysql.cj.jdbc.Driver\n url: jdbc:mysql://jeecg-boot-mysql:3306/jeecg-boot?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai\n username: root\n password: root\n type: com.alibaba.druid.pool.DruidDataSource\n props:\n sql-show: true\n rules:\n sharding:\n binding-tables: sys_log\n key-generators:\n snowflake:\n type: SNOWFLAKE\n props:\n worker-id: 123\n sharding-algorithms:\n table-classbased:\n props:\n strategy: standard\n algorithmClassName: org.jeecg.modules.test.sharding.algorithm.StandardModTableShardAlgorithm\n type: CLASS_BASED\n tables:\n sys_log:\n actual-data-nodes: ds0.sys_log$->{0..1}\n table-strategy:\n standard:\n sharding-algorithm-name: table-classbased\n sharding-column: log_type', 'a93fa455c32cd37ca84631d2bbe13005', '2024-06-22 15:11:57', '2024-06-22 15:11:57', NULL, '0:0:0:0:0:0:0:1', '', 'springboot3', '', NULL, NULL, 'yaml', NULL, '');
INSERT INTO `config_info` VALUES (47, 'jeecg-gateway-dev.yaml', 'DEFAULT_GROUP', 'jeecg:\n route:\n config:\n #type:database nacos yml\n data-type: database\n data-id: jeecg-gateway-router\nspring:\n data:\n redis:\n database: 0\n host: jeecg-boot-redis\n port: 6379\n password:\nknife4j:\n production: false', '19d7cd93eeb85a582c8a6942d499c7f7', '2024-06-22 15:11:57', '2024-06-22 15:11:57', NULL, '0:0:0:0:0:0:0:1', '', 'springboot3', '', NULL, NULL, 'yaml', NULL, '');
@ -214,7 +214,7 @@ CREATE TABLE `his_config_info` (
INDEX `idx_gmt_create`(`gmt_create`) USING BTREE,
INDEX `idx_gmt_modified`(`gmt_modified`) USING BTREE,
INDEX `idx_did`(`data_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 11 CHARACTER SET = utf8 COLLATE = utf8_bin COMMENT = '多租户改造' ROW_FORMAT = DYNAMIC;
) ENGINE = InnoDB AUTO_INCREMENT = 8 CHARACTER SET = utf8 COLLATE = utf8_bin COMMENT = '多租户改造' ROW_FORMAT = DYNAMIC;
-- ----------------------------
-- Records of his_config_info
@ -224,9 +224,6 @@ INSERT INTO `his_config_info` VALUES (43, 4, 'jeecg-dev.yaml', 'DEFAULT_GROUP',
INSERT INTO `his_config_info` VALUES (2, 5, 'jeecg.yaml', 'DEFAULT_GROUP', '', 'server:\n undertow:\n worker-threads: 16\n buffers:\n websocket: 8192\n io: 16384\n error:\n include-exception: true\n include-stacktrace: ALWAYS\n include-message: ALWAYS\n compression:\n enabled: true\n min-response-size: 1024\n mime-types: application/javascript,application/json,application/xml,text/html,text/xml,text/plain,text/css,image/*\nmanagement:\n health:\n mail:\n enabled: false\n endpoints:\n web:\n exposure:\n include: \"*\"\n health:\n sensitive: true\n endpoint:\n health:\n show-details: ALWAYS\nspringdoc:\n autoTagClasses: false\n packagesToScan: org.jeecg\nspring:\n servlet:\n multipart:\n max-file-size: 10MB\n max-request-size: 10MB\n mail:\n host: smtp.163.com\n username: jeecgos@163.com\n password: ??\n properties:\n mail:\n smtp:\n auth: true\n starttls:\n enable: true\n required: true\n quartz:\n job-store-type: jdbc\n initialize-schema: embedded\n auto-startup: false\n startup-delay: 1s\n overwrite-existing-jobs: true\n properties:\n org:\n quartz:\n scheduler:\n instanceName: MyScheduler\n instanceId: AUTO\n jobStore:\n class: org.springframework.scheduling.quartz.LocalDataSourceJobStore\n driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate\n tablePrefix: QRTZ_\n isClustered: true\n misfireThreshold: 12000\n clusterCheckinInterval: 15000\n threadPool:\n class: org.quartz.simpl.SimpleThreadPool\n threadCount: 10\n threadPriority: 5\n threadsInheritContextClassLoaderOfInitializingThread: true\n jackson:\n date-format: yyyy-MM-dd HH:mm:ss\n time-zone: GMT+8\n aop:\n proxy-target-class: true\n activiti:\n check-process-definitions: false\n async-executor-activate: false\n job-executor-activate: false\n jpa:\n open-in-view: false\n freemarker:\n suffix: .ftl\n content-type: text/html\n charset: UTF-8\n cache: false\n prefer-file-system-access: false\n template-loader-path:\n - classpath:/templates\n mvc:\n static-path-pattern: /**\n pathmatch:\n matching-strategy: ant_path_matcher\n resource:\n static-locations: classpath:/static/,classpath:/public/\n autoconfigure:\n exclude:\n - com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure\n - org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration\nmybatis-plus:\n mapper-locations: classpath*:org/jeecg/**/xml/*Mapper.xml\n global-config:\n banner: false\n db-config:\n id-type: ASSIGN_ID\n table-underline: true\n configuration:\n call-setters-on-nulls: true', '7f00366a5d35b2bb444f95499c0c40a9', '2010-05-05 00:00:00', '2025-05-19 09:51:57', NULL, '0:0:0:0:0:0:0:1', 'U', '', '');
INSERT INTO `his_config_info` VALUES (1, 6, 'jeecg-dev.yaml', 'DEFAULT_GROUP', '', 'spring:\n datasource:\n druid:\n stat-view-servlet:\n enabled: true\n loginUsername: admin\n loginPassword: 123456\n allow:\n web-stat-filter:\n enabled: true\n dynamic:\n druid:\n initial-size: 5\n min-idle: 5\n maxActive: 20\n maxWait: 60000\n timeBetweenEvictionRunsMillis: 60000\n minEvictableIdleTimeMillis: 300000\n validationQuery: SELECT 1 FROM DUAL\n testWhileIdle: true\n testOnBorrow: false\n testOnReturn: false\n poolPreparedStatements: true\n maxPoolPreparedStatementPerConnectionSize: 20\n filters: stat,wall,slf4j\n wall:\n selectWhereAlwayTrueCheck: false\n stat:\n merge-sql: true\n slow-sql-millis: 5000\n datasource:\n master:\n url: jdbc:mysql://jeecg-boot-mysql:3306/jeecg-boot?characterEncoding=UTF-8&useUnicode=true&useSSL=false&tinyInt1isBit=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai\n username: root\n password: root\n driver-class-name: com.mysql.cj.jdbc.Driver\n redis:\n database: 0\n host: jeecg-boot-redis\n password:\n port: 6379\n rabbitmq:\n host: jeecg-boot-rabbitmq\n username: guest\n password: guest\n port: 5672\n publisher-confirms: true\n publisher-returns: true\n virtual-host: /\n listener:\n simple:\n acknowledge-mode: manual\n concurrency: 1\n max-concurrency: 1\n retry:\n enabled: true\n flyway:\n enabled: false\n locations: classpath:flyway/sql/mysql\nminidao:\n base-package: org.jeecg.modules.jmreport.*,org.jeecg.modules.drag.*\njeecg:\n firewall:\n dataSourceSafe: false\n lowCodeMode: dev\n signatureSecret: dd05f1c54d63749eda95f9fa6d49v442a\n signUrls: /sys/dict/getDictItems/*,/sys/dict/loadDict/*,/sys/dict/loadDictOrderByValue/*,/sys/dict/loadDictItem/*,/sys/dict/loadTreeData,/sys/api/queryTableDictItemsByCode,/sys/api/queryFilterTableDictInfo,/sys/api/queryTableDictByKeys,/sys/api/translateDictFromTable,/sys/api/translateDictFromTableByKeys,/sys/sendChangePwdSms,/sys/user/sendChangePhoneSms,/sys/sms,/desform/api/sendVerifyCode\n uploadType: local\n domainUrl:\n pc: http://localhost:3100\n app: http://localhost:8051\n path:\n upload: /opt/upFiles\n webapp: /opt/webapp\n shiro:\n excludeUrls: /test/jeecgDemo/demo3,/test/jeecgDemo/redisDemo/**,/category/**,/visual/**,/map/**,/jmreport/bigscreen2/**\n oss:\n endpoint: oss-cn-beijing.aliyuncs.com\n accessKey: ??\n secretKey: ??\n bucketName: jeecgdev\n staticDomain: ??\n elasticsearch:\n cluster-name: jeecg-ES\n cluster-nodes: jeecg-boot-es:9200\n check-enabled: false\n file-view-domain: 127.0.0.1:8012\n minio:\n minio_url: http://minio.jeecg.com\n minio_name: ??\n minio_pass: ??\n bucketName: otatest\n jmreport:\n saasMode:\n firewall:\n dataSourceSafe: false\n lowCodeMode: dev\n wps:\n domain: https://wwo.wps.cn/office/\n appid: ??\n appsecret: ??\n xxljob:\n enabled: true\n adminAddresses: http://jeecg-boot-xxljob:9080/xxl-job-admin\n appname: ${spring.application.name}\n accessToken: \'\'\n logPath: logs/jeecg/job/jobhandler/\n logRetentionDays: 30\n redisson:\n address: jeecg-boot-redis:6379\n password:\n type: STANDALONE\n enabled: true\n ai-chat:\n enabled: false\n apiKey: \"\"\n apiHost: \"https://api.openai.com\"\n timeout: 60\nlogging:\n level:\n org.jeecg.modules.system.mapper : info\ncas:\n prefixUrl: http://localhost:8888/cas\nknife4j:\n production: false\n basic:\n enable: false\n username: jeecg\n password: jeecg1314\njustauth:\n enabled: true\n type:\n GITHUB:\n client-id: ??\n client-secret: ??\n redirect-uri: http://sso.test.com:8080/jeecg-boot/thirdLogin/github/callback\n WECHAT_ENTERPRISE:\n client-id: ??\n client-secret: ??\n redirect-uri: http://sso.test.com:8080/jeecg-boot/thirdLogin/wechat_enterprise/callback\n agent-id: ??\n DINGTALK:\n client-id: ??\n client-secret: ??\n redirect-uri: http://sso.test.com:8080/jeecg-boot/thirdLogin/dingtalk/callback\n cache:\n type: default\n prefix: \'demo::\'\n timeout: 1h\nthird-app:\n enabled: false\n type:\n WECHAT_ENTERPRISE:\n enabled: false\n client-id: ??\n client-secret: ??\n agent-id: ??\n DINGTALK:\n enabled: false\n client-id: ??\n client-secret: ??\n agent-id: ??', '057a1634c9b1ebda338515520bd3924a', '2010-05-05 00:00:00', '2025-05-19 12:08:56', NULL, '0:0:0:0:0:0:0:1', 'U', '', '');
INSERT INTO `his_config_info` VALUES (3, 7, 'jeecg-gateway-router.json', 'DEFAULT_GROUP', '', '[{\n \"id\": \"jeecg-system\",\n \"order\": 0,\n \"predicates\": [{\n \"name\": \"Path\",\n \"args\": {\n \"_genkey_0\": \"/sys/**\",\n \"_genkey_1\": \"/jmreport/**\",\n \"_genkey_3\": \"/online/**\",\n \"_genkey_4\": \"/generic/**\",\n \"_genkey_5\": \"/drag/**\",\n \"_genkey_6\": \"/actuator/**\"\n }\n }],\n \"filters\": [],\n \"uri\": \"lb://jeecg-system\"\n}, {\n \"id\": \"jeecg-demo\",\n \"order\": 1,\n \"predicates\": [{\n \"name\": \"Path\",\n \"args\": {\n \"_genkey_0\": \"/mock/**\",\n \"_genkey_1\": \"/test/**\",\n \"_genkey_2\": \"/bigscreen/template1/**\",\n \"_genkey_3\": \"/bigscreen/template2/**\"\n }\n }],\n \"filters\": [],\n \"uri\": \"lb://jeecg-demo\"\n}, {\n \"id\": \"jeecg-system-websocket\",\n \"order\": 2,\n \"predicates\": [{\n \"name\": \"Path\",\n \"args\": {\n \"_genkey_0\": \"/websocket/**\",\n \"_genkey_1\": \"/newsWebsocket/**\"\n }\n }],\n \"filters\": [],\n \"uri\": \"lb:ws://jeecg-system\"\n}, {\n \"id\": \"jeecg-demo-websocket\",\n \"order\": 3,\n \"predicates\": [{\n \"name\": \"Path\",\n \"args\": {\n \"_genkey_0\": \"/vxeSocket/**\"\n }\n }],\n \"filters\": [],\n \"uri\": \"lb:ws://jeecg-demo\"\n}]', '708c0948118bdb96bdfaa87200a14432', '2010-05-05 00:00:00', '2025-05-21 05:41:09', NULL, '0:0:0:0:0:0:0:1', 'U', '', '');
INSERT INTO `his_config_info` VALUES (1, 8, 'jeecg-dev.yaml', 'DEFAULT_GROUP', '', 'spring:\n datasource:\n druid:\n stat-view-servlet:\n enabled: true\n loginUsername: admin\n loginPassword: 123456\n allow:\n web-stat-filter:\n enabled: true\n dynamic:\n druid:\n initial-size: 5\n min-idle: 5\n maxActive: 20\n maxWait: 60000\n timeBetweenEvictionRunsMillis: 60000\n minEvictableIdleTimeMillis: 300000\n validationQuery: SELECT 1 FROM DUAL\n testWhileIdle: true\n testOnBorrow: false\n testOnReturn: false\n poolPreparedStatements: true\n maxPoolPreparedStatementPerConnectionSize: 20\n filters: stat,wall,slf4j\n wall:\n selectWhereAlwayTrueCheck: false\n stat:\n merge-sql: true\n slow-sql-millis: 5000\n datasource:\n master:\n url: jdbc:mysql://jeecg-boot-mysql:3306/jeecg-boot?characterEncoding=UTF-8&useUnicode=true&useSSL=false&tinyInt1isBit=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai\n username: root\n password: root\n driver-class-name: com.mysql.cj.jdbc.Driver\n redis:\n database: 0\n host: jeecg-boot-redis\n password:\n port: 6379\n rabbitmq:\n host: jeecg-boot-rabbitmq\n username: guest\n password: guest\n port: 5672\n publisher-confirms: true\n publisher-returns: true\n virtual-host: /\n listener:\n simple:\n acknowledge-mode: manual\n concurrency: 1\n max-concurrency: 1\n retry:\n enabled: true\n flyway:\n enabled: false\n locations: classpath:flyway/sql/mysql\nminidao:\n base-package: org.jeecg.modules.jmreport.*,org.jeecg.modules.drag.*\njeecg:\n firewall:\n dataSourceSafe: false\n lowCodeMode: dev\n signatureSecret: dd05f1c54d63749eda95f9fa6d49v442a\n signUrls: /sys/dict/getDictItems/*,/sys/dict/loadDict/*,/sys/dict/loadDictOrderByValue/*,/sys/dict/loadDictItem/*,/sys/dict/loadTreeData,/sys/api/queryTableDictItemsByCode,/sys/api/queryFilterTableDictInfo,/sys/api/queryTableDictByKeys,/sys/api/translateDictFromTable,/sys/api/translateDictFromTableByKeys,/sys/sendChangePwdSms,/sys/user/sendChangePhoneSms,/sys/sms,/desform/api/sendVerifyCode\n uploadType: local\n domainUrl:\n pc: http://localhost:3100\n app: http://localhost:8051\n path:\n upload: /opt/upFiles\n webapp: /opt/webapp\n shiro:\n excludeUrls: /test/jeecgDemo/demo3,/test/jeecgDemo/redisDemo/**,/category/**,/visual/**,/map/**,/jmreport/bigscreen2/**\n oss:\n endpoint: oss-cn-beijing.aliyuncs.com\n accessKey: ??\n secretKey: ??\n bucketName: jeecgdev\n staticDomain: ??\n minio:\n minio_url: http://minio.jeecg.com\n minio_name: ??\n minio_pass: ??\n bucketName: otatest\n jmreport:\n saasMode:\n firewall:\n dataSourceSafe: false\n lowCodeMode: dev\n wps:\n domain: https://wwo.wps.cn/office/\n appid: ??\n appsecret: ??\n xxljob:\n enabled: true\n adminAddresses: http://jeecg-boot-xxljob:9080/xxl-job-admin\n appname: ${spring.application.name}\n accessToken: \'\'\n logPath: logs/jeecg/job/jobhandler/\n logRetentionDays: 30\n redisson:\n address: jeecg-boot-redis:6379\n password:\n type: STANDALONE\n enabled: true\n ai-chat:\n enabled: false\n apiKey: \"\"\n apiHost: \"https://api.openai.com\"\n timeout: 60\n ai-rag:\n embed-store:\n host: 127.0.0.1\n port: 5432\n database: postgres\n user: postgres\n password: postgres\n table: embeddings\nlogging:\n level:\n org.jeecg.modules.system.mapper : info\ncas:\n prefixUrl: http://localhost:8888/cas\nknife4j:\n production: false\n basic:\n enable: false\n username: jeecg\n password: jeecg1314\njustauth:\n enabled: true\n type:\n GITHUB:\n client-id: ??\n client-secret: ??\n redirect-uri: http://sso.test.com:8080/jeecg-boot/thirdLogin/github/callback\n WECHAT_ENTERPRISE:\n client-id: ??\n client-secret: ??\n redirect-uri: http://sso.test.com:8080/jeecg-boot/thirdLogin/wechat_enterprise/callback\n agent-id: ??\n DINGTALK:\n client-id: ??\n client-secret: ??\n redirect-uri: http://sso.test.com:8080/jeecg-boot/thirdLogin/dingtalk/callback\n cache:\n type: default\n prefix: \'demo::\'\n timeout: 1h\nthird-app:\n enabled: false\n type:\n WECHAT_ENTERPRISE:\n enabled: false\n client-id: ??\n client-secret: ??\n agent-id: ??\n DINGTALK:\n enabled: false\n client-id: ??\n client-secret: ??\n agent-id: ??', 'f1d8102a50c7b1f59458e8f9a0112012', '2010-05-05 00:00:00', '2025-05-28 07:47:53', NULL, '0:0:0:0:0:0:0:1', 'U', '', '');
INSERT INTO `his_config_info` VALUES (2, 9, 'jeecg.yaml', 'DEFAULT_GROUP', '', 'server:\n undertow:\n worker-threads: 16\n buffers:\n websocket: 8192\n io: 16384\n error:\n include-exception: true\n include-stacktrace: ALWAYS\n include-message: ALWAYS\n compression:\n enabled: true\n min-response-size: 1024\n mime-types: application/javascript,application/json,application/xml,text/html,text/xml,text/plain,text/css,image/*\nmanagement:\n health:\n mail:\n enabled: false\n endpoints:\n web:\n exposure:\n include: \"*\"\n health:\n sensitive: true\n endpoint:\n health:\n show-details: ALWAYS\nspring:\n servlet:\n multipart:\n max-file-size: 10MB\n max-request-size: 10MB\n mail:\n host: smtp.163.com\n username: jeecgos@163.com\n password: ??\n properties:\n mail:\n smtp:\n auth: true\n starttls:\n enable: true\n required: true\n quartz:\n job-store-type: jdbc\n initialize-schema: embedded\n auto-startup: false\n startup-delay: 1s\n overwrite-existing-jobs: true\n properties:\n org:\n quartz:\n scheduler:\n instanceName: MyScheduler\n instanceId: AUTO\n jobStore:\n class: org.springframework.scheduling.quartz.LocalDataSourceJobStore\n driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate\n tablePrefix: QRTZ_\n isClustered: true\n misfireThreshold: 12000\n clusterCheckinInterval: 15000\n threadPool:\n class: org.quartz.simpl.SimpleThreadPool\n threadCount: 10\n threadPriority: 5\n threadsInheritContextClassLoaderOfInitializingThread: true\n jackson:\n date-format: yyyy-MM-dd HH:mm:ss\n time-zone: GMT+8\n aop:\n proxy-target-class: true\n activiti:\n check-process-definitions: false\n async-executor-activate: false\n job-executor-activate: false\n jpa:\n open-in-view: false\n freemarker:\n suffix: .ftl\n content-type: text/html\n charset: UTF-8\n cache: false\n prefer-file-system-access: false\n template-loader-path:\n - classpath:/templates\n mvc:\n static-path-pattern: /**\n pathmatch:\n matching-strategy: ant_path_matcher\n resource:\n static-locations: classpath:/static/,classpath:/public/\n autoconfigure:\n exclude:\n - com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure\n - org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration\nmybatis-plus:\n mapper-locations: classpath*:org/jeecg/**/xml/*Mapper.xml\n global-config:\n banner: false\n db-config:\n id-type: ASSIGN_ID\n table-underline: true\n configuration:\n call-setters-on-nulls: true', '20596e678c211d4322ead0000c0ffdbc', '2010-05-05 00:00:00', '2025-07-22 02:00:34', NULL, '0:0:0:0:0:0:0:1', 'U', '', '');
INSERT INTO `his_config_info` VALUES (44, 10, 'jeecg.yaml', 'DEFAULT_GROUP', '', 'server:\n undertow:\n # max-http-post-size: 10MB\n worker-threads: 16\n buffers:\n websocket: 8192\n io: 16384\n error:\n include-exception: true\n include-stacktrace: ALWAYS\n include-message: ALWAYS\n compression:\n enabled: true\n min-response-size: 1024\n mime-types: application/javascript,application/json,application/xml,text/html,text/xml,text/plain,text/css,image/*\nmanagement:\n health:\n mail:\n enabled: false\n endpoints:\n web:\n exposure:\n include: metrics,httpexchanges,jeecghttptrace\n # include: \"*\"\n health:\n sensitive: true\n endpoint:\n health:\n show-details: ALWAYS\nspring:\n servlet:\n multipart:\n max-file-size: 10MB\n max-request-size: 10MB\n mail:\n host: smtp.163.com\n username: jeecgos@163.com\n password: ??\n properties:\n mail:\n smtp:\n auth: true\n starttls:\n enable: true\n required: true\n quartz:\n job-store-type: jdbc\n initialize-schema: embedded\n auto-startup: false\n startup-delay: 1s\n overwrite-existing-jobs: true\n properties:\n org:\n quartz:\n scheduler:\n instanceName: MyScheduler\n instanceId: AUTO\n jobStore:\n class: org.springframework.scheduling.quartz.LocalDataSourceJobStore\n driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate\n tablePrefix: QRTZ_\n isClustered: true\n misfireThreshold: 12000\n clusterCheckinInterval: 15000\n threadPool:\n class: org.quartz.simpl.SimpleThreadPool\n threadCount: 10\n threadPriority: 5\n threadsInheritContextClassLoaderOfInitializingThread: true\n jackson:\n date-format: yyyy-MM-dd HH:mm:ss\n time-zone: GMT+8\n aop:\n proxy-target-class: true\n activiti:\n check-process-definitions: false\n async-executor-activate: false\n job-executor-activate: false\n jpa:\n open-in-view: false\n freemarker:\n suffix: .ftl\n content-type: text/html\n charset: UTF-8\n cache: false\n prefer-file-system-access: false\n template-loader-path:\n - classpath:/templates\n mvc:\n static-path-pattern: /**\n pathmatch:\n matching-strategy: ant_path_matcher\n resource:\n static-locations: classpath:/static/,classpath:/public/\n autoconfigure:\n exclude:\n - com.alibaba.druid.spring.boot3.autoconfigure.DruidDataSourceAutoConfigure\n - org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration\nmybatis-plus:\n mapper-locations: classpath*:org/jeecg/**/xml/*Mapper.xml\n global-config:\n banner: false\n db-config:\n id-type: ASSIGN_ID\n table-underline: true\n configuration:\n call-setters-on-nulls: true', '3a9678386fcd1b29874f358115b209d3', '2010-05-05 00:00:00', '2025-07-22 02:00:50', NULL, '0:0:0:0:0:0:0:1', 'U', 'springboot3', '');
-- ----------------------------
-- Table structure for permissions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,5 +0,0 @@
oracle导出编码 export NLS_LANG=AMERICAN_AMERICA.ZHS16GBK
导出用户: jeecgbootos
导入命令: imp scott/tiger@orcl file=jeecgboot-oracle11g.dmp

View File

@ -22,22 +22,22 @@ SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
DROP TABLE IF EXISTS `open_api`;
CREATE TABLE `open_api` (
`id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '接口名称',
`request_method` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '请求方法',
`request_url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '接口地址',
`black_list` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT 'IP 黑名单',
`body` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '请求体内容',
`origin_url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '原始地址',
`status` int(10) NULL DEFAULT NULL COMMENT '状态',
`del_flag` int(10) NULL DEFAULT NULL COMMENT '删除标识',
`create_by` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '创建人',
`create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
`update_by` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '修改人',
`update_time` datetime NULL DEFAULT NULL COMMENT '修改时间',
`headers_json` json NULL COMMENT '请求头json',
`params_json` json NULL COMMENT '请求参数json',
PRIMARY KEY (`id`) USING BTREE
`id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '接口名称',
`request_method` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '请求方法',
`request_url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '接口地址',
`black_list` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT 'IP 黑名单',
`body` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '请求体内容',
`origin_url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '原始地址',
`status` int(10) NULL DEFAULT NULL COMMENT '状态',
`del_flag` int(10) NULL DEFAULT NULL COMMENT '删除标识',
`create_by` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '创建人',
`create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
`update_by` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '修改人',
`update_time` datetime NULL DEFAULT NULL COMMENT '修改时间',
`headers_json` json NULL COMMENT '请求头json',
`params_json` json NULL COMMENT '请求参数json',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '接口表' ROW_FORMAT = DYNAMIC;
-- ----------------------------
@ -50,16 +50,16 @@ INSERT INTO `open_api` VALUES ('1922132683346649090', '根据部门查询用户'
-- ----------------------------
DROP TABLE IF EXISTS `open_api_auth`;
CREATE TABLE `open_api_auth` (
`id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '授权名称',
`ak` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT 'AK',
`sk` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT 'SK',
`create_by` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '创建人',
`create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
`update_by` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '修改人',
`update_time` datetime NULL DEFAULT NULL COMMENT '修改时间',
`system_user_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '关联系统用户名',
PRIMARY KEY (`id`) USING BTREE
`id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '授权名称',
`ak` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT 'AK',
`sk` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT 'SK',
`create_by` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '创建人',
`create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
`update_by` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '修改人',
`update_time` datetime NULL DEFAULT NULL COMMENT '修改时间',
`system_user_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '关联系统用户名',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '权限表' ROW_FORMAT = DYNAMIC;
-- ----------------------------
@ -72,13 +72,13 @@ INSERT INTO `open_api_auth` VALUES ('1922164194775056386', 'scott', 'ak-pFjyNHWR
-- ----------------------------
DROP TABLE IF EXISTS `open_api_log`;
CREATE TABLE `open_api_log` (
`id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`api_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '接口ID',
`call_auth_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '调用ID',
`call_time` datetime NULL DEFAULT NULL COMMENT '调用时间',
`used_time` bigint(20) NULL DEFAULT NULL COMMENT '耗时',
`response_time` datetime NULL DEFAULT NULL COMMENT '响应时间',
PRIMARY KEY (`id`) USING BTREE
`id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`api_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '接口ID',
`call_auth_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '调用ID',
`call_time` datetime NULL DEFAULT NULL COMMENT '调用时间',
`used_time` bigint(20) NULL DEFAULT NULL COMMENT '耗时',
`response_time` datetime NULL DEFAULT NULL COMMENT '响应时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '调用记录表' ROW_FORMAT = DYNAMIC;
-- ----------------------------
@ -117,14 +117,14 @@ INSERT INTO `open_api_log` VALUES ('1922836856287428610', '1922132683346649090',
-- ----------------------------
DROP TABLE IF EXISTS `open_api_permission`;
CREATE TABLE `open_api_permission` (
`id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`api_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '接口ID',
`api_auth_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '认证ID',
`create_by` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '创建人',
`create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
`update_by` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '更新人',
`update_time` datetime NULL DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE
`id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`api_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '接口ID',
`api_auth_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '认证ID',
`create_by` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '创建人',
`create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
`update_by` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '更新人',
`update_time` datetime NULL DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'openapi授权' ROW_FORMAT = DYNAMIC;
-- ----------------------------
@ -165,4 +165,6 @@ INSERT INTO sys_role_permission (id, role_id, permission_id, data_rule_ids, oper
INSERT INTO sys_role_permission (id, role_id, permission_id, data_rule_ids, operate_date, operate_ip) VALUES ('1917881149431058436', 'f6817f48af4fb3af11b9e8bf182f618b', '2025050105554940203', null, '2025-05-01 17:57:53', '0:0:0:0:0:0:0:1');
INSERT INTO sys_role_permission (id, role_id, permission_id, data_rule_ids, operate_date, operate_ip) VALUES ('1917881149431058437', 'f6817f48af4fb3af11b9e8bf182f618b', '2025050105554940204', null, '2025-05-01 17:57:53', '0:0:0:0:0:0:0:1');
INSERT INTO sys_role_permission (id, role_id, permission_id, data_rule_ids, operate_date, operate_ip) VALUES ('1917881149431058438', 'f6817f48af4fb3af11b9e8bf182f618b', '2025050105554940205', null, '2025-05-01 17:57:53', '0:0:0:0:0:0:0:1');
INSERT INTO sys_role_permission (id, role_id, permission_id, data_rule_ids, operate_date, operate_ip) VALUES ('1917881149431058439', 'f6817f48af4fb3af11b9e8bf182f618b', '2025050105554940206', null, '2025-05-01 17:57:53', '0:0:0:0:0:0:0:1');
INSERT INTO sys_role_permission (id, role_id, permission_id, data_rule_ids, operate_date, operate_ip) VALUES ('1917881149431058439', 'f6817f48af4fb3af11b9e8bf182f618b', '2025050105554940206', null, '2025-05-01 17:57:53', '0:0:0:0:0:0:0:1');
INSERT INTO sys_role_permission (id, role_id, permission_id, data_rule_ids, operate_date, operate_ip) VALUES ('1917957659860963330', 'f6817f48af4fb3af11b9e8bf182f618b', '1917957565728198657', null, '2025-05-01 23:01:55', '0:0:0:0:0:0:0:1');
INSERT INTO sys_role_permission (id, role_id, permission_id, data_rule_ids, operate_date, operate_ip) VALUES ('1922109760551858178', 'f6817f48af4fb3af11b9e8bf182f618b', '1922109301837606914', null, '2025-05-13 10:00:53', '0:0:0:0:0:0:0:1');

View File

@ -0,0 +1,45 @@
CREATE TABLE `oauth2_registered_client` (
`id` varchar(100) NOT NULL,
`client_id` varchar(100) NOT NULL,
`client_id_issued_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`client_secret` varchar(200) DEFAULT NULL,
`client_secret_expires_at` timestamp NULL DEFAULT NULL,
`client_name` varchar(200) NOT NULL,
`client_authentication_methods` varchar(1000) NOT NULL,
`authorization_grant_types` varchar(1000) NOT NULL,
`redirect_uris` varchar(1000) DEFAULT NULL,
`post_logout_redirect_uris` varchar(1000) DEFAULT NULL,
`scopes` varchar(1000) NOT NULL,
`client_settings` varchar(2000) NOT NULL,
`token_settings` varchar(2000) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
INSERT INTO `oauth2_registered_client`
(`id`,
`client_id`,
`client_id_issued_at`,
`client_secret`,
`client_secret_expires_at`,
`client_name`,
`client_authentication_methods`,
`authorization_grant_types`,
`redirect_uris`,
`post_logout_redirect_uris`,
`scopes`,
`client_settings`,
`token_settings`)
VALUES
('3eacac0e-0de9-4727-9a64-6bdd4be2ee1f',
'jeecg-client',
now(),
'secret',
null,
'3eacac0e-0de9-4727-9a64-6bdd4be2ee1f',
'client_secret_basic',
'refresh_token,authorization_code,password,app,phone,social',
'http://127.0.0.1:8080/jeecg-',
'http://127.0.0.1:8080/',
'*',
'{"@class":"java.util.Collections$UnmodifiableMap","settings.client.require-proof-key":false,"settings.client.require-authorization-consent":true}',
'{"@class":"java.util.Collections$UnmodifiableMap","settings.token.reuse-refresh-tokens":true,"settings.token.id-token-signature-algorithm":["org.springframework.security.oauth2.jose.jws.SignatureAlgorithm","RS256"],"settings.token.access-token-time-to-live":["java.time.Duration",300000.000000000],"settings.token.access-token-format":{"@class":"org.springframework.security.oauth2.server.authorization.settings.OAuth2TokenFormat","value":"self-contained"},"settings.token.refresh-token-time-to-live":["java.time.Duration",3600.000000000],"settings.token.authorization-code-time-to-live":["java.time.Duration",300000.000000000],"settings.token.device-code-time-to-live":["java.time.Duration",300000.000000000]}');

View File

@ -18,7 +18,7 @@ services:
--max_allowed_packet=128M
--default-authentication-plugin=caching_sha2_password
ports:
- 13306:3306
- 3306:3306
networks:
- jeecg-boot
@ -32,19 +32,6 @@ services:
networks:
- jeecg-boot
jeecg-boot-pgvector:
image: registry.cn-hangzhou.aliyuncs.com/jeecgdocker/pgvector
container_name: jeecg-boot-pgvector
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: vector_db
ports:
- 5432:5432
restart: always
networks:
- jeecg-boot
jeecg-boot-system:
build:
context: ./jeecg-module-system/jeecg-system-start

View File

@ -4,7 +4,7 @@
<parent>
<groupId>org.jeecgframework.boot</groupId>
<artifactId>jeecg-boot-parent</artifactId>
<version>3.8.2</version>
<version>3.8.1</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>jeecg-boot-base-core</artifactId>
@ -117,10 +117,11 @@
<version>${mybatis-plus.version}</version>
</dependency>
<!-- minidao -->
<dependency>
<groupId>org.jeecgframework.boot3</groupId>
<artifactId>minidao-spring-boot-starter-jsqlparser-4.9</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-jsqlparser-4.9</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<!-- druid -->
<dependency>
@ -197,78 +198,26 @@
<version>${java-jwt.version}</version>
</dependency>
<!--shiro-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-starter</artifactId>
<classifier>jakarta</classifier>
<version>${shiro.version}</version>
<exclusions>
<exclusion>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<classifier>jakarta</classifier>
<version>${shiro.version}</version>
<!-- 排除仍使用了javax.servlet的依赖 -->
<exclusions>
<exclusion>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
</exclusion>
<exclusion>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- 引入适配jakarta的依赖包 -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<classifier>jakarta</classifier>
<version>${shiro.version}</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
<classifier>jakarta</classifier>
<version>${shiro.version}</version>
<exclusions>
<exclusion>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- shiro-redis -->
<dependency>
<groupId>org.crazycake</groupId>
<artifactId>shiro-redis</artifactId>
<version>${shiro-redis.version}</version>
<exclusions>
<exclusion>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
</exclusion>
<exclusion>
<artifactId>checkstyle</artifactId>
<groupId>com.puppycrawl.tools</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-authorization-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<!-- 添加spring security cas支持 -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-cas</artifactId>
</dependency>
<!-- <dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
<version>${knife4j-spring-boot-starter.version}</version>
</dependency>-->
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
<version>${knife4j-spring-boot-starter.version}</version>
</dependency>-->
<!-- knife4j 升级springboot3.4.5报错 -->
<dependency>
<groupId>com.github.xiaoymin</groupId>
@ -280,7 +229,7 @@
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.7.0</version>
</dependency>
<!-- 代码生成器 -->
<!-- 如下载失败,请参考此文档 https://help.jeecg.com/java/setup/maven.html -->
<dependency>
@ -388,5 +337,10 @@
<groupId>org.jeecgframework.boot</groupId>
<artifactId>jeecg-boot-starter3-chatgpt</artifactId>
</dependency>
<!-- minidao -->
<dependency>
<groupId>org.jeecgframework.boot3</groupId>
<artifactId>minidao-spring-boot-starter-jsqlparser-4.9</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,21 @@
package org.apache.shiro;
import org.apache.shiro.subject.Subject;
/**
* 兼容处理Online功能使用处理请勿修改
* @author eightmonth@qq.com
* @date 2024/4/29 14:05
*/
public class SecurityUtils {
public static Subject getSubject() {
return new Subject() {
@Override
public Object getPrincipal() {
return Subject.super.getPrincipal();
}
};
}
}

View File

@ -0,0 +1,14 @@
package org.apache.shiro.subject;
import org.jeecg.config.security.utils.SecureUtil;
/**
* 兼容处理Online功能使用处理请勿修改
* @author eightmonth@qq.com
* @date 2024/4/29 14:18
*/
public interface Subject {
default Object getPrincipal() {
return SecureUtil.currentUser();
}
}

View File

@ -1,5 +1,6 @@
package org.jeecg.common.api;
import com.alibaba.fastjson.JSONObject;
import org.jeecg.common.system.vo.*;
import java.util.List;
@ -64,6 +65,13 @@ public interface CommonAPI {
*/
public String getUserIdByName(String username);
/**
* 5根据用户手机号查询用户信息
* @param username
* @return
*/
public LoginUser getUserByPhone(String phone);
/**
* 6字典表的 翻译
@ -144,4 +152,31 @@ public interface CommonAPI {
List<DictModel> translateDictFromTableByKeys(String table, String text, String code, String keys, String dataSource);
//update-end---author:chenrui ---date:20231221 for[issues/#5643]解决分布式下表字典跨库无法查询问题------------
/**
* 登录加载系统字典
* @return
*/
Map<String,List<DictModel>> queryAllDictItems();
/**
* 查询SysDepart集合
* @param userId
* @return
*/
List<SysDepartModel> queryUserDeparts(String userId);
/**
* 根据用户名设置部门ID
* @param username
* @param orgCode
*/
void updateUserDepart(String username,String orgCode,Integer loginTenantId);
/**
* 设置登录租户
* @param username
* @return
*/
JSONObject setLoginTenant(String username);
}

View File

@ -91,12 +91,6 @@ public class MessageDTO implements Serializable {
private Boolean isTimeJob = false;
//---【邮件相关参数】-------------------------------------------------------------
/**
* 枚举org.jeecg.common.constant.enums.NoticeTypeEnum
* 通知类型(system:系统消息、file:知识库、flow:流程、plan:日程计划、meeting:会议)
*/
private String noticeType;
public MessageDTO(){
}

View File

@ -1,7 +1,6 @@
package org.jeecg.common.api.vo;
import com.fasterxml.jackson.annotation.JsonIgnore;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import org.jeecg.common.constant.CommonConstant;

View File

@ -1,5 +1,6 @@
package org.jeecg.common.aspect;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.serializer.PropertyFilter;
import org.apache.shiro.SecurityUtils;
@ -15,12 +16,14 @@ import org.jeecg.common.aspect.annotation.AutoLog;
import org.jeecg.common.constant.CommonConstant;
import org.jeecg.common.constant.enums.ModuleType;
import org.jeecg.common.constant.enums.OperateTypeEnum;
import org.jeecg.config.security.utils.SecureUtil;
import org.jeecg.modules.base.service.BaseCommonService;
import org.jeecg.common.system.vo.LoginUser;
import org.jeecg.common.util.IpUtils;
import org.jeecg.common.util.SpringContextUtils;
import org.jeecg.common.util.oConvertUtils;
import org.springframework.core.StandardReflectionParameterNameDiscoverer;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.validation.BindingResult;
import org.springframework.web.multipart.MultipartFile;
@ -100,7 +103,7 @@ public class AutoLogAspect {
//设置IP地址
dto.setIp(IpUtils.getIpAddr(request));
//获取登录用户信息
LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();
LoginUser sysUser = SecureUtil.currentUser();
if(sysUser!=null){
dto.setUserid(sysUser.getUsername());
dto.setUsername(sysUser.getRealname());

View File

@ -90,7 +90,7 @@ public interface CommonConstant {
/** 登录用户Shiro权限缓存KEY前缀 */
public static String PREFIX_USER_SHIRO_CACHE = "shiro:cache:org.jeecg.config.shiro.ShiroRealm.authorizationCache:";
/** 登录用户Token令牌缓存KEY前缀 */
String PREFIX_USER_TOKEN = "prefix_user_token:";
String PREFIX_USER_TOKEN = "token::jeecg-client::";
// /** Token缓存时间3600秒即一小时 */
// int TOKEN_EXPIRE_TIME = 3600;
@ -303,11 +303,6 @@ public interface CommonConstant {
*/
String SYS_USER_ID_MAPPING_CACHE = "sys:cache:user:id_mapping";
/**
* 系统角色管理员编码
*/
String SYS_ROLE_ADMIN = "admin";
/**
* 考勤补卡业务状态 1同意 2不同意
*/
@ -433,11 +428,6 @@ public interface CommonConstant {
*/
String NOTICE_MSG_BUS_TYPE = "NOTICE_MSG_BUS_TYPE";
/**
* 通知类型,用于区分来源 file 知识 flow 流程 plan 日程 system 系统消息
*/
String NOTICE_TYPE = "noticeType";
/**
* 邮箱消息中地址登录时地址后携带的token,需要替换成真实的token值
*/
@ -639,24 +629,4 @@ public interface CommonConstant {
* 修改手机号验证码请求次数超出
*/
Integer PHONE_SMS_FAIL_CODE = 40002;
/**
* 自定义首页关联关系(ROLE:表示角色 USER:表示用户)
*
*/
String HOME_RELATION_ROLE = "ROLE";
String HOME_RELATION_USER = "USER";
/**
* 是否置顶(0否 1是)
*/
Integer IZ_TOP_1 = 1;
Integer IZ_TOP_0 = 0;
//关注流程缓存前缀
String FLOW_FOCUS_NOTICE_PREFIX = "flow:runtimeData:focus:notice:";
//任务缓办时间缓存前缀
String FLOW_TASK_DELAY_PREFIX = "flow:runtimeData:task:delay:";
}

View File

@ -4,20 +4,6 @@ package org.jeecg.common.constant;
* @author: jeecg-boot
*/
public interface DataBaseConstant {
/**
* 内置的系统变量键列表
*/
public static final String[] SYSTEM_KEYS = {
DataBaseConstant.SYS_ORG_CODE, DataBaseConstant.SYS_ORG_CODE_TABLE, DataBaseConstant.SYS_MULTI_ORG_CODE,
DataBaseConstant.SYS_MULTI_ORG_CODE_TABLE, DataBaseConstant.SYS_ORG_ID, DataBaseConstant.SYS_ORG_ID_TABLE,
DataBaseConstant.SYS_ROLE_CODE, DataBaseConstant.SYS_ROLE_CODE_TABLE, DataBaseConstant.SYS_USER_CODE,
DataBaseConstant.SYS_USER_CODE_TABLE, DataBaseConstant.SYS_USER_ID, DataBaseConstant.SYS_USER_ID_TABLE,
DataBaseConstant.SYS_USER_NAME, DataBaseConstant.SYS_USER_NAME_TABLE, DataBaseConstant.SYS_DATE,
DataBaseConstant.SYS_DATE_TABLE, DataBaseConstant.SYS_TIME, DataBaseConstant.SYS_TIME_TABLE,
DataBaseConstant.SYS_BASE_PATH
};
//*********数据库类型****************************************
/**MYSQL数据库*/

View File

@ -9,14 +9,14 @@ import org.apache.commons.lang3.StringUtils;
public enum DySmsEnum {
/**登录短信模板编码*/
LOGIN_TEMPLATE_CODE("SMS_175435174","敲敲云","code"),
LOGIN_TEMPLATE_CODE("SMS_175435174","敲敲云","code"),
/**忘记密码短信模板编码*/
FORGET_PASSWORD_TEMPLATE_CODE("SMS_175435174","敲敲云","code"),
/**修改密码短信模板编码*/
CHANGE_PASSWORD_TEMPLATE_CODE("SMS_465391221","敲敲云","code"),
/**注册账号短信模板编码*/
REGISTER_TEMPLATE_CODE("SMS_175430166","敲敲云","code");
FORGET_PASSWORD_TEMPLATE_CODE("SMS_175435174","敲敲云","code"),
/**修改密码短信模板编码*/
CHANGE_PASSWORD_TEMPLATE_CODE("SMS_465391221","敲敲云","code"),
/**注册账号短信模板编码*/
REGISTER_TEMPLATE_CODE("SMS_175430166","敲敲云","code");
/**
* 短信模板编码
*/

View File

@ -13,10 +13,6 @@ public enum EmailTemplateEnum {
* 流程催办
*/
BPM_CUIBAN_EMAIL("bpm_cuiban_email", "/templates/email/bpm_cuiban_email.ftl"),
/**
* 流程抄送
*/
BPM_CC_EMAIL("bpm_cc_email", "/templates/email/bpm_cc_email.ftl"),
/**
* 流程新任务
*/

View File

@ -1,75 +0,0 @@
package org.jeecg.common.constant.enums;
/**
* @Description: 文件类型枚举类
*
* @author: wangshuai
* @date: 2025/6/26 17:29
*/
public enum NoticeTypeEnum {
//VUE3专用
NOTICE_TYPE_FILE("知识库消息","file"),
NOTICE_TYPE_FLOW("工作流消息","flow"),
NOTICE_TYPE_PLAN("日程消息","plan"),
//暂时没用到
NOTICE_TYPE_MEETING("会议消息","meeting"),
NOTICE_TYPE_SYSTEM("系统消息","system");
/**
* 文件类型名称
*/
private String name;
/**
* 文件类型值
*/
private String value;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
NoticeTypeEnum(String name, String value) {
this.name = name;
this.value = value;
}
/**
* 获取聊天通知类型
*
* @param value
* @return
*/
public static String getChatNoticeType(String value){
return value + "Notice";
}
/**
* 获取通知名称
*
* @param value
* @return
*/
public static String getNoticeNameByValue(String value){
value = value.replace("Notice","");
for (NoticeTypeEnum e : NoticeTypeEnum.values()) {
if (e.getValue().equals(value)) {
return e.getName();
}
}
return "系统消息";
}
}

View File

@ -3,12 +3,9 @@ package org.jeecg.common.exception;
import cn.hutool.core.util.ObjectUtil;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import io.undertow.server.RequestTooBigException;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authz.AuthorizationException;
import org.apache.shiro.authz.UnauthorizedException;
import org.jeecg.common.api.dto.LogDTO;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.constant.CommonConstant;
@ -25,6 +22,8 @@ import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.data.redis.connection.PoolException;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.util.CollectionUtils;
import org.springframework.validation.ObjectError;
import org.springframework.web.HttpRequestMethodNotSupportedException;
@ -33,7 +32,6 @@ import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.multipart.MaxUploadSizeExceededException;
import org.springframework.web.multipart.MultipartException;
import org.springframework.web.servlet.NoHandlerFoundException;
import java.util.Map;
@ -48,9 +46,27 @@ import java.util.stream.Collectors;
@RestControllerAdvice
@Slf4j
public class JeecgBootExceptionHandler {
@Resource
@Resource
BaseCommonService baseCommonService;
/**
* 验证码错误异常
*/
@ExceptionHandler(JeecgCaptchaException.class)
@ResponseStatus(HttpStatus.OK)
public Result<?> handleJeecgCaptchaException(JeecgCaptchaException e) {
log.error(e.getMessage(), e);
return Result.error(e.getCode(), e.getMessage());
}
@ExceptionHandler(AuthenticationException.class)
@ResponseStatus(HttpStatus.OK)
public Result<?> handleJeecgCaptchaException(AuthenticationException e) {
log.error(e.getMessage(), e);
return Result.error(401, e.getMessage());
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result<?> handleValidationExceptions(MethodArgumentNotValidException e) {
@ -58,7 +74,7 @@ public class JeecgBootExceptionHandler {
addSysLog(e);
return Result.error("校验失败!" + e.getBindingResult().getAllErrors().stream().map(ObjectError::getDefaultMessage).collect(Collectors.joining(",")));
}
/**
* 处理自定义异常
*/
@ -113,8 +129,8 @@ public class JeecgBootExceptionHandler {
return Result.error("数据库中已存在该记录");
}
@ExceptionHandler({UnauthorizedException.class, AuthorizationException.class})
public Result<?> handleAuthorizationException(AuthorizationException e){
@ExceptionHandler(AccessDeniedException.class)
public Result<?> handleAuthorizationException(AccessDeniedException e){
log.error(e.getMessage(), e);
return Result.noauth("没有权限,请联系管理员分配权限!");
}
@ -168,27 +184,6 @@ public class JeecgBootExceptionHandler {
return Result.error("文件大小超出10MB限制, 请压缩或降低文件质量! ");
}
/**
* 处理文件过大异常.
* jdk17中的MultipartException异常类已经被拆分成了MultipartException和MaxUploadSizeExceededException
* for [QQYUN-11716]上传大图片失败没有精确提示
* @param e
* @return
* @author chenrui
* @date 2025/4/8 16:13
*/
@ExceptionHandler(MultipartException.class)
public Result<?> handleMaxUploadSizeExceededException(MultipartException e) {
Throwable cause = e.getCause();
if (cause instanceof IllegalStateException && cause.getCause() instanceof RequestTooBigException) {
log.error("文件大小超出限制: {}", cause.getMessage(), e);
addSysLog(e);
return Result.error("文件大小超出限制, 请压缩或降低文件质量!");
} else {
return handleException(e);
}
}
@ExceptionHandler(DataIntegrityViolationException.class)
public Result<?> handleDataIntegrityViolationException(DataIntegrityViolationException e) {
log.error(e.getMessage(), e);
@ -244,16 +239,11 @@ public class JeecgBootExceptionHandler {
} catch (NullPointerException | BeansException ignored) {
}
if (null != request) {
//update-begin---author:chenrui ---date:20250408 for[QQYUN-11716]上传大图片失败没有精确提示------------
//请求的参数
if (!isTooBigException(e)) {
// 文件上传过大异常时不能获取参数,否则会报错
Map<String, String[]> parameterMap = request.getParameterMap();
if(!CollectionUtils.isEmpty(parameterMap)) {
log.setMethod(oConvertUtils.mapToString(request.getParameterMap()));
}
Map<String, String[]> parameterMap = request.getParameterMap();
if(!CollectionUtils.isEmpty(parameterMap)){
log.setMethod(oConvertUtils.mapToString(request.getParameterMap()));
}
//update-end---author:chenrui ---date:20250408 for[QQYUN-11716]上传大图片失败没有精确提示------------
// 请求地址
log.setRequestUrl(request.getRequestURI());
//设置IP地址
@ -279,26 +269,4 @@ public class JeecgBootExceptionHandler {
}
//update-end---author:chenrui ---date:20240423 for[QQYUN-8732]把错误的日志都抓取了 方便后续处理,单独弄个日志类型------------
/**
* 是否文件过大异常
* for [QQYUN-11716]上传大图片失败没有精确提示
* @param e
* @return
* @author chenrui
* @date 2025/4/8 20:21
*/
private static boolean isTooBigException(Throwable e) {
boolean isTooBigException = false;
if(e instanceof MultipartException){
Throwable cause = e.getCause();
if (cause instanceof IllegalStateException && cause.getCause() instanceof RequestTooBigException){
isTooBigException = true;
}
}
if(e instanceof MaxUploadSizeExceededException){
isTooBigException = true;
}
return isTooBigException;
}
}

View File

@ -0,0 +1,28 @@
package org.jeecg.common.exception;
import lombok.Data;
/**
* @author kezhijie@wuhandsj.com
* @date 2024/1/2 11:38
*/
@Data
public class JeecgCaptchaException extends RuntimeException{
private Integer code;
private static final long serialVersionUID = -9093410345065209053L;
public JeecgCaptchaException(Integer code, String message) {
super(message);
this.code = code;
}
public JeecgCaptchaException(String message, Throwable cause) {
super(message, cause);
}
public JeecgCaptchaException(Throwable cause) {
super(cause);
}
}

View File

@ -1,17 +1,18 @@
package org.jeecg.common.system.base.controller;
import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.beanutils.PropertyUtils;
import org.apache.shiro.SecurityUtils;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.system.query.QueryGenerator;
import org.jeecg.common.system.vo.LoginUser;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.config.JeecgBaseConfig;
import org.jeecg.config.security.utils.SecureUtil;
import org.jeecgframework.poi.excel.ExcelImportUtil;
import org.jeecgframework.poi.excel.def.NormalExcelConstants;
import org.jeecgframework.poi.excel.entity.ExportParams;
@ -19,6 +20,7 @@ import org.jeecgframework.poi.excel.entity.ImportParams;
import org.jeecgframework.poi.excel.entity.enmus.ExcelType;
import org.jeecgframework.poi.excel.view.JeecgEntityExcelView;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.MultipartHttpServletRequest;
import org.springframework.web.servlet.ModelAndView;
@ -51,7 +53,7 @@ public class JeecgController<T, S extends IService<T>> {
protected ModelAndView exportXls(HttpServletRequest request, T object, Class<T> clazz, String title) {
// Step.1 组装查询条件
QueryWrapper<T> queryWrapper = QueryGenerator.initQueryWrapper(object, request.getParameterMap());
LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();
LoginUser sysUser = SecureUtil.currentUser();
// 过滤选中数据
String selections = request.getParameter("selections");
@ -89,7 +91,7 @@ public class JeecgController<T, S extends IService<T>> {
protected ModelAndView exportXlsSheet(HttpServletRequest request, T object, Class<T> clazz, String title,String exportFields,Integer pageNum) {
// Step.1 组装查询条件
QueryWrapper<T> queryWrapper = QueryGenerator.initQueryWrapper(object, request.getParameterMap());
LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();
LoginUser sysUser = SecureUtil.currentUser();
// Step.2 计算分页sheet数据
double total = service.count();
int count = (int)Math.ceil(total/pageNum);

View File

@ -2,6 +2,7 @@ package org.jeecg.common.system.base.entity;
import java.io.Serializable;
import io.swagger.v3.oas.annotations.media.Schema;
import org.jeecgframework.poi.excel.annotation.Excel;
import org.springframework.format.annotation.DateTimeFormat;
@ -9,11 +10,9 @@ import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import io.swagger.v3.oas.annotations.media.Schema;
/**
* @Description: Entity基类

View File

@ -11,7 +11,6 @@ import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import com.baomidou.mybatisplus.core.metadata.OrderItem;
import org.apache.commons.beanutils.PropertyUtils;
import org.jeecg.common.constant.CommonConstant;
import org.jeecg.common.constant.DataBaseConstant;
@ -258,69 +257,8 @@ public class QueryGenerator {
if(parameterMap!=null&& parameterMap.containsKey(ORDER_TYPE)) {
order = parameterMap.get(ORDER_TYPE)[0];
}
if(oConvertUtils.isNotEmpty(column)){
log.info("单字段排序规则>> column:" + column + ",排序方式:" + order);
}
log.debug("排序规则>>列:" + column + ",排序方式:" + order);
// 1. 列表多字段排序优先
if(parameterMap!=null&& parameterMap.containsKey("sortInfoString")) {
// 多字段排序
String sortInfoString = parameterMap.get("sortInfoString")[0];
log.info("多字段排序规则>> sortInfoString:" + sortInfoString);
List<OrderItem> orderItemList = SqlConcatUtil.getQueryConditionOrders(column, order, sortInfoString);
log.info(orderItemList.toString());
if (orderItemList != null && !orderItemList.isEmpty()) {
for (OrderItem item : orderItemList) {
// 一、获取排序数据库字段
String columnName = item.getColumn();
// 1.字典字段,去掉字典翻译文本后缀
if(columnName.endsWith(CommonConstant.DICT_TEXT_SUFFIX)) {
columnName = columnName.substring(0, column.lastIndexOf(CommonConstant.DICT_TEXT_SUFFIX));
}
// 2.实体驼峰字段转为数据库字段
columnName = SqlInjectionUtil.getSqlInjectSortField(columnName);
// 二、设置字段排序规则
if (item.isAsc()) {
queryWrapper.orderByAsc(columnName);
} else {
queryWrapper.orderByDesc(columnName);
}
}
}
return;
}
// 2. 列表单字段默认排序
if(oConvertUtils.isEmpty(column) && parameterMap!=null&& parameterMap.containsKey("defSortString")) {
// 多字段排序
String sortInfoString = parameterMap.get("defSortString")[0];
log.info("默认多字段排序规则>> defSortString:" + sortInfoString);
List<OrderItem> orderItemList = SqlConcatUtil.getQueryConditionOrders(column, order, sortInfoString);
log.info(orderItemList.toString());
if (orderItemList != null && !orderItemList.isEmpty()) {
for (OrderItem item : orderItemList) {
// 一、获取排序数据库字段
String columnName = item.getColumn();
// 1.字典字段,去掉字典翻译文本后缀
if(columnName.endsWith(CommonConstant.DICT_TEXT_SUFFIX)) {
columnName = columnName.substring(0, column.lastIndexOf(CommonConstant.DICT_TEXT_SUFFIX));
}
// 2.实体驼峰字段转为数据库字段
columnName = SqlInjectionUtil.getSqlInjectSortField(columnName);
// 二、设置字段排序规则
if (item.isAsc()) {
queryWrapper.orderByAsc(columnName);
} else {
queryWrapper.orderByDesc(columnName);
}
}
}
return;
}
//update-begin-author:scott date:2022-11-07 for:避免用户自定义表无默认字段{创建时间},导致排序报错
//TODO 避免用户自定义表无默认字段创建时间,导致排序报错
if(DataBaseConstant.CREATE_TIME.equals(column) && !fieldColumnMap.containsKey(DataBaseConstant.CREATE_TIME)){

View File

@ -1,5 +1,7 @@
package org.jeecg.common.system.util;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
@ -10,9 +12,9 @@ import com.google.common.base.Joiner;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Date;
import java.util.Objects;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
@ -20,7 +22,7 @@ import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.SecurityUtils;
import org.jeecg.common.api.CommonAPI;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.constant.CommonConstant;
import org.jeecg.common.constant.DataBaseConstant;
@ -32,6 +34,22 @@ import org.jeecg.common.system.vo.SysUserCacheInfo;
import org.jeecg.common.util.DateUtils;
import org.jeecg.common.util.SpringContextUtils;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.config.security.self.SelfAuthenticationProvider;
import org.jeecg.config.security.self.SelfAuthenticationToken;
import org.jeecg.config.security.utils.SecureUtil;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.core.*;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AccessTokenAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContextHolder;
import org.springframework.security.oauth2.server.authorization.token.DefaultOAuth2TokenContext;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenContext;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
/**
* @Author Scott
@ -45,6 +63,8 @@ public class JwtUtil {
public static final long EXPIRE_TIME = (7 * 12) * 60 * 60 * 1000;
static final String WELL_NUMBER = SymbolConstant.WELL_NUMBER + SymbolConstant.LEFT_CURLY_BRACKET;
public static final String DEFAULT_CLIENT = "jeecg-client";
/**
*
* @param response
@ -80,10 +100,9 @@ public class JwtUtil {
public static boolean verify(String token, String username, String secret) {
try {
// 根据密码生成JWT效验器
Algorithm algorithm = Algorithm.HMAC256(secret);
JWTVerifier verifier = JWT.require(algorithm).withClaim("username", username).build();
JwtDecoder jwtDecoder = SpringContextUtils.getBean(JwtDecoder.class);
// 效验TOKEN
DecodedJWT jwt = verifier.verify(token);
jwtDecoder.decode(token);
return true;
} catch (Exception e) {
log.error(e.getMessage(), e);
@ -107,17 +126,25 @@ public class JwtUtil {
}
/**
* 生成签名,5min后过期
* 生成token
*
* @param username 用户名
* @param secret 用户的密码
* @return 加密的token
*/
public static String sign(String username, String secret) {
Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
Algorithm algorithm = Algorithm.HMAC256(secret);
// 附带username信息
return JWT.create().withClaim("username", username).withExpiresAt(date).sign(algorithm);
Map<String, Object> additionalParameter = new HashMap<>();
additionalParameter.put("username", username);
RegisteredClientRepository registeredClientRepository = SpringContextUtils.getBean(RegisteredClientRepository.class);
SelfAuthenticationProvider selfAuthenticationProvider = SpringContextUtils.getBean(SelfAuthenticationProvider.class);
OAuth2ClientAuthenticationToken client = new OAuth2ClientAuthenticationToken(Objects.requireNonNull(registeredClientRepository.findByClientId("jeecg-client")), ClientAuthenticationMethod.CLIENT_SECRET_BASIC, null);
client.setAuthenticated(true);
SelfAuthenticationToken selfAuthenticationToken = new SelfAuthenticationToken(client, additionalParameter);
selfAuthenticationToken.setAuthenticated(true);
OAuth2AccessTokenAuthenticationToken accessToken = (OAuth2AccessTokenAuthenticationToken) selfAuthenticationProvider.authenticate(selfAuthenticationToken);
return accessToken.getAccessToken().getTokenValue();
}
@ -182,7 +209,7 @@ public class JwtUtil {
//2.通过shiro获取登录用户信息
LoginUser sysUser = null;
try {
sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();
sysUser = SecureUtil.currentUser();
} catch (Exception e) {
log.warn("SecurityUtils.getSubject() 获取用户信息异常:" + e.getMessage());
}

View File

@ -1,22 +1,13 @@
package org.jeecg.common.system.util;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.metadata.OrderItem;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.constant.CommonConstant;
import org.jeecg.common.constant.DataBaseConstant;
import org.jeecg.common.constant.SymbolConstant;
import org.jeecg.common.exception.JeecgBootException;
import org.jeecg.common.system.query.QueryGenerator;
import org.jeecg.common.system.query.QueryRuleEnum;
import org.jeecg.common.util.CommonUtils;
import org.jeecg.common.util.oConvertUtils;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
/**
@ -248,47 +239,5 @@ public class SqlConcatUtil {
private static String getDbType() {
return CommonUtils.getDatabaseType();
}
/**
* 获取前端传过来的 "多字段排序信息: sortInfoString"
* @return
*/
public static List<OrderItem> getQueryConditionOrders(String column, String order, String queryInfoString){
List<OrderItem> list = new ArrayList<>();
if(oConvertUtils.isEmpty(queryInfoString)){
//默认以创建时间倒序查询
if(CommonConstant.ORDER_TYPE_DESC.equalsIgnoreCase(order)){
list.add(OrderItem.desc(column));
}else{
list.add(OrderItem.asc(column));
}
}else{
// 【TV360X-967】URL解码微服务下需要
if (queryInfoString.contains("%22column%22")) {
log.info("queryInfoString 原生 = {}", queryInfoString);
try {
queryInfoString = URLDecoder.decode(queryInfoString, "UTF-8");
} catch (UnsupportedEncodingException e) {
throw new JeecgBootException(e);
}
log.info("queryInfoString 解码 = {}", queryInfoString);
}
JSONArray array = JSONArray.parseArray(queryInfoString);
Iterator it = array.iterator();
while(it.hasNext()){
JSONObject json = (JSONObject)it.next();
String tempColumn = json.getString("column");
if(oConvertUtils.isNotEmpty(tempColumn)){
String tempOrder = json.getString("order");
if(CommonConstant.ORDER_TYPE_DESC.equalsIgnoreCase(tempOrder)){
list.add(OrderItem.desc(tempColumn));
}else{
list.add(OrderItem.asc(tempColumn));
}
}
}
}
return list;
}
}

View File

@ -1,13 +1,18 @@
package org.jeecg.common.system.vo;
import com.alibaba.fastjson2.JSON;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import org.jeecg.common.desensitization.annotation.SensitiveField;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.io.Serializable;
import java.util.Date;
import java.util.Set;
/**
* <p>
@ -20,8 +25,10 @@ import java.util.Date;
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
public class LoginUser {
public class LoginUser implements Serializable {
private static final long serialVersionUID = -7143159031677245866L;
/**
* 登录人id
*/
@ -68,12 +75,6 @@ public class LoginUser {
@SensitiveField
private String avatar;
/**
* 工号
*/
@SensitiveField
private String workNo;
/**
* 生日
*/
@ -144,4 +145,29 @@ public class LoginUser {
/**设备id uniapp推送用*/
private String clientId;
@SensitiveField
private String salt;
@Override
public String toString() {
// 重新构建对象过滤一些敏感字段
LoginUser loginUser = new LoginUser();
loginUser.setId(id);
loginUser.setUsername(username);
loginUser.setRealname(realname);
loginUser.setOrgCode(orgCode);
loginUser.setSex(sex);
loginUser.setEmail(email);
loginUser.setPhone(phone);
loginUser.setDelFlag(delFlag);
loginUser.setStatus(status);
loginUser.setActivitiSync(activitiSync);
loginUser.setUserIdentity(userIdentity);
loginUser.setDepartIds(departIds);
loginUser.setPost(post);
loginUser.setTelephone(telephone);
loginUser.setRelTenantIds(relTenantIds);
loginUser.setClientId(clientId);
return JSON.toJSONString(loginUser);
}
}

View File

@ -8,7 +8,6 @@ import org.jeecg.common.constant.ServiceNameConstants;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
@ -17,7 +16,6 @@ import org.springframework.web.context.request.ServletRequestAttributes;
* @Description: spring上下文工具类
* @author: jeecg-boot
*/
@Lazy(false)
@Component
public class SpringContextUtils implements ApplicationContextAware {

View File

@ -16,13 +16,7 @@ import java.util.regex.Pattern;
* @author zhoujf
*/
@Slf4j
public class SqlInjectionUtil {
/**
* sql注入黑名单数据库名
*/
public final static String XSS_STR_TABLE = "peformance_schema|information_schema";
public class SqlInjectionUtil {
/**
* 默认—sql注入关键词
*/
@ -173,28 +167,7 @@ public class SqlInjectionUtil {
}
return false;
}
/**
* 判断是否存在SQL注入关键词字符串
*
* @param keyword
* @return
*/
@SuppressWarnings("AlibabaUndefineMagicConstant")
private static boolean isExistSqlInjectTableKeyword(String sql, String keyword) {
// 需要匹配的sql注入关键词
String[] matchingTexts = new String[]{"`" + keyword, "(" + keyword, "(`" + keyword};
for (String matchingText : matchingTexts) {
String[] checkTexts = new String[]{" " + matchingText, "from" + matchingText};
for (String checkText : checkTexts) {
if (sql.contains(checkText)) {
return true;
}
}
}
return false;
}
/**
* sql注入过滤处理遇到注入关键字抛异常
*
@ -235,14 +208,6 @@ public class SqlInjectionUtil {
throw new JeecgSqlInjectionException(SqlInjectionUtil.SQL_INJECTION_TIP + value);
}
}
String[] xssTableArr = XSS_STR_TABLE.split("\\|");
for (String xssTableStr : xssTableArr) {
if (isExistSqlInjectTableKeyword(value, xssTableStr)) {
log.error(SqlInjectionUtil.SQL_INJECTION_KEYWORD_TIP, xssTableStr);
log.error(SqlInjectionUtil.SQL_INJECTION_TIP_VARIABLE, value);
throw new JeecgSqlInjectionException(SqlInjectionUtil.SQL_INJECTION_TIP + value);
}
}
// 三、SQL注入检测存在绕过风险 (正则校验)
for (String regularOriginal : XSS_REGULAR_STR_ARRAY) {
@ -279,14 +244,6 @@ public class SqlInjectionUtil {
throw new JeecgSqlInjectionException(SqlInjectionUtil.SQL_INJECTION_TIP + value);
}
}
String[] xssTableArr = XSS_STR_TABLE.split("\\|");
for (String xssTableStr : xssTableArr) {
if (isExistSqlInjectTableKeyword(value, xssTableStr)) {
log.error(SqlInjectionUtil.SQL_INJECTION_KEYWORD_TIP, xssTableStr);
log.error(SqlInjectionUtil.SQL_INJECTION_TIP_VARIABLE, value);
throw new JeecgSqlInjectionException(SqlInjectionUtil.SQL_INJECTION_TIP + value);
}
}
// 三、SQL注入检测存在绕过风险 (正则校验)
for (String regularOriginal : XSS_REGULAR_STR_ARRAY) {

View File

@ -1,5 +1,6 @@
package org.jeecg.common.util;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.jeecg.common.api.CommonAPI;
@ -11,8 +12,6 @@ import org.jeecg.common.exception.JeecgBoot401Exception;
import org.jeecg.common.system.util.JwtUtil;
import org.jeecg.common.system.vo.LoginUser;
import jakarta.servlet.http.HttpServletRequest;
/**
* @Author scott
* @Date 2019/9/23 14:12
@ -106,8 +105,8 @@ public class TokenUtils {
}
// 查询用户信息
LoginUser user = TokenUtils.getLoginUser(username, commonApi, redisUtil);
//LoginUser user = commonApi.getUserByName(username);
//LoginUser user = TokenUtils.getLoginUser(username, commonApi, redisUtil);
LoginUser user = commonApi.getUserByName(username);
if (user == null) {
throw new JeecgBoot401Exception("用户不存在!");
}
@ -158,10 +157,11 @@ public class TokenUtils {
//【重要】此处通过redis原生获取缓存用户是为了解决微服务下system服务挂了其他服务互调不通问题---
if (redisUtil.hasKey(loginUserKey)) {
try {
loginUser = (LoginUser) redisUtil.get(loginUserKey);
Object obj = redisUtil.get(loginUserKey);
loginUser = (LoginUser) obj;
//解密用户
SensitiveInfoUtil.handlerObject(loginUser, false);
} catch (IllegalAccessException e) {
} catch (Exception e) {
e.printStackTrace();
}
} else {

View File

@ -68,13 +68,6 @@ public class DbTypeUtils {
return dbTypeIf(dbType, DbType.ORACLE, DbType.ORACLE_12C, DbType.DM);
}
/**
* 是否是达梦
*/
public static boolean dbTypeIsDm(DbType dbType) {
return dbTypeIf(dbType, DbType.DM);
}
public static boolean dbTypeIsSqlServer(DbType dbType) {
return dbTypeIf(dbType, DbType.SQL_SERVER, DbType.SQL_SERVER2005);
}

View File

@ -1,9 +1,9 @@
package org.jeecg.common.util.encryption;
import org.apache.shiro.lang.codec.Base64;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;
/**
* @Description: AES 加密
@ -48,7 +48,7 @@ public class AesEncryptUtil {
cipher.init(Cipher.ENCRYPT_MODE, keyspec, ivspec);
byte[] encrypted = cipher.doFinal(plaintext);
return Base64.encodeToString(encrypted);
return Base64.getEncoder().encodeToString(encrypted);
} catch (Exception e) {
e.printStackTrace();
@ -66,7 +66,7 @@ public class AesEncryptUtil {
*/
public static String desEncrypt(String data, String key, String iv) throws Exception {
//update-begin-author:taoyan date:2022-5-23 for:VUEN-1084 【vue3】online表单测试发现的新问题 6、解密报错 ---解码失败应该把异常抛出去,在外面处理
byte[] encrypted1 = Base64.decode(data);
byte[] encrypted1 = Base64.getDecoder().decode(data);
Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
SecretKeySpec keyspec = new SecretKeySpec(key.getBytes(), "AES");

View File

@ -34,7 +34,6 @@ public class SsrfFileTypeFilter {
FILE_TYPE_WHITE_LIST.add("bmp");
FILE_TYPE_WHITE_LIST.add("svg");
FILE_TYPE_WHITE_LIST.add("ico");
FILE_TYPE_WHITE_LIST.add("heic");
//文本文件
FILE_TYPE_WHITE_LIST.add("txt");

View File

@ -7,7 +7,6 @@ import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.jeecg.common.constant.CommonConstant;
import org.jeecg.common.constant.SymbolConstant;
import org.jeecg.config.mybatis.MybatisPlusSaasConfig;
import org.springframework.beans.BeanUtils;
import jakarta.servlet.http.HttpServletRequest;
@ -1134,14 +1133,5 @@ public class oConvertUtils {
public static <T> boolean isIn(T obj, T... objs) {
return isIn(obj, objs);
}
/**
* 判断租户ID是否有效
* @param tenantId
* @return
*/
public static boolean isEffectiveTenant(String tenantId) {
return MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL && isNotEmpty(tenantId) && !("0").equals(tenantId);
}
}

View File

@ -3,8 +3,6 @@ package org.jeecg.common.util.security;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.jeecg.common.exception.JeecgSqlInjectionException;
import org.jeecg.common.util.SqlInjectionUtil;
import org.jeecg.common.util.oConvertUtils;
import java.util.*;
import java.util.regex.Matcher;
@ -68,8 +66,6 @@ public abstract class AbstractQueryBlackListHandler {
if(flag == false){
return false;
}
Set<String> xssTableSet = new HashSet<>(Arrays.asList(SqlInjectionUtil.XSS_STR_TABLE.split("\\|")));
for (QueryTable table : list) {
String name = table.getName();
String fieldRule = ruleMap.get(name);
@ -85,16 +81,6 @@ public abstract class AbstractQueryBlackListHandler {
}
}
// 判断是否调用了黑名单数据库
String dbName = table.getDbName();
if (oConvertUtils.isNotEmpty(dbName)) {
dbName = dbName.toLowerCase().trim();
if (xssTableSet.contains(dbName)) {
flag = false;
log.warn("sql黑名单校验数据库【" + dbName + "】禁止查询");
break;
}
}
}
// 返回黑名单校验结果(不合法直接抛出异常)
@ -149,8 +135,6 @@ public abstract class AbstractQueryBlackListHandler {
* 查询的表的信息
*/
protected class QueryTable {
//数据库名
private String dbName;
//表名
private String name;
//表的别名
@ -174,14 +158,6 @@ public abstract class AbstractQueryBlackListHandler {
this.fields.add(field);
}
public String getDbName() {
return dbName;
}
public void setDbName(String dbName) {
this.dbName = dbName;
}
public String getName() {
return name;
}

View File

@ -40,10 +40,6 @@ public class JeecgBaseConfig {
*/
private Firewall firewall;
/**
* shiro拦截排除
*/
private Shiro shiro;
/**
* 上传文件配置
*/
@ -74,12 +70,7 @@ public class JeecgBaseConfig {
/**
* 百度开放API配置
*/
private BaiduApi baiduApi;
/**
* 高德开放API配置
*/
private GaoDeApi gaoDeApi;
private BaiduApi baiduApi;
public String getCustomResourcePrefixPath() {
return customResourcePrefixPath;
@ -113,14 +104,6 @@ public class JeecgBaseConfig {
this.signatureSecret = signatureSecret;
}
public Shiro getShiro() {
return shiro;
}
public void setShiro(Shiro shiro) {
this.shiro = shiro;
}
public Path getPath() {
return path;
}
@ -177,11 +160,4 @@ public class JeecgBaseConfig {
this.baiduApi = baiduApi;
}
public GaoDeApi getGaoDeApi() {
return gaoDeApi;
}
public void setGaoDeApi(GaoDeApi gaoDeApi) {
this.gaoDeApi = gaoDeApi;
}
}

View File

@ -1,9 +1,8 @@
//package org.jeecg.config;
//
//
//import io.swagger.v3.oas.annotations.Operation;
//import com.github.xiaoymin.knife4j.spring.annotations.EnableKnife4j;
//import org.jeecg.common.constant.CommonConstant;
//import org.jeecg.config.mybatis.MybatisPlusSaasConfig;
//import org.springframework.beans.BeansException;
//import org.springframework.beans.factory.config.BeanPostProcessor;
//import org.springframework.context.annotation.Bean;
@ -19,13 +18,15 @@
//import springfox.documentation.builders.ParameterBuilder;
//import springfox.documentation.builders.PathSelectors;
//import springfox.documentation.builders.RequestHandlerSelectors;
//import springfox.documentation.oas.annotations.EnableOpenApi;
//import springfox.documentation.schema.ModelRef;
//import springfox.documentation.service.*;
//import springfox.documentation.spi.DocumentationType;
//import springfox.documentation.spi.service.contexts.SecurityContext;
//import springfox.documentation.spring.web.plugins.Docket;
//import springfox.documentation.spring.web.plugins.WebFluxRequestHandlerProvider;
//import springfox.documentation.spring.web.plugins.WebMvcRequestHandlerProvider;
//import springfox.documentation.swagger2.annotations.EnableSwagger2WebMvc;
//import springfox.documentation.swagger2.annotations.EnableSwagger2;
//
//import java.lang.reflect.Field;
//import java.util.ArrayList;
@ -37,7 +38,8 @@
// * @Author scott
// */
//@Configuration
//@EnableSwagger2WebMvc
//@EnableSwagger2 //开启 Swagger2
//@EnableKnife4j //开启 knife4j可以不写
//@Import(BeanValidatorPluginsConfiguration.class)
//public class Swagger2Config implements WebMvcConfigurer {
//
@ -95,14 +97,6 @@
// List<Parameter> pars = new ArrayList<>();
// tokenPar.name(CommonConstant.X_ACCESS_TOKEN).description("token").modelRef(new ModelRef("string")).parameterType("header").required(false).build();
// pars.add(tokenPar.build());
// //update-begin-author:liusq---date:2024-08-15--for: 开启多租户时全局参数增加租户id
// if(MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL){
// ParameterBuilder tenantPar = new ParameterBuilder();
// tenantPar.name(CommonConstant.TENANT_ID).description("租户ID").modelRef(new ModelRef("string")).parameterType("header").required(false).build();
// pars.add(tenantPar.build());
// }
// //update-end-author:liusq---date:2024-08-15--for: 开启多租户时全局参数增加租户id
//
// return pars;
// }
//
@ -157,7 +151,7 @@
//
// @Override
// public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
// if (bean instanceof WebMvcRequestHandlerProvider) {
// if (bean instanceof WebMvcRequestHandlerProvider || bean instanceof WebFluxRequestHandlerProvider) {
// customizeSpringfoxHandlerMappings(getHandlerMappings(bean));
// }
// return bean;

View File

@ -10,13 +10,11 @@ import io.swagger.v3.oas.models.security.SecurityRequirement;
import io.swagger.v3.oas.models.security.SecurityScheme;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.constant.CommonConstant;
import org.springdoc.core.customizers.OperationCustomizer;
import org.springdoc.core.customizers.GlobalOpenApiCustomizer;
import org.springdoc.core.filters.GlobalOpenApiMethodFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@ -63,71 +61,42 @@ public class Swagger3Config implements WebMvcConfigurer {
}
@Bean
public OperationCustomizer operationCustomizer() {
return (operation, handlerMethod) -> {
String path = getFullPath(handlerMethod);
if (!isExcludedPath(path)) {
operation.addSecurityItem(new SecurityRequirement().addList(CommonConstant.X_ACCESS_TOKEN));
}else{
log.info("忽略加入 X_ACCESS_TOKEN 的 PATH:" + path);
public GlobalOpenApiCustomizer globalOpenApiCustomizer() {
return openApi -> {
// 全局添加鉴权参数
if (openApi.getPaths() != null) {
openApi.getPaths().forEach((path, pathItem) -> {
//log.debug("path: {}", path);
// 检查当前路径是否在排除列表中
boolean isExcluded = excludedPaths.stream().anyMatch(excludedPath ->
excludedPath.equals(path) ||
(excludedPath.endsWith("**") && path.startsWith(excludedPath.substring(0, excludedPath.length() - 2)))
);
if (!isExcluded) {
// 接口添加鉴权参数
pathItem.readOperations()
.forEach(operation ->
operation.addSecurityItem(new SecurityRequirement().addList(CommonConstant.X_ACCESS_TOKEN))
);
}
});
}
return operation;
};
}
private String getFullPath(HandlerMethod handlerMethod) {
StringBuilder fullPath = new StringBuilder();
// 获取类级别的路径
RequestMapping classMapping = handlerMethod.getBeanType().getAnnotation(RequestMapping.class);
if (classMapping != null && classMapping.value().length > 0) {
fullPath.append(classMapping.value()[0]);
}
// 获取方法级别的路径
RequestMapping methodMapping = handlerMethod.getMethodAnnotation(RequestMapping.class);
if (methodMapping != null && methodMapping.value().length > 0) {
String methodPath = methodMapping.value()[0];
// 确保路径正确拼接,处理斜杠
if (!fullPath.toString().endsWith("/") && !methodPath.startsWith("/")) {
fullPath.append("/");
}
fullPath.append(methodPath);
}
return fullPath.toString();
}
private boolean isExcludedPath(String path) {
return excludedPaths.stream()
.anyMatch(pattern -> {
if (pattern.endsWith("/**")) {
// 处理通配符匹配
String basePath = pattern.substring(0, pattern.length() - 3);
return path.startsWith(basePath);
}
// 精确匹配
return pattern.equals(path);
});
}
@Bean
public OpenAPI customOpenAPI() {
return new OpenAPI()
.info(new Info()
.title("JeecgBoot 后台服务API接口文档")
.version("3.8.2")
.version("3.8.0")
.contact(new Contact().name("北京国炬信息技术有限公司").url("www.jeccg.com").email("jeecgos@163.com"))
.description("后台API接口")
.description( "后台API接口")
.termsOfService("NO terms of service")
.license(new License().name("Apache 2.0").url("http://www.apache.org/licenses/LICENSE-2.0.html")))
.addSecurityItem(new SecurityRequirement().addList(CommonConstant.X_ACCESS_TOKEN))
.components(new Components().addSecuritySchemes(CommonConstant.X_ACCESS_TOKEN,
new SecurityScheme()
.name(CommonConstant.X_ACCESS_TOKEN)
.type(SecurityScheme.Type.APIKEY)
.in(SecurityScheme.In.HEADER) // 关键:指定为 header
));
new SecurityScheme().name(CommonConstant.X_ACCESS_TOKEN).type(SecurityScheme.Type.HTTP)));
}
}

View File

@ -15,15 +15,15 @@ import jakarta.annotation.Resource;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.boot.actuate.web.exchanges.InMemoryHttpExchangeRepository;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.jackson.JacksonProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.context.event.EventListener;
import org.springframework.http.CacheControl;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
@ -61,14 +61,6 @@ public class WebMvcConfiguration implements WebMvcConfigurer {
@Autowired(required = false)
private PrometheusMeterRegistry prometheusMeterRegistry;
/**
* meterRegistryPostProcessor
* for [QQYUN-12558]【监控】系统监控的头两个tab不好使接口404
*/
@Autowired(required = false)
@Qualifier("meterRegistryPostProcessor")
private BeanPostProcessor meterRegistryPostProcessor;
/**
* 静态资源的配置 - 使得可以从磁盘中读取 Html、图片、视频、音频等
*/
@ -91,7 +83,7 @@ public class WebMvcConfiguration implements WebMvcConfigurer {
*/
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/").setViewName("redirect:/doc.html");
registry.addViewController("/").setViewName("doc.html");
}
@Bean
@ -157,17 +149,12 @@ public class WebMvcConfiguration implements WebMvcConfigurer {
/**
* 监听应用启动完成事件,确保 PrometheusMeterRegistry 已经初始化
* for [QQYUN-12558]【监控】系统监控的头两个tab不好使接口404
* @param event
* @author chenrui
* @date 2025/5/26 16:46
* 解决metrics端点不显示jvm信息的问题(zyf)
*/
@EventListener
public void onApplicationReady(ApplicationReadyEvent event) {
if(null != meterRegistryPostProcessor){
meterRegistryPostProcessor.postProcessAfterInitialization(prometheusMeterRegistry, "");
}
@Bean
@ConditionalOnBean(name = "meterRegistryPostProcessor")
InitializingBean forcePrometheusPostProcessor(BeanPostProcessor meterRegistryPostProcessor) {
return () -> meterRegistryPostProcessor.postProcessAfterInitialization(prometheusMeterRegistry, "");
}
// /**

View File

@ -8,14 +8,12 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class LowCodeModeConfiguration implements WebMvcConfigurer {
private final LowCodeModeInterceptor lowCodeModeInterceptor;
public LowCodeModeConfiguration(LowCodeModeInterceptor lowCodeModeInterceptor) {
this.lowCodeModeInterceptor = lowCodeModeInterceptor;
public LowCodeModeInterceptor payInterceptor() {
return new LowCodeModeInterceptor();
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(lowCodeModeInterceptor).addPathPatterns(LowCodeUrlsEnum.getLowCodeInterceptUrls());
registry.addInterceptor(payInterceptor()).addPathPatterns(LowCodeUrlsEnum.getLowCodeInterceptUrls());
}
}

View File

@ -2,7 +2,6 @@ package org.jeecg.config.firewall.interceptor;
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.SecurityUtils;
import org.jeecg.common.api.CommonAPI;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.constant.CommonConstant;
@ -11,7 +10,8 @@ import org.jeecg.common.system.vo.LoginUser;
import org.jeecg.common.util.CommonUtils;
import org.jeecg.common.util.SpringContextUtils;
import org.jeecg.config.JeecgBaseConfig;
import org.springframework.stereotype.Component;
import org.jeecg.config.security.utils.SecureUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.servlet.HandlerInterceptor;
import jakarta.annotation.Resource;
@ -38,7 +38,6 @@ import java.util.Set;
* @date 20230904
*/
@Slf4j
@Component
public class LowCodeModeInterceptor implements HandlerInterceptor {
/**
* 低代码开发模式
@ -48,27 +47,23 @@ public class LowCodeModeInterceptor implements HandlerInterceptor {
@Resource
private JeecgBaseConfig jeecgBaseConfig;
@Autowired
private CommonAPI commonAPI;
/**
* 在请求处理之前进行调用
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
CommonAPI commonAPI = null;
log.info("低代码模式,拦截请求路径:" + request.getRequestURI());
//1、验证是否开启低代码开发模式控制
if (jeecgBaseConfig == null) {
jeecgBaseConfig = SpringContextUtils.getBean(JeecgBaseConfig.class);
}
if (commonAPI == null) {
commonAPI = SpringContextUtils.getBean(CommonAPI.class);
}
if (jeecgBaseConfig.getFirewall()!=null && LowCodeModeInterceptor.LOW_CODE_MODE_PROD.equals(jeecgBaseConfig.getFirewall().getLowCodeMode())) {
String requestURI = request.getRequestURI().substring(request.getContextPath().length());
log.info("低代码模式,拦截请求路径:" + requestURI);
LoginUser loginUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();
LoginUser loginUser = SecureUtil.currentUser();
Set<String> hasRoles = null;
if (loginUser == null) {
loginUser = commonAPI.getUserByName(JwtUtil.getUserNameByToken(SpringContextUtils.getHttpServletRequest()));

View File

@ -6,13 +6,13 @@ import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.plugin.*;
import org.apache.shiro.SecurityUtils;
import org.jeecg.common.config.TenantContext;
import org.jeecg.common.constant.TenantConstant;
import org.jeecg.common.system.vo.LoginUser;
import org.jeecg.common.util.SpringContextUtils;
import org.jeecg.common.util.TokenUtils;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.config.security.utils.SecureUtil;
import org.springframework.stereotype.Component;
import java.lang.reflect.Field;
@ -192,7 +192,7 @@ public class MybatisInterceptor implements Interceptor {
private LoginUser getLoginUser() {
LoginUser sysUser = null;
try {
sysUser = SecurityUtils.getSubject().getPrincipal() != null ? (LoginUser) SecurityUtils.getSubject().getPrincipal() : null;
sysUser = SecureUtil.currentUser() != null ? SecureUtil.currentUser() : null;
} catch (Exception e) {
//e.printStackTrace();
sysUser = null;

View File

@ -1,16 +1,11 @@
package org.jeecg.config.mybatis;
import java.util.ArrayList;
import java.util.List;
import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler;
import com.baomidou.mybatisplus.extension.plugins.inner.DynamicTableNameInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
import com.baomidou.mybatisplus.extension.toolkit.JdbcUtils;
import lombok.extern.slf4j.Slf4j;
import me.zhyd.oauth.log.Log;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.LongValue;
import org.jeecg.common.config.TenantContext;
@ -20,27 +15,24 @@ import org.jeecg.common.util.SpringContextUtils;
import org.jeecg.common.util.TokenUtils;
import org.jeecg.common.util.oConvertUtils;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.sql.DataSource;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
/**
* 单数据源配置jeecg.datasource.open = false时生效
* @Author zhoujf
*
*/
@Slf4j
@Configuration
@MapperScan(value={"org.jeecg.**.mapper*"})
public class MybatisPlusSaasConfig {
@Autowired
private DataSource dataSource;
/**
* 是否开启系统模块的租户隔离
* 控制范围:用户、角色、部门、我的部门、字典、分类字典、多数据源、职务、通知公告
@ -130,23 +122,7 @@ public class MybatisPlusSaasConfig {
//update-begin-author:zyf date:20220425 for:【VUEN-606】注入动态表名适配拦截器解决多表名问题
interceptor.addInnerInterceptor(dynamicTableNameInnerInterceptor());
//update-end-author:zyf date:20220425 for:【VUEN-606】注入动态表名适配拦截器解决多表名问题
//update-begin---author:scott ---date:2025-08-02 for【issues/8666】升级mybatisPlus后SqlServer分页使用OFFSET ROWS FETCH NEXT ROWS ONLY导致online报表报错---
DbType dbType = null;
try {
dbType = JdbcUtils.getDbType(dataSource.getConnection().getMetaData().getURL());
log.info("当前数据库类型: {}", dbType);
} catch (SQLException e) {
Log.error(e.getMessage(), e);
}
if (dbType!=null && (dbType == DbType.SQL_SERVER || dbType == DbType.SQL_SERVER2005)) {
// 如果是SQL Server则覆盖为2005分页方式
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.SQL_SERVER2005));
} else {
interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
}
//update-end---author:scott ---date::2025-08-02 for【issues/8666】升级mybatisPlus后SqlServer分页使用OFFSET ROWS FETCH NEXT ROWS ONLY导致online报表报错---
interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
//【jeecg-boot/issues/3847】增加@Version乐观锁支持
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
return interceptor;

View File

@ -0,0 +1,90 @@
package org.jeecg.config.security;
import lombok.AllArgsConstructor;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.settings.OAuth2TokenFormat;
import org.springframework.security.oauth2.server.authorization.settings.TokenSettings;
import org.springframework.stereotype.Component;
import java.time.Duration;
import java.util.Set;
/**
* spring authorization server 注册客户端便捷工具类
* @author eightmonth@qq.com
* @date 2024/3/7 11:22
*/
@Component
@AllArgsConstructor
public class ClientService {
private RegisteredClientRepository registeredClientRepository;
/**
* 修改客户端token有效期
* 认证码、设备码有效期与accessToken有效期保持一致
*/
public void updateTokenValidation(String clientId, Long accessTokenValidation, Long refreshTokenValidation){
RegisteredClient registeredClient = findByClientId(clientId);
RegisteredClient.Builder builder = RegisteredClient.from(registeredClient);
TokenSettings tokenSettings = TokenSettings.builder()
.idTokenSignatureAlgorithm(SignatureAlgorithm.RS256)
.accessTokenTimeToLive(Duration.ofSeconds(accessTokenValidation))
.accessTokenFormat(OAuth2TokenFormat.SELF_CONTAINED)
.reuseRefreshTokens(true)
.refreshTokenTimeToLive(Duration.ofSeconds(refreshTokenValidation))
.authorizationCodeTimeToLive(Duration.ofSeconds(accessTokenValidation))
.deviceCodeTimeToLive(Duration.ofSeconds(accessTokenValidation))
.build();
builder.tokenSettings(tokenSettings);
registeredClientRepository.save(builder.build());
}
/**
* 修改客户端授权类型
* @param clientId
* @param grantTypes
*/
public void updateGrantType(String clientId, Set<AuthorizationGrantType> grantTypes) {
RegisteredClient registeredClient = findByClientId(clientId);
RegisteredClient.Builder builder = RegisteredClient.from(registeredClient);
for (AuthorizationGrantType grantType : grantTypes) {
builder.authorizationGrantType(grantType);
}
registeredClientRepository.save(builder.build());
}
/**
* 修改客户端重定向uri
* @param clientId
* @param redirectUris
*/
public void updateRedirectUris(String clientId, String redirectUris) {
RegisteredClient registeredClient = findByClientId(clientId);
RegisteredClient.Builder builder = RegisteredClient.from(registeredClient);
builder.redirectUri(redirectUris);
registeredClientRepository.save(builder.build());
}
/**
* 修改客户端授权范围
* @param clientId
* @param scopes
*/
public void updateScopes(String clientId, Set<String> scopes) {
RegisteredClient registeredClient = findByClientId(clientId);
RegisteredClient.Builder builder = RegisteredClient.from(registeredClient);
for (String scope : scopes) {
builder.scope(scope);
}
registeredClientRepository.save(builder.build());
}
public RegisteredClient findByClientId(String clientId) {
return registeredClientRepository.findByClientId(clientId);
}
}

View File

@ -0,0 +1,45 @@
package org.jeecg.config.security;
import io.undertow.servlet.spec.HttpServletRequestImpl;
import io.undertow.util.HttpString;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
/**
* 复制仪盘表请求query体携带的token
* @author eightmonth
* @date 2024/7/3 14:04
*/
@Component
@Order(value = Integer.MIN_VALUE)
public class CopyTokenFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 以下为undertow定制代码如切换其它servlet容器需要同步更换
HttpServletRequestImpl undertowRequest = (HttpServletRequestImpl) request;
String token = request.getHeader("Authorization");
if (StringUtils.hasText(token)) {
undertowRequest.getExchange().getRequestHeaders().remove("Authorization");
undertowRequest.getExchange().getRequestHeaders().add(new HttpString("Authorization"), "bearer " + token);
} else {
String bearerToken = request.getParameter("token");
String headerBearerToken = request.getHeader("X-Access-Token");
if (StringUtils.hasText(bearerToken)) {
undertowRequest.getExchange().getRequestHeaders().add(new HttpString("Authorization"), "bearer " + bearerToken);
} else if (StringUtils.hasText(headerBearerToken)) {
undertowRequest.getExchange().getRequestHeaders().add(new HttpString("Authorization"), "bearer " + headerBearerToken);
}
}
filterChain.doFilter(undertowRequest, response);
}
}

View File

@ -0,0 +1,34 @@
package org.jeecg.config.security;
import org.jeecg.common.api.CommonAPI;
import org.jeecg.common.system.vo.LoginUser;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.core.convert.converter.Converter;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
/**
* token只存储用户名与过期时间
* 这里通过取用户名转全量用户信息存储到Security中
* @author eightmonth@qq.com
* @date 2024/7/15 11:05
*/
@Component
public class JeecgAuthenticationConvert implements Converter<Jwt, AbstractAuthenticationToken> {
@Lazy
@Autowired
private CommonAPI commonAPI;
@Override
public AbstractAuthenticationToken convert(Jwt source) {
String username = source.getClaims().get("username").toString();
LoginUser loginUser = commonAPI.getUserByName(username);
return new UsernamePasswordAuthenticationToken(loginUser, null, new ArrayList<>());
}
}

View File

@ -0,0 +1,135 @@
package org.jeecg.config.security;
import org.jeecg.common.system.util.JwtUtil;
import org.springframework.lang.Nullable;
import org.springframework.security.oauth2.core.ClaimAccessor;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
import org.springframework.security.oauth2.jwt.JwsHeader;
import org.springframework.security.oauth2.jwt.JwtClaimsSet;
import org.springframework.security.oauth2.jwt.JwtEncoder;
import org.springframework.security.oauth2.jwt.JwtEncoderParameters;
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationGrantAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.token.*;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import java.time.Duration;
import java.time.Instant;
import java.time.temporal.Temporal;
import java.time.temporal.TemporalUnit;
import java.util.Collections;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
/**
* @author eightmonth@qq.com
* @date 2024/7/11 17:10
*/
public class JeecgOAuth2AccessTokenGenerator implements OAuth2TokenGenerator<OAuth2AccessToken> {
private final JwtEncoder jwtEncoder;
private OAuth2TokenCustomizer<OAuth2TokenClaimsContext> accessTokenCustomizer;
public JeecgOAuth2AccessTokenGenerator(JwtEncoder jwtEncoder) {
this.jwtEncoder = jwtEncoder;
}
@Nullable
@Override
public OAuth2AccessToken generate(OAuth2TokenContext context) {
if (!OAuth2TokenType.ACCESS_TOKEN.equals(context.getTokenType())) {
return null;
}
String issuer = null;
if (context.getAuthorizationServerContext() != null) {
issuer = context.getAuthorizationServerContext().getIssuer();
}
RegisteredClient registeredClient = context.getRegisteredClient();
Instant issuedAt = Instant.now();
Instant expiresAt = issuedAt.plusMillis(JwtUtil.EXPIRE_TIME);
OAuth2TokenClaimsSet.Builder claimsBuilder = OAuth2TokenClaimsSet.builder();
if (StringUtils.hasText(issuer)) {
claimsBuilder.issuer(issuer);
}
claimsBuilder
.subject(context.getPrincipal().getName())
.audience(Collections.singletonList(registeredClient.getClientId()))
.issuedAt(issuedAt)
.expiresAt(expiresAt)
.notBefore(issuedAt)
.id(UUID.randomUUID().toString());
if (!CollectionUtils.isEmpty(context.getAuthorizedScopes())) {
claimsBuilder.claim(OAuth2ParameterNames.SCOPE, context.getAuthorizedScopes());
}
if (this.accessTokenCustomizer != null) {
OAuth2TokenClaimsContext.Builder accessTokenContextBuilder = OAuth2TokenClaimsContext.with(claimsBuilder)
.registeredClient(context.getRegisteredClient())
.principal(context.getPrincipal())
.authorizationServerContext(context.getAuthorizationServerContext())
.authorizedScopes(context.getAuthorizedScopes())
.tokenType(context.getTokenType())
.authorizationGrantType(context.getAuthorizationGrantType());
if (context.getAuthorization() != null) {
accessTokenContextBuilder.authorization(context.getAuthorization());
}
if (context.getAuthorizationGrant() != null) {
accessTokenContextBuilder.authorizationGrant(context.getAuthorizationGrant());
}
OAuth2TokenClaimsContext accessTokenContext = accessTokenContextBuilder.build();
this.accessTokenCustomizer.customize(accessTokenContext);
}
OAuth2TokenClaimsSet accessTokenClaimsSet = claimsBuilder.build();
OAuth2AuthorizationGrantAuthenticationToken oAuth2ResourceOwnerBaseAuthenticationToken = context.getAuthorizationGrant();
String username = (String) oAuth2ResourceOwnerBaseAuthenticationToken.getAdditionalParameters().get("username");
String tokenValue = jwtEncoder.encode(JwtEncoderParameters.from(JwsHeader.with(SignatureAlgorithm.ES256).keyId("jeecg").build(),
JwtClaimsSet.builder().claim("username", username).expiresAt(expiresAt).build())).getTokenValue();
//此处可以做改造将tokenValue随机数换成用户信息方便后续多系统token互通认证通过解密token得到username
return new OAuth2AccessTokenClaims(OAuth2AccessToken.TokenType.BEARER, tokenValue,
accessTokenClaimsSet.getIssuedAt(), accessTokenClaimsSet.getExpiresAt(), context.getAuthorizedScopes(),
accessTokenClaimsSet.getClaims());
}
/**
* Sets the {@link OAuth2TokenCustomizer} that customizes the
* {@link OAuth2TokenClaimsContext#getClaims() claims} for the
* {@link OAuth2AccessToken}.
* @param accessTokenCustomizer the {@link OAuth2TokenCustomizer} that customizes the
* claims for the {@code OAuth2AccessToken}
*/
public void setAccessTokenCustomizer(OAuth2TokenCustomizer<OAuth2TokenClaimsContext> accessTokenCustomizer) {
Assert.notNull(accessTokenCustomizer, "accessTokenCustomizer cannot be null");
this.accessTokenCustomizer = accessTokenCustomizer;
}
private static final class OAuth2AccessTokenClaims extends OAuth2AccessToken implements ClaimAccessor {
private final Map<String, Object> claims;
private OAuth2AccessTokenClaims(TokenType tokenType, String tokenValue, Instant issuedAt, Instant expiresAt,
Set<String> scopes, Map<String, Object> claims) {
super(tokenType, tokenValue, issuedAt, expiresAt, scopes);
this.claims = claims;
}
@Override
public Map<String, Object> getClaims() {
return this.claims;
}
}
}

View File

@ -0,0 +1,104 @@
package org.jeecg.config.security;
import cn.hutool.core.util.ArrayUtil;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.api.CommonAPI;
import org.jeecg.common.system.vo.LoginUser;
import org.jeecg.common.util.RedisUtil;
import org.jeecg.config.security.utils.SecureUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.util.PatternMatchUtils;
import org.springframework.util.StringUtils;
import java.util.Arrays;
import java.util.Objects;
import java.util.Set;
/**
* spring authorization server自定义权限处理根据@PreAuthorize注解判断当前用户是否具备权限
* @author EightMonth
* @date 2024/1/10 17:00
*/
@Service("jps")
@Slf4j
public class JeecgPermissionService {
private final String SPLIT = "::";
private final String PERM_PREFIX = "jps" + SPLIT;
@Lazy
@Autowired
private CommonAPI commonAPI;
@Autowired
private RedisUtil redisUtil;
/**
* 判断接口是否有任意xxxxxx权限
* @param permissions 权限
* @return {boolean}
*/
public boolean requiresPermissions(String... permissions) {
if (ArrayUtil.isEmpty(permissions)) {
return false;
}
LoginUser loginUser = SecureUtil.currentUser();
Object cache = redisUtil.get(buildKey("permission", loginUser.getId()));
Set<String> permissionList;
if (Objects.nonNull(cache)) {
permissionList = (Set<String>) cache;
} else {
permissionList = commonAPI.queryUserAuths(loginUser.getId());
redisUtil.set(buildKey("permission", loginUser.getId()), permissionList);
}
boolean pass = permissionList.stream().filter(StringUtils::hasText)
.anyMatch(x -> PatternMatchUtils.simpleMatch(permissions, x));
if (!pass) {
log.error("权限不足,缺少权限:"+ Arrays.toString(permissions));
}
return pass;
}
/**
* 判断接口是否有任意xxxxxx角色
* @param roles 角色
* @return {boolean}
*/
public boolean requiresRoles(String... roles) {
if (ArrayUtil.isEmpty(roles)) {
return false;
}
LoginUser loginUser = SecureUtil.currentUser();
Object cache = redisUtil.get(buildKey("role", loginUser.getUsername()));
Set<String> roleList;
if (Objects.nonNull(cache)) {
roleList = (Set<String>) cache;
} else {
roleList = commonAPI.queryUserRoles(loginUser.getUsername());
redisUtil.set(buildKey("role", loginUser.getUsername()), roleList);
}
boolean pass = roleList.stream().filter(StringUtils::hasText)
.anyMatch(x -> PatternMatchUtils.simpleMatch(roles, x));
if (!pass) {
log.error("权限不足,缺少角色:" + Arrays.toString(roles));
}
return pass;
}
/**
* 由于缓存key是以人的维度角色列表、权限列表在值中jeecg是以权限列表绑定在角色上形成的权限集合
* 权限发生变更时,需要清理全部人的权限缓存
*/
public void clearCache() {
redisUtil.removeAll(PERM_PREFIX);
}
private String buildKey(String type, String username) {
return PERM_PREFIX + type + SPLIT + username;
}
}

View File

@ -0,0 +1,54 @@
package org.jeecg.config.security;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsent;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import java.util.concurrent.TimeUnit;
/**
* spring authorization server 自定义redis保存授权范围信息
*/
@Component
@RequiredArgsConstructor
public class JeecgRedisOAuth2AuthorizationConsentService implements OAuth2AuthorizationConsentService {
private final RedisTemplate<String, Object> redisTemplate;
private final static Long TIMEOUT = 10L;
@Override
public void save(OAuth2AuthorizationConsent authorizationConsent) {
Assert.notNull(authorizationConsent, "authorizationConsent cannot be null");
redisTemplate.opsForValue().set(buildKey(authorizationConsent), authorizationConsent, TIMEOUT,
TimeUnit.MINUTES);
}
@Override
public void remove(OAuth2AuthorizationConsent authorizationConsent) {
Assert.notNull(authorizationConsent, "authorizationConsent cannot be null");
redisTemplate.delete(buildKey(authorizationConsent));
}
@Override
public OAuth2AuthorizationConsent findById(String registeredClientId, String principalName) {
Assert.hasText(registeredClientId, "registeredClientId cannot be empty");
Assert.hasText(principalName, "principalName cannot be empty");
return (OAuth2AuthorizationConsent) redisTemplate.opsForValue()
.get(buildKey(registeredClientId, principalName));
}
private static String buildKey(String registeredClientId, String principalName) {
return "token:consent:" + registeredClientId + ":" + principalName;
}
private static String buildKey(OAuth2AuthorizationConsent authorizationConsent) {
return buildKey(authorizationConsent.getRegisteredClientId(), authorizationConsent.getPrincipalName());
}
}

View File

@ -0,0 +1,192 @@
package org.jeecg.config.security;
import cn.hutool.core.collection.CollUtil;
import jakarta.annotation.PostConstruct;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.lang.Nullable;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationCode;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.TimeUnit;
/**
* spring authorization server自定义redis保存认证信息
* @author EightMonth
*/
@Component
public class JeecgRedisOAuth2AuthorizationService implements OAuth2AuthorizationService{
private final static Long TIMEOUT = 10L;
private static final String AUTHORIZATION = "token";
private final RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
@Autowired
private RedisConnectionFactory redisConnectionFactory;
/**
* 因为保存sas的认证信息至redis无法使用jeecg对redisTemplate的某些设置。
* 如果在使用时修改redisTemplate属性会发生线程安全问题最终容易引起系统无法正常运行。
* 所以重新建了一个redis client给到sas操作redis并且该redis实例不注入spring 容器中
*/
@PostConstruct
public void initSasRedis() {
redisTemplate.setValueSerializer(RedisSerializer.java());
redisTemplate.setConnectionFactory(redisConnectionFactory);
redisTemplate.afterPropertiesSet();
}
@Override
public void save(OAuth2Authorization authorization) {
Assert.notNull(authorization, "authorization cannot be null");
if (isState(authorization)) {
String token = authorization.getAttribute("state");
redisTemplate.opsForValue().set(buildKey(OAuth2ParameterNames.STATE, token), authorization, TIMEOUT,
TimeUnit.MINUTES);
}
if (isCode(authorization)) {
OAuth2Authorization.Token<OAuth2AuthorizationCode> authorizationCode = authorization
.getToken(OAuth2AuthorizationCode.class);
OAuth2AuthorizationCode authorizationCodeToken = authorizationCode.getToken();
long between = ChronoUnit.MINUTES.between(authorizationCodeToken.getIssuedAt(),
authorizationCodeToken.getExpiresAt());
redisTemplate.opsForValue().set(buildKey(OAuth2ParameterNames.CODE, authorizationCodeToken.getTokenValue()),
authorization, between, TimeUnit.MINUTES);
}
if (isRefreshToken(authorization)) {
OAuth2RefreshToken refreshToken = authorization.getRefreshToken().getToken();
long between = ChronoUnit.SECONDS.between(refreshToken.getIssuedAt(), refreshToken.getExpiresAt());
redisTemplate.opsForValue().set(buildKey(OAuth2ParameterNames.REFRESH_TOKEN, refreshToken.getTokenValue()),
authorization, between, TimeUnit.SECONDS);
}
if (isAccessToken(authorization)) {
OAuth2AccessToken accessToken = authorization.getAccessToken().getToken();
long between = ChronoUnit.SECONDS.between(accessToken.getIssuedAt(), accessToken.getExpiresAt());
redisTemplate.opsForValue().set(buildKey(OAuth2ParameterNames.ACCESS_TOKEN, accessToken.getTokenValue()),
authorization, between, TimeUnit.SECONDS);
// 扩展记录 access-token 、username 的关系 1::token::username::admin::xxx
String tokenUsername = String.format("%s::%s::%s", AUTHORIZATION, authorization.getPrincipalName(), accessToken.getTokenValue());
redisTemplate.opsForValue().set(tokenUsername, accessToken.getTokenValue(), between, TimeUnit.SECONDS);
}
}
@Override
public void remove(OAuth2Authorization authorization) {
Assert.notNull(authorization, "authorization cannot be null");
List<String> keys = new ArrayList<>();
if (isState(authorization)) {
String token = authorization.getAttribute("state");
keys.add(buildKey(OAuth2ParameterNames.STATE, token));
}
if (isCode(authorization)) {
OAuth2Authorization.Token<OAuth2AuthorizationCode> authorizationCode = authorization
.getToken(OAuth2AuthorizationCode.class);
OAuth2AuthorizationCode authorizationCodeToken = authorizationCode.getToken();
keys.add(buildKey(OAuth2ParameterNames.CODE, authorizationCodeToken.getTokenValue()));
}
if (isRefreshToken(authorization)) {
OAuth2RefreshToken refreshToken = authorization.getRefreshToken().getToken();
keys.add(buildKey(OAuth2ParameterNames.REFRESH_TOKEN, refreshToken.getTokenValue()));
}
if (isAccessToken(authorization)) {
OAuth2AccessToken accessToken = authorization.getAccessToken().getToken();
keys.add(buildKey(OAuth2ParameterNames.ACCESS_TOKEN, accessToken.getTokenValue()));
// 扩展记录 access-token 、username 的关系 1::token::username::admin::xxx
String key = String.format("%s::%s::%s", AUTHORIZATION, authorization.getPrincipalName(), accessToken.getTokenValue());
keys.add(key);
}
redisTemplate.delete(keys);
}
@Override
@Nullable
public OAuth2Authorization findById(String id) {
throw new UnsupportedOperationException();
}
@Override
@Nullable
public OAuth2Authorization findByToken(String token, @Nullable OAuth2TokenType tokenType) {
Assert.hasText(token, "token cannot be empty");
Assert.notNull(tokenType, "tokenType cannot be empty");
return (OAuth2Authorization) redisTemplate.opsForValue().get(buildKey(tokenType.getValue(), token));
}
private String buildKey(String type, String id) {
return String.format("%s::%s::%s", AUTHORIZATION, type, id);
}
private static boolean isState(OAuth2Authorization authorization) {
return Objects.nonNull(authorization.getAttribute("state"));
}
private static boolean isCode(OAuth2Authorization authorization) {
OAuth2Authorization.Token<OAuth2AuthorizationCode> authorizationCode = authorization
.getToken(OAuth2AuthorizationCode.class);
return Objects.nonNull(authorizationCode);
}
private static boolean isRefreshToken(OAuth2Authorization authorization) {
return Objects.nonNull(authorization.getRefreshToken());
}
private static boolean isAccessToken(OAuth2Authorization authorization) {
return Objects.nonNull(authorization.getAccessToken());
}
/**
* 扩展方法根据 username 查询是否存在存储的
* @param authentication
* @return
*/
public void removeByUsername(Authentication authentication) {
// 根据 username查询对应access-token
String authenticationName = authentication.getName();
// 扩展记录 access-token 、username 的关系 1::token::username::admin::xxx
String tokenUsernameKey = String.format("%s::%s::*", AUTHORIZATION, authenticationName);
Set<String> keys = redisTemplate.keys(tokenUsernameKey);
if (CollUtil.isEmpty(keys)) {
return;
}
List<Object> tokenList = redisTemplate.opsForValue().multiGet(keys);
for (Object token : tokenList) {
// 根据token 查询存储的 OAuth2Authorization
OAuth2Authorization authorization = this.findByToken((String) token, OAuth2TokenType.ACCESS_TOKEN);
// 根据 OAuth2Authorization 删除相关令牌
this.remove(authorization);
}
}
}

View File

@ -0,0 +1,38 @@
package org.jeecg.config.security;
/**
* 登录模式
* @author EightMonth
* @date 2024/1/10 17:43
*/
public class LoginType {
/**
* 密码模式
*/
public static final String PASSWORD = "password";
/**
* 手机号+验证码模式
*/
public static final String PHONE = "phone";
/**
* app登录
*/
public static final String APP = "app";
/**
* 扫码登录
*/
public static final String SCAN = "scan";
/**
* 所有联合登录比如github\钉钉\企业微信\微信
*/
public static final String SOCIAL = "social";
public static final String SELF = "self";
}

View File

@ -0,0 +1,49 @@
package org.jeecg.config.security;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.AllArgsConstructor;
import org.jeecg.common.system.util.JwtUtil;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
import org.springframework.security.oauth2.server.resource.BearerTokenErrors;
import org.springframework.security.oauth2.server.resource.web.DefaultBearerTokenResolver;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import java.util.Objects;
/**
* 当用户被强退时使客户端token失效
* @author eightmonth@qq.com
* @date 2024/3/7 17:30
*/
@Component
@AllArgsConstructor
public class RedisTokenValidationFilter extends OncePerRequestFilter {
private OAuth2AuthorizationService authorizationService;
private JwtDecoder jwtDecoder;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 从请求中获取token
DefaultBearerTokenResolver defaultBearerTokenResolver = new DefaultBearerTokenResolver();
String token = defaultBearerTokenResolver.resolve(request);
if (Objects.nonNull(token)) {
// 检查认证信息是否已被清除如果已被清除则令该token失效
OAuth2Authorization oAuth2Authorization = authorizationService.findByToken(token, OAuth2TokenType.ACCESS_TOKEN);
if (Objects.isNull(oAuth2Authorization)) {
throw new OAuth2AuthenticationException(BearerTokenErrors.invalidToken("认证信息已失效,请重新登录"));
}
}
filterChain.doFilter(request, response);
}
}

View File

@ -0,0 +1,249 @@
package org.jeecg.config.security;
import com.nimbusds.jose.jwk.Curve;
import com.nimbusds.jose.jwk.ECKey;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import lombok.AllArgsConstructor;
import lombok.SneakyThrows;
import org.jeecg.config.security.app.AppGrantAuthenticationConvert;
import org.jeecg.config.security.app.AppGrantAuthenticationProvider;
import org.jeecg.config.security.password.PasswordGrantAuthenticationConvert;
import org.jeecg.config.security.password.PasswordGrantAuthenticationProvider;
import org.jeecg.config.security.phone.PhoneGrantAuthenticationConvert;
import org.jeecg.config.security.phone.PhoneGrantAuthenticationProvider;
import org.jeecg.config.security.social.SocialGrantAuthenticationConvert;
import org.jeecg.config.security.social.SocialGrantAuthenticationProvider;
import org.jeecg.config.shiro.ignore.InMemoryIgnoreAuth;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.http.MediaType;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer;
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
import org.springframework.security.oauth2.server.authorization.token.*;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.MediaTypeRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.CollectionUtils;
import org.springframework.web.cors.CorsConfiguration;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.SecureRandom;
import java.security.interfaces.ECPrivateKey;
import java.security.interfaces.ECPublicKey;
import java.util.Arrays;
import java.util.stream.Collectors;
/**
* spring authorization server核心配置
* @author eightmonth@qq.com
* @date 2024/1/2 9:29
*/
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
@AllArgsConstructor
public class SecurityConfig {
private JdbcTemplate jdbcTemplate;
private OAuth2AuthorizationService authorizationService;
private JeecgAuthenticationConvert jeecgAuthenticationConvert;
@Bean
@Order(1)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http)
throws Exception {
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
// 注册自定义登录类型
http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
.tokenEndpoint(tokenEndpoint -> tokenEndpoint.accessTokenRequestConverter(new PasswordGrantAuthenticationConvert())
.authenticationProvider(new PasswordGrantAuthenticationProvider(authorizationService, tokenGenerator())))
.tokenEndpoint(tokenEndpoint -> tokenEndpoint.accessTokenRequestConverter(new PhoneGrantAuthenticationConvert())
.authenticationProvider(new PhoneGrantAuthenticationProvider(authorizationService, tokenGenerator())))
.tokenEndpoint(tokenEndpoint -> tokenEndpoint.accessTokenRequestConverter(new AppGrantAuthenticationConvert())
.authenticationProvider(new AppGrantAuthenticationProvider(authorizationService, tokenGenerator())))
.tokenEndpoint(tokenEndpoint -> tokenEndpoint.accessTokenRequestConverter(new SocialGrantAuthenticationConvert())
.authenticationProvider(new SocialGrantAuthenticationProvider(authorizationService, tokenGenerator())))
//开启OpenID Connect 1.0其中oidc为OpenID Connect的缩写。 访问 /.well-known/openid-configuration即可获取认证信息
.oidc(Customizer.withDefaults());
http
//将需要认证的请求重定向到login页面行登录认证。
.exceptionHandling((exceptions) -> exceptions
.defaultAuthenticationEntryPointFor(
new LoginUrlAuthenticationEntryPoint("/sys/login"),
new MediaTypeRequestMatcher(MediaType.TEXT_HTML)
)
);
return http.build();
}
@Bean
@Order(2)
public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http)
throws Exception {
http
//设置所有请求都需要认证未认证的请求都被重定向到login页面进行登录
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers(InMemoryIgnoreAuth.get().stream().map(AntPathRequestMatcher::antMatcher).toList().toArray(new AntPathRequestMatcher[0])).permitAll()
.requestMatchers(AntPathRequestMatcher.antMatcher("/sys/cas/client/validateLogin")).permitAll()
.requestMatchers(AntPathRequestMatcher.antMatcher("/sys/randomImage/**")).permitAll()
.requestMatchers(AntPathRequestMatcher.antMatcher("/sys/checkCaptcha")).permitAll()
.requestMatchers(AntPathRequestMatcher.antMatcher("/sys/login")).permitAll()
.requestMatchers(AntPathRequestMatcher.antMatcher("/sys/mLogin")).permitAll()
.requestMatchers(AntPathRequestMatcher.antMatcher("/sys/logout")).permitAll()
.requestMatchers(AntPathRequestMatcher.antMatcher("/sys/thirdLogin/**")).permitAll()
.requestMatchers(AntPathRequestMatcher.antMatcher("/sys/getEncryptedString")).permitAll()
.requestMatchers(AntPathRequestMatcher.antMatcher("/sys/sms")).permitAll()
.requestMatchers(AntPathRequestMatcher.antMatcher("/sys/phoneLogin")).permitAll()
.requestMatchers(AntPathRequestMatcher.antMatcher("/sys/user/checkOnlyUser")).permitAll()
.requestMatchers(AntPathRequestMatcher.antMatcher("/sys/user/register")).permitAll()
.requestMatchers(AntPathRequestMatcher.antMatcher("/sys/user/phoneVerification")).permitAll()
.requestMatchers(AntPathRequestMatcher.antMatcher("/sys/user/passwordChange")).permitAll()
.requestMatchers(AntPathRequestMatcher.antMatcher("/auth/2step-code")).permitAll()
.requestMatchers(AntPathRequestMatcher.antMatcher("/sys/common/static/**")).permitAll()
.requestMatchers(AntPathRequestMatcher.antMatcher("/sys/common/pdf/**")).permitAll()
.requestMatchers(AntPathRequestMatcher.antMatcher("/generic/**")).permitAll()
.requestMatchers(AntPathRequestMatcher.antMatcher("/sys/getLoginQrcode/**")).permitAll()
.requestMatchers(AntPathRequestMatcher.antMatcher("/sys/getQrcodeToken/**")).permitAll()
.requestMatchers(AntPathRequestMatcher.antMatcher("/sys/checkAuth")).permitAll()
.requestMatchers(AntPathRequestMatcher.antMatcher("/")).permitAll()
.requestMatchers(AntPathRequestMatcher.antMatcher("/doc.html")).permitAll()
.requestMatchers(AntPathRequestMatcher.antMatcher("/**/*.js")).permitAll()
.requestMatchers(AntPathRequestMatcher.antMatcher("/**/*.css")).permitAll()
.requestMatchers(AntPathRequestMatcher.antMatcher("/**/*.html")).permitAll()
.requestMatchers(AntPathRequestMatcher.antMatcher("/**/*.svg")).permitAll()
.requestMatchers(AntPathRequestMatcher.antMatcher("/**/*.pdf")).permitAll()
.requestMatchers(AntPathRequestMatcher.antMatcher("/**/*.jpg")).permitAll()
.requestMatchers(AntPathRequestMatcher.antMatcher("/**/*.png")).permitAll()
.requestMatchers(AntPathRequestMatcher.antMatcher("/**/*.gif")).permitAll()
.requestMatchers(AntPathRequestMatcher.antMatcher("/**/*.ico")).permitAll()
.requestMatchers(AntPathRequestMatcher.antMatcher("/**/*.ttf")).permitAll()
.requestMatchers(AntPathRequestMatcher.antMatcher("/**/*.woff")).permitAll()
.requestMatchers(AntPathRequestMatcher.antMatcher("/**/*.woff2")).permitAll()
.requestMatchers(AntPathRequestMatcher.antMatcher("/druid/**")).permitAll()
.requestMatchers(AntPathRequestMatcher.antMatcher("/swagger-ui.html")).permitAll()
.requestMatchers(AntPathRequestMatcher.antMatcher("/swagger**/**")).permitAll()
.requestMatchers(AntPathRequestMatcher.antMatcher("/webjars/**")).permitAll()
.requestMatchers(AntPathRequestMatcher.antMatcher("/v3/**")).permitAll()
.requestMatchers(AntPathRequestMatcher.antMatcher("/WW_verify*")).permitAll()
.requestMatchers(AntPathRequestMatcher.antMatcher("/sys/annountCement/show/**")).permitAll()
.requestMatchers(AntPathRequestMatcher.antMatcher("/jmreport/**")).permitAll()
.requestMatchers(AntPathRequestMatcher.antMatcher("/**/*.js.map")).permitAll()
.requestMatchers(AntPathRequestMatcher.antMatcher("/**/*.css.map")).permitAll()
.requestMatchers(AntPathRequestMatcher.antMatcher("/api/getUserInfo")).permitAll()
.requestMatchers(AntPathRequestMatcher.antMatcher("/drag/list")).permitAll()
.requestMatchers(AntPathRequestMatcher.antMatcher("/drag/view")).permitAll()
.requestMatchers(AntPathRequestMatcher.antMatcher("/drag/onlDragDatasetHead/getLoginUser")).permitAll()
.requestMatchers(AntPathRequestMatcher.antMatcher("/drag/page/queryById")).permitAll()
.requestMatchers(AntPathRequestMatcher.antMatcher("/drag/onlDragDatasetHead/getAllChartData")).permitAll()
.requestMatchers(AntPathRequestMatcher.antMatcher("/drag/onlDragDatasetHead/getTotalData")).permitAll()
.requestMatchers(AntPathRequestMatcher.antMatcher("/drag/mock/json/**")).permitAll()
.requestMatchers(AntPathRequestMatcher.antMatcher("/test/bigScreen/**")).permitAll()
.requestMatchers(AntPathRequestMatcher.antMatcher("/bigscreen/visual/**")).permitAll()
.requestMatchers(AntPathRequestMatcher.antMatcher("/bigscreen/category/**")).permitAll()
.requestMatchers(AntPathRequestMatcher.antMatcher("/bigscreen/map/**")).permitAll()
.requestMatchers(AntPathRequestMatcher.antMatcher("/bigscreen/template1/**")).permitAll()
.requestMatchers(AntPathRequestMatcher.antMatcher("/bigscreen/template1/**")).permitAll()
.requestMatchers(AntPathRequestMatcher.antMatcher("/websocket/**")).permitAll()
.requestMatchers(AntPathRequestMatcher.antMatcher("/newsWebsocket/**")).permitAll()
.requestMatchers(AntPathRequestMatcher.antMatcher("/vxeSocket/**")).permitAll()
.requestMatchers(AntPathRequestMatcher.antMatcher("/test/seata/**")).permitAll()
.requestMatchers(AntPathRequestMatcher.antMatcher("/error")).permitAll()
.anyRequest().authenticated()
)
.headers(headers -> headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable))
.cors(cors -> cors
.configurationSource(req -> {
CorsConfiguration config = new CorsConfiguration();
config.applyPermitDefaultValues();
config.setAllowedMethods(Arrays.asList("HEAD", "GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"));
return config;
}))
.csrf(AbstractHttpConfigurer::disable)
.oauth2ResourceServer(oauth2 -> oauth2.jwt(jwt -> jwt.jwtAuthenticationConverter(jeecgAuthenticationConvert)));
return http.build();
}
/**
* 数据库保存注册客户端信息
*/
@Bean
public RegisteredClientRepository registeredClientRepository() {
return new JdbcRegisteredClientRepository(jdbcTemplate);
}
/**
*配置 JWK为JWT(id_token)提供加密密钥,用于加密/解密或签名/验签
* JWK详细见https://datatracker.ietf.org/doc/html/draft-ietf-jose-json-web-key-41
*/
@Bean
@SneakyThrows
public JWKSource<SecurityContext> jwkSource() {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("EC");
// 如果不设置secureRandom会存在一个问题当应用重启后原有的token将会全部失效因为重启的keyPair与之前已经不同
SecureRandom secureRandom = SecureRandom.getInstance("SHA1PRNG");
// 重要!生产环境需要修改!
secureRandom.setSeed("jeecg".getBytes());
keyPairGenerator.initialize(256, secureRandom);
KeyPair keyPair = keyPairGenerator.generateKeyPair();
ECPublicKey publicKey = (ECPublicKey) keyPair.getPublic();
ECPrivateKey privateKey = (ECPrivateKey) keyPair.getPrivate();
ECKey jwk = new ECKey.Builder(Curve.P_256, publicKey)
.privateKey(privateKey)
.keyID("jeecg")
.build();
JWKSet jwkSet = new JWKSet(jwk);
return new ImmutableJWKSet<>(jwkSet);
}
@Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
/**
* 配置jwt解析器
*/
@Bean
public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
}
/**
*配置token生成器
*/
@Bean
OAuth2TokenGenerator<?> tokenGenerator() {
JwtGenerator jwtGenerator = new JwtGenerator(new NimbusJwtEncoder(jwkSource()));
OAuth2AccessTokenGenerator accessTokenGenerator = new OAuth2AccessTokenGenerator();
OAuth2RefreshTokenGenerator refreshTokenGenerator = new OAuth2RefreshTokenGenerator();
return new DelegatingOAuth2TokenGenerator(
new JeecgOAuth2AccessTokenGenerator(new NimbusJwtEncoder(jwkSource())),
new OAuth2RefreshTokenGenerator()
);
}
}

View File

@ -0,0 +1,81 @@
package org.jeecg.config.security.app;
import jakarta.servlet.http.HttpServletRequest;
import org.jeecg.config.security.LoginType;
import org.jeecg.config.security.password.PasswordGrantAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.web.authentication.AuthenticationConverter;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
import java.util.HashMap;
import java.util.Map;
/**
* APP模式认证转换器
* @author EightMonth
* @date 2024/1/1
*/
public class AppGrantAuthenticationConvert implements AuthenticationConverter {
@Override
public Authentication convert(HttpServletRequest request) {
String grantType = request.getParameter(OAuth2ParameterNames.GRANT_TYPE);
if (!LoginType.APP.equals(grantType)) {
return null;
}
Authentication clientPrincipal = SecurityContextHolder.getContext().getAuthentication();
//从request中提取请求参数然后存入MultiValueMap<String, String>
MultiValueMap<String, String> parameters = getParameters(request);
// username (REQUIRED)
String username = parameters.getFirst(OAuth2ParameterNames.USERNAME);
if (!StringUtils.hasText(username) ||
parameters.get(OAuth2ParameterNames.USERNAME).size() != 1) {
throw new OAuth2AuthenticationException("无效请求,用户名不能为空!");
}
String password = parameters.getFirst(OAuth2ParameterNames.PASSWORD);
if (!StringUtils.hasText(password) ||
parameters.get(OAuth2ParameterNames.PASSWORD).size() != 1) {
throw new OAuth2AuthenticationException("无效请求,密码不能为空!");
}
//收集要传入PasswordGrantAuthenticationToken构造方法的参数
//该参数接下来在PasswordGrantAuthenticationProvider中使用
Map<String, Object> additionalParameters = new HashMap<>();
//遍历从request中提取的参数排除掉grant_type、client_id、code等字段参数其他参数收集到additionalParameters中
parameters.forEach((key, value) -> {
if (!key.equals(OAuth2ParameterNames.GRANT_TYPE) &&
!key.equals(OAuth2ParameterNames.CLIENT_ID) &&
!key.equals(OAuth2ParameterNames.CODE)) {
additionalParameters.put(key, value.get(0));
}
});
//返回自定义的PasswordGrantAuthenticationToken对象
return new PasswordGrantAuthenticationToken(clientPrincipal, additionalParameters);
}
/**
*从request中提取请求参数然后存入MultiValueMap<String, String>
*/
private static MultiValueMap<String, String> getParameters(HttpServletRequest request) {
Map<String, String[]> parameterMap = request.getParameterMap();
MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>(parameterMap.size());
parameterMap.forEach((key, values) -> {
if (values.length > 0) {
for (String value : values) {
parameters.add(key, value);
}
}
});
return parameters;
}
}

View File

@ -0,0 +1,320 @@
package org.jeecg.config.security.app;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.api.CommonAPI;
import org.jeecg.common.constant.CacheConstant;
import org.jeecg.common.constant.CommonConstant;
import org.jeecg.common.exception.JeecgBootException;
import org.jeecg.common.exception.JeecgCaptchaException;
import org.jeecg.common.system.vo.LoginUser;
import org.jeecg.common.system.vo.SysDepartModel;
import org.jeecg.common.util.Md5Util;
import org.jeecg.common.util.PasswordUtil;
import org.jeecg.common.util.RedisUtil;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.config.JeecgBaseConfig;
import org.jeecg.config.security.password.PasswordGrantAuthenticationToken;
import org.jeecg.modules.base.service.BaseCommonService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.http.HttpStatus;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.oauth2.core.*;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AccessTokenAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContextHolder;
import org.springframework.security.oauth2.server.authorization.token.DefaultOAuth2TokenContext;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenContext;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import java.security.Principal;
import java.time.Instant;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* APP模式认证处理器负责处理该认证模式下的核心逻辑
* @author EightMonth
* @date 2024/1/1
*/
@Slf4j
public class AppGrantAuthenticationProvider implements AuthenticationProvider {
private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-5.2";
private final OAuth2AuthorizationService authorizationService;
private final OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator;
@Lazy
@Autowired
private CommonAPI commonAPI;
@Autowired
private RedisUtil redisUtil;
@Autowired
private JeecgBaseConfig jeecgBaseConfig;
@Autowired
private BaseCommonService baseCommonService;
public AppGrantAuthenticationProvider(OAuth2AuthorizationService authorizationService, OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator) {
Assert.notNull(authorizationService, "authorizationService cannot be null");
Assert.notNull(tokenGenerator, "tokenGenerator cannot be null");
this.authorizationService = authorizationService;
this.tokenGenerator = tokenGenerator;
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
AppGrantAuthenticationToken appGrantAuthenticationToken = (AppGrantAuthenticationToken) authentication;
Map<String, Object> additionalParameter = appGrantAuthenticationToken.getAdditionalParameters();
// 授权类型
AuthorizationGrantType authorizationGrantType = appGrantAuthenticationToken.getGrantType();
// 用户名
String username = (String) additionalParameter.get(OAuth2ParameterNames.USERNAME);
// 密码
String password = (String) additionalParameter.get(OAuth2ParameterNames.PASSWORD);
//请求参数权限范围
String requestScopesStr = (String)additionalParameter.getOrDefault(OAuth2ParameterNames.SCOPE, "*");
//请求参数权限范围专场集合
Set<String> requestScopeSet = Stream.of(requestScopesStr.split(" ")).collect(Collectors.toSet());
// 验证码
String captcha = (String) additionalParameter.get("captcha");
String checkKey = (String) additionalParameter.get("checkKey");
OAuth2ClientAuthenticationToken clientPrincipal = getAuthenticatedClientElseThrowInvalidClient(appGrantAuthenticationToken);
RegisteredClient registeredClient = clientPrincipal.getRegisteredClient();
// 检查登录失败次数
if(isLoginFailOvertimes(username)){
Map<String, Object> map = new HashMap<>();
map.put("message", "该用户登录失败次数过多请于10分钟后再次登录");
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,"fdsafas", Instant.now(), Instant.now().plusNanos(1)), null, map);
}
if(captcha==null){
Map<String, Object> map = new HashMap<>();
map.put("message", "验证码无效");
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,"fdsafas", Instant.now(), Instant.now().plusNanos(1)), null, map);
}
String lowerCaseCaptcha = captcha.toLowerCase();
// 加入密钥作为混淆,避免简单的拼接,被外部利用,用户自定义该密钥即可
String origin = lowerCaseCaptcha+checkKey+jeecgBaseConfig.getSignatureSecret();
String realKey = Md5Util.md5Encode(origin, "utf-8");
Object checkCode = redisUtil.get(realKey);
//当进入登录页时,有一定几率出现验证码错误 #1714
if(checkCode==null || !checkCode.toString().equals(lowerCaseCaptcha)) {
Map<String, Object> map = new HashMap<>();
map.put("message", "验证码错误");
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,"fdsafas", Instant.now(), Instant.now().plusNanos(1)), null, map);
}
if (!registeredClient.getAuthorizationGrantTypes().contains(authorizationGrantType)) {
Map<String, Object> map = new HashMap<>();
map.put("message", "非法登录");
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,"fdsafas", Instant.now(), Instant.now().plusNanos(1)), null, map);
}
// 通过用户名获取用户信息
LoginUser loginUser = commonAPI.getUserByName(username);
//update-begin---author:eightmonth ---date:2024-04-30 for【6168】master分支切sas分支登录发生错误-----------
if (Objects.isNull(loginUser) || !StringUtils.hasText(loginUser.getSalt())) {
redisUtil.del(CacheConstant.SYS_USERS_CACHE+"::"+username);
loginUser = commonAPI.getUserByName(username);
}
//update-end---author:eightmonth ---date::2024-04-30 for【6168】master分支切sas分支登录发生错误--------------
// 检查用户可行性
checkUserIsEffective(loginUser);
// 不使用spring security passwordEncoder针对密码进行匹配使用自有加密匹配针对 spring security使用noop传输
password = PasswordUtil.encrypt(username, password, loginUser.getSalt());
if (!password.equals(loginUser.getPassword())) {
addLoginFailOvertimes(username);
Map<String, Object> map = new HashMap<>();
map.put("message", "用户名或密码不正确");
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,"fdsafas", Instant.now(), Instant.now().plusNanos(1)), null, map);
}
//由于在上面已验证过用户名、密码现在构建一个已认证的对象UsernamePasswordAuthenticationToken
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = UsernamePasswordAuthenticationToken.authenticated(loginUser,clientPrincipal,new ArrayList<>());
DefaultOAuth2TokenContext.Builder tokenContextBuilder = DefaultOAuth2TokenContext.builder()
.registeredClient(registeredClient)
.principal(usernamePasswordAuthenticationToken)
.authorizationServerContext(AuthorizationServerContextHolder.getContext())
.authorizationGrantType(authorizationGrantType)
.authorizedScopes(requestScopeSet)
.authorizationGrant(appGrantAuthenticationToken);
OAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization.withRegisteredClient(registeredClient)
.principalName(clientPrincipal.getName())
.authorizedScopes(requestScopeSet)
.attribute(Principal.class.getName(), username)
.authorizationGrantType(authorizationGrantType);
// ----- Access token -----
OAuth2TokenContext tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.ACCESS_TOKEN).build();
OAuth2Token generatedAccessToken = this.tokenGenerator.generate(tokenContext);
if (generatedAccessToken == null) {
Map<String, Object> map = new HashMap<>();
map.put("message", "无法生成访问token请联系管理系。");
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,"fdsafas", Instant.now(), Instant.now().plusNanos(1)), null, map);
}
OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
generatedAccessToken.getTokenValue(), generatedAccessToken.getIssuedAt(),
generatedAccessToken.getExpiresAt(), tokenContext.getAuthorizedScopes());
if (generatedAccessToken instanceof ClaimAccessor) {
authorizationBuilder.token(accessToken, (metadata) -> {
metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, ((ClaimAccessor) generatedAccessToken).getClaims());
});
} else {
authorizationBuilder.accessToken(accessToken);
}
// ----- Refresh token -----
OAuth2RefreshToken refreshToken = null;
if (registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.REFRESH_TOKEN) &&
// 不向公共客户端颁发刷新令牌
!clientPrincipal.getClientAuthenticationMethod().equals(ClientAuthenticationMethod.NONE)) {
tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.REFRESH_TOKEN).build();
OAuth2Token generatedRefreshToken = this.tokenGenerator.generate(tokenContext);
if (!(generatedRefreshToken instanceof OAuth2RefreshToken)) {
Map<String, Object> map = new HashMap<>();
map.put("message", "无法生成刷新token请联系管理员。");
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,"fdsafas", Instant.now(), Instant.now().plusNanos(1)), null, map);
}
refreshToken = (OAuth2RefreshToken) generatedRefreshToken;
authorizationBuilder.refreshToken(refreshToken);
}
OAuth2Authorization authorization = authorizationBuilder.build();
// 保存认证信息至redis
authorizationService.save(authorization);
// 登录成功删除redis中的验证码
redisUtil.del(realKey);
redisUtil.del(CommonConstant.LOGIN_FAIL + username);
baseCommonService.addLog("用户名: " + username + ",登录成功!", CommonConstant.LOG_TYPE_1, null,loginUser);
JSONObject addition = new JSONObject(new LinkedHashMap<>());
addition.put("token", accessToken.getTokenValue());
// 设置租户
JSONObject jsonObject = commonAPI.setLoginTenant(username);
addition.putAll(jsonObject.getInnerMap());
// 设置登录用户信息
addition.put("userInfo", loginUser);
addition.put("sysAllDictItems", commonAPI.queryAllDictItems());
List<SysDepartModel> departs = commonAPI.queryUserDeparts(loginUser.getId());
addition.put("departs", departs);
if (departs == null || departs.size() == 0) {
addition.put("multi_depart", 0);
} else if (departs.size() == 1) {
commonAPI.updateUserDepart(username, departs.get(0).getOrgCode(),null);
addition.put("multi_depart", 1);
} else {
//查询当前是否有登录部门
if(oConvertUtils.isEmpty(loginUser.getOrgCode())){
commonAPI.updateUserDepart(username, departs.get(0).getOrgCode(),null);
}
addition.put("multi_depart", 2);
}
// 兼容原有shiro登录结果处理
Map<String, Object> map = new HashMap<>();
map.put("result", addition);
map.put("code", 200);
map.put("success", true);
map.put("timestamp", System.currentTimeMillis());
// 返回access_token、refresh_token以及其它信息给到前端
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, accessToken, refreshToken, map);
}
@Override
public boolean supports(Class<?> authentication) {
return AppGrantAuthenticationToken.class.isAssignableFrom(authentication);
}
private static OAuth2ClientAuthenticationToken getAuthenticatedClientElseThrowInvalidClient(Authentication authentication) {
OAuth2ClientAuthenticationToken clientPrincipal = null;
if (OAuth2ClientAuthenticationToken.class.isAssignableFrom(authentication.getPrincipal().getClass())) {
clientPrincipal = (OAuth2ClientAuthenticationToken) authentication.getPrincipal();
}
if (clientPrincipal != null && clientPrincipal.isAuthenticated()) {
return clientPrincipal;
}
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_CLIENT);
}
/**
* 登录失败超出次数5 返回true
* @param username
* @return
*/
private boolean isLoginFailOvertimes(String username){
String key = CommonConstant.LOGIN_FAIL + username;
Object failTime = redisUtil.get(key);
if(failTime!=null){
Integer val = Integer.parseInt(failTime.toString());
if(val>5){
return true;
}
}
return false;
}
/**
* 记录登录失败次数
* @param username
*/
private void addLoginFailOvertimes(String username){
String key = CommonConstant.LOGIN_FAIL + username;
Object failTime = redisUtil.get(key);
Integer val = 0;
if(failTime!=null){
val = Integer.parseInt(failTime.toString());
}
// 10分钟
redisUtil.set(key, ++val, 10);
}
/**
* 校验用户是否有效
*/
private void checkUserIsEffective(LoginUser loginUser) {
//情况1根据用户信息查询该用户不存在
if (Objects.isNull(loginUser)) {
baseCommonService.addLog("用户登录失败,用户不存在!", CommonConstant.LOG_TYPE_1, null);
throw new JeecgBootException("该用户不存在,请注册");
}
//情况2根据用户信息查询该用户已注销
//update-begin---author:王帅 Date:20200601 forif条件永远为falsebug------------
if (CommonConstant.DEL_FLAG_1.equals(loginUser.getDelFlag())) {
//update-end---author:王帅 Date:20200601 forif条件永远为falsebug------------
baseCommonService.addLog("用户登录失败,用户名:" + loginUser.getUsername() + "已注销!", CommonConstant.LOG_TYPE_1, null);
throw new JeecgBootException("该用户已注销");
}
//情况3根据用户信息查询该用户已冻结
if (CommonConstant.USER_FREEZE.equals(loginUser.getStatus())) {
baseCommonService.addLog("用户登录失败,用户名:" + loginUser.getUsername() + "已冻结!", CommonConstant.LOG_TYPE_1, null);
throw new JeecgBootException("该用户已冻结");
}
}
}

View File

@ -0,0 +1,21 @@
package org.jeecg.config.security.app;
import org.jeecg.config.security.LoginType;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationGrantAuthenticationToken;
import java.util.Map;
/**
* APP模式认证专用token类型方法spring authorization server进行认证流转配合convert使用
* @author EightMonth
* @date 2024/1/1
*/
public class AppGrantAuthenticationToken extends OAuth2AuthorizationGrantAuthenticationToken {
public AppGrantAuthenticationToken(Authentication clientPrincipal, Map<String, Object> additionalParameters) {
super(new AuthorizationGrantType(LoginType.APP), clientPrincipal, additionalParameters);
}
}

View File

@ -0,0 +1,82 @@
package org.jeecg.config.security.password;
import jakarta.servlet.http.HttpServletRequest;
import org.jeecg.common.constant.CommonConstant;
import org.jeecg.config.security.LoginType;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.web.authentication.AuthenticationConverter;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
import java.util.HashMap;
import java.util.Map;
/**
* 密码模式认证转换器
* @author EightMonth
* @date 2024/1/1
*/
public class PasswordGrantAuthenticationConvert implements AuthenticationConverter {
@Override
public Authentication convert(HttpServletRequest request) {
String grantType = request.getParameter(OAuth2ParameterNames.GRANT_TYPE);
if (!LoginType.PASSWORD.equals(grantType)) {
return null;
}
Authentication clientPrincipal = SecurityContextHolder.getContext().getAuthentication();
//从request中提取请求参数然后存入MultiValueMap<String, String>
MultiValueMap<String, String> parameters = getParameters(request);
// username (REQUIRED)
String username = parameters.getFirst(OAuth2ParameterNames.USERNAME);
if (!StringUtils.hasText(username) ||
parameters.get(OAuth2ParameterNames.USERNAME).size() != 1) {
throw new OAuth2AuthenticationException("无效请求,用户名不能为空!");
}
String password = parameters.getFirst(OAuth2ParameterNames.PASSWORD);
if (!StringUtils.hasText(password) ||
parameters.get(OAuth2ParameterNames.PASSWORD).size() != 1) {
throw new OAuth2AuthenticationException("无效请求,密码不能为空!");
}
//收集要传入PasswordGrantAuthenticationToken构造方法的参数
//该参数接下来在PasswordGrantAuthenticationProvider中使用
Map<String, Object> additionalParameters = new HashMap<>();
//遍历从request中提取的参数排除掉grant_type、client_id、code等字段参数其他参数收集到additionalParameters中
parameters.forEach((key, value) -> {
if (!key.equals(OAuth2ParameterNames.GRANT_TYPE) &&
!key.equals(OAuth2ParameterNames.CLIENT_ID) &&
!key.equals(OAuth2ParameterNames.CODE)) {
additionalParameters.put(key, value.get(0));
}
});
//返回自定义的PasswordGrantAuthenticationToken对象
return new PasswordGrantAuthenticationToken(clientPrincipal, additionalParameters);
}
/**
*从request中提取请求参数然后存入MultiValueMap<String, String>
*/
private static MultiValueMap<String, String> getParameters(HttpServletRequest request) {
Map<String, String[]> parameterMap = request.getParameterMap();
MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>(parameterMap.size());
parameterMap.forEach((key, values) -> {
if (values.length > 0) {
for (String value : values) {
parameters.add(key, value);
}
}
});
return parameters;
}
}

View File

@ -0,0 +1,319 @@
package org.jeecg.config.security.password;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.api.CommonAPI;
import org.jeecg.common.constant.CacheConstant;
import org.jeecg.common.constant.CommonConstant;
import org.jeecg.common.exception.JeecgBootException;
import org.jeecg.common.exception.JeecgCaptchaException;
import org.jeecg.common.system.vo.LoginUser;
import org.jeecg.common.system.vo.SysDepartModel;
import org.jeecg.common.util.Md5Util;
import org.jeecg.common.util.PasswordUtil;
import org.jeecg.common.util.RedisUtil;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.config.JeecgBaseConfig;
import org.jeecg.modules.base.service.BaseCommonService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.http.HttpStatus;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.oauth2.core.*;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AccessTokenAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContextHolder;
import org.springframework.security.oauth2.server.authorization.token.DefaultOAuth2TokenContext;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenContext;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import java.security.Principal;
import java.time.Instant;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* 密码模式认证处理器,负责处理该认证模式下的核心逻辑
* @author EightMonth
* @date 2024/1/1
*/
@Slf4j
public class PasswordGrantAuthenticationProvider implements AuthenticationProvider {
private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-5.2";
private final OAuth2AuthorizationService authorizationService;
private final OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator;
@Lazy
@Autowired
private CommonAPI commonAPI;
@Autowired
private RedisUtil redisUtil;
@Autowired
private JeecgBaseConfig jeecgBaseConfig;
@Autowired
private BaseCommonService baseCommonService;
public PasswordGrantAuthenticationProvider(OAuth2AuthorizationService authorizationService, OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator) {
Assert.notNull(authorizationService, "authorizationService cannot be null");
Assert.notNull(tokenGenerator, "tokenGenerator cannot be null");
this.authorizationService = authorizationService;
this.tokenGenerator = tokenGenerator;
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
PasswordGrantAuthenticationToken passwordGrantAuthenticationToken = (PasswordGrantAuthenticationToken) authentication;
Map<String, Object> additionalParameter = passwordGrantAuthenticationToken.getAdditionalParameters();
// 授权类型
AuthorizationGrantType authorizationGrantType = passwordGrantAuthenticationToken.getGrantType();
// 用户名
String username = (String) additionalParameter.get(OAuth2ParameterNames.USERNAME);
// 密码
String password = (String) additionalParameter.get(OAuth2ParameterNames.PASSWORD);
//请求参数权限范围
String requestScopesStr = (String)additionalParameter.getOrDefault(OAuth2ParameterNames.SCOPE, "*");
//请求参数权限范围专场集合
Set<String> requestScopeSet = Stream.of(requestScopesStr.split(" ")).collect(Collectors.toSet());
// 验证码
String captcha = (String) additionalParameter.get("captcha");
String checkKey = (String) additionalParameter.get("checkKey");
OAuth2ClientAuthenticationToken clientPrincipal = getAuthenticatedClientElseThrowInvalidClient(passwordGrantAuthenticationToken);
RegisteredClient registeredClient = clientPrincipal.getRegisteredClient();
// 检查登录失败次数
if(isLoginFailOvertimes(username)){
throw new JeecgBootException("该用户登录失败次数过多请于10分钟后再次登录");
}
if(captcha==null){
throw new JeecgBootException("验证码无效");
}
String lowerCaseCaptcha = captcha.toLowerCase();
// 加入密钥作为混淆,避免简单的拼接,被外部利用,用户自定义该密钥即可
String origin = lowerCaseCaptcha+checkKey+jeecgBaseConfig.getSignatureSecret();
String realKey = Md5Util.md5Encode(origin, "utf-8");
Object checkCode = redisUtil.get(realKey);
//当进入登录页时,有一定几率出现验证码错误 #1714
if(checkCode==null || !checkCode.toString().equals(lowerCaseCaptcha)) {
Map<String, Object> map = new HashMap<>();
map.put("message", "验证码错误");
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,"fdsafas", Instant.now(), Instant.now().plusNanos(1)), null, map);
}
if (!registeredClient.getAuthorizationGrantTypes().contains(authorizationGrantType)) {
Map<String, Object> map = new HashMap<>();
map.put("message", "非法登录");
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,"fdsafas", Instant.now(), Instant.now().plusNanos(1)), null, map);
}
// 通过用户名获取用户信息
LoginUser loginUser = commonAPI.getUserByName(username);
//update-begin---author:eightmonth ---date:2024-04-30 for【6168】master分支切sas分支登录发生错误-----------
if (Objects.isNull(loginUser) || !StringUtils.hasText(loginUser.getSalt())) {
redisUtil.del(CacheConstant.SYS_USERS_CACHE+"::"+username);
loginUser = commonAPI.getUserByName(username);
}
//update-end---author:eightmonth ---date::2024-04-30 for【6168】master分支切sas分支登录发生错误--------------
// 检查用户可行性
checkUserIsEffective(loginUser);
// 不使用spring security passwordEncoder针对密码进行匹配使用自有加密匹配针对 spring security使用noop传输
password = PasswordUtil.encrypt(username, password, loginUser.getSalt());
if (!password.equals(loginUser.getPassword())) {
addLoginFailOvertimes(username);
Map<String, Object> map = new HashMap<>();
map.put("message", "用户名或密码不正确");
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,"fdsafas", Instant.now(), Instant.now().plusNanos(1)), null, map);
}
//由于在上面已验证过用户名、密码现在构建一个已认证的对象UsernamePasswordAuthenticationToken
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = UsernamePasswordAuthenticationToken.authenticated(loginUser,clientPrincipal,new ArrayList<>());
DefaultOAuth2TokenContext.Builder tokenContextBuilder = DefaultOAuth2TokenContext.builder()
.registeredClient(registeredClient)
.principal(usernamePasswordAuthenticationToken)
.authorizationServerContext(AuthorizationServerContextHolder.getContext())
.authorizationGrantType(authorizationGrantType)
.authorizedScopes(requestScopeSet)
.authorizationGrant(passwordGrantAuthenticationToken);
OAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization.withRegisteredClient(registeredClient)
.principalName(clientPrincipal.getName())
.authorizedScopes(requestScopeSet)
.attribute(Principal.class.getName(), username)
.authorizationGrantType(authorizationGrantType);
// ----- Access token -----
OAuth2TokenContext tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.ACCESS_TOKEN).build();
OAuth2Token generatedAccessToken = this.tokenGenerator.generate(tokenContext);
if (generatedAccessToken == null) {
Map<String, Object> map = new HashMap<>();
map.put("message", "无法生成访问token请联系管理系。");
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,"fdsafas", Instant.now(), Instant.now().plusNanos(1)), null, map);
}
OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
generatedAccessToken.getTokenValue(), generatedAccessToken.getIssuedAt(),
generatedAccessToken.getExpiresAt(), tokenContext.getAuthorizedScopes());
if (generatedAccessToken instanceof ClaimAccessor) {
authorizationBuilder.token(accessToken, (metadata) -> {
metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, ((ClaimAccessor) generatedAccessToken).getClaims());
});
} else {
authorizationBuilder.accessToken(accessToken);
}
// ----- Refresh token -----
OAuth2RefreshToken refreshToken = null;
if (registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.REFRESH_TOKEN) &&
// 不向公共客户端颁发刷新令牌
!clientPrincipal.getClientAuthenticationMethod().equals(ClientAuthenticationMethod.NONE)) {
tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.REFRESH_TOKEN).build();
OAuth2Token generatedRefreshToken = this.tokenGenerator.generate(tokenContext);
if (!(generatedRefreshToken instanceof OAuth2RefreshToken)) {
Map<String, Object> map = new HashMap<>();
map.put("message", "无法生成访问token请联系管理系。");
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,"fdsafas", Instant.now(), Instant.now().plusNanos(1)), null, map);
}
refreshToken = (OAuth2RefreshToken) generatedRefreshToken;
authorizationBuilder.refreshToken(refreshToken);
}
OAuth2Authorization authorization = authorizationBuilder.build();
// 保存认证信息至redis
authorizationService.save(authorization);
// 登录成功删除redis中的验证码
redisUtil.del(realKey);
redisUtil.del(CommonConstant.LOGIN_FAIL + username);
baseCommonService.addLog("用户名: " + username + ",登录成功!", CommonConstant.LOG_TYPE_1, null,loginUser);
JSONObject addition = new JSONObject(new LinkedHashMap<>());
addition.put("token", accessToken.getTokenValue());
// 设置租户
JSONObject jsonObject = commonAPI.setLoginTenant(username);
addition.putAll(jsonObject.getInnerMap());
// 设置登录用户信息
addition.put("userInfo", loginUser);
addition.put("sysAllDictItems", commonAPI.queryAllDictItems());
List<SysDepartModel> departs = commonAPI.queryUserDeparts(loginUser.getId());
addition.put("departs", departs);
if (departs == null || departs.size() == 0) {
addition.put("multi_depart", 0);
} else if (departs.size() == 1) {
commonAPI.updateUserDepart(username, departs.get(0).getOrgCode(),null);
addition.put("multi_depart", 1);
} else {
//查询当前是否有登录部门
if(oConvertUtils.isEmpty(loginUser.getOrgCode())){
commonAPI.updateUserDepart(username, departs.get(0).getOrgCode(),null);
}
addition.put("multi_depart", 2);
}
// 兼容原有shiro登录结果处理
Map<String, Object> map = new HashMap<>();
map.put("result", addition);
map.put("code", 200);
map.put("success", true);
map.put("timestamp", System.currentTimeMillis());
// 返回access_token、refresh_token以及其它信息给到前端
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, accessToken, refreshToken, map);
}
@Override
public boolean supports(Class<?> authentication) {
return PasswordGrantAuthenticationToken.class.isAssignableFrom(authentication);
}
private static OAuth2ClientAuthenticationToken getAuthenticatedClientElseThrowInvalidClient(Authentication authentication) {
OAuth2ClientAuthenticationToken clientPrincipal = null;
if (OAuth2ClientAuthenticationToken.class.isAssignableFrom(authentication.getPrincipal().getClass())) {
clientPrincipal = (OAuth2ClientAuthenticationToken) authentication.getPrincipal();
}
if (clientPrincipal != null && clientPrincipal.isAuthenticated()) {
return clientPrincipal;
}
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_CLIENT);
}
/**
* 登录失败超出次数5 返回true
* @param username
* @return
*/
private boolean isLoginFailOvertimes(String username){
String key = CommonConstant.LOGIN_FAIL + username;
Object failTime = redisUtil.get(key);
if(failTime!=null){
Integer val = Integer.parseInt(failTime.toString());
if(val>5){
return true;
}
}
return false;
}
/**
* 记录登录失败次数
* @param username
*/
private void addLoginFailOvertimes(String username){
String key = CommonConstant.LOGIN_FAIL + username;
Object failTime = redisUtil.get(key);
Integer val = 0;
if(failTime!=null){
val = Integer.parseInt(failTime.toString());
}
// 10分钟
redisUtil.set(key, ++val, 10);
}
/**
* 校验用户是否有效
*/
private void checkUserIsEffective(LoginUser loginUser) {
//情况1根据用户信息查询该用户不存在
if (Objects.isNull(loginUser)) {
baseCommonService.addLog("用户登录失败,用户不存在!", CommonConstant.LOG_TYPE_1, null);
throw new JeecgBootException("该用户不存在,请注册");
}
//情况2根据用户信息查询该用户已注销
//update-begin---author:王帅 Date:20200601 forif条件永远为falsebug------------
if (CommonConstant.DEL_FLAG_1.equals(loginUser.getDelFlag())) {
//update-end---author:王帅 Date:20200601 forif条件永远为falsebug------------
baseCommonService.addLog("用户登录失败,用户名:" + loginUser.getUsername() + "已注销!", CommonConstant.LOG_TYPE_1, null);
throw new JeecgBootException("该用户已注销");
}
//情况3根据用户信息查询该用户已冻结
if (CommonConstant.USER_FREEZE.equals(loginUser.getStatus())) {
baseCommonService.addLog("用户登录失败,用户名:" + loginUser.getUsername() + "已冻结!", CommonConstant.LOG_TYPE_1, null);
throw new JeecgBootException("该用户已冻结");
}
}
}

View File

@ -0,0 +1,21 @@
package org.jeecg.config.security.password;
import org.jeecg.config.security.LoginType;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationGrantAuthenticationToken;
import java.util.Map;
/**
* 密码模式认证专用token类型方法spring authorization server进行认证流转配合convert使用
* @author EightMonth
* @date 2024/1/1
*/
public class PasswordGrantAuthenticationToken extends OAuth2AuthorizationGrantAuthenticationToken {
public PasswordGrantAuthenticationToken(Authentication clientPrincipal, Map<String, Object> additionalParameters) {
super(new AuthorizationGrantType(LoginType.PASSWORD), clientPrincipal, additionalParameters);
}
}

View File

@ -0,0 +1,77 @@
package org.jeecg.config.security.phone;
import jakarta.servlet.http.HttpServletRequest;
import lombok.AllArgsConstructor;
import org.jeecg.config.security.LoginType;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.web.authentication.AuthenticationConverter;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
import java.util.HashMap;
import java.util.Map;
/**
* 手机号模式认证转换器
* @author EightMonth
* @date 2024/1/1
*/
@AllArgsConstructor
public class PhoneGrantAuthenticationConvert implements AuthenticationConverter {
@Override
public Authentication convert(HttpServletRequest request) {
String grantType = request.getParameter(OAuth2ParameterNames.GRANT_TYPE);
if (!LoginType.PHONE.equals(grantType)) {
return null;
}
Authentication clientPrincipal = SecurityContextHolder.getContext().getAuthentication();
//从request中提取请求参数然后存入MultiValueMap<String, String>
MultiValueMap<String, String> parameters = getParameters(request);
// 验证码
String captcha = parameters.getFirst("captcha");
if (!StringUtils.hasText(captcha)) {
throw new OAuth2AuthenticationException("无效请求,验证码不能为空!");
}
//收集要传入PhoneGrantAuthenticationToken构造方法的参数
//该参数接下来在PhoneGrantAuthenticationProvider中使用
Map<String, Object> additionalParameters = new HashMap<>();
//遍历从request中提取的参数排除掉grant_type、client_id、code等字段参数其他参数收集到additionalParameters中
parameters.forEach((key, value) -> {
if (!key.equals(OAuth2ParameterNames.GRANT_TYPE) &&
!key.equals(OAuth2ParameterNames.CLIENT_ID) &&
!key.equals(OAuth2ParameterNames.CODE)) {
additionalParameters.put(key, value.get(0));
}
});
//返回自定义的PhoneGrantAuthenticationToken对象
return new PhoneGrantAuthenticationToken(clientPrincipal, additionalParameters);
}
/**
*从request中提取请求参数然后存入MultiValueMap<String, String>
*/
private static MultiValueMap<String, String> getParameters(HttpServletRequest request) {
Map<String, String[]> parameterMap = request.getParameterMap();
MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>(parameterMap.size());
parameterMap.forEach((key, values) -> {
if (values.length > 0) {
for (String value : values) {
parameters.add(key, value);
}
}
});
return parameters;
}
}

View File

@ -0,0 +1,292 @@
package org.jeecg.config.security.phone;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.api.CommonAPI;
import org.jeecg.common.constant.CommonConstant;
import org.jeecg.common.exception.JeecgBootException;
import org.jeecg.common.exception.JeecgCaptchaException;
import org.jeecg.common.system.vo.LoginUser;
import org.jeecg.common.system.vo.SysDepartModel;
import org.jeecg.common.util.Md5Util;
import org.jeecg.common.util.PasswordUtil;
import org.jeecg.common.util.RedisUtil;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.config.JeecgBaseConfig;
import org.jeecg.config.security.password.PasswordGrantAuthenticationToken;
import org.jeecg.modules.base.service.BaseCommonService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.http.HttpStatus;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.oauth2.core.*;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AccessTokenAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContextHolder;
import org.springframework.security.oauth2.server.authorization.token.DefaultOAuth2TokenContext;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenContext;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
import org.springframework.util.Assert;
import java.security.Principal;
import java.time.Instant;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* 手机号模式认证处理器,负责处理该认证模式下的核心逻辑
* @author EightMonth
* @date 2024/1/1
*/
@Slf4j
public class PhoneGrantAuthenticationProvider implements AuthenticationProvider {
private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-5.2";
private final OAuth2AuthorizationService authorizationService;
private final OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator;
@Lazy
@Autowired
private CommonAPI commonAPI;
@Autowired
private RedisUtil redisUtil;
@Autowired
private JeecgBaseConfig jeecgBaseConfig;
@Autowired
private BaseCommonService baseCommonService;
public PhoneGrantAuthenticationProvider(OAuth2AuthorizationService authorizationService, OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator) {
Assert.notNull(authorizationService, "authorizationService cannot be null");
Assert.notNull(tokenGenerator, "tokenGenerator cannot be null");
this.authorizationService = authorizationService;
this.tokenGenerator = tokenGenerator;
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
PhoneGrantAuthenticationToken phoneGrantAuthenticationToken = (PhoneGrantAuthenticationToken) authentication;
Map<String, Object> additionalParameter = phoneGrantAuthenticationToken.getAdditionalParameters();
// 授权类型
AuthorizationGrantType authorizationGrantType = phoneGrantAuthenticationToken.getGrantType();
// 手机号
String phone = (String) additionalParameter.get("mobile");
if(isLoginFailOvertimes(phone)){
throw new JeecgBootException("该用户登录失败次数过多请于10分钟后再次登录");
}
//请求参数权限范围
String requestScopesStr = (String)additionalParameter.getOrDefault(OAuth2ParameterNames.SCOPE, "*");
//请求参数权限范围专场集合
Set<String> requestScopeSet = Stream.of(requestScopesStr.split(" ")).collect(Collectors.toSet());
// 验证码
String captcha = (String) additionalParameter.get("captcha");
OAuth2ClientAuthenticationToken clientPrincipal = getAuthenticatedClientElseThrowInvalidClient(phoneGrantAuthenticationToken);
RegisteredClient registeredClient = clientPrincipal.getRegisteredClient();
// 通过手机号获取用户信息
LoginUser loginUser = commonAPI.getUserByPhone(phone);
// 检查用户可行性
checkUserIsEffective(loginUser);
String redisKey = CommonConstant.PHONE_REDIS_KEY_PRE+phone;
Object code = redisUtil.get(redisKey);
if (!captcha.equals(code)) {
//update-begin-author:taoyan date:2022-11-7 for: issues/4109 平台用户登录失败锁定用户
addLoginFailOvertimes(phone);
Map<String, Object> map = new HashMap<>();
map.put("message", "手机验证码错误");
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,"fdsafas", Instant.now(), Instant.now().plusNanos(1)), null, map);
}
if (!registeredClient.getAuthorizationGrantTypes().contains(authorizationGrantType)) {
Map<String, Object> map = new HashMap<>();
map.put("message", "非法登录");
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,"fdsafas", Instant.now(), Instant.now().plusNanos(1)), null, map);
}
//由于在上面已验证过用户名、密码现在构建一个已认证的对象UsernamePasswordAuthenticationToken
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = UsernamePasswordAuthenticationToken.authenticated(loginUser,clientPrincipal,new ArrayList<>());
DefaultOAuth2TokenContext.Builder tokenContextBuilder = DefaultOAuth2TokenContext.builder()
.registeredClient(registeredClient)
.principal(usernamePasswordAuthenticationToken)
.authorizationServerContext(AuthorizationServerContextHolder.getContext())
.authorizationGrantType(authorizationGrantType)
.authorizedScopes(requestScopeSet)
.authorizationGrant(phoneGrantAuthenticationToken);
OAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization.withRegisteredClient(registeredClient)
.principalName(clientPrincipal.getName())
.authorizedScopes(requestScopeSet)
.attribute(Principal.class.getName(), loginUser.getUsername())
.authorizationGrantType(authorizationGrantType);
// ----- Access token -----
OAuth2TokenContext tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.ACCESS_TOKEN).build();
OAuth2Token generatedAccessToken = this.tokenGenerator.generate(tokenContext);
if (generatedAccessToken == null) {
Map<String, Object> map = new HashMap<>();
map.put("message", "无法生成刷新token请联系管理员。");
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,"fdsafas", Instant.now(), Instant.now().plusNanos(1)), null, map);
}
OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
generatedAccessToken.getTokenValue(), generatedAccessToken.getIssuedAt(),
generatedAccessToken.getExpiresAt(), tokenContext.getAuthorizedScopes());
if (generatedAccessToken instanceof ClaimAccessor) {
authorizationBuilder.token(accessToken, (metadata) -> {
metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, ((ClaimAccessor) generatedAccessToken).getClaims());
});
} else {
authorizationBuilder.accessToken(accessToken);
}
// ----- Refresh token -----
OAuth2RefreshToken refreshToken = null;
if (registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.REFRESH_TOKEN) &&
// 不向公共客户端颁发刷新令牌
!clientPrincipal.getClientAuthenticationMethod().equals(ClientAuthenticationMethod.NONE)) {
tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.REFRESH_TOKEN).build();
OAuth2Token generatedRefreshToken = this.tokenGenerator.generate(tokenContext);
if (!(generatedRefreshToken instanceof OAuth2RefreshToken)) {
Map<String, Object> map = new HashMap<>();
map.put("message", "无法生成刷新token请联系管理员。");
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,"fdsafas", Instant.now(), Instant.now().plusNanos(1)), null, map);
}
refreshToken = (OAuth2RefreshToken) generatedRefreshToken;
authorizationBuilder.refreshToken(refreshToken);
}
OAuth2Authorization authorization = authorizationBuilder.build();
// 保存认证信息至redis
authorizationService.save(authorization);
baseCommonService.addLog("用户名: " + loginUser.getUsername() + ",登录成功!", CommonConstant.LOG_TYPE_1, null,loginUser);
JSONObject addition = new JSONObject(new LinkedHashMap<>());
addition.put("token", accessToken.getTokenValue());
// 设置租户
JSONObject jsonObject = commonAPI.setLoginTenant(loginUser.getUsername());
addition.putAll(jsonObject.getInnerMap());
// 设置登录用户信息
addition.put("userInfo", loginUser);
addition.put("sysAllDictItems", commonAPI.queryAllDictItems());
List<SysDepartModel> departs = commonAPI.queryUserDeparts(loginUser.getId());
addition.put("departs", departs);
if (departs == null || departs.size() == 0) {
addition.put("multi_depart", 0);
} else if (departs.size() == 1) {
commonAPI.updateUserDepart(loginUser.getUsername(), departs.get(0).getOrgCode(),null);
addition.put("multi_depart", 1);
} else {
//查询当前是否有登录部门
if(oConvertUtils.isEmpty(loginUser.getOrgCode())){
commonAPI.updateUserDepart(loginUser.getUsername(), departs.get(0).getOrgCode(),null);
}
addition.put("multi_depart", 2);
}
// 兼容原有shiro登录结果处理
Map<String, Object> map = new HashMap<>();
map.put("result", addition);
map.put("code", 200);
map.put("success", true);
map.put("timestamp", System.currentTimeMillis());
// 返回access_token、refresh_token以及其它信息给到前端
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, accessToken, refreshToken, map);
}
@Override
public boolean supports(Class<?> authentication) {
return PhoneGrantAuthenticationToken.class.isAssignableFrom(authentication);
}
private static OAuth2ClientAuthenticationToken getAuthenticatedClientElseThrowInvalidClient(Authentication authentication) {
OAuth2ClientAuthenticationToken clientPrincipal = null;
if (OAuth2ClientAuthenticationToken.class.isAssignableFrom(authentication.getPrincipal().getClass())) {
clientPrincipal = (OAuth2ClientAuthenticationToken) authentication.getPrincipal();
}
if (clientPrincipal != null && clientPrincipal.isAuthenticated()) {
return clientPrincipal;
}
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_CLIENT);
}
/**
* 登录失败超出次数5 返回true
* @param username
* @return
*/
private boolean isLoginFailOvertimes(String username){
String key = CommonConstant.LOGIN_FAIL + username;
Object failTime = redisUtil.get(key);
if(failTime!=null){
Integer val = Integer.parseInt(failTime.toString());
if(val>5){
return true;
}
}
return false;
}
/**
* 记录登录失败次数
* @param username
*/
private void addLoginFailOvertimes(String username){
String key = CommonConstant.LOGIN_FAIL + username;
Object failTime = redisUtil.get(key);
Integer val = 0;
if(failTime!=null){
val = Integer.parseInt(failTime.toString());
}
// 10分钟
redisUtil.set(key, ++val, 10);
}
/**
* 校验用户是否有效
*/
private void checkUserIsEffective(LoginUser loginUser) {
//情况1根据用户信息查询该用户不存在
if (Objects.isNull(loginUser)) {
baseCommonService.addLog("用户登录失败,用户不存在!", CommonConstant.LOG_TYPE_1, null);
throw new JeecgBootException("该用户不存在,请注册");
}
//情况2根据用户信息查询该用户已注销
//update-begin---author:王帅 Date:20200601 forif条件永远为falsebug------------
if (CommonConstant.DEL_FLAG_1.equals(loginUser.getDelFlag())) {
//update-end---author:王帅 Date:20200601 forif条件永远为falsebug------------
baseCommonService.addLog("用户登录失败,用户名:" + loginUser.getUsername() + "已注销!", CommonConstant.LOG_TYPE_1, null);
throw new JeecgBootException("该用户已注销");
}
//情况3根据用户信息查询该用户已冻结
if (CommonConstant.USER_FREEZE.equals(loginUser.getStatus())) {
baseCommonService.addLog("用户登录失败,用户名:" + loginUser.getUsername() + "已冻结!", CommonConstant.LOG_TYPE_1, null);
throw new JeecgBootException("该用户已冻结");
}
}
}

View File

@ -0,0 +1,21 @@
package org.jeecg.config.security.phone;
import org.jeecg.config.security.LoginType;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationGrantAuthenticationToken;
import java.util.Map;
/**
* 手机号模式认证专用token类型方法spring authorization server进行认证流转配合convert使用
* @author EightMonth
* @date 2024/1/1
*/
public class PhoneGrantAuthenticationToken extends OAuth2AuthorizationGrantAuthenticationToken {
public PhoneGrantAuthenticationToken(Authentication clientPrincipal, Map<String, Object> additionalParameters) {
super(new AuthorizationGrantType(LoginType.PHONE), clientPrincipal, additionalParameters);
}
}

View File

@ -0,0 +1,187 @@
package org.jeecg.config.security.self;
import org.jeecg.common.api.CommonAPI;
import org.jeecg.common.constant.CommonConstant;
import org.jeecg.common.exception.JeecgBoot401Exception;
import org.jeecg.common.exception.JeecgBootException;
import org.jeecg.common.system.vo.LoginUser;
import org.jeecg.common.util.RedisUtil;
import org.jeecg.config.JeecgBaseConfig;
import org.jeecg.modules.base.service.BaseCommonService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.oauth2.core.*;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AccessTokenAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.token.DefaultOAuth2TokenContext;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenContext;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import java.security.Principal;
import java.time.Instant;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* 自用生成token处理器不对外开放外部请求无法通过该方式生成token
* @author eightmonth@qq.com
* @date 2024/3/19 11:40
*/
@Component
public class SelfAuthenticationProvider implements AuthenticationProvider {
private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-5.2";
private final OAuth2AuthorizationService authorizationService;
private final OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator;
@Lazy
@Autowired
private CommonAPI commonAPI;
@Autowired
private RedisUtil redisUtil;
@Autowired
private JeecgBaseConfig jeecgBaseConfig;
@Autowired
private BaseCommonService baseCommonService;
public SelfAuthenticationProvider(OAuth2AuthorizationService authorizationService, OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator) {
Assert.notNull(authorizationService, "authorizationService cannot be null");
Assert.notNull(tokenGenerator, "tokenGenerator cannot be null");
this.authorizationService = authorizationService;
this.tokenGenerator = tokenGenerator;
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
SelfAuthenticationToken passwordGrantAuthenticationToken = (SelfAuthenticationToken) authentication;
Map<String, Object> additionalParameter = passwordGrantAuthenticationToken.getAdditionalParameters();
// 授权类型
AuthorizationGrantType authorizationGrantType = passwordGrantAuthenticationToken.getGrantType();
// 用户名
String username = (String) additionalParameter.get(OAuth2ParameterNames.USERNAME);
//请求参数权限范围
String requestScopesStr = "*";
//请求参数权限范围专场集合
Set<String> requestScopeSet = Stream.of(requestScopesStr.split(" ")).collect(Collectors.toSet());
OAuth2ClientAuthenticationToken clientPrincipal = getAuthenticatedClientElseThrowInvalidClient(passwordGrantAuthenticationToken);
RegisteredClient registeredClient = clientPrincipal.getRegisteredClient();
// 通过用户名获取用户信息
// LoginUser loginUser = commonAPI.getUserByName(username);
// 检查用户可行性
// checkUserIsEffective(loginUser);
//由于在上面已验证过用户名、密码现在构建一个已认证的对象UsernamePasswordAuthenticationToken
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = UsernamePasswordAuthenticationToken.authenticated(username,clientPrincipal,new ArrayList<>());
DefaultOAuth2TokenContext.Builder tokenContextBuilder = DefaultOAuth2TokenContext.builder()
.registeredClient(registeredClient)
.principal(usernamePasswordAuthenticationToken)
.authorizationGrantType(authorizationGrantType)
.authorizedScopes(requestScopeSet)
.authorizationGrant(passwordGrantAuthenticationToken);
OAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization.withRegisteredClient(registeredClient)
.principalName(clientPrincipal.getName())
.authorizedScopes(requestScopeSet)
.attribute(Principal.class.getName(), username)
.authorizationGrantType(authorizationGrantType);
// ----- Access token -----
OAuth2TokenContext tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.ACCESS_TOKEN).build();
OAuth2Token generatedAccessToken = this.tokenGenerator.generate(tokenContext);
if (generatedAccessToken == null) {
throw new JeecgBoot401Exception("无法生成刷新token请联系管理员。");
}
OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
generatedAccessToken.getTokenValue(), generatedAccessToken.getIssuedAt(),
generatedAccessToken.getExpiresAt(), tokenContext.getAuthorizedScopes());
if (generatedAccessToken instanceof ClaimAccessor) {
authorizationBuilder.token(accessToken, (metadata) -> {
metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, ((ClaimAccessor) generatedAccessToken).getClaims());
});
} else {
authorizationBuilder.accessToken(accessToken);
}
// ----- Refresh token -----
OAuth2RefreshToken refreshToken = null;
if (registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.REFRESH_TOKEN) &&
// 不向公共客户端颁发刷新令牌
!clientPrincipal.getClientAuthenticationMethod().equals(ClientAuthenticationMethod.NONE)) {
tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.REFRESH_TOKEN).build();
OAuth2Token generatedRefreshToken = this.tokenGenerator.generate(tokenContext);
if (!(generatedRefreshToken instanceof OAuth2RefreshToken)) {
Map<String, Object> map = new HashMap<>();
map.put("message", "无法生成刷新token请联系管理员。");
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,"fdsafas", Instant.now(), Instant.now().plusNanos(1)), null, map);
}
refreshToken = (OAuth2RefreshToken) generatedRefreshToken;
authorizationBuilder.refreshToken(refreshToken);
}
OAuth2Authorization authorization = authorizationBuilder.build();
// 保存认证信息至redis
authorizationService.save(authorization);
// 返回access_token、refresh_token以及其它信息给到前端
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, accessToken);
}
@Override
public boolean supports(Class<?> authentication) {
return SelfAuthenticationToken.class.isAssignableFrom(authentication);
}
private static OAuth2ClientAuthenticationToken getAuthenticatedClientElseThrowInvalidClient(Authentication authentication) {
OAuth2ClientAuthenticationToken clientPrincipal = null;
if (OAuth2ClientAuthenticationToken.class.isAssignableFrom(authentication.getPrincipal().getClass())) {
clientPrincipal = (OAuth2ClientAuthenticationToken) authentication.getPrincipal();
}
if (clientPrincipal != null && clientPrincipal.isAuthenticated()) {
return clientPrincipal;
}
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_CLIENT);
}
/**
* 校验用户是否有效
*/
private void checkUserIsEffective(LoginUser loginUser) {
//情况1根据用户信息查询该用户不存在
if (Objects.isNull(loginUser)) {
baseCommonService.addLog("用户登录失败,用户不存在!", CommonConstant.LOG_TYPE_1, null);
throw new JeecgBootException("该用户不存在,请注册");
}
//情况2根据用户信息查询该用户已注销
//update-begin---author:王帅 Date:20200601 forif条件永远为falsebug------------
if (CommonConstant.DEL_FLAG_1.equals(loginUser.getDelFlag())) {
//update-end---author:王帅 Date:20200601 forif条件永远为falsebug------------
baseCommonService.addLog("用户登录失败,用户名:" + loginUser.getUsername() + "已注销!", CommonConstant.LOG_TYPE_1, null);
throw new JeecgBootException("该用户已注销");
}
//情况3根据用户信息查询该用户已冻结
if (CommonConstant.USER_FREEZE.equals(loginUser.getStatus())) {
baseCommonService.addLog("用户登录失败,用户名:" + loginUser.getUsername() + "已冻结!", CommonConstant.LOG_TYPE_1, null);
throw new JeecgBootException("该用户已冻结");
}
}
}

View File

@ -0,0 +1,19 @@
package org.jeecg.config.security.self;
import org.jeecg.config.security.LoginType;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationGrantAuthenticationToken;
import java.util.Map;
/**
* 自用生成token不支持对外请求仅为程序内部生成token
* @author eightmonth
* @date 2024/3/19 11:37
*/
public class SelfAuthenticationToken extends OAuth2AuthorizationGrantAuthenticationToken {
public SelfAuthenticationToken(Authentication clientPrincipal, Map<String, Object> additionalParameters) {
super(new AuthorizationGrantType(LoginType.SELF), clientPrincipal, additionalParameters);
}
}

View File

@ -0,0 +1,81 @@
package org.jeecg.config.security.social;
import jakarta.servlet.http.HttpServletRequest;
import lombok.AllArgsConstructor;
import org.jeecg.config.security.LoginType;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.web.authentication.AuthenticationConverter;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
import java.util.HashMap;
import java.util.Map;
/**
* 社交模式认证转换器配合github、企业微信、钉钉、微信登录使用
* @author EightMonth
* @date 2024/1/1
*/
@AllArgsConstructor
public class SocialGrantAuthenticationConvert implements AuthenticationConverter {
@Override
public Authentication convert(HttpServletRequest request) {
String grantType = request.getParameter(OAuth2ParameterNames.GRANT_TYPE);
if (!LoginType.SOCIAL.equals(grantType)) {
return null;
}
Authentication clientPrincipal = SecurityContextHolder.getContext().getAuthentication();
//从request中提取请求参数然后存入MultiValueMap<String, String>
MultiValueMap<String, String> parameters = getParameters(request);
String token = parameters.getFirst("token");
if (!StringUtils.hasText(token)) {
throw new OAuth2AuthenticationException("无效请求三方token不能为空");
}
String source = parameters.getFirst("thirdType");
if (!StringUtils.hasText(source)) {
throw new OAuth2AuthenticationException("无效请求,三方来源不能为空!");
}
//收集要传入PhoneGrantAuthenticationToken构造方法的参数
//该参数接下来在PhoneGrantAuthenticationProvider中使用
Map<String, Object> additionalParameters = new HashMap<>();
//遍历从request中提取的参数排除掉grant_type、client_id、code等字段参数其他参数收集到additionalParameters中
parameters.forEach((key, value) -> {
if (!key.equals(OAuth2ParameterNames.GRANT_TYPE) &&
!key.equals(OAuth2ParameterNames.CLIENT_ID) &&
!key.equals(OAuth2ParameterNames.CODE)) {
additionalParameters.put(key, value.get(0));
}
});
//返回自定义的PhoneGrantAuthenticationToken对象
return new SocialGrantAuthenticationToken(clientPrincipal, additionalParameters);
}
/**
*从request中提取请求参数然后存入MultiValueMap<String, String>
*/
private static MultiValueMap<String, String> getParameters(HttpServletRequest request) {
Map<String, String[]> parameterMap = request.getParameterMap();
MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>(parameterMap.size());
parameterMap.forEach((key, values) -> {
if (values.length > 0) {
for (String value : values) {
parameters.add(key, value);
}
}
});
return parameters;
}
}

View File

@ -0,0 +1,278 @@
package org.jeecg.config.security.social;
import com.alibaba.fastjson.JSONObject;
import com.auth0.jwt.JWT;
import com.auth0.jwt.interfaces.DecodedJWT;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.api.CommonAPI;
import org.jeecg.common.constant.CommonConstant;
import org.jeecg.common.exception.JeecgBootException;
import org.jeecg.common.system.vo.LoginUser;
import org.jeecg.common.system.vo.SysDepartModel;
import org.jeecg.common.util.RedisUtil;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.config.JeecgBaseConfig;
import org.jeecg.config.security.password.PasswordGrantAuthenticationToken;
import org.jeecg.modules.base.service.BaseCommonService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.oauth2.core.*;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AccessTokenAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContextHolder;
import org.springframework.security.oauth2.server.authorization.token.DefaultOAuth2TokenContext;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenContext;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
import org.springframework.util.Assert;
import java.security.Principal;
import java.time.Instant;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* 社交模式认证处理器负责处理该认证模式下的核心逻辑配合github、企业微信、钉钉、微信登录使用
* @author EightMonth
* @date 2024/1/1
*/
@Slf4j
public class SocialGrantAuthenticationProvider implements AuthenticationProvider {
private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-5.2";
private final OAuth2AuthorizationService authorizationService;
private final OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator;
@Lazy
@Autowired
private CommonAPI commonAPI;
@Autowired
private RedisUtil redisUtil;
@Autowired
private JeecgBaseConfig jeecgBaseConfig;
@Autowired
private BaseCommonService baseCommonService;
public SocialGrantAuthenticationProvider(OAuth2AuthorizationService authorizationService, OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator) {
Assert.notNull(authorizationService, "authorizationService cannot be null");
Assert.notNull(tokenGenerator, "tokenGenerator cannot be null");
this.authorizationService = authorizationService;
this.tokenGenerator = tokenGenerator;
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
SocialGrantAuthenticationToken socialGrantAuthenticationToken = (SocialGrantAuthenticationToken) authentication;
Map<String, Object> additionalParameter = socialGrantAuthenticationToken.getAdditionalParameters();
// 授权类型
AuthorizationGrantType authorizationGrantType = socialGrantAuthenticationToken.getGrantType();
// 三方token
String token = (String) additionalParameter.get("token");
// 三方来源
String source = (String) additionalParameter.get("thirdType");
//请求参数权限范围
String requestScopesStr = (String)additionalParameter.getOrDefault(OAuth2ParameterNames.SCOPE, "*");
//请求参数权限范围专场集合
Set<String> requestScopeSet = Stream.of(requestScopesStr.split(" ")).collect(Collectors.toSet());
DecodedJWT jwt = JWT.decode(token);
String username = jwt.getClaim("username").asString();
// 通过手机号获取用户信息
LoginUser loginUser = commonAPI.getUserByName(username);
// 检查用户可行性
checkUserIsEffective(loginUser);
OAuth2ClientAuthenticationToken clientPrincipal = getAuthenticatedClientElseThrowInvalidClient(socialGrantAuthenticationToken);
RegisteredClient registeredClient = clientPrincipal.getRegisteredClient();
if (!registeredClient.getAuthorizationGrantTypes().contains(authorizationGrantType)) {
Map<String, Object> map = new HashMap<>();
map.put("message", "非法登录");
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,"fdsafas", Instant.now(), Instant.now().plusNanos(1)), null, map);
}
//由于在上面已验证过用户名、密码现在构建一个已认证的对象UsernamePasswordAuthenticationToken
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = UsernamePasswordAuthenticationToken.authenticated(loginUser,clientPrincipal,new ArrayList<>());
DefaultOAuth2TokenContext.Builder tokenContextBuilder = DefaultOAuth2TokenContext.builder()
.registeredClient(registeredClient)
.principal(usernamePasswordAuthenticationToken)
.authorizationServerContext(AuthorizationServerContextHolder.getContext())
.authorizationGrantType(authorizationGrantType)
.authorizedScopes(requestScopeSet)
.authorizationGrant(socialGrantAuthenticationToken);
OAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization.withRegisteredClient(registeredClient)
.principalName(clientPrincipal.getName())
.authorizedScopes(requestScopeSet)
.attribute(Principal.class.getName(), loginUser.getUsername())
.authorizationGrantType(authorizationGrantType);
// ----- Access token -----
OAuth2TokenContext tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.ACCESS_TOKEN).build();
OAuth2Token generatedAccessToken = this.tokenGenerator.generate(tokenContext);
if (generatedAccessToken == null) {
Map<String, Object> map = new HashMap<>();
map.put("message", "无法生成访问token请联系管理系。");
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,"fdsafas", Instant.now(), Instant.now().plusNanos(1)), null, map);
}
OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
generatedAccessToken.getTokenValue(), generatedAccessToken.getIssuedAt(),
generatedAccessToken.getExpiresAt(), tokenContext.getAuthorizedScopes());
if (generatedAccessToken instanceof ClaimAccessor) {
authorizationBuilder.token(accessToken, (metadata) -> {
metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, ((ClaimAccessor) generatedAccessToken).getClaims());
});
} else {
authorizationBuilder.accessToken(accessToken);
}
// ----- Refresh token -----
OAuth2RefreshToken refreshToken = null;
if (registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.REFRESH_TOKEN) &&
// 不向公共客户端颁发刷新令牌
!clientPrincipal.getClientAuthenticationMethod().equals(ClientAuthenticationMethod.NONE)) {
tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.REFRESH_TOKEN).build();
OAuth2Token generatedRefreshToken = this.tokenGenerator.generate(tokenContext);
if (!(generatedRefreshToken instanceof OAuth2RefreshToken)) {
Map<String, Object> map = new HashMap<>();
map.put("message", "无法生成刷新token请联系管理员。");
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,"fdsafas", Instant.now(), Instant.now().plusNanos(1)), null, map);
}
refreshToken = (OAuth2RefreshToken) generatedRefreshToken;
authorizationBuilder.refreshToken(refreshToken);
}
OAuth2Authorization authorization = authorizationBuilder.build();
// 保存认证信息至redis
authorizationService.save(authorization);
baseCommonService.addLog("用户名: " + loginUser.getUsername() + ",登录成功!", CommonConstant.LOG_TYPE_1, null,loginUser);
JSONObject addition = new JSONObject(new LinkedHashMap<>());
addition.put("token", accessToken.getTokenValue());
// 设置租户
JSONObject jsonObject = commonAPI.setLoginTenant(loginUser.getUsername());
addition.putAll(jsonObject.getInnerMap());
// 设置登录用户信息
addition.put("userInfo", loginUser);
addition.put("sysAllDictItems", commonAPI.queryAllDictItems());
List<SysDepartModel> departs = commonAPI.queryUserDeparts(loginUser.getId());
addition.put("departs", departs);
if (departs == null || departs.size() == 0) {
addition.put("multi_depart", 0);
} else if (departs.size() == 1) {
commonAPI.updateUserDepart(loginUser.getUsername(), departs.get(0).getOrgCode(),null);
addition.put("multi_depart", 1);
} else {
//查询当前是否有登录部门
if(oConvertUtils.isEmpty(loginUser.getOrgCode())){
commonAPI.updateUserDepart(loginUser.getUsername(), departs.get(0).getOrgCode(),null);
}
addition.put("multi_depart", 2);
}
// 兼容原有shiro登录结果处理
Map<String, Object> map = new HashMap<>();
map.put("result", addition);
map.put("code", 200);
map.put("success", true);
map.put("timestamp", System.currentTimeMillis());
// 返回access_token、refresh_token以及其它信息给到前端
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, accessToken, refreshToken, map);
}
@Override
public boolean supports(Class<?> authentication) {
return SocialGrantAuthenticationToken.class.isAssignableFrom(authentication);
}
private static OAuth2ClientAuthenticationToken getAuthenticatedClientElseThrowInvalidClient(Authentication authentication) {
OAuth2ClientAuthenticationToken clientPrincipal = null;
if (OAuth2ClientAuthenticationToken.class.isAssignableFrom(authentication.getPrincipal().getClass())) {
clientPrincipal = (OAuth2ClientAuthenticationToken) authentication.getPrincipal();
}
if (clientPrincipal != null && clientPrincipal.isAuthenticated()) {
return clientPrincipal;
}
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_CLIENT);
}
/**
* 登录失败超出次数5 返回true
* @param username
* @return
*/
private boolean isLoginFailOvertimes(String username){
String key = CommonConstant.LOGIN_FAIL + username;
Object failTime = redisUtil.get(key);
if(failTime!=null){
Integer val = Integer.parseInt(failTime.toString());
if(val>5){
return true;
}
}
return false;
}
/**
* 记录登录失败次数
* @param username
*/
private void addLoginFailOvertimes(String username){
String key = CommonConstant.LOGIN_FAIL + username;
Object failTime = redisUtil.get(key);
Integer val = 0;
if(failTime!=null){
val = Integer.parseInt(failTime.toString());
}
// 10分钟
redisUtil.set(key, ++val, 10);
}
/**
* 校验用户是否有效
*/
private void checkUserIsEffective(LoginUser loginUser) {
//情况1根据用户信息查询该用户不存在
if (Objects.isNull(loginUser)) {
baseCommonService.addLog("用户登录失败,用户不存在!", CommonConstant.LOG_TYPE_1, null);
throw new JeecgBootException("该用户不存在,请注册");
}
//情况2根据用户信息查询该用户已注销
//update-begin---author:王帅 Date:20200601 forif条件永远为falsebug------------
if (CommonConstant.DEL_FLAG_1.equals(loginUser.getDelFlag())) {
//update-end---author:王帅 Date:20200601 forif条件永远为falsebug------------
baseCommonService.addLog("用户登录失败,用户名:" + loginUser.getUsername() + "已注销!", CommonConstant.LOG_TYPE_1, null);
throw new JeecgBootException("该用户已注销");
}
//情况3根据用户信息查询该用户已冻结
if (CommonConstant.USER_FREEZE.equals(loginUser.getStatus())) {
baseCommonService.addLog("用户登录失败,用户名:" + loginUser.getUsername() + "已冻结!", CommonConstant.LOG_TYPE_1, null);
throw new JeecgBootException("该用户已冻结");
}
}
}

View File

@ -0,0 +1,21 @@
package org.jeecg.config.security.social;
import org.jeecg.config.security.LoginType;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationGrantAuthenticationToken;
import java.util.Map;
/**
* 社交模式认证专用token类型方法spring authorization server进行认证流转配合convert使用配合github、企业微信、钉钉、微信登录使用
* @author EightMonth
* @date 2024/1/1
*/
public class SocialGrantAuthenticationToken extends OAuth2AuthorizationGrantAuthenticationToken {
public SocialGrantAuthenticationToken(Authentication clientPrincipal, Map<String, Object> additionalParameters) {
super(new AuthorizationGrantType(LoginType.SOCIAL), clientPrincipal, additionalParameters);
}
}

View File

@ -0,0 +1,23 @@
package org.jeecg.config.security.utils;
import com.alibaba.fastjson2.JSONObject;
import org.jeecg.common.system.vo.LoginUser;
import org.jeecg.common.util.SpringContextUtils;
import org.springframework.security.core.context.SecurityContextHolder;
/**
* 认证信息工具类
* @author EightMonth
* @date 2024/1/10 17:03
*/
public class SecureUtil {
/**
* 通过当前认证信息获取用户信息
* @return
*/
public static LoginUser currentUser() {
String name = SecurityContextHolder.getContext().getAuthentication().getName();
return JSONObject.parseObject(name, LoginUser.class);
}
}

View File

@ -1,28 +0,0 @@
package org.jeecg.config.shiro;
import org.apache.shiro.authc.AuthenticationToken;
/**
* @Author Scott
* @create 2018-07-12 15:19
* @desc
**/
public class JwtToken implements AuthenticationToken {
private static final long serialVersionUID = 1L;
private String token;
public JwtToken(String token) {
this.token = token;
}
@Override
public Object getPrincipal() {
return token;
}
@Override
public Object getCredentials() {
return token;
}
}

View File

@ -1,389 +0,0 @@
package org.jeecg.config.shiro;
import jakarta.annotation.Resource;
import jakarta.servlet.DispatcherType;
import jakarta.servlet.Filter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
import org.apache.shiro.mgt.DefaultSubjectDAO;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.spring.web.ShiroUrlPathHelper;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.crazycake.shiro.*;
import org.jeecg.common.constant.CommonConstant;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.config.JeecgBaseConfig;
import org.jeecg.config.shiro.filters.CustomShiroFilterFactoryBean;
import org.jeecg.config.shiro.filters.JwtFilter;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.*;
import org.springframework.core.env.Environment;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.DelegatingFilterProxy;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import redis.clients.jedis.HostAndPort;
import redis.clients.jedis.JedisCluster;
import java.util.*;
/**
* @author: Scott
* @date: 2018/2/7
* @description: shiro 配置类
*/
@Slf4j
@Configuration
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public class ShiroConfig {
@Resource
private LettuceConnectionFactory lettuceConnectionFactory;
@Autowired
private Environment env;
@Resource
private JeecgBaseConfig jeecgBaseConfig;
@Autowired(required = false)
private RedisProperties redisProperties;
/**
* Filter Chain定义说明
*
* 1、一个URL可以配置多个Filter使用逗号分隔
* 2、当设置多个过滤器时全部验证通过才视为通过
* 3、部分过滤器可指定参数如permsroles
*/
@Bean("shiroFilterFactoryBean")
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
CustomShiroFilterFactoryBean shiroFilterFactoryBean = new CustomShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 拦截器
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
//支持yml方式配置拦截排除
if(jeecgBaseConfig!=null && jeecgBaseConfig.getShiro()!=null){
String shiroExcludeUrls = jeecgBaseConfig.getShiro().getExcludeUrls();
if(oConvertUtils.isNotEmpty(shiroExcludeUrls)){
String[] permissionUrl = shiroExcludeUrls.split(",");
for(String url : permissionUrl){
filterChainDefinitionMap.put(url,"anon");
}
}
}
// 配置不会被拦截的链接 顺序判断
filterChainDefinitionMap.put("/sys/cas/client/validateLogin", "anon"); //cas验证登录
filterChainDefinitionMap.put("/sys/randomImage/**", "anon"); //登录验证码接口排除
filterChainDefinitionMap.put("/sys/checkCaptcha", "anon"); //登录验证码接口排除
filterChainDefinitionMap.put("/sys/smsCheckCaptcha", "anon"); //短信次数发送太多验证码排除
filterChainDefinitionMap.put("/sys/login", "anon"); //登录接口排除
filterChainDefinitionMap.put("/sys/mLogin", "anon"); //登录接口排除
filterChainDefinitionMap.put("/sys/logout", "anon"); //登出接口排除
filterChainDefinitionMap.put("/sys/thirdLogin/**", "anon"); //第三方登录
filterChainDefinitionMap.put("/sys/getEncryptedString", "anon"); //获取加密串
filterChainDefinitionMap.put("/sys/sms", "anon");//短信验证码
filterChainDefinitionMap.put("/sys/phoneLogin", "anon");//手机登录
filterChainDefinitionMap.put("/sys/user/checkOnlyUser", "anon");//校验用户是否存在
filterChainDefinitionMap.put("/sys/user/register", "anon");//用户注册
filterChainDefinitionMap.put("/sys/user/phoneVerification", "anon");//用户忘记密码验证手机号
filterChainDefinitionMap.put("/sys/user/passwordChange", "anon");//用户更改密码
filterChainDefinitionMap.put("/auth/2step-code", "anon");//登录验证码
filterChainDefinitionMap.put("/sys/common/static/**", "anon");//图片预览 &下载文件不限制token
filterChainDefinitionMap.put("/sys/common/pdf/**", "anon");//pdf预览
//filterChainDefinitionMap.put("/sys/common/view/**", "anon");//图片预览不限制token
//filterChainDefinitionMap.put("/sys/common/download/**", "anon");//文件下载不限制token
filterChainDefinitionMap.put("/generic/**", "anon");//pdf预览需要文件
filterChainDefinitionMap.put("/sys/getLoginQrcode/**", "anon"); //登录二维码
filterChainDefinitionMap.put("/sys/getQrcodeToken/**", "anon"); //监听扫码
filterChainDefinitionMap.put("/sys/checkAuth", "anon"); //授权接口排除
filterChainDefinitionMap.put("/openapi/call/**", "anon"); // 开放平台接口排除
//update-begin--Author:scott Date:20221116 for排除静态资源后缀
filterChainDefinitionMap.put("/", "anon");
filterChainDefinitionMap.put("/doc.html", "anon");
filterChainDefinitionMap.put("/**/*.js", "anon");
filterChainDefinitionMap.put("/**/*.css", "anon");
filterChainDefinitionMap.put("/**/*.html", "anon");
filterChainDefinitionMap.put("/**/*.svg", "anon");
filterChainDefinitionMap.put("/**/*.pdf", "anon");
filterChainDefinitionMap.put("/**/*.jpg", "anon");
filterChainDefinitionMap.put("/**/*.png", "anon");
filterChainDefinitionMap.put("/**/*.gif", "anon");
filterChainDefinitionMap.put("/**/*.ico", "anon");
filterChainDefinitionMap.put("/**/*.ttf", "anon");
filterChainDefinitionMap.put("/**/*.woff", "anon");
filterChainDefinitionMap.put("/**/*.woff2", "anon");
filterChainDefinitionMap.put("/**/*.glb", "anon");
filterChainDefinitionMap.put("/**/*.wasm", "anon");
//update-end--Author:scott Date:20221116 for排除静态资源后缀
filterChainDefinitionMap.put("/druid/**", "anon");
filterChainDefinitionMap.put("/swagger-ui.html", "anon");
filterChainDefinitionMap.put("/swagger**/**", "anon");
filterChainDefinitionMap.put("/webjars/**", "anon");
filterChainDefinitionMap.put("/v3/**", "anon");
// update-begin--Author:sunjianlei Date:20210510 for排除消息通告查看详情页面用于第三方APP
filterChainDefinitionMap.put("/sys/annountCement/show/**", "anon");
// update-end--Author:sunjianlei Date:20210510 for排除消息通告查看详情页面用于第三方APP
//积木报表排除
filterChainDefinitionMap.put("/jmreport/**", "anon");
filterChainDefinitionMap.put("/**/*.js.map", "anon");
filterChainDefinitionMap.put("/**/*.css.map", "anon");
//积木BI大屏和仪表盘排除
filterChainDefinitionMap.put("/drag/view", "anon");
filterChainDefinitionMap.put("/drag/page/queryById", "anon");
filterChainDefinitionMap.put("/drag/page/addVisitsNumber", "anon");
filterChainDefinitionMap.put("/drag/page/queryTemplateList", "anon");
filterChainDefinitionMap.put("/drag/share/view/**", "anon");
filterChainDefinitionMap.put("/drag/onlDragDatasetHead/getAllChartData", "anon");
filterChainDefinitionMap.put("/drag/onlDragDatasetHead/getTotalData", "anon");
filterChainDefinitionMap.put("/drag/onlDragDatasetHead/getMapDataByCode", "anon");
filterChainDefinitionMap.put("/drag/onlDragDatasetHead/getTotalDataByCompId", "anon");
filterChainDefinitionMap.put("/drag/mock/json/**", "anon");
filterChainDefinitionMap.put("/drag/onlDragDatasetHead/getDictByCodes", "anon");
filterChainDefinitionMap.put("/jimubi/view", "anon");
filterChainDefinitionMap.put("/jimubi/share/view/**", "anon");
//大屏模板例子
filterChainDefinitionMap.put("/test/bigScreen/**", "anon");
filterChainDefinitionMap.put("/bigscreen/template1/**", "anon");
filterChainDefinitionMap.put("/bigscreen/template2/**", "anon");
//filterChainDefinitionMap.put("/test/jeecgDemo/rabbitMqClientTest/**", "anon"); //MQ测试
//filterChainDefinitionMap.put("/test/jeecgDemo/html", "anon"); //模板页面
//filterChainDefinitionMap.put("/test/jeecgDemo/redis/**", "anon"); //redis测试
//websocket排除
filterChainDefinitionMap.put("/websocket/**", "anon");//系统通知和公告
filterChainDefinitionMap.put("/newsWebsocket/**", "anon");//CMS模块
filterChainDefinitionMap.put("/vxeSocket/**", "anon");//JVxeTable无痕刷新示例
//App vue3版本查询版本接口
filterChainDefinitionMap.put("/sys/version/app3version", "anon");
//仪表盘(按钮通信)
filterChainDefinitionMap.put("/dragChannelSocket/**","anon");
//性能监控——安全隐患泄露TOEKNdurid连接池也有
//filterChainDefinitionMap.put("/actuator/**", "anon");
//测试模块排除
filterChainDefinitionMap.put("/test/seata/**", "anon");
//错误路径排除
filterChainDefinitionMap.put("/error", "anon");
// 企业微信证书排除
filterChainDefinitionMap.put("/WW_verify*", "anon");
// 添加自己的过滤器并且取名为jwt
Map<String, Filter> filterMap = new HashMap<String, Filter>(1);
//如果cloudServer为空 则说明是单体 需要加载跨域配置【微服务跨域切换】
Object cloudServer = env.getProperty(CommonConstant.CLOUD_SERVER_KEY);
filterMap.put("jwt", new JwtFilter(cloudServer==null));
shiroFilterFactoryBean.setFilters(filterMap);
// <!-- 过滤链定义,从上向下顺序执行,一般将/**放在最为下边
filterChainDefinitionMap.put("/**", "jwt");
// 未授权界面返回JSON
shiroFilterFactoryBean.setUnauthorizedUrl("/sys/common/403");
shiroFilterFactoryBean.setLoginUrl("/sys/common/403");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
//update-begin---author:chenrui ---date:20240126 for【QQYUN-7932】AI助手------------
/**
* spring过滤装饰器 <br/>
* 因为shiro的filter不支持异步请求,导致所有的异步请求都会报错. <br/>
* 所以需要用spring的FilterRegistrationBean再代理一下shiro的filter.为他扩展异步支持. <br/>
* 后续所有异步的接口都需要再这里增加registration.addUrlPatterns("/xxx/xxx");
* @return
* @author chenrui
* @date 2024/12/3 19:49
*/
@Bean
public FilterRegistrationBean shiroFilterRegistration() {
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(new DelegatingFilterProxy("shiroFilterFactoryBean"));
registration.setEnabled(true);
//update-begin---author:chenrui ---date:20241202 for[issues/7491]运行时间好长,效率慢 ------------
registration.addUrlPatterns("/test/ai/chat/send");
//update-end---author:chenrui ---date:20241202 for[issues/7491]运行时间好长,效率慢 ------------
registration.addUrlPatterns("/airag/flow/run");
registration.addUrlPatterns("/airag/flow/debug");
registration.addUrlPatterns("/airag/chat/send");
registration.addUrlPatterns("/airag/app/debug");
registration.addUrlPatterns("/airag/app/prompt/generate");
//支持异步
registration.setAsyncSupported(true);
registration.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ASYNC);
return registration;
}
//update-end---author:chenrui ---date:20240126 for【QQYUN-7932】AI助手------------
@Bean("securityManager")
public DefaultWebSecurityManager securityManager(ShiroRealm myRealm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(myRealm);
/*
* 关闭shiro自带的session详情见文档
* http://shiro.apache.org/session-management.html#SessionManagement-
* StatelessApplications%28Sessionless%29
*/
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
securityManager.setSubjectDAO(subjectDAO);
//自定义缓存实现,使用redis
securityManager.setCacheManager(redisCacheManager());
return securityManager;
}
/**
* 下面的代码是添加注解支持
* @return
*/
@Bean
@DependsOn("lifecycleBeanPostProcessor")
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
/**
* 解决重复代理问题 github#994
* 添加前缀判断 不匹配 任何Advisor
*/
defaultAdvisorAutoProxyCreator.setUsePrefix(true);
defaultAdvisorAutoProxyCreator.setAdvisorBeanNamePrefix("_no_advisor");
return defaultAdvisorAutoProxyCreator;
}
@Bean
public static LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
/**
* cacheManager 缓存 redis实现
* 使用的是shiro-redis开源插件
*
* @return
*/
public RedisCacheManager redisCacheManager() {
log.info("===============(1)创建缓存管理器RedisCacheManager");
RedisCacheManager redisCacheManager = new RedisCacheManager();
redisCacheManager.setRedisManager(redisManager());
//redis中针对不同用户缓存(此处的id需要对应user实体中的id字段,用于唯一标识)
redisCacheManager.setPrincipalIdFieldName("id");
//用户权限信息缓存时间
redisCacheManager.setExpire(200000);
return redisCacheManager;
}
/**
* RedisConfig在项目starter项目中
* jeecg-boot-starter-github\jeecg-boot-common\src\main\java\org\jeecg\common\modules\redis\config\RedisConfig.java
*
* 配置shiro redisManager
* 使用的是shiro-redis开源插件
*
* @return
*/
@Bean
public IRedisManager redisManager() {
log.info("===============(2)创建RedisManager,连接Redis..");
IRedisManager manager;
// sentinel cluster redis【issues/5569】shiro集成 redis 不支持 sentinel 方式部署的redis集群 #5569
if (Objects.nonNull(redisProperties)
&& Objects.nonNull(redisProperties.getSentinel())
&& !CollectionUtils.isEmpty(redisProperties.getSentinel().getNodes())) {
RedisSentinelManager sentinelManager = new RedisSentinelManager();
sentinelManager.setMasterName(redisProperties.getSentinel().getMaster());
sentinelManager.setHost(String.join(",", redisProperties.getSentinel().getNodes()));
sentinelManager.setPassword(redisProperties.getPassword());
sentinelManager.setDatabase(redisProperties.getDatabase());
return sentinelManager;
}
// redis 单机支持,在集群为空,或者集群无机器时候使用 add by jzyadmin@163.com
if (lettuceConnectionFactory.getClusterConfiguration() == null || lettuceConnectionFactory.getClusterConfiguration().getClusterNodes().isEmpty()) {
RedisManager redisManager = new RedisManager();
redisManager.setHost(lettuceConnectionFactory.getHostName() + ":" + lettuceConnectionFactory.getPort());
//(lettuceConnectionFactory.getPort());
redisManager.setDatabase(lettuceConnectionFactory.getDatabase());
redisManager.setTimeout(0);
if (!StringUtils.isEmpty(lettuceConnectionFactory.getPassword())) {
redisManager.setPassword(lettuceConnectionFactory.getPassword());
}
manager = redisManager;
}else{
// redis集群支持优先使用集群配置
RedisClusterManager redisManager = new RedisClusterManager();
Set<HostAndPort> portSet = new HashSet<>();
lettuceConnectionFactory.getClusterConfiguration().getClusterNodes().forEach(node -> portSet.add(new HostAndPort(node.getHost() , node.getPort())));
//update-begin--Author:scott Date:20210531 for修改集群模式下未设置redis密码的bug issues/I3QNIC
if (oConvertUtils.isNotEmpty(lettuceConnectionFactory.getPassword())) {
JedisCluster jedisCluster = new JedisCluster(portSet, 2000, 2000, 5,
lettuceConnectionFactory.getPassword(), new GenericObjectPoolConfig());
redisManager.setPassword(lettuceConnectionFactory.getPassword());
redisManager.setJedisCluster(jedisCluster);
} else {
JedisCluster jedisCluster = new JedisCluster(portSet);
redisManager.setJedisCluster(jedisCluster);
}
//update-end--Author:scott Date:20210531 for修改集群模式下未设置redis密码的bug issues/I3QNIC
manager = redisManager;
}
return manager;
}
/**
* 解决 ShiroRequestMappingConfig 获取 requestMappingHandlerMapping Bean 冲突
* spring-boot-autoconfigure:3.4.5 和 spring-boot-actuator-autoconfigure:3.4.5
*/
@Primary
@Bean
public RequestMappingHandlerMapping overridedRequestMappingHandlerMapping() {
RequestMappingHandlerMapping mapping = new RequestMappingHandlerMapping();
mapping.setUrlPathHelper(new ShiroUrlPathHelper());
return mapping;
}
private List<String> rebuildUrl(String[] bases, String[] uris) {
List<String> urls = new ArrayList<>();
for (String base : bases) {
for (String uri : uris) {
urls.add(prefix(base)+prefix(uri));
}
}
return urls;
}
private String prefix(String seg) {
return seg.startsWith("/") ? seg : "/"+seg;
}
}

View File

@ -1,237 +0,0 @@
package org.jeecg.config.shiro;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.jeecg.common.api.CommonAPI;
import org.jeecg.common.config.TenantContext;
import org.jeecg.common.constant.CacheConstant;
import org.jeecg.common.constant.CommonConstant;
import org.jeecg.common.system.util.JwtUtil;
import org.jeecg.common.system.vo.LoginUser;
import org.jeecg.common.util.RedisUtil;
import org.jeecg.common.util.SpringContextUtils;
import org.jeecg.common.util.TokenUtils;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.config.mybatis.MybatisPlusSaasConfig;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.annotation.Lazy;
import org.springframework.context.annotation.Role;
import org.springframework.stereotype.Component;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import java.util.Set;
/**
* @Description: 用户登录鉴权和获取用户授权
* @Author: Scott
* @Date: 2019-4-23 8:13
* @Version: 1.1
*/
@Component
@Slf4j
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public class ShiroRealm extends AuthorizingRealm {
@Lazy
@Resource
private CommonAPI commonApi;
@Lazy
@Resource
private RedisUtil redisUtil;
/**
* 必须重写此方法不然Shiro会报错
*/
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JwtToken;
}
/**
* 权限信息认证(包括角色以及权限)是用户访问controller的时候才进行验证(redis存储的此处权限信息)
* 触发检测用户权限时才会调用此方法例如checkRole,checkPermission
*
* @param principals 身份信息
* @return AuthorizationInfo 权限信息
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
log.debug("===============Shiro权限认证开始============ [ roles、permissions]==========");
String username = null;
String userId = null;
if (principals != null) {
LoginUser sysUser = (LoginUser) principals.getPrimaryPrincipal();
username = sysUser.getUsername();
userId = sysUser.getId();
}
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
// 设置用户拥有的角色集合比如“admin,test”
Set<String> roleSet = commonApi.queryUserRolesById(userId);
//System.out.println(roleSet.toString());
info.setRoles(roleSet);
// 设置用户拥有的权限集合比如“sys:role:add,sys:user:add”
Set<String> permissionSet = commonApi.queryUserAuths(userId);
info.addStringPermissions(permissionSet);
//System.out.println(permissionSet);
log.info("===============Shiro权限认证成功==============");
return info;
}
/**
* 用户信息认证是在用户进行登录的时候进行验证(不存redis)
* 也就是说验证用户输入的账号和密码是否正确,错误抛出异常
*
* @param auth 用户登录的账号密码信息
* @return 返回封装了用户信息的 AuthenticationInfo 实例
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException {
log.debug("===============Shiro身份认证开始============doGetAuthenticationInfo==========");
String token = (String) auth.getCredentials();
if (token == null) {
HttpServletRequest req = SpringContextUtils.getHttpServletRequest();
log.info("————————身份认证失败——————————IP地址: "+ oConvertUtils.getIpAddrByRequest(req) +"URL:"+req.getRequestURI());
throw new AuthenticationException("token为空!");
}
// 校验token有效性
LoginUser loginUser = null;
try {
loginUser = this.checkUserTokenIsEffect(token);
} catch (AuthenticationException e) {
JwtUtil.responseError(SpringContextUtils.getHttpServletResponse(),401,e.getMessage());
e.printStackTrace();
return null;
}
return new SimpleAuthenticationInfo(loginUser, token, getName());
}
/**
* 校验token的有效性
*
* @param token
*/
public LoginUser checkUserTokenIsEffect(String token) throws AuthenticationException {
// 解密获得username用于和数据库进行对比
String username = JwtUtil.getUsername(token);
if (username == null) {
throw new AuthenticationException("token非法无效!");
}
// 查询用户信息
log.debug("———校验token是否有效————checkUserTokenIsEffect——————— "+ token);
LoginUser loginUser = TokenUtils.getLoginUser(username, commonApi, redisUtil);
//LoginUser loginUser = commonApi.getUserByName(username);
if (loginUser == null) {
throw new AuthenticationException("用户不存在!");
}
// 判断用户状态
if (loginUser.getStatus() != 1) {
throw new AuthenticationException("账号已被锁定,请联系管理员!");
}
// 校验token是否超时失效 & 或者账号密码是否错误
if (!jwtTokenRefresh(token, username, loginUser.getPassword())) {
throw new AuthenticationException(CommonConstant.TOKEN_IS_INVALID_MSG);
}
//update-begin-author:taoyan date:20210609 for:校验用户的tenant_id和前端传过来的是否一致
String userTenantIds = loginUser.getRelTenantIds();
if(MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL && oConvertUtils.isNotEmpty(userTenantIds)){
String contextTenantId = TenantContext.getTenant();
log.debug("登录租户:" + contextTenantId);
log.debug("用户拥有那些租户:" + userTenantIds);
//登录用户无租户前端header中租户ID值为 0
String str ="0";
if(oConvertUtils.isNotEmpty(contextTenantId) && !str.equals(contextTenantId)){
//update-begin-author:taoyan date:20211227 for: /issues/I4O14W 用户租户信息变更判断漏洞
String[] arr = userTenantIds.split(",");
if(!oConvertUtils.isIn(contextTenantId, arr)){
boolean isAuthorization = false;
//========================================================================
// 查询用户信息(如果租户不匹配从数据库中重新查询一次用户信息)
String loginUserKey = CacheConstant.SYS_USERS_CACHE + "::" + username;
redisUtil.del(loginUserKey);
LoginUser loginUserFromDb = commonApi.getUserByName(username);
if (oConvertUtils.isNotEmpty(loginUserFromDb.getRelTenantIds())) {
String[] newArray = loginUserFromDb.getRelTenantIds().split(",");
if (oConvertUtils.isIn(contextTenantId, newArray)) {
isAuthorization = true;
}
}
//========================================================================
//*********************************************
if(!isAuthorization){
log.info("租户异常——登录租户:" + contextTenantId);
log.info("租户异常——用户拥有租户组:" + userTenantIds);
throw new AuthenticationException("登录租户授权变更,请重新登陆!");
}
//*********************************************
}
//update-end-author:taoyan date:20211227 for: /issues/I4O14W 用户租户信息变更判断漏洞
}
}
//update-end-author:taoyan date:20210609 for:校验用户的tenant_id和前端传过来的是否一致
return loginUser;
}
/**
* JWTToken刷新生命周期 (实现: 用户在线操作不掉线功能)
* 1、登录成功后将用户的JWT生成的Token作为k、v存储到cache缓存里面(这时候k、v值一样)缓存有效期设置为Jwt有效时间的2倍
* 2、当该用户再次请求时通过JWTFilter层层校验之后会进入到doGetAuthenticationInfo进行身份验证
* 3、当该用户这次请求jwt生成的token值已经超时但该token对应cache中的k还是存在则表示该用户一直在操作只是JWT的token失效了程序会给token对应的k映射的v值重新生成JWTToken并覆盖v值该缓存生命周期重新计算
* 4、当该用户这次请求jwt在生成的token值已经超时并在cache中不存在对应的k则表示该用户账户空闲超时返回用户信息已失效请重新登录。
* 注意: 前端请求Header中设置Authorization保持不变校验有效性以缓存中的token为准。
* 用户过期时间 = Jwt有效时间 * 2。
*
* @param userName
* @param passWord
* @return
*/
public boolean jwtTokenRefresh(String token, String userName, String passWord) {
String cacheToken = String.valueOf(redisUtil.get(CommonConstant.PREFIX_USER_TOKEN + token));
if (oConvertUtils.isNotEmpty(cacheToken)) {
// 校验token有效性
if (!JwtUtil.verify(cacheToken, userName, passWord)) {
String newAuthorization = JwtUtil.sign(userName, passWord);
// 设置超时时间
redisUtil.set(CommonConstant.PREFIX_USER_TOKEN + token, newAuthorization);
redisUtil.expire(CommonConstant.PREFIX_USER_TOKEN + token, JwtUtil.EXPIRE_TIME *2 / 1000);
log.debug("——————————用户在线操作更新token保证不掉线—————————jwtTokenRefresh——————— "+ token);
}
//update-begin--Author:scott Date:20191005 for解决每次请求都重写redis中 token缓存问题
// else {
// // 设置超时时间
// redisUtil.set(CommonConstant.PREFIX_USER_TOKEN + token, cacheToken);
// redisUtil.expire(CommonConstant.PREFIX_USER_TOKEN + token, JwtUtil.EXPIRE_TIME / 1000);
// }
//update-end--Author:scott Date:20191005 for解决每次请求都重写redis中 token缓存问题
return true;
}
//redis中不存在此TOEKN说明token非法返回false
return false;
}
/**
* 清除当前用户的权限认证缓存
*
* @param principals 权限信息
*/
@Override
public void clearCache(PrincipalCollection principals) {
super.clearCache(principals);
//update-begin---author:scott ---date::2024-06-18 for【TV360X-1320】分配权限必须退出重新登录才生效造成很多用户困扰---
super.clearCachedAuthorizationInfo(principals);
//update-end---author:scott ---date::2024-06-18 for【TV360X-1320】分配权限必须退出重新登录才生效造成很多用户困扰---
}
}

View File

@ -1,77 +0,0 @@
package org.jeecg.config.shiro.filters;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.filter.InvalidRequestFilter;
import org.apache.shiro.web.filter.mgt.DefaultFilter;
import org.apache.shiro.web.filter.mgt.FilterChainManager;
import org.apache.shiro.web.filter.mgt.FilterChainResolver;
import org.apache.shiro.web.filter.mgt.PathMatchingFilterChainResolver;
import org.apache.shiro.web.mgt.WebSecurityManager;
import org.apache.shiro.web.servlet.AbstractShiroFilter;
import org.apache.shiro.mgt.SecurityManager;
import org.springframework.beans.factory.BeanInitializationException;
import jakarta.servlet.Filter;
import java.util.Map;
/**
* 自定义ShiroFilterFactoryBean解决资源中文路径问题
* @author: jeecg-boot
*/
@Slf4j
public class CustomShiroFilterFactoryBean extends ShiroFilterFactoryBean {
@Override
public Class getObjectType() {
return MySpringShiroFilter.class;
}
@Override
protected AbstractShiroFilter createInstance() throws Exception {
SecurityManager securityManager = getSecurityManager();
if (securityManager == null) {
String msg = "SecurityManager property must be set.";
throw new BeanInitializationException(msg);
}
if (!(securityManager instanceof WebSecurityManager)) {
String msg = "The security manager does not implement the WebSecurityManager interface.";
throw new BeanInitializationException(msg);
}
FilterChainManager manager = createFilterChainManager();
//Expose the constructed FilterChainManager by first wrapping it in a
// FilterChainResolver implementation. The AbstractShiroFilter implementations
// do not know about FilterChainManagers - only resolvers:
PathMatchingFilterChainResolver chainResolver = new PathMatchingFilterChainResolver();
chainResolver.setFilterChainManager(manager);
Map<String, Filter> filterMap = manager.getFilters();
Filter invalidRequestFilter = filterMap.get(DefaultFilter.invalidRequest.name());
if (invalidRequestFilter instanceof InvalidRequestFilter) {
//此处是关键,设置false跳过URL携带中文400servletPath中文校验bug
((InvalidRequestFilter) invalidRequestFilter).setBlockNonAscii(false);
}
//Now create a concrete ShiroFilter instance and apply the acquired SecurityManager and built
//FilterChainResolver. It doesn't matter that the instance is an anonymous inner class
//here - we're just using it because it is a concrete AbstractShiroFilter instance that accepts
//injection of the SecurityManager and FilterChainResolver:
return new MySpringShiroFilter((WebSecurityManager) securityManager, chainResolver);
}
private static final class MySpringShiroFilter extends AbstractShiroFilter {
protected MySpringShiroFilter(WebSecurityManager webSecurityManager, FilterChainResolver resolver) {
if (webSecurityManager == null) {
throw new IllegalArgumentException("WebSecurityManager property cannot be null.");
} else {
this.setSecurityManager(webSecurityManager);
if (resolver != null) {
this.setFilterChainResolver(resolver);
}
}
}
}
}

View File

@ -1,130 +0,0 @@
package org.jeecg.config.shiro.filters;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;
import org.jeecg.common.config.TenantContext;
import org.jeecg.common.constant.CommonConstant;
import org.jeecg.common.system.util.JwtUtil;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.config.shiro.JwtToken;
import org.jeecg.config.shiro.ignore.InMemoryIgnoreAuth;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.RequestMethod;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
/**
* @Description: 鉴权登录拦截器
* @Author: Scott
* @Date: 2018/10/7
**/
@Slf4j
public class JwtFilter extends BasicHttpAuthenticationFilter {
/**
* 默认开启跨域设置(使用单体)
* 微服务情况下此属性设置为false
*/
private boolean allowOrigin = true;
public JwtFilter(){}
public JwtFilter(boolean allowOrigin){
this.allowOrigin = allowOrigin;
}
/**
* 执行登录认证
*
* @param request
* @param response
* @param mappedValue
* @return
*/
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
try {
// 判断当前路径是不是注解了@IngoreAuth路径如果是则放开验证
if (InMemoryIgnoreAuth.contains(((HttpServletRequest) request).getServletPath())) {
return true;
}
executeLogin(request, response);
return true;
} catch (Exception e) {
JwtUtil.responseError(response,401,CommonConstant.TOKEN_IS_INVALID_MSG);
return false;
//throw new AuthenticationException("Token失效请重新登录", e);
}
}
/**
*
*/
@Override
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String token = httpServletRequest.getHeader(CommonConstant.X_ACCESS_TOKEN);
// update-begin--Author:lvdandan Date:20210105 forJT-355 OA聊天添加token验证获取token参数
if (oConvertUtils.isEmpty(token)) {
token = httpServletRequest.getParameter("token");
}
// update-end--Author:lvdandan Date:20210105 forJT-355 OA聊天添加token验证获取token参数
JwtToken jwtToken = new JwtToken(token);
// 提交给realm进行登入如果错误他会抛出异常并被捕获
getSubject(request, response).login(jwtToken);
// 如果没有抛出异常则代表登入成功返回true
return true;
}
/**
* 对跨域提供支持
*/
@Override
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
if(allowOrigin){
httpServletResponse.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, httpServletRequest.getHeader(HttpHeaders.ORIGIN));
// 允许客户端请求方法
httpServletResponse.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS, "GET,POST,OPTIONS,PUT,DELETE");
// 允许客户端提交的Header
String requestHeaders = httpServletRequest.getHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS);
if (StringUtils.isNotEmpty(requestHeaders)) {
httpServletResponse.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS, requestHeaders);
}
// 允许客户端携带凭证信息(是否允许发送Cookie)
httpServletResponse.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true");
}
// 跨域时会首先发送一个option请求这里我们给option请求直接返回正常状态
if (RequestMethod.OPTIONS.name().equalsIgnoreCase(httpServletRequest.getMethod())) {
httpServletResponse.setStatus(HttpStatus.OK.value());
return false;
}
//update-begin-author:taoyan date:20200708 for:多租户用到
String tenantId = httpServletRequest.getHeader(CommonConstant.TENANT_ID);
TenantContext.setTenant(tenantId);
//update-end-author:taoyan date:20200708 for:多租户用到
return super.preHandle(request, response);
}
/**
* JwtFilter中ThreadLocal需要及时清除 #3634
*
* @param request
* @param response
* @param exception
* @throws Exception
*/
@Override
public void afterCompletion(ServletRequest request, ServletResponse response, Exception exception) throws Exception {
//log.info("------清空线程中多租户的ID={}------",TenantContext.getTenant());
TenantContext.clear();
}
}

View File

@ -1,67 +0,0 @@
package org.jeecg.config.shiro.filters;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.AccessControlFilter;
import lombok.extern.slf4j.Slf4j;
/**
* @Author Scott
* @create 2019-02-01 15:56
* @desc 鉴权请求URL访问权限拦截器
*/
@Slf4j
public class ResourceCheckFilter extends AccessControlFilter {
private String errorUrl;
public String getErrorUrl() {
return errorUrl;
}
public void setErrorUrl(String errorUrl) {
this.errorUrl = errorUrl;
}
/**
* 表示是否允许访问 如果允许访问返回true否则false
*
* @param servletRequest
* @param servletResponse
* @param o 表示写在拦截器中括号里面的字符串 mappedValue 就是 [urls] 配置中拦截器参数部分
* @return
* @throws Exception
*/
@Override
protected boolean isAccessAllowed(ServletRequest servletRequest, ServletResponse servletResponse, Object o) throws Exception {
Subject subject = getSubject(servletRequest, servletResponse);
String url = getPathWithinApplication(servletRequest);
log.info("当前用户正在访问的 url => " + url);
return subject.isPermitted(url);
}
/**
* onAccessDenied表示当访问拒绝时是否已经处理了 如果返回 true 表示需要继续处理; 如果返回 false
* 表示该拦截器实例已经处理了,将直接返回即可。
*
* @param servletRequest
* @param servletResponse
* @return
* @throws Exception
*/
@Override
protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
log.info("当 isAccessAllowed 返回 false 的时候,才会执行 method onAccessDenied ");
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
response.sendRedirect(request.getContextPath() + this.errorUrl);
// 返回 false 表示已经处理,例如页面跳转啥的,表示不在走以下的拦截器了(如果还有配置的话)
return false;
}
}

View File

@ -1,17 +0,0 @@
package org.jeecg.config.vo;
import lombok.Data;
/**
* @Description: 高德开放api配置
*
* @author: wangshuai
* @date: 2025/7/17 20:32
*/
@Data
public class GaoDeApi {
/**应用key*/
private String apiKey;
/**应用秘钥*/
private String secretKey;
}

View File

@ -1,18 +0,0 @@
package org.jeecg.config.vo;
/**
* @Description: TODO
* @author: scott
* @date: 2022年01月21日 14:23
*/
public class Shiro {
private String excludeUrls = "";
public String getExcludeUrls() {
return excludeUrls;
}
public void setExcludeUrls(String excludeUrls) {
this.excludeUrls = excludeUrls;
}
}

View File

@ -2,10 +2,10 @@ package org.jeecg.modules.base.service.impl;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.SecurityUtils;
import org.jeecg.common.api.dto.LogDTO;
import org.jeecg.common.constant.enums.ClientTerminalTypeEnum;
import org.jeecg.common.util.BrowserUtils;
import org.jeecg.config.security.utils.SecureUtil;
import org.jeecg.modules.base.mapper.BaseCommonMapper;
import org.jeecg.modules.base.service.BaseCommonService;
import org.jeecg.common.system.vo.LoginUser;
@ -35,7 +35,7 @@ public class BaseCommonServiceImpl implements BaseCommonService {
logDTO.setId(String.valueOf(IdWorker.getId()));
}
//保存日志异常捕获处理防止数据太大存储失败导致业务失败JT-238
try {
try {
logDTO.setCreateTime(new Date());
baseCommonMapper.saveLog(logDTO);
} catch (Exception e) {
@ -74,7 +74,7 @@ public class BaseCommonServiceImpl implements BaseCommonService {
//获取登录用户信息
if(user==null){
try {
user = (LoginUser) SecurityUtils.getSubject().getPrincipal();
user = SecureUtil.currentUser();
} catch (Exception e) {
//e.printStackTrace();
}

View File

@ -1,104 +0,0 @@
<html>
<head>
<meta http-equiv="content-type" content="text/html;charset=utf-8">
</head>
<body>
<div class="box-content">
<div class="info-top">
<img src="https://www.jeecg.com/images/logo.png" style="float: left; margin: 0 10px 0 0; width: 32px;height:32px" /><div style="color:#fff"><strong>【重要】流程抄送的通知</strong></div>
</div>
<div class="info-wrap">
<div class="tips" style="padding:15px;">
<p style="margin: 10px 0;">
您好,您有一个新的流程抄送任务亟待查看,任务内容如下::
</p>
<table style="width: 400px; border-spacing: 0px; border-collapse: collapse; border: none; margin-top: 20px;"><tbody>
<tr style="height: 45px;">
<td style="width: 150px; height: 40px; background: #F6F6F6;border: 1px solid #DBDBDB; font-size: 14px; font-weight: normal; text-align: left; padding-left: 14px;">
流程名称
</td>
<td style="width: 250px;height: 40px; border: 1px solid #DBDBDB; font-size: 14px; font-weight: normal; text-align: left; padding-left: 14px;">
${bpm_name}<a style="color: #006eff;" href="${url}" target="_blank" rel="noopener">[立刻查看]</a>
</td>
</tr>
<tr style="height: 45px;">
<td style="width: 150px;height: 40px; background: #F6F6F6;border: 1px solid #DBDBDB; font-size: 14px; font-weight: normal; text-align: left; padding-left: 14px;">
抄送任务
</td>
<td style="width: 250px;height: 40px; border: 1px solid #DBDBDB; font-size: 14px; font-weight: normal; text-align: left; padding-left: 14px;">
${bpm_task}
</td>
</tr>
<tr style="height: 45px;">
<td style="width: 150px; height: 40px; background: #F6F6F6;border: 1px solid #DBDBDB; font-size: 14px; font-weight: normal; text-align: left; padding-left: 14px;">
抄送时间
</td>
<td style="width: 250px;height: 40px; border: 1px solid #DBDBDB; font-size: 14px; font-weight: normal; text-align: left; padding-left: 14px;">
${datetime}
</td>
</tr>
<tr style="height: 45px;">
<td style="width: 150px; height: 40px; background: #F6F6F6;border: 1px solid #DBDBDB; font-size: 14px; font-weight: normal; text-align: left; padding-left: 14px;">
抄送内容
</td>
<td style="width: 250px;height: 40px; border: 1px solid #DBDBDB; font-size: 14px; font-weight: normal; text-align: left; padding-left: 14px;">
${remark}
</td>
</tr>
</tbody>
</table>
</div>
<div class="footer">北京国炬平台</div>
</div>
<div style="margin-top: 60px;margin-bottom: 10px;">
<span style="font-size: 13px; font-weight: bold; color: #666;">温馨提醒</span>
<div style="line-height: 24px; margin-top: 10px;">
<div style="font-size: 13px; color: #666;">使用过程中如有任何问题,请联系系统管理员。</div>
</div>
</div>
<div style="width: 600px; margin: 0 auto; margin-top: 50px; font-size: 12px; -webkit-font-smoothing: subpixel-antialiased; text-size-adjust: 100%;">
<p style="text-align: center; line-height: 20.4px; text-size-adjust: 100%; font-family: 'Microsoft YaHei'!important; padding: 0px !important; margin: 0px !important; color: #7e8890 !important;">
<span class="appleLinks">Copyright © 2023-2024 北京国炬信息技术有限公司. 保留所有权利。</span>
</p>
<p style="text-align: center;line-height: 20.4px; text-size-adjust: 100%; font-family: 'Microsoft YaHei'!important; padding: 0px !important; margin: 0px; color: #7e8890 !important; margin-top: 10px;">
<span class="appleLinks">邮件由系统自动发送,请勿直接回复本邮件!</span>
</p>
</div>
</div>
</body>
<style>
.box-content{
width: 80%;
margin: 20px auto;
max-width: 800px;
min-width: 600px;
}
.info-top{
padding: 15px 25px;
border-top-left-radius: 10px;
border-top-right-radius: 10px;
background: #4ea3f2;
color: #fff;
overflow: hidden;
line-height: 32px;
}
.info-wrap{
border-bottom-left-radius: 10px;
border-bottom-right-radius: 10px;
border:1px solid #ddd;
overflow: hidden;
padding: 15px 15px 20px;
}
.footer{
text-align: right;
color: #999;
padding: 0 15px 15px;
}
</style>
</html>

View File

@ -6,7 +6,7 @@
<parent>
<groupId>org.jeecgframework.boot</groupId>
<artifactId>jeecg-boot-module</artifactId>
<version>3.8.2</version>
<version>3.8.1</version>
</parent>
<artifactId>jeecg-boot-module-airag</artifactId>
@ -53,29 +53,19 @@
<artifactId>jeecg-system-cloud-api</artifactId>
</dependency>-->
<!-- aiflow依赖 -->
<dependency>
<groupId>org.jeecgframework.boot3</groupId>
<artifactId>jeecg-aiflow</artifactId>
<version>1.1.1</version>
</dependency>
<!-- beigin 这两个依赖太多每个包50M左右如果你发布需要使用请把<scope>provided</scope>删掉 -->
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-scripting-jsr223</artifactId>
<version>${kotlin.version}</version>
<scope>provided</scope>
<version>1.0.5</version>
</dependency>
<!-- aiflow 脚本依赖 -->
<dependency>
<groupId>com.yomahub</groupId>
<artifactId>liteflow-script-graaljs</artifactId>
<version>${liteflow.version}</version>
<scope>provided</scope>
<scope>runtime</scope>
</dependency>
<!-- end 这两个依赖太多每个包50M左右如果你发布需要使用请把<scope>provided</scope>删掉 -->
<!-- aiflow 脚本依赖 -->
<dependency>
<groupId>com.yomahub</groupId>
<artifactId>liteflow-script-groovy</artifactId>
@ -151,9 +141,9 @@
</dependency>
<!-- langChain4j vextor support -->
<dependency>
<groupId>org.jeecgframework</groupId>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-pgvector</artifactId>
<version>0.35.0</version>
<version>${langchain4j.version}</version>
</dependency>
<!-- langChain4j Document Parser -->
<dependency>

View File

@ -16,10 +16,6 @@ public class AiAppConsts {
* 状态:禁用
*/
public static final String STATUS_DISABLE = "disable";
/**
* 状态:发布
*/
public static final String STATUS_RELEASE = "release";
/**

View File

@ -4,13 +4,10 @@ import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.system.base.controller.JeecgController;
import org.jeecg.common.system.query.QueryGenerator;
import org.jeecg.common.util.AssertUtils;
import org.jeecg.common.util.TokenUtils;
import org.jeecg.config.mybatis.MybatisPlusSaasConfig;
import org.jeecg.config.shiro.IgnoreAuth;
import org.jeecg.modules.airag.app.consts.AiAppConsts;
import org.jeecg.modules.airag.app.entity.AiragApp;
@ -67,7 +64,6 @@ public class AiragAppController extends JeecgController<AiragApp, IAiragAppServi
* @return
*/
@RequestMapping(value = "/edit", method = {RequestMethod.PUT, RequestMethod.POST})
@RequiresPermissions("airag:app:edit")
public Result<String> edit(@RequestBody AiragApp airagApp) {
AssertUtils.assertNotEmpty("参数异常", airagApp);
AssertUtils.assertNotEmpty("请输入应用名称", airagApp.getName());
@ -77,28 +73,6 @@ public class AiragAppController extends JeecgController<AiragApp, IAiragAppServi
return Result.OK("保存完成!", airagApp.getId());
}
/**
* 发布应用
*
* @return
*/
@RequestMapping(value = "/release", method = RequestMethod.POST)
public Result<String> release(@RequestParam(name = "id") String id, @RequestParam(name = "release") Boolean release) {
AssertUtils.assertNotEmpty("id必须填写", id);
if (release == null) {
release = true;
}
AiragApp airagApp = new AiragApp();
airagApp.setId(id);
if (release) {
airagApp.setStatus(AiAppConsts.STATUS_RELEASE);
} else {
airagApp.setStatus(AiAppConsts.STATUS_ENABLE);
}
airagAppService.updateById(airagApp);
return Result.OK(release ? "发布成功" : "取消发布成功");
}
/**
* 通过id删除
*
@ -106,23 +80,23 @@ public class AiragAppController extends JeecgController<AiragApp, IAiragAppServi
* @return
*/
@DeleteMapping(value = "/delete")
@RequiresPermissions("airag:app:delete")
public Result<String> delete(HttpServletRequest request,@RequestParam(name = "id", required = true) String id) {
//update-begin---author:chenrui ---date:20250606 for[issues/8337]关于ai工作列表的数据权限问题 #8337------------
//如果是saas隔离的情况下判断当前租户id是否是当前租户下的
if (MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL) {
AiragApp app = airagAppService.getById(id);
//获取当前租户
String currentTenantId = TokenUtils.getTenantIdByRequest(request);
if (null == app || !app.getTenantId().equals(currentTenantId)) {
return Result.error("删除AI应用失败不能删除其他租户的AI应用");
}
}
//update-end---author:chenrui ---date:20250606 for[issues/8337]关于ai工作列表的数据权限问题 #8337------------
public Result<String> delete(@RequestParam(name = "id", required = true) String id) {
airagAppService.removeById(id);
return Result.OK("删除成功!");
}
/**
* 批量删除
*
* @param ids
* @return
*/
@DeleteMapping(value = "/deleteBatch")
public Result<String> deleteBatch(@RequestParam(name = "ids", required = true) String ids) {
this.airagAppService.removeByIds(Arrays.asList(ids.split(",")));
return Result.OK("批量删除成功!");
}
/**
* 通过id查询
*

View File

@ -1,24 +1,16 @@
package org.jeecg.modules.airag.app.controller;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.constant.CommonConstant;
import org.jeecg.common.util.CommonUtils;
import org.jeecg.config.shiro.IgnoreAuth;
import org.jeecg.modules.airag.app.service.IAiragChatService;
import org.jeecg.modules.airag.app.vo.ChatConversation;
import org.jeecg.modules.airag.app.vo.ChatSendParams;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.MultipartHttpServletRequest;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
/**
* airag应用-chat
*
@ -33,15 +25,6 @@ public class AiragChatController {
@Autowired
IAiragChatService chatService;
@Value(value = "${jeecg.path.upload}")
private String uploadpath;
/**
* 本地local miniominio 阿里alioss
*/
@Value(value="${jeecg.uploadType}")
private String uploadType;
/**
* 发送消息
@ -76,19 +59,6 @@ public class AiragChatController {
return chatService.send(chatSendParams);
}
/**
* 获取所有对话
*
* @return 返回一个Result对象包含所有对话的信息
* @author chenrui
* @date 2025/2/25 11:42
*/
@IgnoreAuth
@GetMapping(value = "/init")
public Result<?> initChat(@RequestParam(name = "id", required = true) String id) {
return chatService.initChat(id);
}
/**
* 获取所有对话
*
@ -171,36 +141,4 @@ public class AiragChatController {
return chatService.stop(requestId);
}
/**
* 上传文件
* for [QQYUN-12135]AI聊天上传图片提示非法token
*
* @param request
* @param response
* @return
* @throws Exception
* @author chenrui
* @date 2025/4/25 11:04
*/
@IgnoreAuth
@PostMapping(value = "/upload")
public Result<?> upload(HttpServletRequest request, HttpServletResponse response) throws Exception {
String bizPath = "airag";
MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest) request;
// 获取上传文件对象
MultipartFile file = multipartRequest.getFile("file");
String savePath;
if (CommonConstant.UPLOAD_TYPE_LOCAL.equals(uploadType)) {
savePath = CommonUtils.uploadLocal(file, bizPath, uploadpath);
} else {
savePath = CommonUtils.upload(file, bizPath, uploadType);
}
Result<?> result = new Result<>();
result.setMessage(savePath);
result.setSuccess(true);
return result;
}
}

View File

@ -39,13 +39,12 @@ public class AiragApp implements Serializable {
*/
@TableId(type = IdType.ASSIGN_ID)
@Schema(description = "主键")
private java.lang.String id;
private String id;
/**
* 创建人
*/
@Schema(description = "创建人")
@Dict(dictTable = "sys_user",dicCode = "username",dicText = "realname")
private java.lang.String createBy;
private String createBy;
/**
* 创建日期
*/
@ -57,7 +56,7 @@ public class AiragApp implements Serializable {
* 更新人
*/
@Schema(description = "更新人")
private java.lang.String updateBy;
private String updateBy;
/**
* 更新日期
*/
@ -69,95 +68,95 @@ public class AiragApp implements Serializable {
* 所属部门
*/
@Schema(description = "所属部门")
private java.lang.String sysOrgCode;
private String sysOrgCode;
/**
* 租户id
*/
@Excel(name = "租户id", width = 15)
@Schema(description = "租户id")
private java.lang.String tenantId;
private String tenantId;
/**
* 应用名称
*/
@Excel(name = "应用名称", width = 15)
@Schema(description = "应用名称")
private java.lang.String name;
private String name;
/**
* 应用描述
*/
@Excel(name = "应用描述", width = 15)
@Schema(description = "应用描述")
private java.lang.String descr;
private String descr;
/**
* 应用图标
*/
@Excel(name = "应用图标", width = 15)
@Schema(description = "应用图标")
private java.lang.String icon;
private String icon;
/**
* 应用类型
*/
@Excel(name = "应用类型", width = 15, dicCode = "ai_app_type")
@Dict(dicCode = "ai_app_type")
@Schema(description = "应用类型")
private java.lang.String type;
private String type;
/**
* 开场白
*/
@Excel(name = "开场白", width = 15)
@Schema(description = "开场白")
private java.lang.String prologue;
private String prologue;
/**
* 预设问题
*/
@Excel(name = "预设问题", width = 15)
@Schema(description = "预设问题")
private java.lang.String presetQuestion;
private String presetQuestion;
/**
* 提示词
*/
@Excel(name = "提示词", width = 15)
@Schema(description = "提示词")
private java.lang.String prompt;
private String prompt;
/**
* 模型配置
*/
@Excel(name = "模型配置", width = 15, dictTable = "airag_model where model_type = 'LLM' ", dicText = "name", dicCode = "id")
@Dict(dictTable = "airag_model where model_type = 'LLM' ", dicText = "name", dicCode = "id")
@Schema(description = "模型配置")
private java.lang.String modelId;
private String modelId;
/**
* 历史消息数
*/
@Excel(name = "历史消息数", width = 15)
@Schema(description = "历史消息数")
private java.lang.Integer msgNum;
private Integer msgNum;
/**
* 知识库
*/
@Excel(name = "知识库", width = 15, dictTable = "airag_knowledge where status = 'enable'", dicText = "name", dicCode = "id")
@Dict(dictTable = "airag_knowledge where status = 'enable'", dicText = "name", dicCode = "id")
@Schema(description = "知识库")
private java.lang.String knowledgeIds;
private String knowledgeIds;
/**
* 流程
*/
@Excel(name = "流程", width = 15, dictTable = "airag_flow where status = 'enable' ", dicText = "name", dicCode = "id")
@Dict(dictTable = "airag_flow where status = 'enable' ", dicText = "name", dicCode = "id")
@Schema(description = "流程")
private java.lang.String flowId;
private String flowId;
/**
* 快捷指令
*/
@Excel(name = "快捷指令", width = 15)
@Schema(description = "快捷指令")
private java.lang.String quickCommand;
private String quickCommand;
/**
* 状态enable=启用、disable=禁用、release=发布)
* 状态
*/
@Excel(name = "状态", width = 15)
@Schema(description = "状态")
private java.lang.String status;
private String status;
/**
@ -165,7 +164,7 @@ public class AiragApp implements Serializable {
*/
@Excel(name = "元数据", width = 15)
@Schema(description = "元数据")
private java.lang.String metadata;
private String metadata;
/**
* 知识库ids

View File

@ -1,6 +1,5 @@
package org.jeecg.modules.airag.app.mapper;
import com.baomidou.mybatisplus.annotation.InterceptorIgnore;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.jeecg.modules.airag.app.entity.AiragApp;
@ -12,14 +11,4 @@ import org.jeecg.modules.airag.app.entity.AiragApp;
*/
public interface AiragAppMapper extends BaseMapper<AiragApp> {
/**
* 根据ID查询app信息(忽略租户)
* @param id
* @return
* @author chenrui
* @date 2025/4/21 16:03
*/
@InterceptorIgnore(tenantLine = "true")
AiragApp getByIdIgnoreTenant(String id);
}

View File

@ -2,8 +2,4 @@
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.jeecg.modules.airag.app.mapper.AiragAppMapper">
<select id="getByIdIgnoreTenant" resultType="org.jeecg.modules.airag.app.entity.AiragApp">
SELECT * FROM airag_app WHERE id = #{id}
</select>
</mapper>

View File

@ -92,14 +92,4 @@ public interface IAiragChatService {
* @date 2025/3/3 19:49
*/
Result<?> clearMessage(String conversationId);
/**
* 初始化聊天(忽略租户)
* [QQYUN-12113]分享之后的聊天,应用、模型、知识库不根据租户查询
* @param appId
* @return
* @author chenrui
* @date 2025/4/21 14:17
*/
Result<?> initChat(String appId);
}

View File

@ -13,7 +13,6 @@ import org.jeecg.common.system.util.JwtUtil;
import org.jeecg.common.util.*;
import org.jeecg.modules.airag.app.consts.AiAppConsts;
import org.jeecg.modules.airag.app.entity.AiragApp;
import org.jeecg.modules.airag.app.mapper.AiragAppMapper;
import org.jeecg.modules.airag.app.service.IAiragAppService;
import org.jeecg.modules.airag.app.service.IAiragChatService;
import org.jeecg.modules.airag.app.vo.AppDebugParams;
@ -64,7 +63,7 @@ public class AiragChatServiceImpl implements IAiragChatService {
RedisTemplate redisTemplate;
@Autowired
AiragAppMapper airagAppMapper;
IAiragAppService airagAppService;
@Autowired
IAiragFlowService airagFlowService;
@ -86,7 +85,7 @@ public class AiragChatServiceImpl implements IAiragChatService {
// 获取app信息
AiragApp app = null;
if (oConvertUtils.isNotEmpty(chatSendParams.getAppId())) {
app = airagAppMapper.getByIdIgnoreTenant(chatSendParams.getAppId());
app = airagAppService.getById(chatSendParams.getAppId());
}
ChatConversation chatConversation = getOrCreateChatConversation(app, conversationId);
// 更新标题
@ -147,19 +146,13 @@ public class AiragChatServiceImpl implements IAiragChatService {
try {
// 发送完成事件
emitter.send(SseEmitter.event().data(eventData));
} catch (Exception e) {
} catch (IOException e) {
log.error("终止会话时发生错误", e);
try {
// 防止异常冒泡
emitter.completeWithError(e);
} catch (Exception ignore) {}
} finally {
// 从缓存中移除emitter
AiragLocalCache.remove(AiragConsts.CACHE_TYPE_SSE, eventData.getRequestId());
// 关闭emitter
try {
emitter.complete();
} catch (Exception ignore) {}
emitter.complete();
}
}
@ -244,12 +237,6 @@ public class AiragChatServiceImpl implements IAiragChatService {
return Result.ok();
}
@Override
public Result<?> initChat(String appId) {
AiragApp app = airagAppMapper.getByIdIgnoreTenant(appId);
return Result.ok(app);
}
@Override
public Result<?> deleteConversation(String conversationId) {
AssertUtils.assertNotEmpty("请选择要删除的会话", conversationId);
@ -291,7 +278,7 @@ public class AiragChatServiceImpl implements IAiragChatService {
* @author chenrui
* @date 2025/2/25 19:27
*/
private String getConversationCacheKey(String conversationId, HttpServletRequest httpRequest) {
private String getConversationCacheKey(String conversationId,HttpServletRequest httpRequest) {
if (oConvertUtils.isEmpty(conversationId)) {
return null;
}
@ -389,7 +376,7 @@ public class AiragChatServiceImpl implements IAiragChatService {
* @author chenrui
* @date 2025/2/25 19:27
*/
private void saveChatConversation(ChatConversation chatConversation, boolean temp, HttpServletRequest httpRequest) {
private void saveChatConversation(ChatConversation chatConversation, boolean temp,HttpServletRequest httpRequest) {
if (null == chatConversation) {
return;
}
@ -427,7 +414,8 @@ public class AiragChatServiceImpl implements IAiragChatService {
case AiragConsts.MESSAGE_ROLE_USER:
List<Content> contents = new ArrayList<>();
List<MessageHistory.ImageHistory> images = history.getImages();
if (oConvertUtils.isObjectNotEmpty(images) && !images.isEmpty()) {
if (oConvertUtils.isObjectNotEmpty(images)
&& !images.isEmpty()) {
contents.addAll(images.stream().map(imageHistory -> {
if (oConvertUtils.isNotEmpty(imageHistory.getUrl())) {
return ImageContent.from(imageHistory.getUrl());
@ -464,7 +452,8 @@ public class AiragChatServiceImpl implements IAiragChatService {
* @author chenrui
* @date 2025/2/25 19:05
*/
private void appendMessage(List<ChatMessage> messages, ChatMessage message, ChatConversation chatConversation, String topicId) {
private void appendMessage(List<ChatMessage> messages, ChatMessage message, ChatConversation
chatConversation, String topicId) {
if (message.type().equals(ChatMessageType.SYSTEM)) {
// 系统消息,放到消息列表最前面,并且不记录历史
@ -478,7 +467,11 @@ public class AiragChatServiceImpl implements IAiragChatService {
histories = new ArrayList<>();
}
// 消息记录
MessageHistory historyMessage = MessageHistory.builder().conversationId(chatConversation.getId()).topicId(topicId).datetime(DateUtils.now()).build();
MessageHistory historyMessage = MessageHistory.builder()
.conversationId(chatConversation.getId())
.topicId(topicId)
.datetime(DateUtils.now())
.build();
if (message.type().equals(ChatMessageType.USER)) {
historyMessage.setRole(AiragConsts.MESSAGE_ROLE_USER);
StringBuilder textContent = new StringBuilder();
@ -523,21 +516,8 @@ public class AiragChatServiceImpl implements IAiragChatService {
// 每次会话都生成一个新的,用来缓存emitter
String requestId = UUIDGenerator.generate();
SseEmitter emitter = new SseEmitter(-0L);
emitter.onError(throwable -> {
log.warn("SEE向客户端发送消息失败: {}", throwable.getMessage());
AiragLocalCache.remove(AiragConsts.CACHE_TYPE_SSE, requestId);
try {
emitter.complete();
} catch (Exception ignore) {}
});
EventData eventRequestId = new EventData(requestId, null, EventData.EVENT_INIT_REQUEST_ID, chatConversation.getId(), topicId);
eventRequestId.setData(EventMessageData.builder().message("").build());
sendMessage2Client(emitter, eventRequestId);
// 缓存emitter
AiragLocalCache.put(AiragConsts.CACHE_TYPE_SSE, requestId, emitter);
// 缓存开始发送时间
log.info("[AI-CHAT]开始发送消息,requestId:{}", requestId);
AiragLocalCache.put(AiragConsts.CACHE_TYPE_SSE_SEND_TIME, requestId, System.currentTimeMillis());
try {
// 组装用户消息
UserMessage userMessage = aiChatHandler.buildUserMessage(sendParams.getContent(), sendParams.getImages());
@ -609,51 +589,36 @@ public class AiragChatServiceImpl implements IAiragChatService {
SseEmitter emitter = AiragLocalCache.get(AiragConsts.CACHE_TYPE_SSE, requestId);
flowRunParams.setEventCallback(eventData -> {
if (EventData.EVENT_FLOW_FINISHED.equals(eventData.getEvent())) {
// 打印耗时日志
printChatDuration(requestId, "流程执行完毕");
// 已经执行完了,删除时间缓存
AiragLocalCache.remove(AiragConsts.CACHE_TYPE_SSE_SEND_TIME, requestId);
EventFlowData data = (EventFlowData) eventData.getData();
if(data.isSuccess()) {
Object outputs = data.getOutputs();
if (oConvertUtils.isObjectNotEmpty(outputs)) {
AiMessage aiMessage;
if (outputs instanceof String) {
// 兼容推理模型
String messageText = String.valueOf(outputs);
messageText = messageText.replaceAll("<think>([\\s\\S]*?)</think>", "> $1");
aiMessage = new AiMessage(messageText);
} else {
aiMessage = new AiMessage(JSONObject.toJSONString(outputs));
}
EventData msgEventData = new EventData(requestId, null, EventData.EVENT_MESSAGE, chatConversation.getId(), topicId);
EventMessageData messageEventData = EventMessageData.builder().message(aiMessage.text()).build();
msgEventData.setData(messageEventData);
msgEventData.setRequestId(requestId);
sendMessage2Client(emitter, msgEventData);
appendMessage(messages, aiMessage, chatConversation, topicId);
// 保存会话
saveChatConversation(chatConversation, false, httpRequest);
Object outputs = data.getOutputs();
if (oConvertUtils.isObjectNotEmpty(outputs)) {
AiMessage aiMessage;
if (outputs instanceof String) {
// 兼容推理模型
String messageText = String.valueOf(outputs);
messageText = messageText.replaceAll("<think>([\\s\\S]*?)</think>", "> $1");
aiMessage = new AiMessage(messageText);
} else {
aiMessage = new AiMessage(JSONObject.toJSONString(outputs));
}
}else{
//update-begin---author:chenrui ---date:20250425 for[QQYUN-12203]AI 聊天,超时或者服务器报错,给个友好提示------------
// 失败
String message = data.getMessage();
if (message != null && message.contains(FlowConsts.FLOW_ERROR_MSG_LLM_TIMEOUT)) {
message = "当前用户较多,排队中,请稍后再试!";
EventData errEventData = new EventData(requestId, null, EventData.EVENT_MESSAGE, chatConversation.getId(), topicId);
errEventData.setData(EventMessageData.builder().message("\n" + message).build());
sendMessage2Client(emitter, errEventData);
errEventData = new EventData(requestId, null, EventData.EVENT_MESSAGE_END, chatConversation.getId(), topicId);
// 如果是超时,主动关闭SSE,防止流程切面中返回异常消息导致前端不能正常展示上面的{普通消息}.
closeSSE(emitter, errEventData);
EventData msgEventData = new EventData(requestId, null, EventData.EVENT_MESSAGE, chatConversation.getId(), topicId);
EventMessageData messageEventData = EventMessageData.builder()
.message(aiMessage.text())
.build();
msgEventData.setData(messageEventData);
try {
String eventStr = JSONObject.toJSONString(msgEventData);
log.debug("[AI应用]接收FLOW返回消息:{}", eventStr);
emitter.send(SseEmitter.event().data(eventStr));
} catch (IOException e) {
throw new RuntimeException(e);
}
//update-end---author:chenrui ---date:20250425 for[QQYUN-12203]AI 聊天,超时或者服务器报错,给个友好提示------------
appendMessage(messages, aiMessage, chatConversation, topicId);
// 保存会话
saveChatConversation(chatConversation, false, httpRequest);
}
}
});
// 打印流程耗时日志
printChatDuration(requestId, "开始执行流程");
airagFlowService.runFlow(flowRunParams);
}
@ -684,26 +649,24 @@ public class AiragChatServiceImpl implements IAiragChatService {
String metadataStr = aiApp.getMetadata();
if (oConvertUtils.isNotEmpty(metadataStr)) {
JSONObject metadata = JSONObject.parseObject(metadataStr);
if (oConvertUtils.isNotEmpty(metadata)) {
if(oConvertUtils.isNotEmpty(metadata)){
if (metadata.containsKey("temperature")) {
aiChatParams.setTemperature(metadata.getDouble("temperature"));
}
if (metadata.containsKey("topP")) {
aiChatParams.setTopP(metadata.getDouble("topP"));
aiChatParams.setTopP(metadata.getDouble("temperature"));
}
if (metadata.containsKey("presencePenalty")) {
aiChatParams.setPresencePenalty(metadata.getDouble("presencePenalty"));
aiChatParams.setPresencePenalty(metadata.getDouble("temperature"));
}
if (metadata.containsKey("frequencyPenalty")) {
aiChatParams.setFrequencyPenalty(metadata.getDouble("frequencyPenalty"));
aiChatParams.setFrequencyPenalty(metadata.getDouble("temperature"));
}
if (metadata.containsKey("maxTokens")) {
aiChatParams.setMaxTokens(metadata.getInteger("maxTokens"));
aiChatParams.setMaxTokens(metadata.getInteger("temperature"));
}
}
}
// 打印流程耗时日志
printChatDuration(requestId, "构造应用自定义参数完成");
// 发消息
sendWithDefault(requestId, chatConversation, topicId, modelId, messages, aiChatParams);
}
@ -720,9 +683,10 @@ public class AiragChatServiceImpl implements IAiragChatService {
* @author chenrui
* @date 2025/2/25 19:24
*/
private void sendWithDefault(String requestId, ChatConversation chatConversation, String topicId, String modelId, List<ChatMessage> messages, AIChatParams aiChatParams) {
private void sendWithDefault(String requestId, ChatConversation chatConversation, String topicId, String modelId,
List<ChatMessage> messages,AIChatParams aiChatParams) {
// 调用ai聊天
if (null == aiChatParams) {
if(null == aiChatParams){
aiChatParams = new AIChatParams();
}
aiChatParams.setKnowIds(chatConversation.getApp().getKnowIds());
@ -730,15 +694,13 @@ public class AiragChatServiceImpl implements IAiragChatService {
HttpServletRequest httpRequest = SpringContextUtils.getHttpServletRequest();
TokenStream chatStream;
try {
// 打印流程耗时日志
printChatDuration(requestId, "开始向LLM发送消息");
if (oConvertUtils.isNotEmpty(modelId)) {
chatStream = aiChatHandler.chat(modelId, messages, aiChatParams);
} else {
chatStream = aiChatHandler.chatByDefaultModel(messages, aiChatParams);
}
} catch (Exception e) {
log.error(e.getMessage(), e);
log.error(e.getMessage(),e);
throw new JeecgBootBizTipException("调用大模型接口失败:" + e.getMessage());
}
/**
@ -747,120 +709,91 @@ public class AiragChatServiceImpl implements IAiragChatService {
AtomicBoolean isThinking = new AtomicBoolean(false);
// ai聊天响应逻辑
chatStream.onNext((String resMessage) -> {
// 兼容推理模型
if ("<think>".equals(resMessage)) {
isThinking.set(true);
resMessage = "> ";
}
if ("</think>".equals(resMessage)) {
isThinking.set(false);
resMessage = "\n\n";
}
if (isThinking.get()) {
if (null != resMessage && resMessage.contains("\n")) {
resMessage = "\n> ";
}
}
EventData eventData = new EventData(requestId, null, EventData.EVENT_MESSAGE, chatConversation.getId(), topicId);
EventMessageData messageEventData = EventMessageData.builder().message(resMessage).build();
eventData.setData(messageEventData);
eventData.setRequestId(requestId);
// sse
SseEmitter emitter = AiragLocalCache.get(AiragConsts.CACHE_TYPE_SSE, requestId);
if (null == emitter) {
log.warn("[AI应用]接收LLM返回会话已关闭");
return;
}
sendMessage2Client(emitter, eventData);
}).onComplete((responseMessage) -> {
// 打印流程耗时日志
printChatDuration(requestId, "LLM输出消息完成");
AiragLocalCache.remove(AiragConsts.CACHE_TYPE_SSE_SEND_TIME, requestId);
// 记录ai的回复
AiMessage aiMessage = responseMessage.content();
FinishReason finishReason = responseMessage.finishReason();
String respText = aiMessage.text();
// sse
SseEmitter emitter = AiragLocalCache.get(AiragConsts.CACHE_TYPE_SSE, requestId);
if (null == emitter) {
log.warn("[AI应用]接收LLM返回会话已关闭");
return;
}
if (FinishReason.STOP.equals(finishReason) || null == finishReason) {
// 正常结束
EventData eventData = new EventData(requestId, null, EventData.EVENT_MESSAGE_END, chatConversation.getId(), topicId);
appendMessage(messages, aiMessage, chatConversation, topicId);
// 保存会话
saveChatConversation(chatConversation, false, httpRequest);
closeSSE(emitter, eventData);
} else if (FinishReason.TOOL_EXECUTION.equals(finishReason)) {
// 需要执行工具
// TODO author: chenrui for: date:2025/3/7
} else if (FinishReason.LENGTH.equals(finishReason)) {
// 上下文长度超过限制
log.error("调用模型异常:上下文长度超过限制:{}", responseMessage.tokenUsage());
EventData eventData = new EventData(requestId, null, EventData.EVENT_MESSAGE, chatConversation.getId(), topicId);
eventData.setData(EventMessageData.builder().message("\n上下文长度超过限制请调整模型最大Tokens").build());
sendMessage2Client(emitter, eventData);
eventData = new EventData(requestId, null, EventData.EVENT_MESSAGE_END, chatConversation.getId(), topicId);
closeSSE(emitter, eventData);
} else {
// 异常结束
log.error("调用模型异常:" + respText);
if (respText.contains("insufficient Balance")) {
respText = "大预言模型账号余额不足!";
}
EventData eventData = new EventData(requestId, null, EventData.EVENT_FLOW_ERROR, chatConversation.getId(), topicId);
eventData.setData(EventFlowData.builder().success(false).message(respText).build());
closeSSE(emitter, eventData);
}
}).onError((Throwable error) -> {
// 打印流程耗时日志
printChatDuration(requestId, "LLM输出消息异常");
AiragLocalCache.remove(AiragConsts.CACHE_TYPE_SSE_SEND_TIME, requestId);
// sse
SseEmitter emitter = AiragLocalCache.get(AiragConsts.CACHE_TYPE_SSE, requestId);
if (null == emitter) {
log.warn("[AI应用]接收LLM返回会话已关闭{}", requestId);
return;
}
log.error(error.getMessage(), error);
String errMsg = error.getMessage();
if (errMsg != null && errMsg.contains("timeout")) {
//update-begin---author:chenrui ---date:20250425 for[QQYUN-12203]AI 聊天,超时或者服务器报错,给个友好提示------------
errMsg = "当前用户较多,排队中,请稍后再试!";
EventData eventData = new EventData(requestId, null, EventData.EVENT_MESSAGE, chatConversation.getId(), topicId);
eventData.setData(EventMessageData.builder().message("\n" + errMsg).build());
sendMessage2Client(emitter, eventData);
eventData = new EventData(requestId, null, EventData.EVENT_MESSAGE_END, chatConversation.getId(), topicId);
closeSSE(emitter, eventData);
//update-end---author:chenrui ---date:20250425 for[QQYUN-12203]AI 聊天,超时或者服务器报错,给个友好提示------------
} else {
errMsg = "调用大模型接口失败,详情请查看后台日志。";
EventData eventData = new EventData(requestId, null, EventData.EVENT_FLOW_ERROR, chatConversation.getId(), topicId);
eventData.setData(EventFlowData.builder().success(false).message(errMsg).build());
closeSSE(emitter, eventData);
}
}).start();
}
/**
* 发送消息到客户端
*
* @param emitter
* @param eventData
* @author chenrui
* @date 2025/4/22 19:58
*/
private static void sendMessage2Client(SseEmitter emitter, EventData eventData) {
try {
log.info("发送消息:{}", eventData.getRequestId());
String eventStr = JSONObject.toJSONString(eventData);
log.debug("[AI应用]接收LLM返回消息:{}", eventStr);
emitter.send(SseEmitter.event().data(eventStr));
} catch (IOException e) {
log.error("发送消息失败", e);
}
// 兼容推理模型
if ("<think>".equals(resMessage)) {
isThinking.set(true);
resMessage = "> ";
}
if ("</think>".equals(resMessage)) {
isThinking.set(false);
resMessage = "\n\n";
}
if (isThinking.get()) {
if (null != resMessage && resMessage.contains("\n")) {
resMessage = "\n> ";
}
}
EventData eventData = new EventData(requestId, null, EventData.EVENT_MESSAGE, chatConversation.getId(), topicId);
EventMessageData messageEventData = EventMessageData.builder()
.message(resMessage)
.build();
eventData.setData(messageEventData);
// sse
SseEmitter emitter = AiragLocalCache.get(AiragConsts.CACHE_TYPE_SSE, requestId);
if (null == emitter) {
log.warn("[AI应用]接收LLM返回会话已关闭");
return;
}
try {
String eventStr = JSONObject.toJSONString(eventData);
log.debug("[AI应用]接收LLM返回消息:{}", eventStr);
emitter.send(SseEmitter.event().data(eventStr));
} catch (IOException e) {
throw new RuntimeException(e);
}
})
.onComplete((responseMessage) -> {
// 记录ai的回复
AiMessage aiMessage = responseMessage.content();
FinishReason finishReason = responseMessage.finishReason();
String respText = aiMessage.text();
// sse
SseEmitter emitter = AiragLocalCache.get(AiragConsts.CACHE_TYPE_SSE, requestId);
if (null == emitter) {
log.warn("[AI应用]接收LLM返回会话已关闭");
return;
}
if (FinishReason.STOP.equals(finishReason) || null == finishReason) {
// 正常结束
EventData eventData = new EventData(requestId, null, EventData.EVENT_MESSAGE_END, chatConversation.getId(), topicId);
try {
log.debug("[AI应用]接收LLM返回消息完成:{}", respText);
emitter.send(SseEmitter.event().data(eventData));
} catch (IOException e) {
throw new RuntimeException(e);
}
appendMessage(messages, aiMessage, chatConversation, topicId);
// 保存会话
saveChatConversation(chatConversation,false,httpRequest);
closeSSE(emitter, eventData);
} else if (FinishReason.TOOL_EXECUTION.equals(finishReason)) {
// 需要执行工具
// TODO author: chenrui for: date:2025/3/7
} else {
// 异常结束
log.error("调用模型异常:" + respText);
if (respText.contains("insufficient Balance")) {
respText = "大预言模型账号余额不足!";
}
EventData eventData = new EventData(requestId, null, EventData.EVENT_FLOW_ERROR, chatConversation.getId(), topicId);
eventData.setData(EventFlowData.builder().success(false).message(respText).build());
closeSSE(emitter, eventData);
}
})
.onError((Throwable error) -> {
// sse
SseEmitter emitter = AiragLocalCache.get(AiragConsts.CACHE_TYPE_SSE, requestId);
if (null == emitter) {
log.warn("[AI应用]接收LLM返回会话已关闭");
return;
}
String errMsg = "调用大模型接口失败:" + error.getMessage();
log.error(errMsg, error);
EventData eventData = new EventData(requestId, null, EventData.EVENT_FLOW_ERROR, chatConversation.getId(), topicId);
eventData.setData(EventFlowData.builder().success(false).message(errMsg).build());
closeSSE(emitter, eventData);
})
.start();
}
/**
@ -904,7 +837,12 @@ public class AiragChatServiceImpl implements IAiragChatService {
}
CompletableFuture.runAsync(() -> {
List<ChatMessage> messages = new LinkedList<>();
String systemMsgStr = "根据用户的问题,总结会话标题.\n" + "要求如下:\n" + "1. 使用中文回答.\n" + "2. 标题长度控制在5个汉字10个英文字符以内\n" + "3. 直接回复会话标题,不要有其他任何无关描述\n" + "4. 如果无法总结,回复不知道\n";
String systemMsgStr = "根据用户的问题,总结会话标题.\n" +
"要求如下:\n" +
"1. 使用中文回答.\n" +
"2. 标题长度控制在5个汉字10个英文字符以内\n" +
"3. 直接回复会话标题,不要有其他任何无关描述\n" +
"4. 如果无法总结,回复不知道\n";
messages.add(new SystemMessage(systemMsgStr));
messages.add(new UserMessage(question));
String summaryTitle;
@ -938,7 +876,6 @@ public class AiragChatServiceImpl implements IAiragChatService {
/**
* 获取用户名
*
* @param httpRequest
* @return
* @author chenrui
@ -948,9 +885,9 @@ public class AiragChatServiceImpl implements IAiragChatService {
try {
TokenUtils.getTokenByRequest();
String token;
if (null != httpRequest) {
if(null != httpRequest){
token = TokenUtils.getTokenByRequest(httpRequest);
} else {
}else{
token = TokenUtils.getTokenByRequest();
}
if (TokenUtils.verifyToken(token, sysBaseApi, redisUtil)) {
@ -961,19 +898,4 @@ public class AiragChatServiceImpl implements IAiragChatService {
}
return null;
}
/**
* 打印耗时
* @param requestId
* @param message
* @author chenrui
* @date 2025/4/28 15:15
*/
private static void printChatDuration(String requestId,String message) {
Long beginTime = AiragLocalCache.get(AiragConsts.CACHE_TYPE_SSE_SEND_TIME, requestId);
if (null != beginTime) {
log.info("[AI-CHAT]{},requestId:{},耗时:{}s", message, requestId, (System.currentTimeMillis() - beginTime) / 1000);
}
}
}

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