mirror of
https://github.com/jeecgboot/JeecgBoot.git
synced 2026-01-24 12:06:52 +08:00
Compare commits
133 Commits
main
...
springboot
| Author | SHA1 | Date | |
|---|---|---|---|
| a1486dae34 | |||
| 7d3ba49a66 | |||
| 7ba2527c67 | |||
| d988a637b2 | |||
| 33cbc6e25b | |||
| 5bcb649384 | |||
| d02753ce50 | |||
| 0bfe5689cc | |||
| aa55c3a86c | |||
| 6a6d29254b | |||
| c4a87d7c58 | |||
| 47107a53c2 | |||
| 52cd43d17c | |||
| 7ff70930ef | |||
| 45f83dff43 | |||
| fcbaed2f7f | |||
| 4acd5b3464 | |||
| e10df20e98 | |||
| d0f85ccc7e | |||
| d12fe9d8de | |||
| 223d374edd | |||
| 7f011f199e | |||
| 873b8bd823 | |||
| 2f3aff9584 | |||
| 1f74cb538f | |||
| 2cce917ab2 | |||
| fd320be203 | |||
| 6e5c0eebce | |||
| cae8e7e8d2 | |||
| ada97acb04 | |||
| 01547ef83e | |||
| b3ed442b55 | |||
| 9cca8f8719 | |||
| ae6069882f | |||
| f0a5edaa49 | |||
| ab98b26cac | |||
| 64b3c9e42e | |||
| 8eb81493ce | |||
| 044fc47586 | |||
| bbe18c582c | |||
| b894125b53 | |||
| 2e236703b2 | |||
| a771d24a57 | |||
| 69f5d12de7 | |||
| 5b5999e786 | |||
| fc3fe39d95 | |||
| 48e23aafab | |||
| 657b84d3cf | |||
| 2021bf39f8 | |||
| fdeb37c3d0 | |||
| f9123208e1 | |||
| accb8f2f9f | |||
| c643994546 | |||
| 6934a0adee | |||
| 93e32a7177 | |||
| c9f5bb4409 | |||
| 10b68858d6 | |||
| da72e8f9c5 | |||
| 73e86686dc | |||
| f43d0d486b | |||
| 65bde3331b | |||
| b60942aa86 | |||
| 197b267e71 | |||
| 79f7134bd5 | |||
| 6d432bc186 | |||
| 415307eb9f | |||
| 48e20b2af5 | |||
| b7924b9ca8 | |||
| a10a2e0a9d | |||
| 4aa88189ed | |||
| fdb05443c2 | |||
| 65d737db6d | |||
| f04f7f9abf | |||
| 935e118d15 | |||
| e218367332 | |||
| 3a3f3cf367 | |||
| 0e762b4157 | |||
| f4712baa39 | |||
| 7d8b653d6e | |||
| cf7f3f94be | |||
| 49f63b92ac | |||
| 5670a15b20 | |||
| 9e9ef20b7c | |||
| 0c034031d1 | |||
| 491a038b5a | |||
| 8a4fcb0023 | |||
| e93dcc1a7e | |||
| 383cbf250f | |||
| 9fe1450ac9 | |||
| 88b9b12998 | |||
| 9e25566271 | |||
| 8e54e06978 | |||
| e5c082ae13 | |||
| 96ab98ac3e | |||
| 1632c241ee | |||
| e9d05b0e75 | |||
| 6ade7e22f8 | |||
| 43d47c08cb | |||
| e616c5d8fe | |||
| cddf23c787 | |||
| 70a37309dd | |||
| 48555b5219 | |||
| 06d58f202f | |||
| 628870af9b | |||
| b46a6438e6 | |||
| 5488f99723 | |||
| 6bc1fe8d21 | |||
| 7cac16320c | |||
| 24dbd1db39 | |||
| 46b026b989 | |||
| 94c45f5e0f | |||
| 8950e19d4e | |||
| 99eb88f71c | |||
| 824d7839d8 | |||
| c88f9d95d4 | |||
| beb0bc2f64 | |||
| f741db874c | |||
| d684c09392 | |||
| 364be22dd0 | |||
| 20efa3bf9a | |||
| c7977dda3d | |||
| c27c5a9a9b | |||
| 0ab280f812 | |||
| c3066dac17 | |||
| b650d512b3 | |||
| 925ec9447d | |||
| 411a73c1bf | |||
| 84077e6e24 | |||
| 184cf97304 | |||
| 5f425b49b2 | |||
| 3ac8ee304a | |||
| 0faac01bb7 | |||
| 74d88a8fcc |
1
.gitignore
vendored
1
.gitignore
vendored
@ -13,5 +13,6 @@ os_del.cmd
|
|||||||
os_del_doc.cmd
|
os_del_doc.cmd
|
||||||
.svn
|
.svn
|
||||||
derby.log
|
derby.log
|
||||||
|
*.log
|
||||||
.cursor
|
.cursor
|
||||||
.history
|
.history
|
||||||
@ -3,6 +3,9 @@ AIGC应用平台介绍
|
|||||||
|
|
||||||
一个全栈式 AI 开发平台,旨在帮助开发者快速构建和部署个性化的 AI 应用。
|
一个全栈式 AI 开发平台,旨在帮助开发者快速构建和部署个性化的 AI 应用。
|
||||||
|
|
||||||
|
> JDK说明:AI流程编排引擎暂时不支持jdk21,所以目前只能使用jdk8或者jdk17启动项目。
|
||||||
|
|
||||||
|
|
||||||
JeecgBoot平台的AIGC功能模块,是一套类似`Dify`的`AIGC应用开发平台`+`知识库问答`,是一款基于LLM大语言模型AI应用平台和 RAG 的知识库问答系统。
|
JeecgBoot平台的AIGC功能模块,是一套类似`Dify`的`AIGC应用开发平台`+`知识库问答`,是一款基于LLM大语言模型AI应用平台和 RAG 的知识库问答系统。
|
||||||
其直观的界面结合了 AI 流程编排、RAG 管道、知识库管理、模型管理、对接向量库、实时运行可观察等,让您可以快速从原型到生产,拥有AI服务能力。
|
其直观的界面结合了 AI 流程编排、RAG 管道、知识库管理、模型管理、对接向量库、实时运行可观察等,让您可以快速从原型到生产,拥有AI服务能力。
|
||||||
|
|
||||||
@ -106,10 +109,6 @@ JeecgBoot平台的AIGC功能模块,是一套类似`Dify`的`AIGC应用开发
|
|||||||
| ChatGTP | √ |
|
| ChatGTP | √ |
|
||||||
| Qwq | √ |
|
| Qwq | √ |
|
||||||
| 智库 | √ |
|
| 智库 | √ |
|
||||||
| claude | √ |
|
|
||||||
| vl模型 | √ |
|
|
||||||
| 千帆大模型 | √ |
|
|
||||||
| 通义千问 | √ |
|
|
||||||
| Ollama本地搭建大模型 | √ |
|
| Ollama本地搭建大模型 | √ |
|
||||||
| 等等。。 | √ |
|
| 等等。。 | √ |
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
[中文](./README.md) | English
|
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@ -7,12 +7,12 @@
|
|||||||
JEECG BOOT AI Low Code Platform
|
JEECG BOOT AI Low Code Platform
|
||||||
===============
|
===============
|
||||||
|
|
||||||
Current version: 3.9.1 (Release date: 2026-01-22)
|
Current version: 3.9.0 (Release date: 2025-12-01)
|
||||||
|
|
||||||
|
|
||||||
[](https://github.com/zhangdaiscott/jeecg-boot/blob/master/LICENSE)
|
[](https://github.com/zhangdaiscott/jeecg-boot/blob/master/LICENSE)
|
||||||
[](http://www.jeecg.com)
|
[](http://www.jeecg.com)
|
||||||
[](https://github.com/zhangdaiscott/jeecg-boot)
|
[](https://github.com/zhangdaiscott/jeecg-boot)
|
||||||
[](https://github.com/zhangdaiscott/jeecg-boot)
|
[](https://github.com/zhangdaiscott/jeecg-boot)
|
||||||
[](https://github.com/zhangdaiscott/jeecg-boot)
|
[](https://github.com/zhangdaiscott/jeecg-boot)
|
||||||
|
|
||||||
126
README-Enterprise.md
Normal file
126
README-Enterprise.md
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
|
||||||
|
JeecgBoot低代码平台(商业版介绍)
|
||||||
|
===============
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
项目介绍
|
||||||
|
-----------------------------------
|
||||||
|
|
||||||
|
<h3 align="center">企业级AI低代码平台</h3>
|
||||||
|
|
||||||
|
|
||||||
|
JeecgBoot是一款集成AI应用的,基于BPM流程的低代码平台,旨在帮助企业快速实现低代码开发和构建个性化AI应用,支持MCP和插件,实现聊天式业务操作(如 “一句话创建用户”)!
|
||||||
|
|
||||||
|
前后端分离架构Ant Design&Vue3,SpringBoot,SpringCloud Alibaba,Mybatis-plus,Shiro。强大的代码生成器让前后端代码一键生成,无需写任何代码! 引领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://www.bilibili.com/video/BV1Nk4y1o7Qc)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### 商业版功能简述
|
||||||
|
|
||||||
|
> 详细的功能介绍,[请联系官方](https://jeecg.com/vip)
|
||||||
|
|
||||||
|
```
|
||||||
|
│─更多商业功能
|
||||||
|
│ ├─流程设计器
|
||||||
|
│ ├─简流设计器(类钉钉版)
|
||||||
|
│ ├─门户设计(NEW)
|
||||||
|
│ ├─表单设计器
|
||||||
|
│ ├─大屏设计器
|
||||||
|
│ └─我的任务
|
||||||
|
│ └─历史流程
|
||||||
|
│ └─历史流程
|
||||||
|
│ └─流程实例管理
|
||||||
|
│ └─流程监听管理
|
||||||
|
│ └─流程表达式
|
||||||
|
│ └─我发起的流程
|
||||||
|
│ └─我的抄送
|
||||||
|
│ └─流程委派、抄送、跳转
|
||||||
|
│ └─OA办公组件
|
||||||
|
│ └─零代码应用管理(无需编码,在线搭建应用系统)
|
||||||
|
│ ├─积木报表企业版(含jimureport、jimubi)
|
||||||
|
│ ├─AI流程设计器源码
|
||||||
|
│ ├─Online全模块功能和源码
|
||||||
|
│ ├─AI写文章(CMS)
|
||||||
|
│ ├─AI表单字段建议(表单设计器)
|
||||||
|
│ ├─OA办公协同组件
|
||||||
|
│ ├─在线聊天功能
|
||||||
|
│ ├─设计表单移动适配
|
||||||
|
│ ├─设计表单支持外部填报
|
||||||
|
│ ├─设计表单AI字段建议
|
||||||
|
│ ├─设计表单视图功能(支持多种类型含日历、表格、看板、甘特图)
|
||||||
|
│ └─。。。
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##### 流程设计
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##### 表单设计器
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
19
README.md
19
README.md
@ -1,15 +1,14 @@
|
|||||||
中文 | [English](./README.en-US.md)
|
|
||||||
|
|
||||||
JeecgBoot AI低代码平台
|
JeecgBoot AI低代码平台
|
||||||
===============
|
===============
|
||||||
|
|
||||||
当前最新版本: 3.9.1(发布日期:2026-01-22)
|
当前最新版本: 3.9.0(发布日期:2025-12-01)
|
||||||
|
|
||||||
|
|
||||||
[](https://github.com/jeecgboot/JeecgBoot/blob/master/LICENSE)
|
[](https://github.com/jeecgboot/JeecgBoot/blob/master/LICENSE)
|
||||||
[](https://jeecg.com)
|
[](https://jeecg.com)
|
||||||
[](https://jeecg.blog.csdn.net)
|
[](https://jeecg.blog.csdn.net)
|
||||||
[](https://github.com/jeecgboot/JeecgBoot)
|
[](https://github.com/jeecgboot/JeecgBoot)
|
||||||
[](https://github.com/jeecgboot/JeecgBoot)
|
[](https://github.com/jeecgboot/JeecgBoot)
|
||||||
[](https://github.com/jeecgboot/JeecgBoot)
|
[](https://github.com/jeecgboot/JeecgBoot)
|
||||||
|
|
||||||
@ -233,6 +232,20 @@ JeecgBoot平台提供了一套完善的AI应用管理系统模块,是一套类
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
开源版与企业版区别?
|
||||||
|
-----------------------------------
|
||||||
|
|
||||||
|
- JeecgBoot开源版采用 [Apache-2.0 license](LICENSE) 协议附加补充条款:允许商用使用,不会造成侵权行为,允许基于本平台软件开展业务系统开发(但在任何情况下,您不得使用本软件开发可能被认为与本软件竞争的软件).
|
||||||
|
- 商业版与开源版主要区别在于商业版提供了技术支持 和 更多的企业级功能(例如:Online图表、流程监控、流程设计、流程审批、表单设计器、表单视图、积木报表企业版、OA办公、商业APP、零代码应用、Online模块源码等功能). [更多商业功能介绍,点击查看](README-Enterprise.md)
|
||||||
|
- JeecgBoot未来发展方向是:零代码平台的建设,也就是团队的另外一款产品 [敲敲云零代码](https://www.qiaoqiaoyun.com) ,无需编码即可通过拖拽快速搭建企业级应用,与JeecgBoot低代码平台形成互补,满足从简单业务到复杂系统的全场景开发需求,目前已经开源,[欢迎下载](https://qiaoqiaoyun.com/downloadCode)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### Jeecg Boot 产品功能蓝图
|
### Jeecg Boot 产品功能蓝图
|
||||||

|

|
||||||
|
|
||||||
|
|||||||
1
jeecg-boot/.gitignore
vendored
1
jeecg-boot/.gitignore
vendored
@ -13,3 +13,4 @@ os_del.cmd
|
|||||||
os_del_doc.cmd
|
os_del_doc.cmd
|
||||||
.svn
|
.svn
|
||||||
derby.log
|
derby.log
|
||||||
|
*.log
|
||||||
@ -1,12 +1,13 @@
|
|||||||
|
|
||||||
JeecgBoot 低代码开发平台
|
JeecgBoot 低代码开发平台
|
||||||
===============
|
===============
|
||||||
|
|
||||||
当前最新版本: 3.9.1(发布日期: 2026-01-22)
|
当前最新版本: 3.9.0(发布日期:2025-12-01)
|
||||||
|
|
||||||
|
|
||||||
[](https://github.com/zhangdaiscott/jeecg-boot/blob/master/LICENSE)
|
[](https://github.com/zhangdaiscott/jeecg-boot/blob/master/LICENSE)
|
||||||
[](http://jeecg.com/aboutusIndex)
|
[](http://jeecg.com/aboutusIndex)
|
||||||
[](https://github.com/zhangdaiscott/jeecg-boot)
|
[](https://github.com/zhangdaiscott/jeecg-boot)
|
||||||
[](https://github.com/zhangdaiscott/jeecg-boot)
|
[](https://github.com/zhangdaiscott/jeecg-boot)
|
||||||
[](https://github.com/zhangdaiscott/jeecg-boot)
|
[](https://github.com/zhangdaiscott/jeecg-boot)
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
BIN
jeecg-boot/db/其他数据库脚本/jeecgboot-oracle11g.dmp
Normal file
BIN
jeecg-boot/db/其他数据库脚本/jeecgboot-oracle11g.dmp
Normal file
Binary file not shown.
31499
jeecg-boot/db/其他数据库脚本/jeecgboot-oracle11g.sql
Normal file
31499
jeecg-boot/db/其他数据库脚本/jeecgboot-oracle11g.sql
Normal file
File diff suppressed because one or more lines are too long
27491
jeecg-boot/db/其他数据库脚本/jeecgboot-postgresql17.sql
Normal file
27491
jeecg-boot/db/其他数据库脚本/jeecgboot-postgresql17.sql
Normal file
File diff suppressed because one or more lines are too long
50451
jeecg-boot/db/其他数据库脚本/jeecgboot-sqlserver2017.sql
Normal file
50451
jeecg-boot/db/其他数据库脚本/jeecgboot-sqlserver2017.sql
Normal file
File diff suppressed because one or more lines are too long
45
jeecg-boot/db/增量SQL/sas升级脚本.sql
Normal file
45
jeecg-boot/db/增量SQL/sas升级脚本.sql
Normal 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]}');
|
||||||
@ -4,7 +4,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>org.jeecgframework.boot3</groupId>
|
<groupId>org.jeecgframework.boot3</groupId>
|
||||||
<artifactId>jeecg-boot-parent</artifactId>
|
<artifactId>jeecg-boot-parent</artifactId>
|
||||||
<version>3.9.1</version>
|
<version>3.9.0</version>
|
||||||
</parent>
|
</parent>
|
||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
<artifactId>jeecg-boot-base-core</artifactId>
|
<artifactId>jeecg-boot-base-core</artifactId>
|
||||||
@ -192,76 +192,19 @@
|
|||||||
<version>${java-jwt.version}</version>
|
<version>${java-jwt.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!--shiro-->
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.apache.shiro</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>shiro-spring-boot-starter</artifactId>
|
<artifactId>spring-boot-starter-oauth2-authorization-server</artifactId>
|
||||||
<classifier>jakarta</classifier>
|
|
||||||
<version>${shiro.version}</version>
|
|
||||||
<exclusions>
|
|
||||||
<exclusion>
|
|
||||||
<groupId>org.apache.shiro</groupId>
|
|
||||||
<artifactId>shiro-spring</artifactId>
|
|
||||||
</exclusion>
|
|
||||||
</exclusions>
|
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.apache.shiro</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>shiro-spring</artifactId>
|
<artifactId>spring-boot-starter-oauth2-resource-server</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>
|
</dependency>
|
||||||
<!-- 引入适配jakarta的依赖包 -->
|
<!-- 添加spring security cas支持 -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.apache.shiro</groupId>
|
<groupId>org.springframework.security</groupId>
|
||||||
<artifactId>shiro-core</artifactId>
|
<artifactId>spring-security-cas</artifactId>
|
||||||
<classifier>jakarta</classifier>
|
|
||||||
<version>${shiro.version}</version>
|
|
||||||
<exclusions>
|
|
||||||
<exclusion>
|
|
||||||
<groupId>commons-beanutils</groupId>
|
|
||||||
<artifactId>commons-beanutils</artifactId>
|
|
||||||
</exclusion>
|
|
||||||
</exclusions>
|
|
||||||
</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>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
@ -305,7 +248,7 @@
|
|||||||
<optional>true</optional>
|
<optional>true</optional>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- minio文件存储服务 -->
|
<!-- mini文件存储服务 -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.minio</groupId>
|
<groupId>io.minio</groupId>
|
||||||
<artifactId>minio</artifactId>
|
<artifactId>minio</artifactId>
|
||||||
|
|||||||
@ -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();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,5 +1,6 @@
|
|||||||
package org.jeecg.common.api;
|
package org.jeecg.common.api;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson.JSONObject;
|
||||||
import org.jeecg.common.api.dto.AiragFlowDTO;
|
import org.jeecg.common.api.dto.AiragFlowDTO;
|
||||||
import org.jeecg.common.system.vo.*;
|
import org.jeecg.common.system.vo.*;
|
||||||
|
|
||||||
@ -65,6 +66,13 @@ public interface CommonAPI {
|
|||||||
*/
|
*/
|
||||||
public String getUserIdByName(String username);
|
public String getUserIdByName(String username);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 5根据用户手机号查询用户信息
|
||||||
|
* @param username
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public LoginUser getUserByPhone(String phone);
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 6字典表的 翻译
|
* 6字典表的 翻译
|
||||||
@ -154,4 +162,31 @@ public interface CommonAPI {
|
|||||||
*/
|
*/
|
||||||
Object runAiragFlow(AiragFlowDTO airagFlowDTO);
|
Object runAiragFlow(AiragFlowDTO airagFlowDTO);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 登录加载系统字典
|
||||||
|
* @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);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -33,10 +33,4 @@ public class AiragFlowDTO implements Serializable {
|
|||||||
* 输入参数
|
* 输入参数
|
||||||
*/
|
*/
|
||||||
private Map<String, Object> inputParams;
|
private Map<String, Object> inputParams;
|
||||||
|
|
||||||
/**
|
|
||||||
* 是否流式返回
|
|
||||||
*/
|
|
||||||
private boolean isStream;
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
package org.jeecg.common.aspect;
|
package org.jeecg.common.aspect;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson.JSON;
|
||||||
import com.alibaba.fastjson.JSONObject;
|
import com.alibaba.fastjson.JSONObject;
|
||||||
import com.alibaba.fastjson.serializer.PropertyFilter;
|
import com.alibaba.fastjson.serializer.PropertyFilter;
|
||||||
import org.apache.shiro.SecurityUtils;
|
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.CommonConstant;
|
||||||
import org.jeecg.common.constant.enums.ModuleType;
|
import org.jeecg.common.constant.enums.ModuleType;
|
||||||
import org.jeecg.common.constant.enums.OperateTypeEnum;
|
import org.jeecg.common.constant.enums.OperateTypeEnum;
|
||||||
|
import org.jeecg.config.security.utils.SecureUtil;
|
||||||
import org.jeecg.modules.base.service.BaseCommonService;
|
import org.jeecg.modules.base.service.BaseCommonService;
|
||||||
import org.jeecg.common.system.vo.LoginUser;
|
import org.jeecg.common.system.vo.LoginUser;
|
||||||
import org.jeecg.common.util.IpUtils;
|
import org.jeecg.common.util.IpUtils;
|
||||||
import org.jeecg.common.util.SpringContextUtils;
|
import org.jeecg.common.util.SpringContextUtils;
|
||||||
import org.jeecg.common.util.oConvertUtils;
|
import org.jeecg.common.util.oConvertUtils;
|
||||||
import org.springframework.core.StandardReflectionParameterNameDiscoverer;
|
import org.springframework.core.StandardReflectionParameterNameDiscoverer;
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.validation.BindingResult;
|
import org.springframework.validation.BindingResult;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
@ -100,7 +103,7 @@ public class AutoLogAspect {
|
|||||||
//设置IP地址
|
//设置IP地址
|
||||||
dto.setIp(IpUtils.getIpAddr(request));
|
dto.setIp(IpUtils.getIpAddr(request));
|
||||||
//获取登录用户信息
|
//获取登录用户信息
|
||||||
LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();
|
LoginUser sysUser = SecureUtil.currentUser();
|
||||||
if(sysUser!=null){
|
if(sysUser!=null){
|
||||||
dto.setUserid(sysUser.getUsername());
|
dto.setUserid(sysUser.getUsername());
|
||||||
dto.setUsername(sysUser.getRealname());
|
dto.setUsername(sysUser.getRealname());
|
||||||
|
|||||||
@ -90,9 +90,9 @@ public interface CommonConstant {
|
|||||||
/** 登录用户Shiro权限缓存KEY前缀 */
|
/** 登录用户Shiro权限缓存KEY前缀 */
|
||||||
public static String PREFIX_USER_SHIRO_CACHE = "shiro:cache:org.jeecg.config.shiro.ShiroRealm.authorizationCache:";
|
public static String PREFIX_USER_SHIRO_CACHE = "shiro:cache:org.jeecg.config.shiro.ShiroRealm.authorizationCache:";
|
||||||
/** 登录用户Token令牌缓存KEY前缀 */
|
/** 登录用户Token令牌缓存KEY前缀 */
|
||||||
String PREFIX_USER_TOKEN = "prefix_user_token:";
|
String PREFIX_USER_TOKEN = "token::jeecg-client::";
|
||||||
/** 登录用户Token令牌作废提示信息,比如 “不允许同一账号多地同时登录,会往这个变量存提示信息” */
|
/** 登录用户Token令牌作废提示信息,比如 “不允许同一账号多地同时登录,会往这个变量存提示信息” */
|
||||||
String PREFIX_USER_TOKEN_ERROR_MSG = "prefix_user_token:error:msg_";
|
String PREFIX_USER_TOKEN_ERROR_MSG = "token::jeecg-client::error:msg_";
|
||||||
|
|
||||||
/**============================== 【是否允许同一账号多地同时登录】登录客户端类型常量 ==============================*/
|
/**============================== 【是否允许同一账号多地同时登录】登录客户端类型常量 ==============================*/
|
||||||
/** 客户端类型:PC端 */
|
/** 客户端类型:PC端 */
|
||||||
@ -101,11 +101,11 @@ public interface CommonConstant {
|
|||||||
String CLIENT_TYPE_APP = "APP";
|
String CLIENT_TYPE_APP = "APP";
|
||||||
/** 客户端类型:手机号登录 */
|
/** 客户端类型:手机号登录 */
|
||||||
String CLIENT_TYPE_PHONE = "PHONE";
|
String CLIENT_TYPE_PHONE = "PHONE";
|
||||||
String PREFIX_USER_TOKEN_PC = "prefix_user_token:single_login:pc:";
|
String PREFIX_USER_TOKEN_PC = "token::jeecg-client::single_login:pc:";
|
||||||
/** 单点登录:用户在APP端的Token缓存KEY前缀 (username -> token) */
|
/** 单点登录:用户在APP端的Token缓存KEY前缀 (username -> token) */
|
||||||
String PREFIX_USER_TOKEN_APP = "prefix_user_token:single_login:app:";
|
String PREFIX_USER_TOKEN_APP = "token::jeecg-client::single_login:app:";
|
||||||
/** 单点登录:用户在手机号登录的Token缓存KEY前缀 (username -> token) */
|
/** 单点登录:用户在手机号登录的Token缓存KEY前缀 (username -> token) */
|
||||||
String PREFIX_USER_TOKEN_PHONE = "prefix_user_token:single_login:phone:";
|
String PREFIX_USER_TOKEN_PHONE = "token::jeecg-client::single_login:phone:";
|
||||||
/**============================== 【是否允许同一账号多地同时登录】登录客户端类型常量 ==============================*/
|
/**============================== 【是否允许同一账号多地同时登录】登录客户端类型常量 ==============================*/
|
||||||
|
|
||||||
// /** Token缓存时间:3600秒即一小时 */
|
// /** Token缓存时间:3600秒即一小时 */
|
||||||
|
|||||||
@ -47,8 +47,4 @@ public interface TenantConstant {
|
|||||||
*/
|
*/
|
||||||
String APP_ADMIN = "appAdmin";
|
String APP_ADMIN = "appAdmin";
|
||||||
|
|
||||||
/**
|
|
||||||
* 增加SignatureCheck注解POST请求的URL
|
|
||||||
*/
|
|
||||||
String[] SIGNATURE_CHECK_POST_URL = { "/sys/tenant/joinTenantByHouseNumber", "/sys/tenant/invitationUser" };
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,8 +6,6 @@ import jakarta.servlet.http.HttpServletRequest;
|
|||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.commons.lang3.exception.ExceptionUtils;
|
import org.apache.commons.lang3.exception.ExceptionUtils;
|
||||||
import org.apache.shiro.SecurityUtils;
|
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.dto.LogDTO;
|
||||||
import org.jeecg.common.api.vo.Result;
|
import org.jeecg.common.api.vo.Result;
|
||||||
import org.jeecg.common.constant.CommonConstant;
|
import org.jeecg.common.constant.CommonConstant;
|
||||||
@ -24,6 +22,8 @@ import org.springframework.dao.DataIntegrityViolationException;
|
|||||||
import org.springframework.dao.DuplicateKeyException;
|
import org.springframework.dao.DuplicateKeyException;
|
||||||
import org.springframework.data.redis.connection.PoolException;
|
import org.springframework.data.redis.connection.PoolException;
|
||||||
import org.springframework.http.HttpStatus;
|
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.util.CollectionUtils;
|
||||||
import org.springframework.validation.ObjectError;
|
import org.springframework.validation.ObjectError;
|
||||||
import org.springframework.web.HttpRequestMethodNotSupportedException;
|
import org.springframework.web.HttpRequestMethodNotSupportedException;
|
||||||
@ -48,9 +48,27 @@ import java.util.stream.Collectors;
|
|||||||
@Slf4j
|
@Slf4j
|
||||||
public class JeecgBootExceptionHandler {
|
public class JeecgBootExceptionHandler {
|
||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
BaseCommonService baseCommonService;
|
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)
|
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||||
public Result<?> handleValidationExceptions(MethodArgumentNotValidException e) {
|
public Result<?> handleValidationExceptions(MethodArgumentNotValidException e) {
|
||||||
log.error(e.getMessage(), e);
|
log.error(e.getMessage(), e);
|
||||||
@ -112,8 +130,8 @@ public class JeecgBootExceptionHandler {
|
|||||||
return Result.error("数据库中已存在该记录");
|
return Result.error("数据库中已存在该记录");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ExceptionHandler({UnauthorizedException.class, AuthorizationException.class})
|
@ExceptionHandler(AccessDeniedException.class)
|
||||||
public Result<?> handleAuthorizationException(AuthorizationException e){
|
public Result<?> handleAuthorizationException(AccessDeniedException e){
|
||||||
log.error(e.getMessage(), e);
|
log.error(e.getMessage(), e);
|
||||||
return Result.noauth("没有权限,请联系管理员分配权限!");
|
return Result.noauth("没有权限,请联系管理员分配权限!");
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,5 +1,6 @@
|
|||||||
package org.jeecg.common.system.base.controller;
|
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.conditions.query.QueryWrapper;
|
||||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
@ -12,6 +13,7 @@ import org.jeecg.common.system.query.QueryGenerator;
|
|||||||
import org.jeecg.common.system.vo.LoginUser;
|
import org.jeecg.common.system.vo.LoginUser;
|
||||||
import org.jeecg.common.util.oConvertUtils;
|
import org.jeecg.common.util.oConvertUtils;
|
||||||
import org.jeecg.config.JeecgBaseConfig;
|
import org.jeecg.config.JeecgBaseConfig;
|
||||||
|
import org.jeecg.config.security.utils.SecureUtil;
|
||||||
import org.jeecgframework.poi.excel.ExcelImportUtil;
|
import org.jeecgframework.poi.excel.ExcelImportUtil;
|
||||||
import org.jeecgframework.poi.excel.def.NormalExcelConstants;
|
import org.jeecgframework.poi.excel.def.NormalExcelConstants;
|
||||||
import org.jeecgframework.poi.excel.entity.ExportParams;
|
import org.jeecgframework.poi.excel.entity.ExportParams;
|
||||||
@ -20,6 +22,7 @@ import org.jeecgframework.poi.excel.entity.enmus.ExcelType;
|
|||||||
import org.jeecgframework.poi.excel.view.JeecgEntityExcelView;
|
import org.jeecgframework.poi.excel.view.JeecgEntityExcelView;
|
||||||
import org.jeecgframework.poi.handler.inter.IExcelExportServer;
|
import org.jeecgframework.poi.handler.inter.IExcelExportServer;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
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.MultipartFile;
|
||||||
import org.springframework.web.multipart.MultipartHttpServletRequest;
|
import org.springframework.web.multipart.MultipartHttpServletRequest;
|
||||||
import org.springframework.web.servlet.ModelAndView;
|
import org.springframework.web.servlet.ModelAndView;
|
||||||
@ -52,7 +55,7 @@ public class JeecgController<T, S extends IService<T>> {
|
|||||||
protected ModelAndView exportXls(HttpServletRequest request, T object, Class<T> clazz, String title) {
|
protected ModelAndView exportXls(HttpServletRequest request, T object, Class<T> clazz, String title) {
|
||||||
// Step.1 组装查询条件
|
// Step.1 组装查询条件
|
||||||
QueryWrapper<T> queryWrapper = QueryGenerator.initQueryWrapper(object, request.getParameterMap());
|
QueryWrapper<T> queryWrapper = QueryGenerator.initQueryWrapper(object, request.getParameterMap());
|
||||||
LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();
|
LoginUser sysUser = SecureUtil.currentUser();
|
||||||
|
|
||||||
// 过滤选中数据
|
// 过滤选中数据
|
||||||
String selections = request.getParameter("selections");
|
String selections = request.getParameter("selections");
|
||||||
@ -94,7 +97,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) {
|
protected ModelAndView exportXlsSheet(HttpServletRequest request, T object, Class<T> clazz, String title,String exportFields,Integer pageNum) {
|
||||||
// Step.1 组装查询条件
|
// Step.1 组装查询条件
|
||||||
QueryWrapper<T> queryWrapper = QueryGenerator.initQueryWrapper(object, request.getParameterMap());
|
QueryWrapper<T> queryWrapper = QueryGenerator.initQueryWrapper(object, request.getParameterMap());
|
||||||
LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();
|
LoginUser sysUser = SecureUtil.currentUser();
|
||||||
// Step.2 计算分页sheet数据
|
// Step.2 计算分页sheet数据
|
||||||
double total = service.count();
|
double total = service.count();
|
||||||
int count = (int)Math.ceil(total/pageNum);
|
int count = (int)Math.ceil(total/pageNum);
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
package org.jeecg.common.system.util;
|
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.JWT;
|
||||||
import com.auth0.jwt.JWTVerifier;
|
import com.auth0.jwt.JWTVerifier;
|
||||||
import com.auth0.jwt.algorithms.Algorithm;
|
import com.auth0.jwt.algorithms.Algorithm;
|
||||||
@ -10,9 +12,9 @@ import com.google.common.base.Joiner;
|
|||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.util.Date;
|
import java.util.*;
|
||||||
import java.util.Objects;
|
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
import jakarta.servlet.ServletResponse;
|
import jakarta.servlet.ServletResponse;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
@ -20,7 +22,7 @@ import jakarta.servlet.http.HttpServletResponse;
|
|||||||
import jakarta.servlet.http.HttpSession;
|
import jakarta.servlet.http.HttpSession;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
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.api.vo.Result;
|
||||||
import org.jeecg.common.constant.CommonConstant;
|
import org.jeecg.common.constant.CommonConstant;
|
||||||
import org.jeecg.common.constant.DataBaseConstant;
|
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.DateUtils;
|
||||||
import org.jeecg.common.util.SpringContextUtils;
|
import org.jeecg.common.util.SpringContextUtils;
|
||||||
import org.jeecg.common.util.oConvertUtils;
|
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
|
* @Author Scott
|
||||||
@ -47,30 +65,33 @@ public class JwtUtil {
|
|||||||
public static final long APP_EXPIRE_TIME = (30 * 12) * 60 * 60 * 1000L;
|
public static final long APP_EXPIRE_TIME = (30 * 12) * 60 * 60 * 1000L;
|
||||||
static final String WELL_NUMBER = SymbolConstant.WELL_NUMBER + SymbolConstant.LEFT_CURLY_BRACKET;
|
static final String WELL_NUMBER = SymbolConstant.WELL_NUMBER + SymbolConstant.LEFT_CURLY_BRACKET;
|
||||||
|
|
||||||
|
public static final String DEFAULT_CLIENT = "jeecg-client";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param response
|
* @param response
|
||||||
* @param code
|
* @param code
|
||||||
* @param errorMsg
|
* @param errorMsg
|
||||||
*/
|
*/
|
||||||
public static void responseError(HttpServletResponse response, Integer code, String errorMsg) {
|
public static void responseError(ServletResponse response, Integer code, String errorMsg) {
|
||||||
try {
|
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
|
||||||
Result jsonResult = new Result(code, errorMsg);
|
// issues/I4YH95浏览器显示乱码问题
|
||||||
jsonResult.setSuccess(false);
|
httpServletResponse.setHeader("Content-type", "text/html;charset=UTF-8");
|
||||||
|
response.setContentType("application/json;charset=UTF-8");
|
||||||
// 设置响应头和内容类型
|
Result jsonResult = new Result(code, errorMsg);
|
||||||
response.setStatus(code);
|
jsonResult.setSuccess(false);
|
||||||
response.setHeader("Content-type", "text/html;charset=UTF-8");
|
OutputStream os = null;
|
||||||
response.setContentType("application/json;charset=UTF-8");
|
try {
|
||||||
// 使用 ObjectMapper 序列化为 JSON 字符串
|
os = httpServletResponse.getOutputStream();
|
||||||
ObjectMapper objectMapper = new ObjectMapper();
|
httpServletResponse.setCharacterEncoding("UTF-8");
|
||||||
String json = objectMapper.writeValueAsString(jsonResult);
|
httpServletResponse.setStatus(code);
|
||||||
response.getWriter().write(json);
|
os.write(new ObjectMapper().writeValueAsString(jsonResult).getBytes("UTF-8"));
|
||||||
response.getWriter().flush();
|
os.flush();
|
||||||
} catch (IOException e) {
|
os.close();
|
||||||
|
} catch (IOException e) {
|
||||||
log.error(e.getMessage(), e);
|
log.error(e.getMessage(), e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 校验token是否正确
|
* 校验token是否正确
|
||||||
@ -82,10 +103,9 @@ public class JwtUtil {
|
|||||||
public static boolean verify(String token, String username, String secret) {
|
public static boolean verify(String token, String username, String secret) {
|
||||||
try {
|
try {
|
||||||
// 根据密码生成JWT效验器
|
// 根据密码生成JWT效验器
|
||||||
Algorithm algorithm = Algorithm.HMAC256(secret);
|
JwtDecoder jwtDecoder = SpringContextUtils.getBean(JwtDecoder.class);
|
||||||
JWTVerifier verifier = JWT.require(algorithm).withClaim("username", username).build();
|
|
||||||
// 效验TOKEN
|
// 效验TOKEN
|
||||||
DecodedJWT jwt = verifier.verify(token);
|
jwtDecoder.decode(token);
|
||||||
return true;
|
return true;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.warn("Token验证失败:" + e.getMessage(),e);
|
log.warn("Token验证失败:" + e.getMessage(),e);
|
||||||
@ -109,7 +129,7 @@ public class JwtUtil {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 生成签名,5min后过期
|
* 生成token
|
||||||
*
|
*
|
||||||
* @param username 用户名
|
* @param username 用户名
|
||||||
* @param secret 用户的密码
|
* @param secret 用户的密码
|
||||||
@ -118,29 +138,18 @@ public class JwtUtil {
|
|||||||
*/
|
*/
|
||||||
@Deprecated
|
@Deprecated
|
||||||
public static String sign(String username, String secret) {
|
public static String sign(String username, String secret) {
|
||||||
Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
|
Map<String, Object> additionalParameter = new HashMap<>();
|
||||||
Algorithm algorithm = Algorithm.HMAC256(secret);
|
additionalParameter.put("username", username);
|
||||||
// 附带username信息
|
|
||||||
return JWT.create().withClaim("username", username).withExpiresAt(date).sign(algorithm);
|
|
||||||
|
|
||||||
}
|
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);
|
||||||
* 生成签名,5min后过期
|
SelfAuthenticationToken selfAuthenticationToken = new SelfAuthenticationToken(client, additionalParameter);
|
||||||
*
|
selfAuthenticationToken.setAuthenticated(true);
|
||||||
* @param username 用户名
|
OAuth2AccessTokenAuthenticationToken accessToken = (OAuth2AccessTokenAuthenticationToken) selfAuthenticationProvider.authenticate(selfAuthenticationToken);
|
||||||
* @param secret 用户的密码
|
return accessToken.getAccessToken().getTokenValue();
|
||||||
* @param expireTime 过期时间
|
|
||||||
* @return 加密的token
|
|
||||||
* @deprecated 请使用sign(String username, String secret, String clientType)方法代替
|
|
||||||
*/
|
|
||||||
@Deprecated
|
|
||||||
public static String sign(String username, String secret, Long expireTime) {
|
|
||||||
Date date = new Date(System.currentTimeMillis() + expireTime);
|
|
||||||
Algorithm algorithm = Algorithm.HMAC256(secret);
|
|
||||||
// 附带username信息
|
|
||||||
return JWT.create().withClaim("username", username).withExpiresAt(date).sign(algorithm);
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -154,20 +163,36 @@ public class JwtUtil {
|
|||||||
* @return 加密的token
|
* @return 加密的token
|
||||||
*/
|
*/
|
||||||
public static String sign(String username, String secret, String clientType) {
|
public static String sign(String username, String secret, String clientType) {
|
||||||
|
Map<String, Object> additionalParameter = new HashMap<>();
|
||||||
|
additionalParameter.put("username", username);
|
||||||
|
additionalParameter.put("clientType", clientType);
|
||||||
|
|
||||||
// 根据客户端类型选择对应的过期时间
|
// 根据客户端类型选择对应的过期时间
|
||||||
long expireTime = CommonConstant.CLIENT_TYPE_APP.equalsIgnoreCase(clientType)
|
long expireTime = CommonConstant.CLIENT_TYPE_APP.equalsIgnoreCase(clientType)
|
||||||
? APP_EXPIRE_TIME
|
? APP_EXPIRE_TIME
|
||||||
: EXPIRE_TIME;
|
: EXPIRE_TIME;
|
||||||
Date date = new Date(System.currentTimeMillis() + expireTime);
|
additionalParameter.put("expireTime", expireTime);
|
||||||
Algorithm algorithm = Algorithm.HMAC256(secret);
|
|
||||||
// 附带username和clientType信息
|
RegisteredClientRepository registeredClientRepository = SpringContextUtils.getBean(RegisteredClientRepository.class);
|
||||||
return JWT.create()
|
SelfAuthenticationProvider selfAuthenticationProvider = SpringContextUtils.getBean(SelfAuthenticationProvider.class);
|
||||||
.withClaim("username", username)
|
|
||||||
.withClaim("clientType", clientType)
|
OAuth2ClientAuthenticationToken client = new OAuth2ClientAuthenticationToken(
|
||||||
.withExpiresAt(date)
|
Objects.requireNonNull(registeredClientRepository.findByClientId(DEFAULT_CLIENT)),
|
||||||
.sign(algorithm);
|
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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 从token中获取客户端类型
|
* 从token中获取客户端类型
|
||||||
* for [JHHB-1030]【鉴权】移动端用户token到期后续期时间变成pc端时长
|
* for [JHHB-1030]【鉴权】移动端用户token到期后续期时间变成pc端时长
|
||||||
@ -248,7 +273,7 @@ public class JwtUtil {
|
|||||||
//2.通过shiro获取登录用户信息
|
//2.通过shiro获取登录用户信息
|
||||||
LoginUser sysUser = null;
|
LoginUser sysUser = null;
|
||||||
try {
|
try {
|
||||||
sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();
|
sysUser = SecureUtil.currentUser();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.warn("SecurityUtils.getSubject() 获取用户信息异常:" + e.getMessage());
|
log.warn("SecurityUtils.getSubject() 获取用户信息异常:" + e.getMessage());
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,7 +3,9 @@ package org.jeecg.common.system.util;
|
|||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.jeecg.common.system.annotation.EnumDict;
|
import org.jeecg.common.system.annotation.EnumDict;
|
||||||
import org.jeecg.common.system.vo.DictModel;
|
import org.jeecg.common.system.vo.DictModel;
|
||||||
|
import org.jeecg.common.util.SpringContextUtils;
|
||||||
import org.jeecg.common.util.oConvertUtils;
|
import org.jeecg.common.util.oConvertUtils;
|
||||||
|
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
|
||||||
import org.springframework.core.io.Resource;
|
import org.springframework.core.io.Resource;
|
||||||
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
|
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
|
||||||
import org.springframework.core.io.support.ResourcePatternResolver;
|
import org.springframework.core.io.support.ResourcePatternResolver;
|
||||||
@ -11,7 +13,6 @@ import org.springframework.core.type.classreading.CachingMetadataReaderFactory;
|
|||||||
import org.springframework.core.type.classreading.MetadataReader;
|
import org.springframework.core.type.classreading.MetadataReader;
|
||||||
import org.springframework.core.type.classreading.MetadataReaderFactory;
|
import org.springframework.core.type.classreading.MetadataReaderFactory;
|
||||||
import org.springframework.util.ClassUtils;
|
import org.springframework.util.ClassUtils;
|
||||||
|
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
@ -182,10 +183,10 @@ public class ResourceUtil {
|
|||||||
for (DictModel dm : dictItemList) {
|
for (DictModel dm : dictItemList) {
|
||||||
String value = dm.getValue();
|
String value = dm.getValue();
|
||||||
if (keySet.contains(value)) {
|
if (keySet.contains(value)) {
|
||||||
// 修复bug:获取或创建该dictCode对应的list,而不是每次都创建新的list
|
List<DictModel> list = new ArrayList<>();
|
||||||
List<DictModel> list = map.computeIfAbsent(code, k -> new ArrayList<>());
|
|
||||||
list.add(new DictModel(value, dm.getText()));
|
list.add(new DictModel(value, dm.getText()));
|
||||||
//break;
|
map.put(code, list);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,13 +1,18 @@
|
|||||||
package org.jeecg.common.system.vo;
|
package org.jeecg.common.system.vo;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson2.JSON;
|
||||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
import lombok.experimental.Accessors;
|
import lombok.experimental.Accessors;
|
||||||
import org.jeecg.common.desensitization.annotation.SensitiveField;
|
import org.jeecg.common.desensitization.annotation.SensitiveField;
|
||||||
import org.springframework.format.annotation.DateTimeFormat;
|
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.Date;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <p>
|
* <p>
|
||||||
@ -20,8 +25,10 @@ import java.util.Date;
|
|||||||
@Data
|
@Data
|
||||||
@EqualsAndHashCode(callSuper = false)
|
@EqualsAndHashCode(callSuper = false)
|
||||||
@Accessors(chain = true)
|
@Accessors(chain = true)
|
||||||
public class LoginUser {
|
public class LoginUser implements Serializable {
|
||||||
|
|
||||||
|
|
||||||
|
private static final long serialVersionUID = -7143159031677245866L;
|
||||||
/**
|
/**
|
||||||
* 登录人id
|
* 登录人id
|
||||||
*/
|
*/
|
||||||
@ -148,4 +155,30 @@ public class LoginUser {
|
|||||||
* 主岗位
|
* 主岗位
|
||||||
*/
|
*/
|
||||||
private String mainDepPostId;
|
private String mainDepPostId;
|
||||||
|
|
||||||
|
@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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -56,9 +56,7 @@ public class CommonUtils {
|
|||||||
public static String uploadOnlineImage(byte[] data,String basePath,String bizPath,String uploadType){
|
public static String uploadOnlineImage(byte[] data,String basePath,String bizPath,String uploadType){
|
||||||
String dbPath = null;
|
String dbPath = null;
|
||||||
String fileName = "image" + Math.round(Math.random() * 100000000000L);
|
String fileName = "image" + Math.round(Math.random() * 100000000000L);
|
||||||
//update-begin---author:wangshuai---date:2026-01-08---for:【QQYUN-14535】ai生成图片的后缀不一致的,导致不展示---
|
fileName += "." + PoiPublicUtil.getFileExtendName(data);
|
||||||
fileName += "." + PoiPublicUtil.getFileExtendName(data).toLowerCase();
|
|
||||||
//update-end---author:wangshuai---date:2026-01-08---for:【QQYUN-14535】ai生成图片的后缀不一致的,导致不展示---
|
|
||||||
try {
|
try {
|
||||||
if(CommonConstant.UPLOAD_TYPE_LOCAL.equals(uploadType)){
|
if(CommonConstant.UPLOAD_TYPE_LOCAL.equals(uploadType)){
|
||||||
File file = new File(basePath + File.separator + bizPath + File.separator );
|
File file = new File(basePath + File.separator + bizPath + File.separator );
|
||||||
|
|||||||
@ -46,7 +46,7 @@ public class RestUtil {
|
|||||||
|
|
||||||
public static String getBaseUrl() {
|
public static String getBaseUrl() {
|
||||||
String basepath = getDomain() + getPath();
|
String basepath = getDomain() + getPath();
|
||||||
log.debug(" RestUtil.getBaseUrl: " + basepath);
|
log.info(" RestUtil.getBaseUrl: " + basepath);
|
||||||
return basepath;
|
return basepath;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -199,7 +199,7 @@ public class RestUtil {
|
|||||||
* @return ResponseEntity<responseType>
|
* @return ResponseEntity<responseType>
|
||||||
*/
|
*/
|
||||||
public static <T> ResponseEntity<T> request(String url, HttpMethod method, HttpHeaders headers, JSONObject variables, Object params, Class<T> responseType) {
|
public static <T> ResponseEntity<T> request(String url, HttpMethod method, HttpHeaders headers, JSONObject variables, Object params, Class<T> responseType) {
|
||||||
log.debug(" RestUtil --- request --- url = "+ url);
|
log.info(" RestUtil --- request --- url = "+ url);
|
||||||
if (StringUtils.isEmpty(url)) {
|
if (StringUtils.isEmpty(url)) {
|
||||||
throw new RuntimeException("url 不能为空");
|
throw new RuntimeException("url 不能为空");
|
||||||
}
|
}
|
||||||
@ -230,7 +230,7 @@ public class RestUtil {
|
|||||||
String current = headers.getFirst(HttpHeaders.CONTENT_LENGTH);
|
String current = headers.getFirst(HttpHeaders.CONTENT_LENGTH);
|
||||||
if (current == null || !current.equals(String.valueOf(contentLength))) {
|
if (current == null || !current.equals(String.valueOf(contentLength))) {
|
||||||
headers.setContentLength(contentLength);
|
headers.setContentLength(contentLength);
|
||||||
log.debug(" RestUtil --- request --- 修正/设置 Content-Length = " + contentLength + (current!=null?" (原值="+current+")":""));
|
log.info(" RestUtil --- request --- 修正/设置 Content-Length = " + contentLength + (current!=null?" (原值="+current+")":""));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 发送请求
|
// 发送请求
|
||||||
@ -252,7 +252,7 @@ public class RestUtil {
|
|||||||
*/
|
*/
|
||||||
public static <T> ResponseEntity<T> request(String url, HttpMethod method, HttpHeaders headers,
|
public static <T> ResponseEntity<T> request(String url, HttpMethod method, HttpHeaders headers,
|
||||||
JSONObject variables, Object params, Class<T> responseType, int timeout) {
|
JSONObject variables, Object params, Class<T> responseType, int timeout) {
|
||||||
log.debug(" RestUtil --- request --- url = "+ url + ", timeout = " + timeout);
|
log.info(" RestUtil --- request --- url = "+ url + ", timeout = " + timeout);
|
||||||
|
|
||||||
if (StringUtils.isEmpty(url)) {
|
if (StringUtils.isEmpty(url)) {
|
||||||
throw new RuntimeException("url 不能为空");
|
throw new RuntimeException("url 不能为空");
|
||||||
@ -302,7 +302,7 @@ public class RestUtil {
|
|||||||
String current = headers.getFirst(HttpHeaders.CONTENT_LENGTH);
|
String current = headers.getFirst(HttpHeaders.CONTENT_LENGTH);
|
||||||
if (current == null || !current.equals(String.valueOf(contentLength))) {
|
if (current == null || !current.equals(String.valueOf(contentLength))) {
|
||||||
headers.setContentLength(contentLength);
|
headers.setContentLength(contentLength);
|
||||||
log.debug(" RestUtil --- request(timeout) --- 修正/设置 Content-Length = " + contentLength + (current!=null?" (原值="+current+")":""));
|
log.info(" RestUtil --- request(timeout) --- 修正/设置 Content-Length = " + contentLength + (current!=null?" (原值="+current+")":""));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,17 +1,14 @@
|
|||||||
package org.jeecg.common.util;
|
package org.jeecg.common.util;
|
||||||
|
|
||||||
import org.apache.shiro.SecurityUtils;
|
import org.springframework.security.core.context.SecurityContext;
|
||||||
import org.apache.shiro.mgt.SecurityManager;
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
import org.apache.shiro.subject.Subject;
|
|
||||||
import org.apache.shiro.util.ThreadContext;
|
|
||||||
|
|
||||||
import java.util.concurrent.*;
|
import java.util.concurrent.*;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @date 2025-09-04
|
* @date 2025-09-04
|
||||||
* @author scott
|
* @author scott
|
||||||
*
|
*
|
||||||
* @Description: 支持shiro的API,获取当前登录人方法的线程池
|
* @Description: 支持Spring Security的API,获取当前登录人方法的线程池
|
||||||
*/
|
*/
|
||||||
public class ShiroThreadPoolExecutor extends ThreadPoolExecutor {
|
public class ShiroThreadPoolExecutor extends ThreadPoolExecutor {
|
||||||
|
|
||||||
@ -21,16 +18,14 @@ public class ShiroThreadPoolExecutor extends ThreadPoolExecutor {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void execute(Runnable command) {
|
public void execute(Runnable command) {
|
||||||
Subject subject = SecurityUtils.getSubject();
|
SecurityContext context = SecurityContextHolder.getContext();
|
||||||
SecurityManager securityManager = SecurityUtils.getSecurityManager();
|
|
||||||
super.execute(() -> {
|
super.execute(() -> {
|
||||||
|
SecurityContext previousContext = SecurityContextHolder.getContext();
|
||||||
try {
|
try {
|
||||||
ThreadContext.bind(securityManager);
|
SecurityContextHolder.setContext(context);
|
||||||
ThreadContext.bind(subject);
|
|
||||||
command.run();
|
command.run();
|
||||||
} finally {
|
} finally {
|
||||||
ThreadContext.unbindSubject();
|
SecurityContextHolder.setContext(previousContext);
|
||||||
ThreadContext.unbindSecurityManager();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
package org.jeecg.common.util;
|
package org.jeecg.common.util;
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.jeecg.common.api.CommonAPI;
|
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.util.JwtUtil;
|
||||||
import org.jeecg.common.system.vo.LoginUser;
|
import org.jeecg.common.system.vo.LoginUser;
|
||||||
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @Author scott
|
* @Author scott
|
||||||
* @Date 2019/9/23 14:12
|
* @Date 2019/9/23 14:12
|
||||||
@ -110,8 +109,8 @@ public class TokenUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 查询用户信息
|
// 查询用户信息
|
||||||
LoginUser user = TokenUtils.getLoginUser(username, commonApi, redisUtil);
|
//LoginUser user = TokenUtils.getLoginUser(username, commonApi, redisUtil);
|
||||||
//LoginUser user = commonApi.getUserByName(username);
|
LoginUser user = commonApi.getUserByName(username);
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
throw new JeecgBoot401Exception("用户不存在!");
|
throw new JeecgBoot401Exception("用户不存在!");
|
||||||
}
|
}
|
||||||
@ -169,10 +168,11 @@ public class TokenUtils {
|
|||||||
//【重要】此处通过redis原生获取缓存用户,是为了解决微服务下system服务挂了,其他服务互调不通问题---
|
//【重要】此处通过redis原生获取缓存用户,是为了解决微服务下system服务挂了,其他服务互调不通问题---
|
||||||
if (redisUtil.hasKey(loginUserKey)) {
|
if (redisUtil.hasKey(loginUserKey)) {
|
||||||
try {
|
try {
|
||||||
loginUser = (LoginUser) redisUtil.get(loginUserKey);
|
Object obj = redisUtil.get(loginUserKey);
|
||||||
|
loginUser = (LoginUser) obj;
|
||||||
//解密用户
|
//解密用户
|
||||||
SensitiveInfoUtil.handlerObject(loginUser, false);
|
SensitiveInfoUtil.handlerObject(loginUser, false);
|
||||||
} catch (IllegalAccessException e) {
|
} catch (Exception e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
package org.jeecg.common.util.encryption;
|
package org.jeecg.common.util.encryption;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.shiro.lang.codec.Base64;
|
|
||||||
import org.jeecg.common.util.oConvertUtils;
|
import org.jeecg.common.util.oConvertUtils;
|
||||||
import javax.crypto.Cipher;
|
import javax.crypto.Cipher;
|
||||||
import javax.crypto.spec.IvParameterSpec;
|
import javax.crypto.spec.IvParameterSpec;
|
||||||
import javax.crypto.spec.SecretKeySpec;
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.Base64;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AES 工具 (兼容历史 NoPadding + 新 PKCS5Padding)
|
* AES 工具 (兼容历史 NoPadding + 新 PKCS5Padding)
|
||||||
@ -23,7 +23,7 @@ public class AesEncryptUtil {
|
|||||||
SecretKeySpec ks = new SecretKeySpec(KEY.getBytes(StandardCharsets.UTF_8), "AES");
|
SecretKeySpec ks = new SecretKeySpec(KEY.getBytes(StandardCharsets.UTF_8), "AES");
|
||||||
IvParameterSpec ivSpec = new IvParameterSpec(IV.getBytes(StandardCharsets.UTF_8));
|
IvParameterSpec ivSpec = new IvParameterSpec(IV.getBytes(StandardCharsets.UTF_8));
|
||||||
cipher.init(Cipher.DECRYPT_MODE, ks, ivSpec);
|
cipher.init(Cipher.DECRYPT_MODE, ks, ivSpec);
|
||||||
byte[] plain = cipher.doFinal(Base64.decode(cipherBase64));
|
byte[] plain = cipher.doFinal(Base64.getDecoder().decode(cipherBase64));
|
||||||
return new String(plain, StandardCharsets.UTF_8);
|
return new String(plain, StandardCharsets.UTF_8);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -33,7 +33,7 @@ public class AesEncryptUtil {
|
|||||||
SecretKeySpec ks = new SecretKeySpec(KEY.getBytes(StandardCharsets.UTF_8), "AES");
|
SecretKeySpec ks = new SecretKeySpec(KEY.getBytes(StandardCharsets.UTF_8), "AES");
|
||||||
IvParameterSpec ivSpec = new IvParameterSpec(IV.getBytes(StandardCharsets.UTF_8));
|
IvParameterSpec ivSpec = new IvParameterSpec(IV.getBytes(StandardCharsets.UTF_8));
|
||||||
cipher.init(Cipher.DECRYPT_MODE, ks, ivSpec);
|
cipher.init(Cipher.DECRYPT_MODE, ks, ivSpec);
|
||||||
byte[] data = cipher.doFinal(Base64.decode(cipherBase64));
|
byte[] data = cipher.doFinal(Base64.getDecoder().decode(cipherBase64));
|
||||||
return new String(data, StandardCharsets.UTF_8)
|
return new String(data, StandardCharsets.UTF_8)
|
||||||
.replace("\u0000",""); // 旧填充 0
|
.replace("\u0000",""); // 旧填充 0
|
||||||
}
|
}
|
||||||
@ -93,7 +93,7 @@ public class AesEncryptUtil {
|
|||||||
IvParameterSpec ivspec = new IvParameterSpec(IV.getBytes(StandardCharsets.UTF_8));
|
IvParameterSpec ivspec = new IvParameterSpec(IV.getBytes(StandardCharsets.UTF_8));
|
||||||
cipher.init(Cipher.ENCRYPT_MODE, keyspec, ivspec);
|
cipher.init(Cipher.ENCRYPT_MODE, keyspec, ivspec);
|
||||||
byte[] encrypted = cipher.doFinal(plaintext);
|
byte[] encrypted = cipher.doFinal(plaintext);
|
||||||
return Base64.encodeToString(encrypted);
|
return Base64.getEncoder().encodeToString(encrypted);
|
||||||
}catch(Exception e){
|
}catch(Exception e){
|
||||||
throw new IllegalStateException("legacy encrypt error", e);
|
throw new IllegalStateException("legacy encrypt error", e);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1232,25 +1232,4 @@ public class oConvertUtils {
|
|||||||
.toArray(String[]::new);
|
.toArray(String[]::new);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* String转换long类型
|
|
||||||
*
|
|
||||||
* @param v
|
|
||||||
* @param def
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
public static long getLong(Object v, long def) {
|
|
||||||
if (v == null) {
|
|
||||||
return def;
|
|
||||||
};
|
|
||||||
if (v instanceof Number) {
|
|
||||||
return ((Number) v).longValue();
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
return Long.parseLong(v.toString());
|
|
||||||
} catch (Exception e) {
|
|
||||||
return def;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,24 +0,0 @@
|
|||||||
package org.jeecg.config;
|
|
||||||
|
|
||||||
import lombok.Data;
|
|
||||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ai配置类,通用的配置可以放到这里面
|
|
||||||
*
|
|
||||||
* @Author: wangshuai
|
|
||||||
* @Date: 2025/12/17 14:00
|
|
||||||
*/
|
|
||||||
@Data
|
|
||||||
@Component
|
|
||||||
@ConfigurationProperties(prefix = AiRagConfigBean.PREFIX)
|
|
||||||
public class AiRagConfigBean {
|
|
||||||
public static final String PREFIX = "jeecg.airag";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 敏感节点
|
|
||||||
* stdio mpc命令行功能开启,sql:AI流程SQL节点开启
|
|
||||||
*/
|
|
||||||
private String allowSensitiveNodes = "";
|
|
||||||
}
|
|
||||||
@ -12,6 +12,7 @@ import java.util.HashMap;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @author eightmonth@qq.com
|
||||||
* 启动程序修改DruidWallConfig配置
|
* 启动程序修改DruidWallConfig配置
|
||||||
* 允许SELECT语句的WHERE子句是一个永真条件
|
* 允许SELECT语句的WHERE子句是一个永真条件
|
||||||
* @author eightmonth
|
* @author eightmonth
|
||||||
|
|||||||
@ -42,10 +42,6 @@ public class JeecgBaseConfig {
|
|||||||
*/
|
*/
|
||||||
private Firewall firewall;
|
private Firewall firewall;
|
||||||
|
|
||||||
/**
|
|
||||||
* shiro拦截排除
|
|
||||||
*/
|
|
||||||
private Shiro shiro;
|
|
||||||
/**
|
/**
|
||||||
* 上传文件配置
|
* 上传文件配置
|
||||||
*/
|
*/
|
||||||
@ -138,14 +134,6 @@ public class JeecgBaseConfig {
|
|||||||
this.signatureSecret = signatureSecret;
|
this.signatureSecret = signatureSecret;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Shiro getShiro() {
|
|
||||||
return shiro;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setShiro(Shiro shiro) {
|
|
||||||
this.shiro = shiro;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Path getPath() {
|
public Path getPath() {
|
||||||
return path;
|
return path;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -136,7 +136,7 @@ public class Swagger3Config implements WebMvcConfigurer {
|
|||||||
return new OpenAPI()
|
return new OpenAPI()
|
||||||
.info(new Info()
|
.info(new Info()
|
||||||
.title("JeecgBoot 后台服务API接口文档")
|
.title("JeecgBoot 后台服务API接口文档")
|
||||||
.version("3.9.1")
|
.version("3.9.0")
|
||||||
.contact(new Contact().name("北京国炬信息技术有限公司").url("www.jeccg.com").email("jeecgos@163.com"))
|
.contact(new Contact().name("北京国炬信息技术有限公司").url("www.jeccg.com").email("jeecgos@163.com"))
|
||||||
.description("后台API接口")
|
.description("后台API接口")
|
||||||
.termsOfService("NO terms of service")
|
.termsOfService("NO terms of service")
|
||||||
|
|||||||
@ -1,32 +0,0 @@
|
|||||||
package org.jeecg.config;
|
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
|
||||||
import org.springframework.context.annotation.Bean;
|
|
||||||
import org.springframework.context.annotation.Configuration;
|
|
||||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 任务调度器配置
|
|
||||||
* 提供 ThreadPoolTaskScheduler Bean 用于 AI RAG 流程调度等功能
|
|
||||||
* 仅当容器中不存在 ThreadPoolTaskScheduler 时才创建
|
|
||||||
*
|
|
||||||
* @author jeecg
|
|
||||||
*/
|
|
||||||
@Slf4j
|
|
||||||
@Configuration
|
|
||||||
public class TaskSchedulerConfig {
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
@ConditionalOnMissingBean(ThreadPoolTaskScheduler.class)
|
|
||||||
public ThreadPoolTaskScheduler taskScheduler() {
|
|
||||||
log.info("初始化定时任务调度器 ThreadPoolTaskScheduler");
|
|
||||||
|
|
||||||
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
|
|
||||||
scheduler.setPoolSize(10);
|
|
||||||
scheduler.setThreadNamePrefix("airag-scheduler-");
|
|
||||||
scheduler.setWaitForTasksToCompleteOnShutdown(true);
|
|
||||||
scheduler.setAwaitTerminationSeconds(60);
|
|
||||||
return scheduler;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,6 +1,9 @@
|
|||||||
package org.jeecg.config.firewall.interceptor;
|
package org.jeecg.config.firewall.interceptor;
|
||||||
|
|
||||||
import com.alibaba.fastjson.JSON;
|
import com.alibaba.fastjson.JSON;
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.shiro.SecurityUtils;
|
import org.apache.shiro.SecurityUtils;
|
||||||
import org.jeecg.common.api.CommonAPI;
|
import org.jeecg.common.api.CommonAPI;
|
||||||
@ -14,9 +17,6 @@ import org.jeecg.config.JeecgBaseConfig;
|
|||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.web.servlet.HandlerInterceptor;
|
import org.springframework.web.servlet.HandlerInterceptor;
|
||||||
|
|
||||||
import jakarta.annotation.Resource;
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.PrintWriter;
|
import java.io.PrintWriter;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|||||||
@ -6,13 +6,13 @@ import org.apache.ibatis.executor.Executor;
|
|||||||
import org.apache.ibatis.mapping.MappedStatement;
|
import org.apache.ibatis.mapping.MappedStatement;
|
||||||
import org.apache.ibatis.mapping.SqlCommandType;
|
import org.apache.ibatis.mapping.SqlCommandType;
|
||||||
import org.apache.ibatis.plugin.*;
|
import org.apache.ibatis.plugin.*;
|
||||||
import org.apache.shiro.SecurityUtils;
|
|
||||||
import org.jeecg.common.config.TenantContext;
|
import org.jeecg.common.config.TenantContext;
|
||||||
import org.jeecg.common.constant.TenantConstant;
|
import org.jeecg.common.constant.TenantConstant;
|
||||||
import org.jeecg.common.system.vo.LoginUser;
|
import org.jeecg.common.system.vo.LoginUser;
|
||||||
import org.jeecg.common.util.SpringContextUtils;
|
import org.jeecg.common.util.SpringContextUtils;
|
||||||
import org.jeecg.common.util.TokenUtils;
|
import org.jeecg.common.util.TokenUtils;
|
||||||
import org.jeecg.common.util.oConvertUtils;
|
import org.jeecg.common.util.oConvertUtils;
|
||||||
|
import org.jeecg.config.security.utils.SecureUtil;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import java.lang.reflect.Field;
|
import java.lang.reflect.Field;
|
||||||
@ -189,7 +189,7 @@ public class MybatisInterceptor implements Interceptor {
|
|||||||
private LoginUser getLoginUser() {
|
private LoginUser getLoginUser() {
|
||||||
LoginUser sysUser = null;
|
LoginUser sysUser = null;
|
||||||
try {
|
try {
|
||||||
sysUser = SecurityUtils.getSubject().getPrincipal() != null ? (LoginUser) SecurityUtils.getSubject().getPrincipal() : null;
|
sysUser = SecureUtil.currentUser() != null ? SecureUtil.currentUser() : null;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
//e.printStackTrace();
|
//e.printStackTrace();
|
||||||
sysUser = null;
|
sysUser = null;
|
||||||
|
|||||||
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,80 @@
|
|||||||
|
package org.jeecg.config.security;
|
||||||
|
|
||||||
|
import jakarta.servlet.FilterChain;
|
||||||
|
import jakarta.servlet.ServletException;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletRequestWrapper;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
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;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 仪盘表请求query体携带的token
|
||||||
|
* @author eightmonth
|
||||||
|
* @date 2024/7/3 14:04
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@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容器,需要同步更换
|
||||||
|
String token = request.getHeader("Authorization");
|
||||||
|
String bearerToken = request.getParameter("token");
|
||||||
|
String headerBearerToken = request.getHeader("X-Access-Token");
|
||||||
|
String finalToken;
|
||||||
|
|
||||||
|
log.debug("【仪盘表请求query体携带的token】CopyTokenFilter token: {}, bearerToken: {}, headerBearerToken: {}", token, bearerToken, headerBearerToken);
|
||||||
|
|
||||||
|
if (StringUtils.hasText(token)) {
|
||||||
|
finalToken = "bearer " + token;
|
||||||
|
} else if (StringUtils.hasText(bearerToken)) {
|
||||||
|
finalToken = "bearer " + bearerToken;
|
||||||
|
} else if (StringUtils.hasText(headerBearerToken)) {
|
||||||
|
finalToken = "bearer " + headerBearerToken;
|
||||||
|
} else {
|
||||||
|
finalToken = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (finalToken != null) {
|
||||||
|
HttpServletRequestWrapper wrapper = new HttpServletRequestWrapper(request) {
|
||||||
|
@Override
|
||||||
|
public String getHeader(String name) {
|
||||||
|
if ("Authorization".equalsIgnoreCase(name)) {
|
||||||
|
return finalToken;
|
||||||
|
}
|
||||||
|
return super.getHeader(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Enumeration<String> getHeaders(String name) {
|
||||||
|
if ("Authorization".equalsIgnoreCase(name)) {
|
||||||
|
return Collections.enumeration(Collections.singleton(finalToken));
|
||||||
|
}
|
||||||
|
return super.getHeaders(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Enumeration<String> getHeaderNames() {
|
||||||
|
List<String> names = Collections.list(super.getHeaderNames());
|
||||||
|
if (!names.contains("Authorization")) {
|
||||||
|
names.add("Authorization");
|
||||||
|
}
|
||||||
|
return Collections.enumeration(names);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
filterChain.doFilter(wrapper, response);
|
||||||
|
} else {
|
||||||
|
filterChain.doFilter(request, response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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<>());
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断接口是否有任意xxx,xxx权限
|
||||||
|
* @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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断接口是否有任意xxx,xxx角色
|
||||||
|
* @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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -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";
|
||||||
|
}
|
||||||
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,310 @@
|
|||||||
|
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 lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.jeecg.common.system.util.JwtUtil;
|
||||||
|
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
|
||||||
|
@Slf4j
|
||||||
|
public class SecurityConfig {
|
||||||
|
|
||||||
|
private JdbcTemplate jdbcTemplate;
|
||||||
|
private OAuth2AuthorizationService authorizationService;
|
||||||
|
private JeecgAuthenticationConvert jeecgAuthenticationConvert;
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@Order(1)
|
||||||
|
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http)
|
||||||
|
throws Exception {
|
||||||
|
// 使用新的配置方式替代弃用的applyDefaultSecurity
|
||||||
|
http.securityMatcher(new AntPathRequestMatcher("/oauth2/**"))
|
||||||
|
.authorizeHttpRequests(authorize ->
|
||||||
|
authorize.anyRequest().authenticated()
|
||||||
|
)
|
||||||
|
.csrf(csrf -> csrf.disable())
|
||||||
|
.with(new OAuth2AuthorizationServerConfigurer(), oauth2 -> {
|
||||||
|
oauth2
|
||||||
|
.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());
|
||||||
|
});
|
||||||
|
|
||||||
|
//请求接口异常处理:无Token和Token无效的情况
|
||||||
|
http.exceptionHandling(exceptions -> exceptions
|
||||||
|
.authenticationEntryPoint((request, response, authException) -> {
|
||||||
|
// 记录详细的异常信息 - 未认证
|
||||||
|
log.error("接口访问失败(未认证),请求路径:{},错误信息:{}", request.getRequestURI(), authException.getMessage(), authException);
|
||||||
|
JwtUtil.responseError(response, 401, "Token无效或已过期");
|
||||||
|
})
|
||||||
|
.accessDeniedHandler((request, response, accessDeniedException) -> {
|
||||||
|
// 记录详细的异常信息 - token无效或权限不足
|
||||||
|
log.error("接口访问失败(token无效或权限不足),请求路径:{},错误信息:{}", request.getRequestURI(), accessDeniedException.getMessage(), accessDeniedException);
|
||||||
|
JwtUtil.responseError(response, 403, "权限不足");
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
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("/api/getUserInfo")).permitAll()
|
||||||
|
|
||||||
|
//积木报表排除
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/jmreport/**")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/**/*.js.map")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/**/*.css.map")).permitAll()
|
||||||
|
//积木BI大屏和仪表盘排除
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/drag/view")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/drag/onlDragDatasetHead/getLoginUser")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/drag/page/queryById")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/drag/page/addVisitsNumber")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/drag/page/queryTemplateList")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/drag/share/view/**")).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("/jimubi/view")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/jimubi/share/view/**")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/drag/onlDragDatasetHead/getMapDataByCode")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/drag/onlDragDatasetHead/getTotalDataByCompId")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/drag/onlDragDatasetHead/queryAllById")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/drag/onlDragDatasetHead/getDictByCodes")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/dragChannelSocket/**")).permitAll()
|
||||||
|
//大屏模板例子
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/test/bigScreen/**")).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()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/openapi/call/**")).permitAll()
|
||||||
|
// APP版本信息
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/sys/version/app3version")).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)
|
||||||
|
// 配置OAuth2资源服务器,并添加JWT异常处理
|
||||||
|
.oauth2ResourceServer(oauth2 -> oauth2
|
||||||
|
.jwt(jwt -> jwt.jwtAuthenticationConverter(jeecgAuthenticationConvert))
|
||||||
|
.authenticationEntryPoint((request, response, authException) -> {
|
||||||
|
// 处理JWT解析失败的情况
|
||||||
|
log.error("JWT验证失败,请求路径:{},错误信息:{}", request.getRequestURI(), authException.getMessage(), authException);
|
||||||
|
JwtUtil.responseError(response, 401, "Token无效或已过期");
|
||||||
|
})
|
||||||
|
.accessDeniedHandler((request, response, accessDeniedException) -> {
|
||||||
|
// 处理权限不足的情况
|
||||||
|
log.error("权限验证失败,请求路径:{},错误信息:{}", request.getRequestURI(), accessDeniedException.getMessage(), accessDeniedException);
|
||||||
|
JwtUtil.responseError(response, 403, "权限不足");
|
||||||
|
})
|
||||||
|
)
|
||||||
|
// 全局异常处理
|
||||||
|
.exceptionHandling(exceptions -> exceptions
|
||||||
|
.authenticationEntryPoint((request, response, authException) -> {
|
||||||
|
// 记录详细的异常信息 - 未认证
|
||||||
|
log.error("接口访问失败(未认证),请求路径:{},错误信息:{}", request.getRequestURI(), authException.getMessage(), authException);
|
||||||
|
JwtUtil.responseError(response, 401, "Token无效或已过期");
|
||||||
|
})
|
||||||
|
.accessDeniedHandler((request, response, accessDeniedException) -> {
|
||||||
|
// 记录详细的异常信息 - token无效或权限不足
|
||||||
|
log.error("接口访问失败(token无效或权限不足),请求路径:{},错误信息:{}", request.getRequestURI(), accessDeniedException.getMessage(), accessDeniedException);
|
||||||
|
JwtUtil.responseError(response, 403, "权限不足");
|
||||||
|
})
|
||||||
|
);
|
||||||
|
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()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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 for:if条件永远为falsebug------------
|
||||||
|
if (CommonConstant.DEL_FLAG_1.equals(loginUser.getDelFlag())) {
|
||||||
|
//update-end---author:王帅 Date:20200601 for:if条件永远为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("该用户已冻结");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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 for:if条件永远为falsebug------------
|
||||||
|
if (CommonConstant.DEL_FLAG_1.equals(loginUser.getDelFlag())) {
|
||||||
|
//update-end---author:王帅 Date:20200601 for:if条件永远为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("该用户已冻结");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -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 for:if条件永远为falsebug------------
|
||||||
|
if (CommonConstant.DEL_FLAG_1.equals(loginUser.getDelFlag())) {
|
||||||
|
//update-end---author:王帅 Date:20200601 for:if条件永远为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("该用户已冻结");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -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 for:if条件永远为falsebug------------
|
||||||
|
if (CommonConstant.DEL_FLAG_1.equals(loginUser.getDelFlag())) {
|
||||||
|
//update-end---author:王帅 Date:20200601 for:if条件永远为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("该用户已冻结");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -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 for:if条件永远为falsebug------------
|
||||||
|
if (CommonConstant.DEL_FLAG_1.equals(loginUser.getDelFlag())) {
|
||||||
|
//update-end---author:王帅 Date:20200601 for:if条件永远为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("该用户已冻结");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,26 @@
|
|||||||
|
package org.jeecg.config.security.utils;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson2.JSONObject;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.jeecg.common.system.vo.LoginUser;
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 认证信息工具类
|
||||||
|
* @author EightMonth
|
||||||
|
* @date 2024/1/10 17:03
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
public class SecureUtil {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通过当前认证信息获取用户信息
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public static LoginUser currentUser() {
|
||||||
|
String userInfoJson = SecurityContextHolder.getContext().getAuthentication().getName();
|
||||||
|
//log.info("SecureUtil.currentUser: {}", userInfoJson);
|
||||||
|
return JSONObject.parseObject(userInfoJson, LoginUser.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,395 +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.config.BeanDefinition;
|
|
||||||
import org.springframework.boot.web.servlet.FilterRegistrationBean;
|
|
||||||
import org.springframework.context.annotation.*;
|
|
||||||
import org.springframework.core.annotation.AnnotationUtils;
|
|
||||||
import org.springframework.core.env.Environment;
|
|
||||||
import org.springframework.core.type.filter.AnnotationTypeFilter;
|
|
||||||
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
|
|
||||||
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
|
|
||||||
import org.springframework.util.CollectionUtils;
|
|
||||||
import org.springframework.util.StringUtils;
|
|
||||||
import org.springframework.web.bind.annotation.*;
|
|
||||||
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.lang.reflect.Method;
|
|
||||||
import java.util.*;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @author: Scott
|
|
||||||
* @date: 2018/2/7
|
|
||||||
* @description: shiro 配置类
|
|
||||||
*/
|
|
||||||
|
|
||||||
@Slf4j
|
|
||||||
@Configuration
|
|
||||||
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、部分过滤器可指定参数,如perms,roles
|
|
||||||
*/
|
|
||||||
@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"); // 开放平台接口排除
|
|
||||||
|
|
||||||
// 代码逻辑说明: 排除静态资源后缀
|
|
||||||
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");
|
|
||||||
|
|
||||||
filterChainDefinitionMap.put("/druid/**", "anon");
|
|
||||||
filterChainDefinitionMap.put("/swagger-ui.html", "anon");
|
|
||||||
filterChainDefinitionMap.put("/swagger**/**", "anon");
|
|
||||||
filterChainDefinitionMap.put("/webjars/**", "anon");
|
|
||||||
filterChainDefinitionMap.put("/v3/**", "anon");
|
|
||||||
|
|
||||||
filterChainDefinitionMap.put("/sys/annountCement/show/**", "anon");
|
|
||||||
|
|
||||||
//积木报表排除
|
|
||||||
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("/drag/onlDragDatasetHead/queryAllById", "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");
|
|
||||||
//App vue3版本查询版本接口
|
|
||||||
filterChainDefinitionMap.put("/sys/version/app3version", "anon");
|
|
||||||
|
|
||||||
//性能监控——安全隐患泄露TOEKN(durid连接池也有)
|
|
||||||
//filterChainDefinitionMap.put("/actuator/**", "anon");
|
|
||||||
//测试模块排除
|
|
||||||
filterChainDefinitionMap.put("/test/seata/**", "anon");
|
|
||||||
|
|
||||||
//错误路径排除
|
|
||||||
filterChainDefinitionMap.put("/error", "anon");
|
|
||||||
// 企业微信证书排除
|
|
||||||
filterChainDefinitionMap.put("/WW_verify*", "anon");
|
|
||||||
|
|
||||||
filterChainDefinitionMap.put("/openapi/call/**", "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;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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);
|
|
||||||
// 代码逻辑说明: [issues/7491]运行耗时长,效率慢
|
|
||||||
registration.addUrlPatterns("/test/ai/chat/send");
|
|
||||||
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.addUrlPatterns("/airag/chat/receive/**");
|
|
||||||
// 添加SSE接口的异步支持
|
|
||||||
registration.addUrlPatterns("/airag/extData/evaluator/debug");
|
|
||||||
registration.addUrlPatterns("/drag/onlDragDatasetHead/generateChartSse");
|
|
||||||
registration.addUrlPatterns("/drag/onlDragDatasetHead/updateChartOptSse");
|
|
||||||
registration.addUrlPatterns("/drag/onlDragDatasetHead/generateSqlSse");
|
|
||||||
//支持异步
|
|
||||||
registration.setAsyncSupported(true);
|
|
||||||
registration.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ASYNC);
|
|
||||||
return registration;
|
|
||||||
}
|
|
||||||
|
|
||||||
@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);
|
|
||||||
}
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,239 +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.debug("===============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) {
|
|
||||||
log.error("—————校验 check token 失败——————————"+ e.getMessage(), e);
|
|
||||||
// 重新抛出异常,让JwtFilter统一处理,避免返回两次错误响应
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
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())) {
|
|
||||||
// 用户登录Token过期提示信息
|
|
||||||
String userLoginTokenErrorMsg = oConvertUtils.getString(redisUtil.get(CommonConstant.PREFIX_USER_TOKEN_ERROR_MSG + token));
|
|
||||||
throw new AuthenticationException(oConvertUtils.isEmpty(userLoginTokenErrorMsg)? CommonConstant.TOKEN_IS_INVALID_MSG: userLoginTokenErrorMsg);
|
|
||||||
}
|
|
||||||
// 代码逻辑说明: 校验用户的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)){
|
|
||||||
// 代码逻辑说明: /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("登录租户授权变更,请重新登陆!");
|
|
||||||
}
|
|
||||||
//*********************************************
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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)) {
|
|
||||||
// 从token中解析客户端类型,保持续期时使用相同的客户端类型
|
|
||||||
String clientType = JwtUtil.getClientType(token);
|
|
||||||
String newAuthorization = JwtUtil.sign(userName, passWord, clientType);
|
|
||||||
// 根据客户端类型设置对应的缓存有效时间
|
|
||||||
long expireTime = CommonConstant.CLIENT_TYPE_APP.equalsIgnoreCase(clientType)
|
|
||||||
? JwtUtil.APP_EXPIRE_TIME * 2 / 1000
|
|
||||||
: JwtUtil.EXPIRE_TIME * 2 / 1000;
|
|
||||||
redisUtil.set(CommonConstant.PREFIX_USER_TOKEN + token, newAuthorization);
|
|
||||||
redisUtil.expire(CommonConstant.PREFIX_USER_TOKEN + token, expireTime);
|
|
||||||
log.debug("——————————用户在线操作,更新token保证不掉线—————————jwtTokenRefresh——————— "+ token);
|
|
||||||
}
|
|
||||||
// else {
|
|
||||||
// // 设置超时时间
|
|
||||||
// redisUtil.set(CommonConstant.PREFIX_USER_TOKEN + token, cacheToken);
|
|
||||||
// redisUtil.expire(CommonConstant.PREFIX_USER_TOKEN + token, JwtUtil.EXPIRE_TIME / 1000);
|
|
||||||
// }
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
//redis中不存在此TOEKN,说明token非法返回false
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 清除当前用户的权限认证缓存
|
|
||||||
*
|
|
||||||
* @param principals 权限信息
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void clearCache(PrincipalCollection principals) {
|
|
||||||
super.clearCache(principals);
|
|
||||||
// 代码逻辑说明: 【TV360X-1320】分配权限必须退出重新登录才生效,造成很多用户困扰---
|
|
||||||
super.clearCachedAuthorizationInfo(principals);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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携带中文400,servletPath中文校验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);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,132 +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) {
|
|
||||||
// 使用异常中的具体错误信息,保留"不允许同一账号多地同时登录"等具体提示
|
|
||||||
String errorMsg = e.getMessage();
|
|
||||||
if (oConvertUtils.isEmpty(errorMsg)) {
|
|
||||||
errorMsg = CommonConstant.TOKEN_IS_INVALID_MSG;
|
|
||||||
}
|
|
||||||
JwtUtil.responseError((HttpServletResponse)response, 401, errorMsg);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
|
|
||||||
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
|
|
||||||
String token = httpServletRequest.getHeader(CommonConstant.X_ACCESS_TOKEN);
|
|
||||||
// 代码逻辑说明: JT-355 OA聊天添加token验证,获取token参数
|
|
||||||
if (oConvertUtils.isEmpty(token)) {
|
|
||||||
token = httpServletRequest.getParameter("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;
|
|
||||||
}
|
|
||||||
// 代码逻辑说明: 多租户用到
|
|
||||||
String tenantId = httpServletRequest.getHeader(CommonConstant.TENANT_ID);
|
|
||||||
TenantContext.setTenant(tenantId);
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,32 +0,0 @@
|
|||||||
package org.jeecg.config.sign.annotation;
|
|
||||||
|
|
||||||
import java.lang.annotation.ElementType;
|
|
||||||
import java.lang.annotation.Retention;
|
|
||||||
import java.lang.annotation.RetentionPolicy;
|
|
||||||
import java.lang.annotation.Target;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 签名校验注解
|
|
||||||
* 用于方法级别的签名验证,功能等同于yml中的jeecg.signUrls配置
|
|
||||||
* 参考DragSignatureAspect的设计思路,使用AOP切面实现
|
|
||||||
*
|
|
||||||
* @author GitHub Copilot
|
|
||||||
* @since 2025-12-15
|
|
||||||
*/
|
|
||||||
@Target(ElementType.METHOD)
|
|
||||||
@Retention(RetentionPolicy.RUNTIME)
|
|
||||||
public @interface SignatureCheck {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 是否启用签名校验
|
|
||||||
* @return true-启用(默认), false-禁用
|
|
||||||
*/
|
|
||||||
boolean enabled() default true;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 签名校验失败时的错误消息
|
|
||||||
* @return 错误消息
|
|
||||||
*/
|
|
||||||
String errorMessage() default "Sign签名校验失败!";
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,112 +0,0 @@
|
|||||||
package org.jeecg.config.sign.aspect;
|
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.aspectj.lang.JoinPoint;
|
|
||||||
import org.aspectj.lang.annotation.Aspect;
|
|
||||||
import org.aspectj.lang.annotation.Before;
|
|
||||||
import org.aspectj.lang.annotation.Pointcut;
|
|
||||||
import org.aspectj.lang.reflect.MethodSignature;
|
|
||||||
import org.jeecg.config.sign.annotation.SignatureCheck;
|
|
||||||
import org.jeecg.config.sign.interceptor.SignAuthInterceptor;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
import org.springframework.web.context.request.RequestContextHolder;
|
|
||||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
|
||||||
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
|
||||||
import org.springframework.web.bind.annotation.RequestBody;
|
|
||||||
|
|
||||||
import java.lang.annotation.Annotation;
|
|
||||||
import java.lang.reflect.Method;
|
|
||||||
import java.util.Arrays;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 基于AOP的签名验证切面
|
|
||||||
* 复用SignAuthInterceptor的成熟签名验证逻辑
|
|
||||||
*
|
|
||||||
* @author GitHub Copilot
|
|
||||||
* @since 2025-12-15
|
|
||||||
*/
|
|
||||||
@Aspect
|
|
||||||
@Slf4j
|
|
||||||
@Component("signatureCheckAspect")
|
|
||||||
public class SignatureCheckAspect {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 复用SignAuthInterceptor的签名验证逻辑
|
|
||||||
*/
|
|
||||||
private final SignAuthInterceptor signAuthInterceptor = new SignAuthInterceptor();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 验签切点:拦截所有标记了@SignatureCheck注解的方法
|
|
||||||
*/
|
|
||||||
@Pointcut("@annotation(org.jeecg.config.sign.annotation.SignatureCheck)")
|
|
||||||
private void signatureCheckPointCut() {
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 开始验签
|
|
||||||
*/
|
|
||||||
@Before("signatureCheckPointCut()")
|
|
||||||
public void doSignatureValidation(JoinPoint point) throws Exception {
|
|
||||||
// 获取方法上的注解
|
|
||||||
MethodSignature signature = (MethodSignature) point.getSignature();
|
|
||||||
Method method = signature.getMethod();
|
|
||||||
SignatureCheck signatureCheck = method.getAnnotation(SignatureCheck.class);
|
|
||||||
|
|
||||||
log.info("AOP签名验证: {}.{}", method.getDeclaringClass().getSimpleName(), method.getName());
|
|
||||||
|
|
||||||
// 如果注解被禁用,直接返回
|
|
||||||
if (!signatureCheck.enabled()) {
|
|
||||||
log.info("签名验证已禁用,跳过");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// update-begin---author:sjlei---date:20260115 for: 查找带有@RequestBody注解的参数,解决签名校验时读取请求体为空的问题
|
|
||||||
Object bodyParam = null;
|
|
||||||
Object[] args = point.getArgs();
|
|
||||||
Annotation[][] parameterAnnotations = method.getParameterAnnotations();
|
|
||||||
for (int i = 0; i < args.length; i++) {
|
|
||||||
Object arg = args[i];
|
|
||||||
Annotation[] annotations = parameterAnnotations[i];
|
|
||||||
boolean hasRequestBodyAnnotation = Arrays.stream(annotations).anyMatch(annotation -> annotation.annotationType().equals(RequestBody.class));
|
|
||||||
if (hasRequestBodyAnnotation) {
|
|
||||||
// 捕获携带@RequestBody注解的参数,供签名校验使用
|
|
||||||
bodyParam = arg;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// update-end-----author:sjlei---date:20260115 for: 查找带有@RequestBody注解的参数,解决签名校验时读取请求体为空的问题
|
|
||||||
|
|
||||||
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
|
||||||
if (attributes == null) {
|
|
||||||
log.error("无法获取请求上下文");
|
|
||||||
throw new IllegalArgumentException("无法获取请求上下文");
|
|
||||||
}
|
|
||||||
|
|
||||||
HttpServletRequest request = attributes.getRequest();
|
|
||||||
log.info("X-SIGN: {}, X-TIMESTAMP: {}", request.getHeader("X-SIGN"), request.getHeader("X-TIMESTAMP"));
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 直接调用SignAuthInterceptor的验证逻辑
|
|
||||||
signAuthInterceptor.validateSignature(request, bodyParam);
|
|
||||||
log.info("AOP签名验证通过");
|
|
||||||
|
|
||||||
} catch (IllegalArgumentException e) {
|
|
||||||
// 使用注解中配置的错误消息,或者保留原始错误消息
|
|
||||||
String errorMessage = signatureCheck.errorMessage();
|
|
||||||
log.error("AOP签名验证失败: {}", e.getMessage());
|
|
||||||
|
|
||||||
if ("Sign签名校验失败!".equals(errorMessage)) {
|
|
||||||
// 如果是默认错误消息,使用原始的详细错误信息
|
|
||||||
throw e;
|
|
||||||
} else {
|
|
||||||
// 如果是自定义错误消息,使用自定义消息
|
|
||||||
throw new IllegalArgumentException(errorMessage, e);
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
// 包装其他异常
|
|
||||||
String errorMessage = signatureCheck.errorMessage();
|
|
||||||
log.error("AOP签名验证异常: {}", e.getMessage());
|
|
||||||
throw new IllegalArgumentException(errorMessage, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,7 +1,6 @@
|
|||||||
package org.jeecg.config.sign.interceptor;
|
package org.jeecg.config.sign.interceptor;
|
||||||
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.jeecg.common.constant.TenantConstant;
|
|
||||||
import org.jeecg.common.util.PathMatcherUtil;
|
import org.jeecg.common.util.PathMatcherUtil;
|
||||||
import org.jeecg.config.JeecgBaseConfig;
|
import org.jeecg.config.JeecgBaseConfig;
|
||||||
import org.jeecg.config.filter.RequestBodyReserveFilter;
|
import org.jeecg.config.filter.RequestBodyReserveFilter;
|
||||||
@ -65,8 +64,6 @@ public class SignAuthConfiguration implements WebMvcConfigurer {
|
|||||||
//------------------------------------------------------------
|
//------------------------------------------------------------
|
||||||
// 建议此处只添加post请求地址而不是所有的都需要走过滤器
|
// 建议此处只添加post请求地址而不是所有的都需要走过滤器
|
||||||
registration.addUrlPatterns(signUrlsArray);
|
registration.addUrlPatterns(signUrlsArray);
|
||||||
// 增加注解签名请求
|
|
||||||
registration.addUrlPatterns(TenantConstant.SIGNATURE_CHECK_POST_URL);
|
|
||||||
return registration;
|
return registration;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -33,104 +33,63 @@ public class SignAuthInterceptor implements HandlerInterceptor {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
|
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
|
||||||
log.info("签名拦截器 Interceptor request URI = " + request.getRequestURI());
|
log.debug("Sign Interceptor request URI = " + request.getRequestURI());
|
||||||
|
HttpServletRequest requestWrapper = new BodyReaderHttpServletRequestWrapper(request);
|
||||||
|
//获取全部参数(包括URL和body上的)
|
||||||
|
SortedMap<String, String> allParams = HttpUtils.getAllParams(requestWrapper);
|
||||||
|
//对参数进行签名验证
|
||||||
|
String headerSign = request.getHeader(CommonConstant.X_SIGN);
|
||||||
|
String xTimestamp = request.getHeader(CommonConstant.X_TIMESTAMP);
|
||||||
|
|
||||||
try {
|
if(oConvertUtils.isEmpty(xTimestamp)){
|
||||||
// 调用验证逻辑
|
Result<?> result = Result.error("Sign签名校验失败,时间戳为空!");
|
||||||
validateSignature(request);
|
log.error("Sign 签名校验失败!Header xTimestamp 为空");
|
||||||
return true;
|
//校验失败返回前端
|
||||||
} catch (IllegalArgumentException e) {
|
|
||||||
// 验证失败,返回错误响应
|
|
||||||
log.error("Sign 签名校验失败!{}", e.getMessage());
|
|
||||||
response.setCharacterEncoding("UTF-8");
|
response.setCharacterEncoding("UTF-8");
|
||||||
response.setContentType("application/json; charset=utf-8");
|
response.setContentType("application/json; charset=utf-8");
|
||||||
PrintWriter out = response.getWriter();
|
PrintWriter out = response.getWriter();
|
||||||
Result<?> result = Result.error(e.getMessage());
|
out.print(JSON.toJSON(result));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
//客户端时间
|
||||||
|
Long clientTimestamp = Long.parseLong(xTimestamp);
|
||||||
|
|
||||||
|
int length = 14;
|
||||||
|
int length1000 = 1000;
|
||||||
|
//1.校验签名时间(兼容X_TIMESTAMP的新老格式)
|
||||||
|
if (xTimestamp.length() == length) {
|
||||||
|
//a. X_TIMESTAMP格式是 yyyyMMddHHmmss (例子:20220308152143)
|
||||||
|
if ((DateUtils.getCurrentTimestamp() - clientTimestamp) > MAX_EXPIRE) {
|
||||||
|
log.error("签名验证失败:X-TIMESTAMP已过期,注意系统时间和服务器时间是否有误差!");
|
||||||
|
throw new IllegalArgumentException("签名验证失败:X-TIMESTAMP已过期");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
//b. X_TIMESTAMP格式是 时间戳 (例子:1646552406000)
|
||||||
|
if ((System.currentTimeMillis() - clientTimestamp) > (MAX_EXPIRE * length1000)) {
|
||||||
|
log.error("签名验证失败:X-TIMESTAMP已过期,注意系统时间和服务器时间是否有误差!");
|
||||||
|
throw new IllegalArgumentException("签名验证失败:X-TIMESTAMP已过期");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//2.校验签名
|
||||||
|
boolean isSigned = SignUtil.verifySign(allParams,headerSign);
|
||||||
|
|
||||||
|
if (isSigned) {
|
||||||
|
log.debug("Sign 签名通过!Header Sign : {}",headerSign);
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
log.debug("sign allParams: {}", allParams);
|
||||||
|
log.error("request URI = " + request.getRequestURI());
|
||||||
|
log.error("Sign 签名校验失败!Header Sign : {}",headerSign);
|
||||||
|
//校验失败返回前端
|
||||||
|
response.setCharacterEncoding("UTF-8");
|
||||||
|
response.setContentType("application/json; charset=utf-8");
|
||||||
|
PrintWriter out = response.getWriter();
|
||||||
|
Result<?> result = Result.error("Sign签名校验失败!");
|
||||||
out.print(JSON.toJSON(result));
|
out.print(JSON.toJSON(result));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 签名验证核心逻辑
|
|
||||||
* 提取出来供AOP切面复用
|
|
||||||
* @param request HTTP请求
|
|
||||||
* @throws IllegalArgumentException 验证失败时抛出异常
|
|
||||||
*/
|
|
||||||
public void validateSignature(HttpServletRequest request) throws IllegalArgumentException {
|
|
||||||
validateSignature(request, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 签名验证核心逻辑
|
|
||||||
* 提取出来供AOP切面复用
|
|
||||||
* @param request HTTP请求
|
|
||||||
* @throws IllegalArgumentException 验证失败时抛出异常
|
|
||||||
*/
|
|
||||||
public void validateSignature(HttpServletRequest request, Object bodyParam) throws IllegalArgumentException {
|
|
||||||
try {
|
|
||||||
log.debug("开始签名验证: {} {}", request.getMethod(), request.getRequestURI());
|
|
||||||
|
|
||||||
HttpServletRequest requestWrapper = new BodyReaderHttpServletRequestWrapper(request);
|
|
||||||
//获取全部参数(包括URL和body上的)
|
|
||||||
SortedMap<String, String> allParams = HttpUtils.getAllParams(requestWrapper, bodyParam);
|
|
||||||
log.debug("提取参数: {}", allParams);
|
|
||||||
|
|
||||||
//对参数进行签名验证
|
|
||||||
String headerSign = request.getHeader(CommonConstant.X_SIGN);
|
|
||||||
String xTimestamp = request.getHeader(CommonConstant.X_TIMESTAMP);
|
|
||||||
|
|
||||||
if(oConvertUtils.isEmpty(xTimestamp)){
|
|
||||||
log.error("Sign签名校验失败,时间戳为空!");
|
|
||||||
throw new IllegalArgumentException("Sign签名校验失败,请求参数不完整!");
|
|
||||||
}
|
|
||||||
|
|
||||||
//客户端时间
|
|
||||||
Long clientTimestamp = Long.parseLong(xTimestamp);
|
|
||||||
|
|
||||||
int length = 14;
|
|
||||||
int length1000 = 1000;
|
|
||||||
//1.校验签名时间(兼容X_TIMESTAMP的新老格式)
|
|
||||||
if (xTimestamp.length() == length) {
|
|
||||||
//a. X_TIMESTAMP格式是 yyyyMMddHHmmss (例子:20220308152143)
|
|
||||||
long currentTimestamp = DateUtils.getCurrentTimestamp();
|
|
||||||
long timeDiff = currentTimestamp - clientTimestamp;
|
|
||||||
log.debug("时间戳验证(yyyyMMddHHmmss): 时间差{}秒", timeDiff);
|
|
||||||
|
|
||||||
if (timeDiff > MAX_EXPIRE) {
|
|
||||||
log.error("时间戳已过期: {}秒 > {}秒", timeDiff, MAX_EXPIRE);
|
|
||||||
throw new IllegalArgumentException("签名验证失败,请求时效性验证失败!");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
//b. X_TIMESTAMP格式是 时间戳 (例子:1646552406000)
|
|
||||||
long currentTime = System.currentTimeMillis();
|
|
||||||
long timeDiff = currentTime - clientTimestamp;
|
|
||||||
long maxExpireMs = MAX_EXPIRE * length1000;
|
|
||||||
log.debug("时间戳验证(Unix): 时间差{}ms", timeDiff);
|
|
||||||
|
|
||||||
if (timeDiff > maxExpireMs) {
|
|
||||||
log.error("时间戳已过期: {}ms > {}ms", timeDiff, maxExpireMs);
|
|
||||||
throw new IllegalArgumentException("签名验证失败,请求时效性验证失败!");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//2.校验签名
|
|
||||||
boolean isSigned = SignUtil.verifySign(allParams,headerSign);
|
|
||||||
|
|
||||||
if (isSigned) {
|
|
||||||
log.debug("签名验证通过");
|
|
||||||
} else {
|
|
||||||
log.error("签名验证失败, 参数: {}", allParams);
|
|
||||||
throw new IllegalArgumentException("Sign签名校验失败!");
|
|
||||||
}
|
|
||||||
} catch (IllegalArgumentException e) {
|
|
||||||
// 重新抛出签名验证异常
|
|
||||||
throw e;
|
|
||||||
} catch (Exception e) {
|
|
||||||
// 包装其他异常(如IOException)
|
|
||||||
log.error("签名验证异常: {}", e.getMessage());
|
|
||||||
throw new IllegalArgumentException("Sign签名校验失败:" + e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -35,7 +35,7 @@ public class HttpUtils {
|
|||||||
* @date 20210621
|
* @date 20210621
|
||||||
* @param request
|
* @param request
|
||||||
*/
|
*/
|
||||||
public static SortedMap<String, String> getAllParams(HttpServletRequest request, Object bodyParam) throws IOException {
|
public static SortedMap<String, String> getAllParams(HttpServletRequest request) throws IOException {
|
||||||
|
|
||||||
SortedMap<String, String> result = new TreeMap<>();
|
SortedMap<String, String> result = new TreeMap<>();
|
||||||
// 获取URL上最后带逗号的参数变量 sys/dict/getDictItems/sys_user,realname,username
|
// 获取URL上最后带逗号的参数变量 sys/dict/getDictItems/sys_user,realname,username
|
||||||
@ -65,13 +65,7 @@ public class HttpUtils {
|
|||||||
Map<String, String> allRequestParam = new HashMap<>(16);
|
Map<String, String> allRequestParam = new HashMap<>(16);
|
||||||
// get请求不需要拿body参数
|
// get请求不需要拿body参数
|
||||||
if (!HttpMethod.GET.name().equals(request.getMethod())) {
|
if (!HttpMethod.GET.name().equals(request.getMethod())) {
|
||||||
if (bodyParam != null) {
|
allRequestParam = getAllRequestParam(request);
|
||||||
// update-begin---author:sjlei---date:20260115 for: 解决签名校验时读取请求体为空的问题
|
|
||||||
allRequestParam = JSONObject.parseObject(JSONObject.toJSONString(bodyParam), Map.class);
|
|
||||||
// update-end-----author:sjlei---date:20260115 for: 解决签名校验时读取请求体为空的问题
|
|
||||||
} else {
|
|
||||||
allRequestParam = getAllRequestParam(request);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// 将URL的参数和body参数进行合并
|
// 将URL的参数和body参数进行合并
|
||||||
if (allRequestParam != null) {
|
if (allRequestParam != null) {
|
||||||
|
|||||||
@ -11,12 +11,7 @@ import org.springframework.util.DigestUtils;
|
|||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
import java.io.UnsupportedEncodingException;
|
import java.io.UnsupportedEncodingException;
|
||||||
import java.net.URLDecoder;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.SortedMap;
|
import java.util.SortedMap;
|
||||||
import java.util.TreeMap;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 签名工具类
|
* 签名工具类
|
||||||
@ -54,7 +49,12 @@ public class SignUtil {
|
|||||||
String paramsJsonStr = JSONObject.toJSONString(params);
|
String paramsJsonStr = JSONObject.toJSONString(params);
|
||||||
log.debug("Param paramsJsonStr : {}", paramsJsonStr);
|
log.debug("Param paramsJsonStr : {}", paramsJsonStr);
|
||||||
//设置签名秘钥
|
//设置签名秘钥
|
||||||
String signatureSecret = SignUtil.getSignatureSecret();
|
JeecgBaseConfig jeecgBaseConfig = SpringContextUtils.getBean(JeecgBaseConfig.class);
|
||||||
|
String signatureSecret = jeecgBaseConfig.getSignatureSecret();
|
||||||
|
String curlyBracket = SymbolConstant.DOLLAR + SymbolConstant.LEFT_CURLY_BRACKET;
|
||||||
|
if(oConvertUtils.isEmpty(signatureSecret) || signatureSecret.contains(curlyBracket)){
|
||||||
|
throw new JeecgBootException("签名密钥 ${jeecg.signatureSecret} 缺少配置 !!");
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
//【issues/I484RW】2.4.6部署后,下拉搜索框提示“sign签名检验失败”
|
//【issues/I484RW】2.4.6部署后,下拉搜索框提示“sign签名检验失败”
|
||||||
return DigestUtils.md5DigestAsHex((paramsJsonStr + signatureSecret).getBytes("UTF-8")).toUpperCase();
|
return DigestUtils.md5DigestAsHex((paramsJsonStr + signatureSecret).getBytes("UTF-8")).toUpperCase();
|
||||||
@ -63,129 +63,4 @@ public class SignUtil {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 通过前端签名算法生成签名
|
|
||||||
*
|
|
||||||
* @param url 请求的完整URL(包含查询参数)
|
|
||||||
* @param requestParams 使用 @RequestParam 获取的参数集合
|
|
||||||
* @param requestBodyParams 使用 @RequestBody 获取的参数集合
|
|
||||||
* @return 计算得到的签名(大写MD5),若参数不足返回 null
|
|
||||||
*/
|
|
||||||
public static String generateRequestSign(String url, Map<String, Object> requestParams, Map<String, Object> requestBodyParams) {
|
|
||||||
if (oConvertUtils.isEmpty(url)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
// 解析URL上的查询参数与路径变量
|
|
||||||
Map<String, String> urlParams = parseQueryString(url);
|
|
||||||
// 合并URL参数与@RequestParam参数,确保数值和布尔类型转换为字符串
|
|
||||||
Map<String, String> mergedParams = mergeObject(urlParams, requestParams);
|
|
||||||
// 按需合并@RequestBody参数
|
|
||||||
if (requestBodyParams != null && !requestBodyParams.isEmpty()) {
|
|
||||||
mergedParams = mergeObject(mergedParams, requestBodyParams);
|
|
||||||
}
|
|
||||||
// 按键名升序排序,保持与前端一致的签名顺序
|
|
||||||
SortedMap<String, String> sortedParams = new TreeMap<>(mergedParams);
|
|
||||||
// 去除时间戳字段,避免参与签名
|
|
||||||
sortedParams.remove("_t");
|
|
||||||
// 序列化为JSON字符串
|
|
||||||
String paramsJsonStr = JSONObject.toJSONString(sortedParams);
|
|
||||||
// 读取签名秘钥
|
|
||||||
String signatureSecret = getSignatureSecret();
|
|
||||||
// 计算MD5摘要并转大写
|
|
||||||
return DigestUtils.md5DigestAsHex((paramsJsonStr + signatureSecret).getBytes(StandardCharsets.UTF_8)).toUpperCase();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 解析URL中的查询参数,并处理末尾逗号分隔的路径变量片段。
|
|
||||||
*
|
|
||||||
* @param url 请求的完整URL
|
|
||||||
* @return 解析后的参数映射,数值与布尔类型均转换为字符串
|
|
||||||
*/
|
|
||||||
private static Map<String, String> parseQueryString(String url) {
|
|
||||||
Map<String, String> result = new HashMap<>(16);
|
|
||||||
int fragmentIndex = url.indexOf('#');
|
|
||||||
if (fragmentIndex >= 0) {
|
|
||||||
url = url.substring(0, fragmentIndex);
|
|
||||||
}
|
|
||||||
int questionIndex = url.indexOf('?');
|
|
||||||
String paramString = null;
|
|
||||||
if (questionIndex >= 0 && questionIndex < url.length() - 1) {
|
|
||||||
paramString = url.substring(questionIndex + 1);
|
|
||||||
}
|
|
||||||
// 处理路径变量末尾以逗号分隔的段,例如 /sys/dict/getDictItems/sys_user,realname,username
|
|
||||||
int lastSlashIndex = url.lastIndexOf(SymbolConstant.SINGLE_SLASH);
|
|
||||||
if (lastSlashIndex >= 0 && lastSlashIndex < url.length() - 1) {
|
|
||||||
String lastPathVariable = url.substring(lastSlashIndex + 1);
|
|
||||||
int qIndexInPath = lastPathVariable.indexOf('?');
|
|
||||||
if (qIndexInPath >= 0) {
|
|
||||||
lastPathVariable = lastPathVariable.substring(0, qIndexInPath);
|
|
||||||
}
|
|
||||||
if (lastPathVariable.contains(SymbolConstant.COMMA)) {
|
|
||||||
String decodedPathVariable = URLDecoder.decode(lastPathVariable, StandardCharsets.UTF_8);
|
|
||||||
result.put(X_PATH_VARIABLE, decodedPathVariable);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (oConvertUtils.isNotEmpty(paramString)) {
|
|
||||||
String[] pairs = paramString.split(SymbolConstant.AND);
|
|
||||||
for (String pair : pairs) {
|
|
||||||
int equalIndex = pair.indexOf('=');
|
|
||||||
if (equalIndex > 0 && equalIndex < pair.length() - 1) {
|
|
||||||
String key = pair.substring(0, equalIndex);
|
|
||||||
String value = pair.substring(equalIndex + 1);
|
|
||||||
// 解码并统一类型为字符串
|
|
||||||
String decodedKey = URLDecoder.decode(key, StandardCharsets.UTF_8);
|
|
||||||
String decodedValue = URLDecoder.decode(value, StandardCharsets.UTF_8);
|
|
||||||
result.put(decodedKey, decodedValue);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 合并两个参数映射,并保证数值与布尔类型统一转为字符串。
|
|
||||||
*
|
|
||||||
* @param target 初始参数映射
|
|
||||||
* @param source 待合并的参数映射
|
|
||||||
* @return 合并后的新映射
|
|
||||||
*/
|
|
||||||
private static Map<String, String> mergeObject(Map<String, String> target, Map<String, Object> source) {
|
|
||||||
Map<String, String> merged = new HashMap<>(16);
|
|
||||||
if (target != null && !target.isEmpty()) {
|
|
||||||
merged.putAll(target);
|
|
||||||
}
|
|
||||||
if (source != null && !source.isEmpty()) {
|
|
||||||
for (Map.Entry<String, Object> entry : source.entrySet()) {
|
|
||||||
String key = entry.getKey();
|
|
||||||
Object value = entry.getValue();
|
|
||||||
if (value instanceof Number) {
|
|
||||||
// 数值类型转字符串,保持前后端一致
|
|
||||||
merged.put(key, String.valueOf(value));
|
|
||||||
} else if (value instanceof Boolean) {
|
|
||||||
// 布尔类型转字符串,保持前后端一致
|
|
||||||
merged.put(key, String.valueOf(value));
|
|
||||||
} else if (value != null) {
|
|
||||||
merged.put(key, String.valueOf(value));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return merged;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 读取并校验签名秘钥配置。
|
|
||||||
*
|
|
||||||
* @return 有效的签名秘钥
|
|
||||||
*/
|
|
||||||
private static String getSignatureSecret() {
|
|
||||||
JeecgBaseConfig jeecgBaseConfig = SpringContextUtils.getBean(JeecgBaseConfig.class);
|
|
||||||
String signatureSecret = jeecgBaseConfig.getSignatureSecret();
|
|
||||||
String curlyBracket = SymbolConstant.DOLLAR + SymbolConstant.LEFT_CURLY_BRACKET;
|
|
||||||
if (oConvertUtils.isEmpty(signatureSecret) || signatureSecret.contains(curlyBracket)) {
|
|
||||||
throw new JeecgBootException("签名密钥 ${jeecg.signatureSecret} 缺少配置 !!");
|
|
||||||
}
|
|
||||||
return signatureSecret;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -2,10 +2,10 @@ package org.jeecg.modules.base.service.impl;
|
|||||||
|
|
||||||
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
|
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.shiro.SecurityUtils;
|
|
||||||
import org.jeecg.common.api.dto.LogDTO;
|
import org.jeecg.common.api.dto.LogDTO;
|
||||||
import org.jeecg.common.constant.enums.ClientTerminalTypeEnum;
|
import org.jeecg.common.constant.enums.ClientTerminalTypeEnum;
|
||||||
import org.jeecg.common.util.BrowserUtils;
|
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.mapper.BaseCommonMapper;
|
||||||
import org.jeecg.modules.base.service.BaseCommonService;
|
import org.jeecg.modules.base.service.BaseCommonService;
|
||||||
import org.jeecg.common.system.vo.LoginUser;
|
import org.jeecg.common.system.vo.LoginUser;
|
||||||
@ -74,7 +74,7 @@ public class BaseCommonServiceImpl implements BaseCommonService {
|
|||||||
//获取登录用户信息
|
//获取登录用户信息
|
||||||
if(user==null){
|
if(user==null){
|
||||||
try {
|
try {
|
||||||
user = (LoginUser) SecurityUtils.getSubject().getPrincipal();
|
user = SecureUtil.currentUser();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
//e.printStackTrace();
|
//e.printStackTrace();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,429 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""
|
|
||||||
MCP Stdio 工具 - 修复编码问题
|
|
||||||
确保所有输出都使用UTF-8编码
|
|
||||||
"""
|
|
||||||
|
|
||||||
import json
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
from typing import Dict, Any
|
|
||||||
import logging
|
|
||||||
|
|
||||||
# 强制使用UTF-8编码
|
|
||||||
if sys.platform == "win32":
|
|
||||||
# Windows需要特殊处理
|
|
||||||
import io
|
|
||||||
|
|
||||||
sys.stdin = io.TextIOWrapper(sys.stdin.buffer, encoding='utf-8')
|
|
||||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
|
||||||
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')
|
|
||||||
else:
|
|
||||||
# Unix-like系统
|
|
||||||
sys.stdin.reconfigure(encoding='utf-8')
|
|
||||||
sys.stdout.reconfigure(encoding='utf-8')
|
|
||||||
sys.stderr.reconfigure(encoding='utf-8')
|
|
||||||
|
|
||||||
# 设置环境变量
|
|
||||||
os.environ['PYTHONIOENCODING'] = 'utf-8'
|
|
||||||
os.environ['PYTHONUTF8'] = '1'
|
|
||||||
|
|
||||||
# 配置日志
|
|
||||||
logging.basicConfig(
|
|
||||||
level=logging.INFO,
|
|
||||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
|
||||||
encoding='utf-8'
|
|
||||||
)
|
|
||||||
logger = logging.getLogger("mcp-tool")
|
|
||||||
|
|
||||||
|
|
||||||
class FixedMCPServer:
|
|
||||||
"""修复编码问题的MCP服务器"""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.tools = {}
|
|
||||||
self.initialize_tools()
|
|
||||||
|
|
||||||
def initialize_tools(self):
|
|
||||||
"""初始化工具集"""
|
|
||||||
|
|
||||||
# 获取时间
|
|
||||||
self.tools["get_time"] = {
|
|
||||||
"name": "get_time",
|
|
||||||
"description": "获取当前时间",
|
|
||||||
"inputSchema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"format": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "时间格式",
|
|
||||||
"enum": ["iso", "timestamp", "human", "chinese"],
|
|
||||||
"default": "iso"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# 文本处理工具
|
|
||||||
self.tools["text_process"] = {
|
|
||||||
"name": "text_process",
|
|
||||||
"description": "文本处理工具",
|
|
||||||
"inputSchema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"text": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "输入文本"
|
|
||||||
},
|
|
||||||
"operation": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "操作类型",
|
|
||||||
"enum": ["length", "upper", "lower", "reverse", "count_words"],
|
|
||||||
"default": "length"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["text"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# 数据格式工具
|
|
||||||
self.tools["format_data"] = {
|
|
||||||
"name": "format_data",
|
|
||||||
"description": "格式化数据",
|
|
||||||
"inputSchema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"data": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "原始数据"
|
|
||||||
},
|
|
||||||
"format": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "格式类型",
|
|
||||||
"enum": ["json", "yaml", "xml"],
|
|
||||||
"default": "json"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["data"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def handle_request(self, request: Dict[str, Any]) -> Dict[str, Any]:
|
|
||||||
"""处理请求"""
|
|
||||||
try:
|
|
||||||
method = request.get("method")
|
|
||||||
params = request.get("params", {})
|
|
||||||
|
|
||||||
if method == "tools/list":
|
|
||||||
return self.handle_tools_list()
|
|
||||||
elif method == "tools/call":
|
|
||||||
return self.handle_tool_call(params)
|
|
||||||
elif method == "ping":
|
|
||||||
return {"result": "pong"}
|
|
||||||
else:
|
|
||||||
return self.create_error_response(
|
|
||||||
code=-32601,
|
|
||||||
message="Method not found"
|
|
||||||
)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error handling request: {e}")
|
|
||||||
return self.create_error_response(
|
|
||||||
code=-32603,
|
|
||||||
message=f"Internal error: {str(e)}"
|
|
||||||
)
|
|
||||||
|
|
||||||
def handle_tools_list(self) -> Dict[str, Any]:
|
|
||||||
"""列出所有工具 - 确保返回标准JSON"""
|
|
||||||
return {
|
|
||||||
"result": {
|
|
||||||
"tools": list(self.tools.values())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def handle_tool_call(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
||||||
"""调用工具 - 修复响应格式"""
|
|
||||||
name = params.get("name")
|
|
||||||
arguments = params.get("arguments", {})
|
|
||||||
|
|
||||||
if name not in self.tools:
|
|
||||||
return self.create_error_response(
|
|
||||||
code=-32602,
|
|
||||||
message=f"Tool '{name}' not found"
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
if name == "get_time":
|
|
||||||
result = self.execute_get_time(arguments)
|
|
||||||
elif name == "text_process":
|
|
||||||
result = self.execute_text_process(arguments)
|
|
||||||
elif name == "format_data":
|
|
||||||
result = self.execute_format_data(arguments)
|
|
||||||
else:
|
|
||||||
return self.create_error_response(
|
|
||||||
code=-32602,
|
|
||||||
message="Tool not implemented"
|
|
||||||
)
|
|
||||||
|
|
||||||
# 确保返回正确的MCP响应格式
|
|
||||||
return self.create_success_response(result)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Tool execution error: {e}")
|
|
||||||
return self.create_error_response(
|
|
||||||
code=-32603,
|
|
||||||
message=f"Tool execution failed: {str(e)}"
|
|
||||||
)
|
|
||||||
|
|
||||||
def execute_get_time(self, args: Dict[str, Any]) -> Dict[str, Any]:
|
|
||||||
"""获取时间 - 支持中文"""
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
try:
|
|
||||||
format_type = args.get("format", "iso")
|
|
||||||
now = datetime.now()
|
|
||||||
|
|
||||||
if format_type == "iso":
|
|
||||||
result = now.isoformat()
|
|
||||||
elif format_type == "timestamp":
|
|
||||||
result = now.timestamp()
|
|
||||||
elif format_type == "human":
|
|
||||||
result = now.strftime("%Y-%m-%d %H:%M:%S")
|
|
||||||
elif format_type == "chinese":
|
|
||||||
result = now.strftime("%Y年%m月%d日 %H时%M分%S秒")
|
|
||||||
else:
|
|
||||||
result = now.isoformat()
|
|
||||||
logger.info(f"当前系统时间:{result}")
|
|
||||||
return {
|
|
||||||
"status": "success",
|
|
||||||
"format": format_type,
|
|
||||||
"time": result,
|
|
||||||
"timestamp": now.timestamp(),
|
|
||||||
"date": now.strftime("%Y-%m-%d"),
|
|
||||||
"time_12h": now.strftime("%I:%M:%S %p")
|
|
||||||
}
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
return {
|
|
||||||
"status": "error",
|
|
||||||
"error": str(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
def execute_text_process(self, args: Dict[str, Any]) -> Dict[str, Any]:
|
|
||||||
"""文本处理"""
|
|
||||||
try:
|
|
||||||
text = args.get("text", "")
|
|
||||||
operation = args.get("operation", "length")
|
|
||||||
|
|
||||||
if operation == "length":
|
|
||||||
result = len(text)
|
|
||||||
result_str = f"文本长度: {result} 个字符"
|
|
||||||
elif operation == "upper":
|
|
||||||
result = text.upper()
|
|
||||||
result_str = f"大写: {result}"
|
|
||||||
elif operation == "lower":
|
|
||||||
result = text.lower()
|
|
||||||
result_str = f"小写: {result}"
|
|
||||||
elif operation == "reverse":
|
|
||||||
result = text[::-1]
|
|
||||||
result_str = f"反转: {result}"
|
|
||||||
elif operation == "count_words":
|
|
||||||
words = len(text.split())
|
|
||||||
result = words
|
|
||||||
result_str = f"单词数: {words}"
|
|
||||||
else:
|
|
||||||
raise ValueError(f"未知操作: {operation}")
|
|
||||||
|
|
||||||
return {
|
|
||||||
"status": "success",
|
|
||||||
"operation": operation,
|
|
||||||
"original_text": text,
|
|
||||||
"result": result,
|
|
||||||
"result_str": result_str,
|
|
||||||
"text_length": len(text)
|
|
||||||
}
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
return {
|
|
||||||
"status": "error",
|
|
||||||
"error": str(e),
|
|
||||||
"operation": args.get("operation", "")
|
|
||||||
}
|
|
||||||
|
|
||||||
def execute_format_data(self, args: Dict[str, Any]) -> Dict[str, Any]:
|
|
||||||
"""格式化数据"""
|
|
||||||
try:
|
|
||||||
data_str = args.get("data", "")
|
|
||||||
format_type = args.get("format", "json")
|
|
||||||
|
|
||||||
# 尝试解析为JSON
|
|
||||||
try:
|
|
||||||
data = json.loads(data_str)
|
|
||||||
is_json = True
|
|
||||||
except:
|
|
||||||
data = data_str
|
|
||||||
is_json = False
|
|
||||||
|
|
||||||
if format_type == "json":
|
|
||||||
if is_json:
|
|
||||||
result = json.dumps(data, ensure_ascii=False, indent=2)
|
|
||||||
else:
|
|
||||||
# 如果不是JSON,包装成JSON
|
|
||||||
result = json.dumps({"text": data}, ensure_ascii=False, indent=2)
|
|
||||||
elif format_type == "yaml":
|
|
||||||
import yaml
|
|
||||||
result = yaml.dump(data, allow_unicode=True, default_flow_style=False)
|
|
||||||
elif format_type == "xml":
|
|
||||||
# 简单的XML格式化
|
|
||||||
if isinstance(data, dict):
|
|
||||||
result = "<data>"
|
|
||||||
for k, v in data.items():
|
|
||||||
result += f"\n <{k}>{v}</{k}>"
|
|
||||||
result += "\n</data>"
|
|
||||||
else:
|
|
||||||
result = f"<text>{data}</text>"
|
|
||||||
else:
|
|
||||||
result = str(data)
|
|
||||||
|
|
||||||
return {
|
|
||||||
"status": "success",
|
|
||||||
"format": format_type,
|
|
||||||
"original": data_str,
|
|
||||||
"formatted": result,
|
|
||||||
"length": len(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
return {
|
|
||||||
"status": "error",
|
|
||||||
"error": str(e),
|
|
||||||
"format": args.get("format", "")
|
|
||||||
}
|
|
||||||
|
|
||||||
def create_success_response(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
|
||||||
"""创建成功响应 - 确保符合MCP规范"""
|
|
||||||
# 将数据转换为JSON字符串作为文本内容
|
|
||||||
content_text = json.dumps(data, ensure_ascii=False, indent=2)
|
|
||||||
|
|
||||||
return {
|
|
||||||
"result": {
|
|
||||||
"content": [
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"text": content_text
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"isError": False
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def create_error_response(self, code: int, message: str) -> Dict[str, Any]:
|
|
||||||
"""创建错误响应"""
|
|
||||||
return {
|
|
||||||
"error": {
|
|
||||||
"code": code,
|
|
||||||
"message": message
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def safe_json_dump(data: Dict[str, Any]) -> str:
|
|
||||||
"""安全的JSON序列化,确保UTF-8编码"""
|
|
||||||
try:
|
|
||||||
return json.dumps(data, ensure_ascii=False, separators=(',', ':'))
|
|
||||||
except:
|
|
||||||
# 如果失败,使用ASCII转义
|
|
||||||
return json.dumps(data, ensure_ascii=True, separators=(',', ':'))
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
"""主函数 - 修复Stdio通信"""
|
|
||||||
logger.info("启动MCP Stdio服务器 (修复编码版)...")
|
|
||||||
|
|
||||||
server = FixedMCPServer()
|
|
||||||
|
|
||||||
# 初始握手消息
|
|
||||||
init_message = {
|
|
||||||
"jsonrpc": "2.0",
|
|
||||||
"id": 1,
|
|
||||||
"result": {
|
|
||||||
"protocolVersion": "2024-11-05",
|
|
||||||
"capabilities": {
|
|
||||||
"tools": {}
|
|
||||||
},
|
|
||||||
"serverInfo": {
|
|
||||||
"name": "fixed-mcp-server",
|
|
||||||
"version": "1.0.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# 发送初始化响应
|
|
||||||
try:
|
|
||||||
sys.stdout.write(safe_json_dump(init_message) + "\n")
|
|
||||||
sys.stdout.flush()
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"发送初始化消息失败: {e}")
|
|
||||||
return
|
|
||||||
|
|
||||||
logger.info("MCP服务器已初始化")
|
|
||||||
|
|
||||||
# 主循环
|
|
||||||
line_num = 0
|
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
line = sys.stdin.readline()
|
|
||||||
if not line:
|
|
||||||
logger.info("输入流结束")
|
|
||||||
break
|
|
||||||
|
|
||||||
line = line.strip()
|
|
||||||
line_num += 1
|
|
||||||
|
|
||||||
if not line:
|
|
||||||
continue
|
|
||||||
|
|
||||||
logger.info(f"收到第 {line_num} 行: {line[:100]}...")
|
|
||||||
|
|
||||||
try:
|
|
||||||
request = json.loads(line)
|
|
||||||
logger.info(f"解析请求: {request.get('method', 'unknown')}")
|
|
||||||
|
|
||||||
# 处理请求
|
|
||||||
response = server.handle_request(request)
|
|
||||||
response["jsonrpc"] = "2.0"
|
|
||||||
response["id"] = request.get("id")
|
|
||||||
|
|
||||||
# 发送响应
|
|
||||||
response_json = safe_json_dump(response)
|
|
||||||
sys.stdout.write(response_json + "\n")
|
|
||||||
sys.stdout.flush()
|
|
||||||
|
|
||||||
logger.info(f"发送响应: {response.get('result', response.get('error', {}))}")
|
|
||||||
|
|
||||||
except json.JSONDecodeError as e:
|
|
||||||
logger.error(f"JSON解析错误: {e}")
|
|
||||||
error_response = {
|
|
||||||
"jsonrpc": "2.0",
|
|
||||||
"error": {
|
|
||||||
"code": -32700,
|
|
||||||
"message": f"Parse error at line {line_num}"
|
|
||||||
},
|
|
||||||
"id": None
|
|
||||||
}
|
|
||||||
sys.stdout.write(safe_json_dump(error_response) + "\n")
|
|
||||||
sys.stdout.flush()
|
|
||||||
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
logger.info("接收到中断信号")
|
|
||||||
break
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"未处理的错误: {e}")
|
|
||||||
break
|
|
||||||
|
|
||||||
logger.info("MCP服务器已停止")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
Binary file not shown.
@ -6,7 +6,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>org.jeecgframework.boot3</groupId>
|
<groupId>org.jeecgframework.boot3</groupId>
|
||||||
<artifactId>jeecg-boot-module</artifactId>
|
<artifactId>jeecg-boot-module</artifactId>
|
||||||
<version>3.9.1</version>
|
<version>3.9.0</version>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
<artifactId>jeecg-boot-module-airag</artifactId>
|
<artifactId>jeecg-boot-module-airag</artifactId>
|
||||||
@ -33,7 +33,7 @@
|
|||||||
<properties>
|
<properties>
|
||||||
<kotlin.version>2.2.0</kotlin.version>
|
<kotlin.version>2.2.0</kotlin.version>
|
||||||
<liteflow.version>2.15.0</liteflow.version>
|
<liteflow.version>2.15.0</liteflow.version>
|
||||||
<apache-tika.version>3.2.3</apache-tika.version>
|
<apache-tika.version>2.9.1</apache-tika.version>
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
<dependencyManagement>
|
<dependencyManagement>
|
||||||
@ -41,14 +41,14 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>dev.langchain4j</groupId>
|
<groupId>dev.langchain4j</groupId>
|
||||||
<artifactId>langchain4j-bom</artifactId>
|
<artifactId>langchain4j-bom</artifactId>
|
||||||
<version>1.9.1</version>
|
<version>1.3.0</version>
|
||||||
<type>pom</type>
|
<type>pom</type>
|
||||||
<scope>import</scope>
|
<scope>import</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>dev.langchain4j</groupId>
|
<groupId>dev.langchain4j</groupId>
|
||||||
<artifactId>langchain4j-community-bom</artifactId>
|
<artifactId>langchain4j-community-bom</artifactId>
|
||||||
<version>1.9.1-beta17</version>
|
<version>1.3.0-beta9</version>
|
||||||
<type>pom</type>
|
<type>pom</type>
|
||||||
<scope>import</scope>
|
<scope>import</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
@ -75,7 +75,7 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.jeecgframework.boot3</groupId>
|
<groupId>org.jeecgframework.boot3</groupId>
|
||||||
<artifactId>jeecg-aiflow</artifactId>
|
<artifactId>jeecg-aiflow</artifactId>
|
||||||
<version>3.9.1-beta</version>
|
<version>3.9.0.1</version>
|
||||||
<exclusions>
|
<exclusions>
|
||||||
<exclusion>
|
<exclusion>
|
||||||
<groupId>commons-io</groupId>
|
<groupId>commons-io</groupId>
|
||||||
@ -107,16 +107,16 @@
|
|||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.yomahub</groupId>
|
<groupId>com.yomahub</groupId>
|
||||||
<artifactId>liteflow-script-groovy</artifactId>
|
<artifactId>liteflow-script-python</artifactId>
|
||||||
<version>${liteflow.version}</version>
|
<version>${liteflow.version}</version>
|
||||||
<scope>runtime</scope>
|
<scope>provided</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
<!-- end 注意:这几个依赖体积较大,每个约50MB。若发布时需要使用,请将 <scope>provided</scope> 删除 -->
|
<!-- end 注意:这几个依赖体积较大,每个约50MB。若发布时需要使用,请将 <scope>provided</scope> 删除 -->
|
||||||
|
|
||||||
<!-- aiflow 脚本依赖 -->
|
<!-- aiflow 脚本依赖 -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.yomahub</groupId>
|
<groupId>com.yomahub</groupId>
|
||||||
<artifactId>liteflow-script-python</artifactId>
|
<artifactId>liteflow-script-groovy</artifactId>
|
||||||
<version>${liteflow.version}</version>
|
<version>${liteflow.version}</version>
|
||||||
<scope>runtime</scope>
|
<scope>runtime</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
@ -151,11 +151,6 @@
|
|||||||
<groupId>dev.langchain4j</groupId>
|
<groupId>dev.langchain4j</groupId>
|
||||||
<artifactId>langchain4j-open-ai</artifactId>
|
<artifactId>langchain4j-open-ai</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
<!-- langChain4j mcp support -->
|
|
||||||
<dependency>
|
|
||||||
<groupId>dev.langchain4j</groupId>
|
|
||||||
<artifactId>langchain4j-mcp</artifactId>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>dev.langchain4j</groupId>
|
<groupId>dev.langchain4j</groupId>
|
||||||
<artifactId>langchain4j-ollama</artifactId>
|
<artifactId>langchain4j-ollama</artifactId>
|
||||||
@ -202,11 +197,7 @@
|
|||||||
<artifactId>langchain4j-pgvector</artifactId>
|
<artifactId>langchain4j-pgvector</artifactId>
|
||||||
<version>1.3.0-beta9</version>
|
<version>1.3.0-beta9</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<!-- langChain4j Document Parser 适用于excel、ppt、word -->
|
<!-- langChain4j Document Parser -->
|
||||||
<dependency>
|
|
||||||
<groupId>dev.langchain4j</groupId>
|
|
||||||
<artifactId>langchain4j-document-parser-apache-poi</artifactId>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.apache.tika</groupId>
|
<groupId>org.apache.tika</groupId>
|
||||||
<artifactId>tika-core</artifactId>
|
<artifactId>tika-core</artifactId>
|
||||||
@ -233,12 +224,7 @@
|
|||||||
<artifactId>tika-parser-text-module</artifactId>
|
<artifactId>tika-parser-text-module</artifactId>
|
||||||
<version>${apache-tika.version}</version>
|
<version>${apache-tika.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<!-- word模版引擎 -->
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.deepoove</groupId>
|
|
||||||
<artifactId>poi-tl</artifactId>
|
|
||||||
<version>1.12.2</version>
|
|
||||||
</dependency>
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
</project>
|
</project>
|
||||||
@ -44,19 +44,4 @@ public class AiAppConsts {
|
|||||||
*/
|
*/
|
||||||
public static final String APP_METADATA_FLOW_INPUTS = "flowInputs";
|
public static final String APP_METADATA_FLOW_INPUTS = "flowInputs";
|
||||||
|
|
||||||
/**
|
|
||||||
* 是否开启记忆
|
|
||||||
*/
|
|
||||||
public static final Integer IZ_OPEN_MEMORY = 1;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 会话标题最大长度
|
|
||||||
*/
|
|
||||||
public static final int CONVERSATION_MAX_TITLE_LENGTH = 10;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* AI写作的应用id
|
|
||||||
*/
|
|
||||||
public static final String WRITER_APP_ID = "2010634128233779202";
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -104,68 +104,4 @@ public class Prompts {
|
|||||||
" - 反幻觉校验:\"所有数据需标注来源,不确定信息用[需核实]标记\"\n" +
|
" - 反幻觉校验:\"所有数据需标注来源,不确定信息用[需核实]标记\"\n" +
|
||||||
" - 风格校准器:\"对比[目标风格]与生成内容的余弦相似度,低于0.7时启动重写\"\n" +
|
" - 风格校准器:\"对比[目标风格]与生成内容的余弦相似度,低于0.7时启动重写\"\n" +
|
||||||
" - 伦理审查模块:\"自动过滤涉及隐私/偏见/违法内容,替换为[合规表达]\"";
|
" - 伦理审查模块:\"自动过滤涉及隐私/偏见/违法内容,替换为[合规表达]\"";
|
||||||
|
|
||||||
/**
|
|
||||||
* 提示词生成角色及通用要求
|
|
||||||
*/
|
|
||||||
public static final String GENERATE_GUIDE_HEADER = "# 角色\n" +
|
|
||||||
"你是一位AI提示词专家,请根据提供的配置信息,生成针对AI智能体的“使用指南”提示词。\n" +
|
|
||||||
"\n" +
|
|
||||||
"## 通用要求\n" +
|
|
||||||
"1. 生成的内容将作为系统提示词的一部分。\n" +
|
|
||||||
"2. **严禁**包含任何角色设定开场白(如“你是一个...AI助手”、“在对话过程中...”等)。\n" +
|
|
||||||
"3. **只输出提示词内容**,不要包含任何解释、寒暄或Markdown代码块标记。\n" +
|
|
||||||
"4. 语气专业、清晰、指令性强。\n" +
|
|
||||||
"5. 说明内容请使用中文。\n\n";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 变量生成提示词
|
|
||||||
*/
|
|
||||||
public static final String GENERATE_VAR_PART = "## 任务:生成变量使用指南\n" +
|
|
||||||
"### 输入信息\n" +
|
|
||||||
"**变量列表**:\n" +
|
|
||||||
"%s\n" +
|
|
||||||
"### 要求\n" +
|
|
||||||
"1. 请生成一段**变量使用指南**。\n" +
|
|
||||||
"2. **遍历生成**:请遍历【输入信息】中的所有变量,为**每一个**变量生成一条具体的使用指南。\n" +
|
|
||||||
"3. **格式要求**:请仿照以下句式,根据变量的实际含义生成(确保包含{{变量名}}):\n" +
|
|
||||||
" 例如:针对name变量 -> “回复问题时,请称呼你的用户为{{name}}。”\n" +
|
|
||||||
" 例如:针对age变量 -> “用户的年龄是{{age}},请在对话中适时使用。”\n" +
|
|
||||||
" 例如:针对其他变量 -> “用户的[变量描述]是{{[变量名]}},请在对话中适时使用。”\n" +
|
|
||||||
"4. **通用更新指令**:请在变量指南的最后,单独生成一条指令,明确指示AI:“当从用户对话中获取到上述变量(<列出所有变量名,用顿号分隔>)的**新信息**时,**必须立即调用** `update_variable` 工具进行存储。**注意**:调用前请检查上下文,如果已调用过该工具或变量值未改变,**严禁**重复调用。”\n" +
|
|
||||||
"5. **保留原文**:如果输入信息中包含具体的行为指令(如“回复问题时,请称呼你的用户为{{name}}”),请在生成的指南中**直接引用原文**,不要进行改写或格式化,以免改变用户的原意。\n\n";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 记忆库生成提示词
|
|
||||||
*/
|
|
||||||
public static final String GENERATE_MEMORY_PART = "## 任务:生成记忆库使用指南\n" +
|
|
||||||
"### 输入信息\n" +
|
|
||||||
"**记忆库描述**:\n" +
|
|
||||||
"%s\n" +
|
|
||||||
"### 要求\n" +
|
|
||||||
"1. 请生成一段**记忆库使用指南**,加入【工具使用强制协议】:\n" +
|
|
||||||
" - **全自动存储(无需用户指令)**:你必须时刻像一个观察者一样分析对话。一旦检测到符合记忆库描述的信息(尤其是:**姓名、职业、年龄**、联系方式、偏好、经历等),**立即**调用 `add_memory` 工具存储。**绝对不要**询问用户是否需要存储,也不要等待用户明确指令。这是你的后台职责。\n" +
|
|
||||||
" - **全自动检索(强制优先)**:\n" +
|
|
||||||
" * **禁止直接反问**:当用户提出依赖个人信息的问题(如“推荐适合我的...”或“我之前说过...”)时,**绝对禁止**直接反问用户“你的爱好是什么?”。\n" +
|
|
||||||
" * **必须先查后答**:你必须**先假设**记忆库中已经有了答案,并**立即调用** `query_memory` 进行验证。只有当工具返回“未找到相关信息”后,你才有资格询问用户。\n" +
|
|
||||||
" * **宁可查空,不可不查**:即使你觉得可能没有记录,也必须先走一遍查询流程。\n" +
|
|
||||||
" - **动态调整**:请根据【输入信息】中提供的**记忆库状态描述**,明确界定哪些信息属于“自动捕获”的范围。\n" +
|
|
||||||
" - **行为准则**:\n" +
|
|
||||||
" * 你的记忆动作应该是**主动且无感**的。用户只负责聊天,你负责记住一切重要细节。\n" +
|
|
||||||
" * **禁止口头空谈**:严禁只回复“我知道了”、“已记住”而实际不调用工具。这是严重错误。\n" +
|
|
||||||
" - **示例演示**:\n" +
|
|
||||||
" * 自动存储(职业):用户说“我是网络工程师” -> (捕捉到职业信息) -> **立即自动调用** `add_memory(content='用户职业是网络工程师')` -> (存储成功) -> 回复“原来是同行,网络工程很有趣...”。\n" +
|
|
||||||
" * 自动查询(场景):用户说“根据我的爱好推荐旅游地点” -> **严禁**直接问“你有什么爱好?” -> **必须立即调用** `query_memory(queryText='用户爱好')` -> (若查到:爬山) -> 回复“既然你喜欢爬山,推荐去黄山...”。\n" +
|
|
||||||
" * 自动查询(常规):用户问“今天吃什么好?” -> (需要了解口味) -> **立即自动调用** `query_memory(queryText='用户饮食偏好')` -> (获取到不吃香菜) -> 回复“推荐一家不放香菜的...”。\n\n";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ai写作提示词
|
|
||||||
*/
|
|
||||||
public static final String AI_WRITER_PROMPT ="请撰写一篇关于 [{}] 的文章。文章的内容格式:{},语气:{},语言:{},长度:{}。";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ai写作回复提示词
|
|
||||||
*/
|
|
||||||
public static final String AI_REPLY_PROMPT = "请针对如下内容:[{}] 做个回复。回复内容参考:[{}], 回复格式:{},语气:{},语言:{},长度:{}。";
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,7 +4,6 @@ import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
|||||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
|
||||||
import org.jeecg.common.api.vo.Result;
|
import org.jeecg.common.api.vo.Result;
|
||||||
import org.jeecg.common.system.base.controller.JeecgController;
|
import org.jeecg.common.system.base.controller.JeecgController;
|
||||||
import org.jeecg.common.system.query.QueryGenerator;
|
import org.jeecg.common.system.query.QueryGenerator;
|
||||||
@ -18,10 +17,12 @@ import org.jeecg.modules.airag.app.service.IAiragAppService;
|
|||||||
import org.jeecg.modules.airag.app.service.IAiragChatService;
|
import org.jeecg.modules.airag.app.service.IAiragChatService;
|
||||||
import org.jeecg.modules.airag.app.vo.AppDebugParams;
|
import org.jeecg.modules.airag.app.vo.AppDebugParams;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||||
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @Description: AI应用
|
* @Description: AI应用
|
||||||
@ -66,7 +67,7 @@ public class AiragAppController extends JeecgController<AiragApp, IAiragAppServi
|
|||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
@RequestMapping(value = "/edit", method = {RequestMethod.PUT, RequestMethod.POST})
|
@RequestMapping(value = "/edit", method = {RequestMethod.PUT, RequestMethod.POST})
|
||||||
@RequiresPermissions("airag:app:edit")
|
@PreAuthorize("@jps.requiresPermissions('airag:app:edit')")
|
||||||
public Result<String> edit(@RequestBody AiragApp airagApp) {
|
public Result<String> edit(@RequestBody AiragApp airagApp) {
|
||||||
AssertUtils.assertNotEmpty("参数异常", airagApp);
|
AssertUtils.assertNotEmpty("参数异常", airagApp);
|
||||||
AssertUtils.assertNotEmpty("请输入应用名称", airagApp.getName());
|
AssertUtils.assertNotEmpty("请输入应用名称", airagApp.getName());
|
||||||
@ -105,7 +106,7 @@ public class AiragAppController extends JeecgController<AiragApp, IAiragAppServi
|
|||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
@DeleteMapping(value = "/delete")
|
@DeleteMapping(value = "/delete")
|
||||||
@RequiresPermissions("airag:app:delete")
|
@PreAuthorize("@jps.requiresPermissions('airag:app:delete')")
|
||||||
public Result<String> delete(HttpServletRequest request,@RequestParam(name = "id", required = true) String id) {
|
public Result<String> delete(HttpServletRequest request,@RequestParam(name = "id", required = true) String id) {
|
||||||
//update-begin---author:chenrui ---date:20250606 for:[issues/8337]关于ai工作列表的数据权限问题 #8337------------
|
//update-begin---author:chenrui ---date:20250606 for:[issues/8337]关于ai工作列表的数据权限问题 #8337------------
|
||||||
//如果是saas隔离的情况下,判断当前租户id是否是当前租户下的
|
//如果是saas隔离的情况下,判断当前租户id是否是当前租户下的
|
||||||
@ -178,16 +179,4 @@ public class AiragAppController extends JeecgController<AiragApp, IAiragAppServi
|
|||||||
return (SseEmitter) airagAppService.generatePrompt(prompt,false);
|
return (SseEmitter) airagAppService.generatePrompt(prompt,false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 根据应用ID生成变量和记忆提示词 (SSE)
|
|
||||||
* for: 【QQYUN-14479】提示词单独拆分
|
|
||||||
* @param variables
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
@PostMapping(value = "/prompt/generateMemoryByAppId")
|
|
||||||
public SseEmitter generatePromptByAppIdSse(@RequestParam(name = "variables") String variables,
|
|
||||||
@RequestParam(name = "memoryId") String memoryId) {
|
|
||||||
return (SseEmitter) airagAppService.generateMemoryByAppId(variables, memoryId,false);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,7 +8,6 @@ import org.jeecg.common.constant.CommonConstant;
|
|||||||
import org.jeecg.common.util.CommonUtils;
|
import org.jeecg.common.util.CommonUtils;
|
||||||
import org.jeecg.config.shiro.IgnoreAuth;
|
import org.jeecg.config.shiro.IgnoreAuth;
|
||||||
import org.jeecg.modules.airag.app.service.IAiragChatService;
|
import org.jeecg.modules.airag.app.service.IAiragChatService;
|
||||||
import org.jeecg.modules.airag.app.vo.AiWriteGenerateVo;
|
|
||||||
import org.jeecg.modules.airag.app.vo.ChatConversation;
|
import org.jeecg.modules.airag.app.vo.ChatConversation;
|
||||||
import org.jeecg.modules.airag.app.vo.ChatSendParams;
|
import org.jeecg.modules.airag.app.vo.ChatSendParams;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
@ -103,19 +102,6 @@ public class AiragChatController {
|
|||||||
return chatService.getConversations(appId);
|
return chatService.getConversations(appId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 根据类型获取所有对话
|
|
||||||
*
|
|
||||||
* @return 返回一个Result对象,包含所有对话的信息
|
|
||||||
* @author wangshuai
|
|
||||||
* @date 2025/12/11 11:42
|
|
||||||
*/
|
|
||||||
@IgnoreAuth
|
|
||||||
@GetMapping(value = "/getConversationsByType")
|
|
||||||
public Result<?> getConversationsByType(@RequestParam(value = "sessionType") String sessionType) {
|
|
||||||
return chatService.getConversationsByType(sessionType);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 删除会话
|
* 删除会话
|
||||||
*
|
*
|
||||||
@ -127,22 +113,7 @@ public class AiragChatController {
|
|||||||
@IgnoreAuth
|
@IgnoreAuth
|
||||||
@DeleteMapping(value = "/conversation/{id}")
|
@DeleteMapping(value = "/conversation/{id}")
|
||||||
public Result<?> deleteConversation(@PathVariable("id") String id) {
|
public Result<?> deleteConversation(@PathVariable("id") String id) {
|
||||||
return chatService.deleteConversation(id,"");
|
return chatService.deleteConversation(id);
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 删除会话
|
|
||||||
*
|
|
||||||
* @param id
|
|
||||||
* @return
|
|
||||||
* @author wangshuai
|
|
||||||
* @date 2025/12/11 20:00
|
|
||||||
*/
|
|
||||||
@IgnoreAuth
|
|
||||||
@DeleteMapping(value = "/conversation/{id}/{sessionType}")
|
|
||||||
public Result<?> deleteConversationByType(@PathVariable("id") String id,
|
|
||||||
@PathVariable("sessionType") String sessionType) {
|
|
||||||
return chatService.deleteConversation(id,sessionType);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -168,9 +139,8 @@ public class AiragChatController {
|
|||||||
*/
|
*/
|
||||||
@IgnoreAuth
|
@IgnoreAuth
|
||||||
@GetMapping(value = "/messages")
|
@GetMapping(value = "/messages")
|
||||||
public Result<?> getMessages(@RequestParam(value = "conversationId", required = true) String conversationId,
|
public Result<?> getMessages(@RequestParam(value = "conversationId", required = true) String conversationId) {
|
||||||
@RequestParam(value = "sessionType", required = false) String sessionType) {
|
return chatService.getMessages(conversationId);
|
||||||
return chatService.getMessages(conversationId, sessionType);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -183,21 +153,7 @@ public class AiragChatController {
|
|||||||
@IgnoreAuth
|
@IgnoreAuth
|
||||||
@GetMapping(value = "/messages/clear/{conversationId}")
|
@GetMapping(value = "/messages/clear/{conversationId}")
|
||||||
public Result<?> clearMessage(@PathVariable(value = "conversationId") String conversationId) {
|
public Result<?> clearMessage(@PathVariable(value = "conversationId") String conversationId) {
|
||||||
return chatService.clearMessage(conversationId, "");
|
return chatService.clearMessage(conversationId);
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 清空消息
|
|
||||||
*
|
|
||||||
* @return
|
|
||||||
* @author wangshuai
|
|
||||||
* @date 2025/12/11 19:06
|
|
||||||
*/
|
|
||||||
@IgnoreAuth
|
|
||||||
@GetMapping(value = "/messages/clear/{conversationId}/{sessionType}")
|
|
||||||
public Result<?> clearMessageByType(@PathVariable(value = "conversationId") String conversationId,
|
|
||||||
@PathVariable(value = "sessionType") String sessionType) {
|
|
||||||
return chatService.clearMessage(conversationId, sessionType);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -261,25 +217,4 @@ public class AiragChatController {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* ai海报生成
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
@PostMapping("/genAiPoster")
|
|
||||||
public Result<String> genAiPoster(@RequestBody ChatSendParams chatSendParams){
|
|
||||||
String imageUrl = chatService.genAiPoster(chatSendParams);
|
|
||||||
return Result.OK(imageUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 生成ai写作
|
|
||||||
*
|
|
||||||
* @param aiWriteGenerateVo
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
@PostMapping("/genAiWriter")
|
|
||||||
public SseEmitter genAiWriter(@RequestBody AiWriteGenerateVo aiWriteGenerateVo){
|
|
||||||
return chatService.genAiWriter(aiWriteGenerateVo);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -173,29 +173,6 @@ public class AiragApp implements Serializable {
|
|||||||
@Schema(description = "插件")
|
@Schema(description = "插件")
|
||||||
private java.lang.String plugins;
|
private java.lang.String plugins;
|
||||||
|
|
||||||
/**
|
|
||||||
* 是否开启记忆(0 不开启,1开启)
|
|
||||||
*/
|
|
||||||
@Schema(description = "是否开启记忆(0 不开启,1开启)")
|
|
||||||
private java.lang.Integer izOpenMemory;
|
|
||||||
/**
|
|
||||||
* 记忆库,知识库的id
|
|
||||||
*/
|
|
||||||
@Schema(description = "记忆库")
|
|
||||||
private java.lang.String memoryId;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 变量
|
|
||||||
*/
|
|
||||||
@Schema(description = "变量")
|
|
||||||
private java.lang.String variables;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 记忆和变量提示词
|
|
||||||
*/
|
|
||||||
@Schema(description = "记忆和变量提示词")
|
|
||||||
private java.lang.String memoryPrompt;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 知识库ids
|
* 知识库ids
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
package org.jeecg.modules.airag.app.service;
|
package org.jeecg.modules.airag.app.service;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.extension.service.IService;
|
import com.baomidou.mybatisplus.extension.service.IService;
|
||||||
|
import org.jeecg.common.api.vo.Result;
|
||||||
import org.jeecg.modules.airag.app.entity.AiragApp;
|
import org.jeecg.modules.airag.app.entity.AiragApp;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -20,14 +21,4 @@ public interface IAiragAppService extends IService<AiragApp> {
|
|||||||
* @date 2025/3/12 14:45
|
* @date 2025/3/12 14:45
|
||||||
*/
|
*/
|
||||||
Object generatePrompt(String prompt,boolean blocking);
|
Object generatePrompt(String prompt,boolean blocking);
|
||||||
|
|
||||||
/**
|
|
||||||
* 根据应用id生成提示词
|
|
||||||
*
|
|
||||||
* @param variables
|
|
||||||
* @param memoryId
|
|
||||||
* @param blocking
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
Object generateMemoryByAppId(String variables, String memoryId, boolean blocking);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
package org.jeecg.modules.airag.app.service;
|
package org.jeecg.modules.airag.app.service;
|
||||||
|
|
||||||
import org.jeecg.common.api.vo.Result;
|
import org.jeecg.common.api.vo.Result;
|
||||||
import org.jeecg.modules.airag.app.vo.AiWriteGenerateVo;
|
|
||||||
import org.jeecg.modules.airag.app.vo.AppDebugParams;
|
import org.jeecg.modules.airag.app.vo.AppDebugParams;
|
||||||
import org.jeecg.modules.airag.app.vo.ChatConversation;
|
import org.jeecg.modules.airag.app.vo.ChatConversation;
|
||||||
import org.jeecg.modules.airag.app.vo.ChatSendParams;
|
import org.jeecg.modules.airag.app.vo.ChatSendParams;
|
||||||
@ -60,23 +59,21 @@ public interface IAiragChatService {
|
|||||||
* 获取对话聊天记录
|
* 获取对话聊天记录
|
||||||
*
|
*
|
||||||
* @param conversationId
|
* @param conversationId
|
||||||
* @param sessionType 类型
|
|
||||||
* @return
|
* @return
|
||||||
* @author chenrui
|
* @author chenrui
|
||||||
* @date 2025/2/26 15:16
|
* @date 2025/2/26 15:16
|
||||||
*/
|
*/
|
||||||
Result<?> getMessages(String conversationId, String sessionType);
|
Result<?> getMessages(String conversationId);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 删除会话
|
* 删除会话
|
||||||
*
|
*
|
||||||
* @param conversationId
|
* @param conversationId
|
||||||
* @param sessionType
|
|
||||||
* @return
|
* @return
|
||||||
* @author chenrui
|
* @author chenrui
|
||||||
* @date 2025/3/3 16:55
|
* @date 2025/3/3 16:55
|
||||||
*/
|
*/
|
||||||
Result<?> deleteConversation(String conversationId, String sessionType);
|
Result<?> deleteConversation(String conversationId);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 更新会话标题
|
* 更新会话标题
|
||||||
@ -90,12 +87,11 @@ public interface IAiragChatService {
|
|||||||
/**
|
/**
|
||||||
* 清空消息
|
* 清空消息
|
||||||
* @param conversationId
|
* @param conversationId
|
||||||
* @param sessionType
|
|
||||||
* @return
|
* @return
|
||||||
* @author chenrui
|
* @author chenrui
|
||||||
* @date 2025/3/3 19:49
|
* @date 2025/3/3 19:49
|
||||||
*/
|
*/
|
||||||
Result<?> clearMessage(String conversationId, String sessionType);
|
Result<?> clearMessage(String conversationId);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 初始化聊天(忽略租户)
|
* 初始化聊天(忽略租户)
|
||||||
@ -115,27 +111,4 @@ public interface IAiragChatService {
|
|||||||
* @date 2025/8/11 17:39
|
* @date 2025/8/11 17:39
|
||||||
*/
|
*/
|
||||||
SseEmitter receiveByRequestId(String requestId);
|
SseEmitter receiveByRequestId(String requestId);
|
||||||
|
|
||||||
/**
|
|
||||||
* 根据类型获取会话列表
|
|
||||||
*
|
|
||||||
* @param sessionType
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
Result<?> getConversationsByType(String sessionType);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 生成海报图片
|
|
||||||
* @param chatSendParams
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
String genAiPoster(ChatSendParams chatSendParams);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 生成ai创作
|
|
||||||
*
|
|
||||||
* @param chatSendParams
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
SseEmitter genAiWriter(AiWriteGenerateVo chatSendParams);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,44 +0,0 @@
|
|||||||
package org.jeecg.modules.airag.app.service;
|
|
||||||
|
|
||||||
import org.jeecg.modules.airag.app.entity.AiragApp;
|
|
||||||
import org.jeecg.modules.airag.common.handler.AIChatParams;
|
|
||||||
|
|
||||||
public interface IAiragVariableService {
|
|
||||||
/**
|
|
||||||
* 更新变量值
|
|
||||||
*
|
|
||||||
* @param userId
|
|
||||||
* @param appId
|
|
||||||
* @param name
|
|
||||||
* @param value
|
|
||||||
*/
|
|
||||||
void updateVariable(String userId, String appId, String name, String value);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 追加提示词
|
|
||||||
*
|
|
||||||
* @param username
|
|
||||||
* @param app
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
String additionalPrompt(String username, AiragApp app);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 初始化变量(仅不存在时设置)
|
|
||||||
*
|
|
||||||
* @param userId
|
|
||||||
* @param appId
|
|
||||||
* @param name
|
|
||||||
* @param defaultValue
|
|
||||||
*/
|
|
||||||
void initVariable(String userId, String appId, String name, String defaultValue);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 添加变量更新工具
|
|
||||||
*
|
|
||||||
* @param params
|
|
||||||
* @param aiApp
|
|
||||||
* @param username
|
|
||||||
*/
|
|
||||||
void addUpdateVariableTool(AiragApp aiApp, String username, AIChatParams params);
|
|
||||||
}
|
|
||||||
@ -1,6 +1,5 @@
|
|||||||
package org.jeecg.modules.airag.app.service.impl;
|
package org.jeecg.modules.airag.app.service.impl;
|
||||||
|
|
||||||
import com.alibaba.fastjson.JSONArray;
|
|
||||||
import com.alibaba.fastjson.JSONObject;
|
import com.alibaba.fastjson.JSONObject;
|
||||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
import dev.langchain4j.data.message.AiMessage;
|
import dev.langchain4j.data.message.AiMessage;
|
||||||
@ -11,15 +10,12 @@ import dev.langchain4j.model.output.FinishReason;
|
|||||||
import dev.langchain4j.service.TokenStream;
|
import dev.langchain4j.service.TokenStream;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.jeecg.common.api.vo.Result;
|
import org.jeecg.common.api.vo.Result;
|
||||||
import org.jeecg.common.exception.JeecgBootBizTipException;
|
|
||||||
import org.jeecg.common.util.AssertUtils;
|
import org.jeecg.common.util.AssertUtils;
|
||||||
import org.jeecg.common.util.UUIDGenerator;
|
import org.jeecg.common.util.UUIDGenerator;
|
||||||
import org.jeecg.common.util.oConvertUtils;
|
|
||||||
import org.jeecg.modules.airag.app.consts.Prompts;
|
import org.jeecg.modules.airag.app.consts.Prompts;
|
||||||
import org.jeecg.modules.airag.app.entity.AiragApp;
|
import org.jeecg.modules.airag.app.entity.AiragApp;
|
||||||
import org.jeecg.modules.airag.app.mapper.AiragAppMapper;
|
import org.jeecg.modules.airag.app.mapper.AiragAppMapper;
|
||||||
import org.jeecg.modules.airag.app.service.IAiragAppService;
|
import org.jeecg.modules.airag.app.service.IAiragAppService;
|
||||||
import org.jeecg.modules.airag.app.vo.AppVariableVo;
|
|
||||||
import org.jeecg.modules.airag.common.consts.AiragConsts;
|
import org.jeecg.modules.airag.common.consts.AiragConsts;
|
||||||
import org.jeecg.modules.airag.common.handler.AIChatParams;
|
import org.jeecg.modules.airag.common.handler.AIChatParams;
|
||||||
import org.jeecg.modules.airag.common.handler.IAIChatHandler;
|
import org.jeecg.modules.airag.common.handler.IAIChatHandler;
|
||||||
@ -27,8 +23,6 @@ import org.jeecg.modules.airag.common.utils.AiragLocalCache;
|
|||||||
import org.jeecg.modules.airag.common.vo.event.EventData;
|
import org.jeecg.modules.airag.common.vo.event.EventData;
|
||||||
import org.jeecg.modules.airag.common.vo.event.EventFlowData;
|
import org.jeecg.modules.airag.common.vo.event.EventFlowData;
|
||||||
import org.jeecg.modules.airag.common.vo.event.EventMessageData;
|
import org.jeecg.modules.airag.common.vo.event.EventMessageData;
|
||||||
import org.jeecg.modules.airag.llm.entity.AiragKnowledge;
|
|
||||||
import org.jeecg.modules.airag.llm.service.IAiragKnowledgeService;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||||
@ -37,7 +31,6 @@ import java.io.IOException;
|
|||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
import java.util.regex.Pattern;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @Description: AI应用
|
* @Description: AI应用
|
||||||
@ -52,9 +45,6 @@ public class AiragAppServiceImpl extends ServiceImpl<AiragAppMapper, AiragApp> i
|
|||||||
@Autowired
|
@Autowired
|
||||||
IAIChatHandler aiChatHandler;
|
IAIChatHandler aiChatHandler;
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private IAiragKnowledgeService airagKnowledgeService;
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Object generatePrompt(String prompt, boolean blocking) {
|
public Object generatePrompt(String prompt, boolean blocking) {
|
||||||
AssertUtils.assertNotEmpty("请输入提示词", prompt);
|
AssertUtils.assertNotEmpty("请输入提示词", prompt);
|
||||||
@ -72,167 +62,81 @@ public class AiragAppServiceImpl extends ServiceImpl<AiragAppMapper, AiragApp> i
|
|||||||
}
|
}
|
||||||
return Result.OK("success", promptValue);
|
return Result.OK("success", promptValue);
|
||||||
}else{
|
}else{
|
||||||
//update-begin---author:wangshuai---date:2026-01-08---for: 将流式输出单独抽出去,变量和记忆也需要---
|
SseEmitter emitter = new SseEmitter(-0L);
|
||||||
return startSseChat(messages, params);
|
// 异步运行(流式)
|
||||||
//update-end---author:wangshuai---date:2026-01-08---for: 将流式输出单独抽出去,变量和记忆也需要---
|
TokenStream tokenStream = aiChatHandler.chatByDefaultModel(messages, params);
|
||||||
}
|
/**
|
||||||
}
|
* 是否正在思考
|
||||||
|
*/
|
||||||
//update-begin---author:wangshuai---date:2026-01-05---for:【QQYUN-14479】增加一个开启记忆的按钮。下面为提示词和记忆,将记忆提示词单独拆分---
|
AtomicBoolean isThinking = new AtomicBoolean(false);
|
||||||
@Override
|
String requestId = UUIDGenerator.generate();
|
||||||
public Object generateMemoryByAppId(String variables, String memoryId, boolean blocking) {
|
// ai聊天响应逻辑
|
||||||
if(oConvertUtils.isEmpty(variables) && oConvertUtils.isEmpty(memoryId)){
|
tokenStream.onPartialResponse((String resMessage) -> {
|
||||||
throw new JeecgBootBizTipException("请先添加变量或者记忆后再次重试!");
|
// 兼容推理模型
|
||||||
}
|
if ("<think>".equals(resMessage)) {
|
||||||
// 构建变量描述
|
isThinking.set(true);
|
||||||
StringBuilder variablesDesc = new StringBuilder();
|
resMessage = "> ";
|
||||||
if (oConvertUtils.isNotEmpty(variables)) {
|
}
|
||||||
List<AppVariableVo> variableList = JSONArray.parseArray(variables, AppVariableVo.class);
|
if ("</think>".equals(resMessage)) {
|
||||||
if (variableList != null && !variableList.isEmpty()) {
|
isThinking.set(false);
|
||||||
for (AppVariableVo var : variableList) {
|
resMessage = "\n\n";
|
||||||
if (var.getEnable() != null && !var.getEnable()) {
|
}
|
||||||
continue;
|
if (isThinking.get()) {
|
||||||
}
|
if (null != resMessage && resMessage.contains("\n")) {
|
||||||
String name = var.getName();
|
resMessage = "\n> ";
|
||||||
if (oConvertUtils.isNotEmpty(var.getAction())) {
|
|
||||||
String action = var.getAction();
|
|
||||||
if (oConvertUtils.isNotEmpty(name)) {
|
|
||||||
try {
|
|
||||||
// 使用正则替换未被{{}}包裹的变量名
|
|
||||||
String regex = "(?<!\\{\\{)\\b" + Pattern.quote(name) + "\\b(?!\\}\\})";
|
|
||||||
action = action.replaceAll(regex, "{{" + name + "}}");
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.warn("变量名替换异常: name={}", name, e);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
variablesDesc.append(action).append("\n");
|
EventData eventData = new EventData(requestId, null, EventData.EVENT_MESSAGE);
|
||||||
} else {
|
EventMessageData messageEventData = EventMessageData.builder()
|
||||||
variablesDesc.append("- {{").append(name).append("}}");
|
.message(resMessage)
|
||||||
if (oConvertUtils.isNotEmpty(var.getDescription())) {
|
.build();
|
||||||
variablesDesc.append(": ").append(var.getDescription());
|
eventData.setData(messageEventData);
|
||||||
}
|
|
||||||
variablesDesc.append("\n");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 构建Prompt
|
|
||||||
StringBuilder promptBuilder = new StringBuilder(Prompts.GENERATE_GUIDE_HEADER);
|
|
||||||
if (!variablesDesc.isEmpty()) {
|
|
||||||
promptBuilder.append(String.format(Prompts.GENERATE_VAR_PART, variablesDesc.toString()));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 构建记忆状态描述
|
|
||||||
if (oConvertUtils.isNotEmpty(memoryId)) {
|
|
||||||
String memoryDescr = "";
|
|
||||||
AiragKnowledge memory = airagKnowledgeService.getById(memoryId);
|
|
||||||
if (memory != null && oConvertUtils.isNotEmpty(memory.getDescr())) {
|
|
||||||
memoryDescr += "记忆库描述:" + memory.getDescr();
|
|
||||||
}
|
|
||||||
promptBuilder.append(String.format(Prompts.GENERATE_MEMORY_PART, memoryDescr));
|
|
||||||
}
|
|
||||||
|
|
||||||
String prompt = promptBuilder.toString();
|
|
||||||
|
|
||||||
List<ChatMessage> messages = List.of(new UserMessage(prompt));
|
|
||||||
|
|
||||||
AIChatParams params = new AIChatParams();
|
|
||||||
params.setTemperature(0.7);
|
|
||||||
|
|
||||||
if(blocking){
|
|
||||||
String promptValue = aiChatHandler.completionsByDefaultModel(messages, params);
|
|
||||||
if (promptValue == null || promptValue.isEmpty()) {
|
|
||||||
return Result.error("生成失败");
|
|
||||||
}
|
|
||||||
return Result.OK("success", promptValue);
|
|
||||||
}else{
|
|
||||||
return startSseChat(messages, params);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 发送聊天
|
|
||||||
* @param messages
|
|
||||||
* @param params
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
private SseEmitter startSseChat(List<ChatMessage> messages, AIChatParams params) {
|
|
||||||
SseEmitter emitter = new SseEmitter(-0L);
|
|
||||||
// 异步运行(流式)
|
|
||||||
TokenStream tokenStream = aiChatHandler.chatByDefaultModel(messages, params);
|
|
||||||
/**
|
|
||||||
* 是否正在思考
|
|
||||||
*/
|
|
||||||
AtomicBoolean isThinking = new AtomicBoolean(false);
|
|
||||||
String requestId = UUIDGenerator.generate();
|
|
||||||
// ai聊天响应逻辑
|
|
||||||
tokenStream.onPartialResponse((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);
|
|
||||||
EventMessageData messageEventData = EventMessageData.builder()
|
|
||||||
.message(resMessage)
|
|
||||||
.build();
|
|
||||||
eventData.setData(messageEventData);
|
|
||||||
try {
|
|
||||||
String eventStr = JSONObject.toJSONString(eventData);
|
|
||||||
log.debug("[AI应用]接收LLM返回消息:{}", eventStr);
|
|
||||||
emitter.send(SseEmitter.event().data(eventStr));
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.onCompleteResponse((responseMessage) -> {
|
|
||||||
// 记录ai的回复
|
|
||||||
AiMessage aiMessage = responseMessage.aiMessage();
|
|
||||||
FinishReason finishReason = responseMessage.finishReason();
|
|
||||||
String respText = aiMessage.text();
|
|
||||||
if (FinishReason.STOP.equals(finishReason) || null == finishReason) {
|
|
||||||
// 正常结束
|
|
||||||
EventData eventData = new EventData(requestId, null, EventData.EVENT_MESSAGE_END);
|
|
||||||
try {
|
try {
|
||||||
log.debug("[AI应用]接收LLM返回消息完成:{}", respText);
|
String eventStr = JSONObject.toJSONString(eventData);
|
||||||
emitter.send(SseEmitter.event().data(eventData));
|
log.debug("[AI应用]接收LLM返回消息:{}", eventStr);
|
||||||
|
emitter.send(SseEmitter.event().data(eventStr));
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
}
|
}
|
||||||
closeSSE(emitter, eventData);
|
})
|
||||||
} else {
|
.onCompleteResponse((responseMessage) -> {
|
||||||
// 异常结束
|
// 记录ai的回复
|
||||||
log.error("调用模型异常:" + respText);
|
AiMessage aiMessage = responseMessage.aiMessage();
|
||||||
if (respText.contains("insufficient Balance")) {
|
FinishReason finishReason = responseMessage.finishReason();
|
||||||
respText = "大预言模型账号余额不足!";
|
String respText = aiMessage.text();
|
||||||
|
if (FinishReason.STOP.equals(finishReason) || null == finishReason) {
|
||||||
|
// 正常结束
|
||||||
|
EventData eventData = new EventData(requestId, null, EventData.EVENT_MESSAGE_END);
|
||||||
|
try {
|
||||||
|
log.debug("[AI应用]接收LLM返回消息完成:{}", respText);
|
||||||
|
emitter.send(SseEmitter.event().data(eventData));
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
closeSSE(emitter, eventData);
|
||||||
|
} else {
|
||||||
|
// 异常结束
|
||||||
|
log.error("调用模型异常:" + respText);
|
||||||
|
if (respText.contains("insufficient Balance")) {
|
||||||
|
respText = "大预言模型账号余额不足!";
|
||||||
|
}
|
||||||
|
EventData eventData = new EventData(requestId, null, EventData.EVENT_FLOW_ERROR);
|
||||||
|
eventData.setData(EventFlowData.builder().success(false).message(respText).build());
|
||||||
|
closeSSE(emitter, eventData);
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
.onError((Throwable error) -> {
|
||||||
|
// sse
|
||||||
|
String errMsg = "调用大模型接口失败:" + error.getMessage();
|
||||||
|
log.error(errMsg, error);
|
||||||
EventData eventData = new EventData(requestId, null, EventData.EVENT_FLOW_ERROR);
|
EventData eventData = new EventData(requestId, null, EventData.EVENT_FLOW_ERROR);
|
||||||
eventData.setData(EventFlowData.builder().success(false).message(respText).build());
|
eventData.setData(EventFlowData.builder().success(false).message(errMsg).build());
|
||||||
closeSSE(emitter, eventData);
|
closeSSE(emitter, eventData);
|
||||||
}
|
})
|
||||||
})
|
.start();
|
||||||
.onError((Throwable error) -> {
|
return emitter;
|
||||||
// sse
|
}
|
||||||
String errMsg = "调用大模型接口失败:" + error.getMessage();
|
|
||||||
log.error(errMsg, error);
|
|
||||||
EventData eventData = new EventData(requestId, null, EventData.EVENT_FLOW_ERROR);
|
|
||||||
eventData.setData(EventFlowData.builder().success(false).message(errMsg).build());
|
|
||||||
closeSSE(emitter, eventData);
|
|
||||||
})
|
|
||||||
.start();
|
|
||||||
return emitter;
|
|
||||||
}
|
}
|
||||||
//update-end---author:wangshuai---date:2026-01-05---for:【QQYUN-14479】增加一个开启记忆的按钮。下面为提示词和记忆,将记忆提示词单独拆分---
|
|
||||||
|
|
||||||
private static void closeSSE(SseEmitter emitter, EventData eventData) {
|
private static void closeSSE(SseEmitter emitter, EventData eventData) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@ -1,36 +1,24 @@
|
|||||||
package org.jeecg.modules.airag.app.service.impl;
|
package org.jeecg.modules.airag.app.service.impl;
|
||||||
|
|
||||||
import cn.hutool.core.util.StrUtil;
|
|
||||||
import com.alibaba.fastjson.JSONArray;
|
import com.alibaba.fastjson.JSONArray;
|
||||||
import com.alibaba.fastjson.JSONObject;
|
import com.alibaba.fastjson.JSONObject;
|
||||||
import dev.langchain4j.agent.tool.ToolExecutionRequest;
|
import dev.langchain4j.agent.tool.ToolExecutionRequest;
|
||||||
import dev.langchain4j.agent.tool.ToolSpecification;
|
|
||||||
import dev.langchain4j.data.document.Document;
|
|
||||||
import dev.langchain4j.data.image.Image;
|
import dev.langchain4j.data.image.Image;
|
||||||
import dev.langchain4j.data.message.*;
|
import dev.langchain4j.data.message.*;
|
||||||
import dev.langchain4j.model.output.FinishReason;
|
import dev.langchain4j.model.output.FinishReason;
|
||||||
import dev.langchain4j.service.TokenStream;
|
import dev.langchain4j.service.TokenStream;
|
||||||
import dev.langchain4j.service.tool.ToolExecutor;
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.commons.io.FilenameUtils;
|
|
||||||
import org.apache.tika.parser.AutoDetectParser;
|
|
||||||
import org.jeecg.common.api.vo.Result;
|
import org.jeecg.common.api.vo.Result;
|
||||||
import org.jeecg.common.constant.SymbolConstant;
|
|
||||||
import org.jeecg.common.exception.JeecgBootBizTipException;
|
import org.jeecg.common.exception.JeecgBootBizTipException;
|
||||||
import org.jeecg.common.exception.JeecgBootException;
|
import org.jeecg.common.exception.JeecgBootException;
|
||||||
import org.jeecg.common.system.api.ISysBaseAPI;
|
import org.jeecg.common.system.api.ISysBaseAPI;
|
||||||
import org.jeecg.common.system.util.JwtUtil;
|
import org.jeecg.common.system.util.JwtUtil;
|
||||||
import org.jeecg.common.util.*;
|
import org.jeecg.common.util.*;
|
||||||
import org.jeecg.config.JeecgBaseConfig;
|
|
||||||
import org.jeecg.config.vo.Path;
|
|
||||||
import org.jeecg.modules.airag.app.consts.AiAppConsts;
|
import org.jeecg.modules.airag.app.consts.AiAppConsts;
|
||||||
import org.jeecg.modules.airag.app.consts.Prompts;
|
|
||||||
import org.jeecg.modules.airag.app.entity.AiragApp;
|
import org.jeecg.modules.airag.app.entity.AiragApp;
|
||||||
import org.jeecg.modules.airag.app.mapper.AiragAppMapper;
|
import org.jeecg.modules.airag.app.mapper.AiragAppMapper;
|
||||||
import org.jeecg.modules.airag.app.service.IAiragChatService;
|
import org.jeecg.modules.airag.app.service.IAiragChatService;
|
||||||
import org.jeecg.modules.airag.app.service.IAiragVariableService;
|
|
||||||
import org.jeecg.modules.airag.app.vo.AiWriteGenerateVo;
|
|
||||||
import org.jeecg.modules.airag.app.vo.AppDebugParams;
|
import org.jeecg.modules.airag.app.vo.AppDebugParams;
|
||||||
import org.jeecg.modules.airag.app.vo.ChatConversation;
|
import org.jeecg.modules.airag.app.vo.ChatConversation;
|
||||||
import org.jeecg.modules.airag.app.vo.ChatSendParams;
|
import org.jeecg.modules.airag.app.vo.ChatSendParams;
|
||||||
@ -47,26 +35,18 @@ import org.jeecg.modules.airag.flow.consts.FlowConsts;
|
|||||||
import org.jeecg.modules.airag.flow.entity.AiragFlow;
|
import org.jeecg.modules.airag.flow.entity.AiragFlow;
|
||||||
import org.jeecg.modules.airag.flow.service.IAiragFlowService;
|
import org.jeecg.modules.airag.flow.service.IAiragFlowService;
|
||||||
import org.jeecg.modules.airag.flow.vo.api.FlowRunParams;
|
import org.jeecg.modules.airag.flow.vo.api.FlowRunParams;
|
||||||
import org.jeecg.modules.airag.llm.consts.LLMConsts;
|
|
||||||
import org.jeecg.modules.airag.llm.document.TikaDocumentParser;
|
|
||||||
import org.jeecg.modules.airag.llm.entity.AiragModel;
|
import org.jeecg.modules.airag.llm.entity.AiragModel;
|
||||||
import org.jeecg.modules.airag.llm.handler.AIChatHandler;
|
import org.jeecg.modules.airag.llm.handler.AIChatHandler;
|
||||||
import org.jeecg.modules.airag.llm.handler.JeecgToolsProvider;
|
import org.jeecg.modules.airag.llm.handler.JeecgToolsProvider;
|
||||||
import org.jeecg.modules.airag.llm.mapper.AiragModelMapper;
|
import org.jeecg.modules.airag.llm.mapper.AiragModelMapper;
|
||||||
import org.jeecg.modules.airag.llm.service.IAiragFlowPluginService;
|
|
||||||
import org.jeecg.modules.airag.llm.service.IAiragKnowledgeService;
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.data.redis.core.BoundValueOperations;
|
import org.springframework.data.redis.core.BoundValueOperations;
|
||||||
import org.springframework.data.redis.core.RedisTemplate;
|
import org.springframework.data.redis.core.RedisTemplate;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.util.CollectionUtils;
|
|
||||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.concurrent.*;
|
import java.util.concurrent.*;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
@ -106,18 +86,6 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
|||||||
@Autowired
|
@Autowired
|
||||||
AiragModelMapper airagModelMapper;
|
AiragModelMapper airagModelMapper;
|
||||||
|
|
||||||
@Autowired
|
|
||||||
IAiragFlowPluginService airagFlowPluginService;
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
IAiragKnowledgeService airagKnowledgeService;
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
IAiragVariableService airagVariableService;
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
JeecgBaseConfig jeecgBaseConfig;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 重新接收消息
|
* 重新接收消息
|
||||||
*/
|
*/
|
||||||
@ -137,13 +105,10 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
|||||||
if (oConvertUtils.isNotEmpty(chatSendParams.getAppId())) {
|
if (oConvertUtils.isNotEmpty(chatSendParams.getAppId())) {
|
||||||
app = airagAppMapper.getByIdIgnoreTenant(chatSendParams.getAppId());
|
app = airagAppMapper.getByIdIgnoreTenant(chatSendParams.getAppId());
|
||||||
}
|
}
|
||||||
//update-begin---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
ChatConversation chatConversation = getOrCreateChatConversation(app, conversationId);
|
||||||
ChatConversation chatConversation = getOrCreateChatConversation(app, conversationId, chatSendParams.getSessionType());
|
|
||||||
//update-end---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
|
||||||
// 更新标题
|
// 更新标题
|
||||||
if (oConvertUtils.isEmpty(chatConversation.getTitle())) {
|
if (oConvertUtils.isEmpty(chatConversation.getTitle())) {
|
||||||
int maxLength = AiAppConsts.CONVERSATION_MAX_TITLE_LENGTH;
|
chatConversation.setTitle(userMessage.length() > 5 ? userMessage.substring(0, 5) : userMessage);
|
||||||
chatConversation.setTitle(userMessage.length() > maxLength ? userMessage.substring(0, maxLength) : userMessage);
|
|
||||||
}
|
}
|
||||||
//update-begin---author:chenrui ---date:20251106 for:[issues/8545]新建AI应用的时候只能选择没有自定义参数的AI流程------------
|
//update-begin---author:chenrui ---date:20251106 for:[issues/8545]新建AI应用的时候只能选择没有自定义参数的AI流程------------
|
||||||
// 保存工作流入参配置(如果有)
|
// 保存工作流入参配置(如果有)
|
||||||
@ -151,12 +116,6 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
|||||||
chatConversation.setFlowInputs(chatSendParams.getFlowInputs());
|
chatConversation.setFlowInputs(chatSendParams.getFlowInputs());
|
||||||
}
|
}
|
||||||
//update-end---author:chenrui ---date:20251106 for:[issues/8545]新建AI应用的时候只能选择没有自定义参数的AI流程------------
|
//update-end---author:chenrui ---date:20251106 for:[issues/8545]新建AI应用的时候只能选择没有自定义参数的AI流程------------
|
||||||
//是否保存会话
|
|
||||||
if(null != chatSendParams.getIzSaveSession()){
|
|
||||||
chatConversation.setIzSaveSession(chatSendParams.getIzSaveSession());
|
|
||||||
}
|
|
||||||
// 保存变量
|
|
||||||
saveVariables(app);
|
|
||||||
// 发送消息
|
// 发送消息
|
||||||
return doChat(chatConversation, topicId, chatSendParams);
|
return doChat(chatConversation, topicId, chatSendParams);
|
||||||
}
|
}
|
||||||
@ -171,9 +130,7 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
|||||||
String topicId = oConvertUtils.getString(appDebugParams.getTopicId(), UUIDGenerator.generate());
|
String topicId = oConvertUtils.getString(appDebugParams.getTopicId(), UUIDGenerator.generate());
|
||||||
AiragApp app = appDebugParams.getApp();
|
AiragApp app = appDebugParams.getApp();
|
||||||
app.setId("__DEBUG_APP");
|
app.setId("__DEBUG_APP");
|
||||||
//update-begin---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
ChatConversation chatConversation = getOrCreateChatConversation(app, topicId);
|
||||||
ChatConversation chatConversation = getOrCreateChatConversation(app, topicId, "");
|
|
||||||
//update-end---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
|
||||||
//update-begin---author:chenrui ---date:20251106 for:[issues/8545]新建AI应用的时候只能选择没有自定义参数的AI流程------------
|
//update-begin---author:chenrui ---date:20251106 for:[issues/8545]新建AI应用的时候只能选择没有自定义参数的AI流程------------
|
||||||
// 保存工作流入参配置(如果有)
|
// 保存工作流入参配置(如果有)
|
||||||
if (oConvertUtils.isObjectNotEmpty(appDebugParams.getFlowInputs())) {
|
if (oConvertUtils.isObjectNotEmpty(appDebugParams.getFlowInputs())) {
|
||||||
@ -183,9 +140,7 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
|||||||
// 发送消息
|
// 发送消息
|
||||||
SseEmitter emitter = doChat(chatConversation, topicId, appDebugParams);
|
SseEmitter emitter = doChat(chatConversation, topicId, appDebugParams);
|
||||||
//保存会话
|
//保存会话
|
||||||
//update-begin---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
saveChatConversation(chatConversation, true, null);
|
||||||
saveChatConversation(chatConversation, true, null, "");
|
|
||||||
//update-end---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
|
||||||
return emitter;
|
return emitter;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -292,11 +247,9 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Result<?> getMessages(String conversationId, String sessionType) {
|
public Result<?> getMessages(String conversationId) {
|
||||||
AssertUtils.assertNotEmpty("请先选择会话", conversationId);
|
AssertUtils.assertNotEmpty("请先选择会话", conversationId);
|
||||||
//update-begin---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
String key = getConversationCacheKey(conversationId, null);
|
||||||
String key = getConversationCacheKey(conversationId, null, sessionType);
|
|
||||||
//update-end---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
|
||||||
if (oConvertUtils.isEmpty(key)) {
|
if (oConvertUtils.isEmpty(key)) {
|
||||||
return Result.ok(Collections.emptyList());
|
return Result.ok(Collections.emptyList());
|
||||||
}
|
}
|
||||||
@ -320,7 +273,6 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
|||||||
.role(msg.getRole())
|
.role(msg.getRole())
|
||||||
.content(msg.getContent())
|
.content(msg.getContent())
|
||||||
.images(msg.getImages())
|
.images(msg.getImages())
|
||||||
.files(msg.getFiles())
|
|
||||||
.datetime(msg.getDatetime())
|
.datetime(msg.getDatetime())
|
||||||
.build();
|
.build();
|
||||||
// 不设置toolExecutionRequests和toolExecutionResult
|
// 不设置toolExecutionRequests和toolExecutionResult
|
||||||
@ -330,30 +282,21 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
|||||||
}
|
}
|
||||||
result.put("messages", messages);
|
result.put("messages", messages);
|
||||||
result.put("flowInputs", chatConversation.getFlowInputs());
|
result.put("flowInputs", chatConversation.getFlowInputs());
|
||||||
//update-begin---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
|
||||||
if(oConvertUtils.isNotEmpty(sessionType)){
|
|
||||||
result.put("appData", chatConversation.getApp());
|
|
||||||
}
|
|
||||||
//update-end---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
|
||||||
return Result.ok(result);
|
return Result.ok(result);
|
||||||
//update-end---author:chenrui ---date:20251106 for:[issues/8545]新建AI应用的时候只能选择没有自定义参数的AI流程------------
|
//update-end---author:chenrui ---date:20251106 for:[issues/8545]新建AI应用的时候只能选择没有自定义参数的AI流程------------
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Result<?> clearMessage(String conversationId, String sessionType) {
|
public Result<?> clearMessage(String conversationId) {
|
||||||
AssertUtils.assertNotEmpty("请先选择会话", conversationId);
|
AssertUtils.assertNotEmpty("请先选择会话", conversationId);
|
||||||
//update-begin---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
String key = getConversationCacheKey(conversationId, null);
|
||||||
String key = getConversationCacheKey(conversationId, null,sessionType);
|
|
||||||
//update-end---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
|
||||||
if (oConvertUtils.isEmpty(key)) {
|
if (oConvertUtils.isEmpty(key)) {
|
||||||
return Result.ok(Collections.emptyList());
|
return Result.ok(Collections.emptyList());
|
||||||
}
|
}
|
||||||
ChatConversation chatConversation = (ChatConversation) redisTemplate.boundValueOps(key).get();
|
ChatConversation chatConversation = (ChatConversation) redisTemplate.boundValueOps(key).get();
|
||||||
if (null != chatConversation && oConvertUtils.isObjectNotEmpty(chatConversation.getMessages())) {
|
if (null != chatConversation && oConvertUtils.isObjectNotEmpty(chatConversation.getMessages())) {
|
||||||
chatConversation.getMessages().clear();
|
chatConversation.getMessages().clear();
|
||||||
//update-begin---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
saveChatConversation(chatConversation);
|
||||||
saveChatConversation(chatConversation,sessionType);
|
|
||||||
//update-end---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
|
||||||
}
|
}
|
||||||
return Result.ok();
|
return Result.ok();
|
||||||
}
|
}
|
||||||
@ -500,11 +443,9 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Result<?> deleteConversation(String conversationId, String sessionType) {
|
public Result<?> deleteConversation(String conversationId) {
|
||||||
AssertUtils.assertNotEmpty("请选择要删除的会话", conversationId);
|
AssertUtils.assertNotEmpty("请选择要删除的会话", conversationId);
|
||||||
//update-begin---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
String key = getConversationCacheKey(conversationId, null);
|
||||||
String key = getConversationCacheKey(conversationId, null, sessionType);
|
|
||||||
//update-end---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
|
||||||
if (oConvertUtils.isNotEmpty(key)) {
|
if (oConvertUtils.isNotEmpty(key)) {
|
||||||
Boolean delete = redisTemplate.delete(key);
|
Boolean delete = redisTemplate.delete(key);
|
||||||
if (delete) {
|
if (delete) {
|
||||||
@ -522,18 +463,14 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
|||||||
AssertUtils.assertNotEmpty("请先选择会话", updateTitleParams);
|
AssertUtils.assertNotEmpty("请先选择会话", updateTitleParams);
|
||||||
AssertUtils.assertNotEmpty("请先选择会话", updateTitleParams.getId());
|
AssertUtils.assertNotEmpty("请先选择会话", updateTitleParams.getId());
|
||||||
AssertUtils.assertNotEmpty("请输入会话标题", updateTitleParams.getTitle());
|
AssertUtils.assertNotEmpty("请输入会话标题", updateTitleParams.getTitle());
|
||||||
String key = getConversationCacheKey(updateTitleParams.getId(), null, updateTitleParams.getSessionType());
|
String key = getConversationCacheKey(updateTitleParams.getId(), null);
|
||||||
if (oConvertUtils.isEmpty(key)) {
|
if (oConvertUtils.isEmpty(key)) {
|
||||||
log.warn("[ai-chat]删除会话:未找到会话:{}", updateTitleParams.getId());
|
log.warn("[ai-chat]删除会话:未找到会话:{}", updateTitleParams.getId());
|
||||||
return Result.ok();
|
return Result.ok();
|
||||||
}
|
}
|
||||||
ChatConversation chatConversation = (ChatConversation) redisTemplate.boundValueOps(key).get();
|
ChatConversation chatConversation = (ChatConversation) redisTemplate.boundValueOps(key).get();
|
||||||
//update-begin---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
chatConversation.setTitle(updateTitleParams.getTitle());
|
||||||
if (chatConversation != null) {
|
saveChatConversation(chatConversation);
|
||||||
chatConversation.setTitle(updateTitleParams.getTitle());
|
|
||||||
}
|
|
||||||
saveChatConversation(chatConversation,updateTitleParams.getSessionType());
|
|
||||||
//update-end---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
|
||||||
return Result.ok();
|
return Result.ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -542,21 +479,15 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
|||||||
*
|
*
|
||||||
* @param conversationId
|
* @param conversationId
|
||||||
* @param httpRequest
|
* @param httpRequest
|
||||||
* @param sessionType 会话类型
|
|
||||||
* @return
|
* @return
|
||||||
* @author chenrui
|
* @author chenrui
|
||||||
* @date 2025/2/25 19:27
|
* @date 2025/2/25 19:27
|
||||||
*/
|
*/
|
||||||
private String getConversationCacheKey(String conversationId, HttpServletRequest httpRequest, String sessionType) {
|
private String getConversationCacheKey(String conversationId, HttpServletRequest httpRequest) {
|
||||||
if (oConvertUtils.isEmpty(conversationId)) {
|
if (oConvertUtils.isEmpty(conversationId)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
String key = getConversationDirCacheKey(httpRequest);
|
String key = getConversationDirCacheKey(httpRequest);
|
||||||
//update-begin---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
|
||||||
if(oConvertUtils.isNotEmpty(sessionType)){
|
|
||||||
key = key + ":" + sessionType;
|
|
||||||
}
|
|
||||||
//update-end---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
|
||||||
key = key + ":" + conversationId;
|
key = key + ":" + conversationId;
|
||||||
return key;
|
return key;
|
||||||
}
|
}
|
||||||
@ -591,21 +522,18 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
|||||||
*
|
*
|
||||||
* @param app
|
* @param app
|
||||||
* @param conversationId
|
* @param conversationId
|
||||||
* @param sessionType
|
|
||||||
* @return
|
* @return
|
||||||
* @author chenrui
|
* @author chenrui
|
||||||
* @date 2025/2/25 19:19
|
* @date 2025/2/25 19:19
|
||||||
*/
|
*/
|
||||||
@NotNull
|
@NotNull
|
||||||
private ChatConversation getOrCreateChatConversation(AiragApp app, String conversationId, String sessionType) {
|
private ChatConversation getOrCreateChatConversation(AiragApp app, String conversationId) {
|
||||||
if (oConvertUtils.isObjectEmpty(app)) {
|
if (oConvertUtils.isObjectEmpty(app)) {
|
||||||
app = new AiragApp();
|
app = new AiragApp();
|
||||||
app.setId(AiAppConsts.DEFAULT_APP_ID);
|
app.setId(AiAppConsts.DEFAULT_APP_ID);
|
||||||
}
|
}
|
||||||
ChatConversation chatConversation = null;
|
ChatConversation chatConversation = null;
|
||||||
//update-begin---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
String key = getConversationCacheKey(conversationId, null);
|
||||||
String key = getConversationCacheKey(conversationId, null,sessionType);
|
|
||||||
//update-end---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
|
||||||
if (oConvertUtils.isNotEmpty(key)) {
|
if (oConvertUtils.isNotEmpty(key)) {
|
||||||
chatConversation = (ChatConversation) redisTemplate.boundValueOps(key).get();
|
chatConversation = (ChatConversation) redisTemplate.boundValueOps(key).get();
|
||||||
}
|
}
|
||||||
@ -641,8 +569,8 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
|||||||
* @author chenrui
|
* @author chenrui
|
||||||
* @date 2025/2/25 19:27
|
* @date 2025/2/25 19:27
|
||||||
*/
|
*/
|
||||||
private void saveChatConversation(ChatConversation chatConversation, String sessionType) {
|
private void saveChatConversation(ChatConversation chatConversation) {
|
||||||
saveChatConversation(chatConversation, false, null, sessionType);
|
saveChatConversation(chatConversation, false, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -653,19 +581,11 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
|||||||
* @author chenrui
|
* @author chenrui
|
||||||
* @date 2025/2/25 19:27
|
* @date 2025/2/25 19:27
|
||||||
*/
|
*/
|
||||||
private void saveChatConversation(ChatConversation chatConversation, boolean temp, HttpServletRequest httpRequest, String sessionType) {
|
private void saveChatConversation(ChatConversation chatConversation, boolean temp, HttpServletRequest httpRequest) {
|
||||||
if (null == chatConversation) {
|
if (null == chatConversation) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
String key = getConversationCacheKey(chatConversation.getId(), httpRequest);
|
||||||
//如果是不保存会话直接返回
|
|
||||||
if(null != chatConversation.getIzSaveSession() && !chatConversation.getIzSaveSession()){
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
//update-begin---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
|
||||||
String key = getConversationCacheKey(chatConversation.getId(), httpRequest, sessionType);
|
|
||||||
//update-end---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
|
||||||
if (oConvertUtils.isEmpty(key)) {
|
if (oConvertUtils.isEmpty(key)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -760,10 +680,6 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
|||||||
* @date 2025/2/25 19:05
|
* @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) {
|
||||||
appendMessage(messages, message, chatConversation, topicId, null, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void appendMessage(List<ChatMessage> messages, ChatMessage message, ChatConversation chatConversation, String topicId, List<String> files, String saveContent) {
|
|
||||||
|
|
||||||
if (message.type().equals(ChatMessageType.SYSTEM)) {
|
if (message.type().equals(ChatMessageType.SYSTEM)) {
|
||||||
// 系统消息,放到消息列表最前面,并且不记录历史
|
// 系统消息,放到消息列表最前面,并且不记录历史
|
||||||
@ -793,22 +709,8 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
|||||||
textContent.append(((TextContent) content).text()).append("\n");
|
textContent.append(((TextContent) content).text()).append("\n");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
//update-begin---author:wangshuai---date:2026-01-12---for:【QQYUN-14261】【AI】AI助手,支持多模态能力- 文档---
|
historyMessage.setContent(textContent.toString());
|
||||||
if (oConvertUtils.isNotEmpty(saveContent)) {
|
|
||||||
historyMessage.setContent(saveContent);
|
|
||||||
} else {
|
|
||||||
historyMessage.setContent(textContent.toString());
|
|
||||||
}
|
|
||||||
historyMessage.setImages(images);
|
historyMessage.setImages(images);
|
||||||
// 保存文件信息
|
|
||||||
if (oConvertUtils.isNotEmpty(files)) {
|
|
||||||
List<MessageHistory.FileHistory> fileHistories = new ArrayList<>();
|
|
||||||
for (String file : files) {
|
|
||||||
fileHistories.add(new MessageHistory.FileHistory(file));
|
|
||||||
}
|
|
||||||
historyMessage.setFiles(fileHistories);
|
|
||||||
}
|
|
||||||
//update-end---author:wangshuai---date:2026-01-12---for:【QQYUN-14261】【AI】AI助手,支持多模态能力- 文档---
|
|
||||||
} else if (message.type().equals(ChatMessageType.AI)) {
|
} else if (message.type().equals(ChatMessageType.AI)) {
|
||||||
historyMessage.setRole(AiragConsts.MESSAGE_ROLE_AI);
|
historyMessage.setRole(AiragConsts.MESSAGE_ROLE_AI);
|
||||||
AiMessage aiMessage = (AiMessage) message;
|
AiMessage aiMessage = (AiMessage) message;
|
||||||
@ -864,20 +766,9 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
|||||||
AiragLocalCache.put(AiragConsts.CACHE_TYPE_SSE_HISTORY_MSG, requestId, new CopyOnWriteArrayList<>());
|
AiragLocalCache.put(AiragConsts.CACHE_TYPE_SSE_HISTORY_MSG, requestId, new CopyOnWriteArrayList<>());
|
||||||
try {
|
try {
|
||||||
// 组装用户消息
|
// 组装用户消息
|
||||||
String content = sendParams.getContent();
|
UserMessage userMessage = aiChatHandler.buildUserMessage(sendParams.getContent(), sendParams.getImages());
|
||||||
//将文件内容给提示词
|
|
||||||
if(!CollectionUtils.isEmpty(sendParams.getFiles())){
|
|
||||||
content = buildContentWithFiles(content, sendParams.getFiles());
|
|
||||||
}
|
|
||||||
UserMessage userMessage = aiChatHandler.buildUserMessage(content, sendParams.getImages());
|
|
||||||
// 追加消息
|
// 追加消息
|
||||||
//update-begin---author:wangshuai---date:2026-01-09---for:【QQYUN-14261】【AI】AI助手,支持多模态能力- 文档---
|
appendMessage(messages, userMessage, chatConversation, topicId);
|
||||||
appendMessage(messages, userMessage, chatConversation, topicId, sendParams.getFiles(), sendParams.getContent());
|
|
||||||
//update-end---author:wangshuai---date:2026-01-09---for:【QQYUN-14261】【AI】AI助手,支持多模态能力- 文档---
|
|
||||||
// 绘画AI逻辑:当开启生成绘画时调用
|
|
||||||
if (oConvertUtils.isObjectNotEmpty(sendParams.getEnableDraw()) && sendParams.getEnableDraw()) {
|
|
||||||
return genImageChat(emitter,sendParams,requestId,messages,chatConversation,topicId);
|
|
||||||
}
|
|
||||||
/* 这里应该是有几种情况:
|
/* 这里应该是有几种情况:
|
||||||
* 1. 非ai应用:获取默认模型->开始聊天
|
* 1. 非ai应用:获取默认模型->开始聊天
|
||||||
* 2. AI应用-聊天助手(ChatAssistant):从应用信息组装模型和提示词->开始聊天
|
* 2. AI应用-聊天助手(ChatAssistant):从应用信息组装模型和提示词->开始聊天
|
||||||
@ -890,7 +781,7 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
|||||||
sendWithFlow(requestId, aiApp.getFlowId(), chatConversation, topicId, messages, sendParams);
|
sendWithFlow(requestId, aiApp.getFlowId(), chatConversation, topicId, messages, sendParams);
|
||||||
} else {
|
} else {
|
||||||
// AI应用-聊天助手(ChatAssistant):从应用信息组装模型和提示词
|
// AI应用-聊天助手(ChatAssistant):从应用信息组装模型和提示词
|
||||||
sendWithAppChat(requestId, messages, chatConversation, topicId, sendParams, aiApp.getFlowId(), aiApp.getMemoryId());
|
sendWithAppChat(requestId, messages, chatConversation, topicId, sendParams);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 发消息
|
// 发消息
|
||||||
@ -898,13 +789,7 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
|||||||
if (oConvertUtils.isObjectNotEmpty(sendParams.getEnableSearch())) {
|
if (oConvertUtils.isObjectNotEmpty(sendParams.getEnableSearch())) {
|
||||||
aiChatParams.setEnableSearch(sendParams.getEnableSearch());
|
aiChatParams.setEnableSearch(sendParams.getEnableSearch());
|
||||||
}
|
}
|
||||||
// 设置深度思考搜索参数
|
sendWithDefault(requestId, chatConversation, topicId, null, messages, aiChatParams);
|
||||||
if (oConvertUtils.isObjectNotEmpty(sendParams.getEnableThink())) {
|
|
||||||
aiChatParams.setReturnThinking(sendParams.getEnableThink());
|
|
||||||
}
|
|
||||||
//update-begin---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
|
||||||
sendWithDefault(requestId, chatConversation, topicId, null, messages, aiChatParams, sendParams.getSessionType());
|
|
||||||
//update-end---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
|
||||||
}
|
}
|
||||||
// 发送就绪消息
|
// 发送就绪消息
|
||||||
EventData eventRequestId = new EventData(requestId, null, EventData.EVENT_INIT_REQUEST_ID, chatConversation.getId(), topicId);
|
EventData eventRequestId = new EventData(requestId, null, EventData.EVENT_INIT_REQUEST_ID, chatConversation.getId(), topicId);
|
||||||
@ -919,59 +804,6 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
|||||||
return emitter;
|
return emitter;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 生成图片
|
|
||||||
*
|
|
||||||
* @param emitter
|
|
||||||
* @param sendParams
|
|
||||||
* @param requestId
|
|
||||||
* @param messages
|
|
||||||
* @param chatConversation
|
|
||||||
* @param topicId
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
private SseEmitter genImageChat(SseEmitter emitter, ChatSendParams sendParams, String requestId, List<ChatMessage> messages, ChatConversation chatConversation, String topicId) {
|
|
||||||
AssertUtils.assertNotEmpty("请选择绘画模型", sendParams.getDrawModelId());
|
|
||||||
AIChatParams aiChatParams = new AIChatParams();
|
|
||||||
try {
|
|
||||||
List<String> images = sendParams.getImages();
|
|
||||||
List<Map<String, Object>> imageList = new ArrayList<>();
|
|
||||||
if(CollectionUtils.isEmpty(images)) {
|
|
||||||
//生成图片
|
|
||||||
imageList = aiChatHandler.imageGenerate(sendParams.getDrawModelId(), sendParams.getContent(), aiChatParams);
|
|
||||||
} else {
|
|
||||||
//图生图
|
|
||||||
imageList = aiChatHandler.imageEdit(sendParams.getDrawModelId(), sendParams.getContent(), images, aiChatParams);
|
|
||||||
}
|
|
||||||
// 记录历史消息
|
|
||||||
String imageMarkdown = imageList.stream().map(map -> {
|
|
||||||
String newUrl = this.uploadImage(map);
|
|
||||||
return "";
|
|
||||||
}).collect(Collectors.joining("\n"));
|
|
||||||
AiMessage aiMessage = new AiMessage(imageMarkdown);
|
|
||||||
appendMessage(messages, aiMessage, chatConversation, topicId);
|
|
||||||
// 处理绘画结果并通过SSE返回给客户端
|
|
||||||
EventData eventData = new EventData(requestId, null, EventData.EVENT_MESSAGE, chatConversation.getId(), topicId);
|
|
||||||
EventMessageData messageEventData = EventMessageData.builder().message(imageMarkdown).build();
|
|
||||||
eventData.setData(messageEventData);
|
|
||||||
eventData.setRequestId(requestId);
|
|
||||||
sendMessage2Client(emitter, eventData);
|
|
||||||
// 保存会话
|
|
||||||
saveChatConversation(chatConversation, false, SpringContextUtils.getHttpServletRequest(), sendParams.getSessionType());
|
|
||||||
eventData = new EventData(requestId, null, EventData.EVENT_MESSAGE_END, chatConversation.getId(), topicId);
|
|
||||||
eventData.setRequestId(requestId);
|
|
||||||
sendMessage2Client(emitter, eventData);
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("绘画AI调用异常", e);
|
|
||||||
EventData errorEventData = new EventData(requestId, null, EventData.EVENT_FLOW_ERROR, chatConversation.getId(), topicId);
|
|
||||||
EventMessageData messageEventData = EventMessageData.builder().message("绘画AI调用失败:" + e.getMessage()).build();
|
|
||||||
errorEventData.setData(messageEventData);
|
|
||||||
errorEventData.setRequestId(requestId);
|
|
||||||
closeSSE(emitter, errorEventData);
|
|
||||||
}
|
|
||||||
return emitter;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 运行流程
|
* 运行流程
|
||||||
*
|
*
|
||||||
@ -1043,9 +875,7 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
|||||||
sendMessage2Client(emitter, msgEventData);
|
sendMessage2Client(emitter, msgEventData);
|
||||||
appendMessage(messages, aiMessage, chatConversation, topicId);
|
appendMessage(messages, aiMessage, chatConversation, topicId);
|
||||||
// 保存会话
|
// 保存会话
|
||||||
//update-begin---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
saveChatConversation(chatConversation, false, httpRequest);
|
||||||
saveChatConversation(chatConversation, false, httpRequest, sendParams.getSessionType());
|
|
||||||
//update-end---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
|
||||||
}
|
}
|
||||||
}else{
|
}else{
|
||||||
//update-begin---author:chenrui ---date:20250425 for:[QQYUN-12203]AI 聊天,超时或者服务器报错,给个友好提示------------
|
//update-begin---author:chenrui ---date:20250425 for:[QQYUN-12203]AI 聊天,超时或者服务器报错,给个友好提示------------
|
||||||
@ -1078,31 +908,16 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
|||||||
* @param chatConversation
|
* @param chatConversation
|
||||||
* @param topicId
|
* @param topicId
|
||||||
* @param sendParams
|
* @param sendParams
|
||||||
* @param flowId
|
|
||||||
* @param memoryId
|
|
||||||
* @return
|
* @return
|
||||||
* @author chenrui
|
* @author chenrui
|
||||||
* @date 2025/2/28 10:41
|
* @date 2025/2/28 10:41
|
||||||
*/
|
*/
|
||||||
private void sendWithAppChat(String requestId, List<ChatMessage> messages, ChatConversation chatConversation, String topicId, ChatSendParams sendParams, String flowId, String memoryId) {
|
private void sendWithAppChat(String requestId, List<ChatMessage> messages, ChatConversation chatConversation, String topicId, ChatSendParams sendParams) {
|
||||||
AiragApp aiApp = chatConversation.getApp();
|
AiragApp aiApp = chatConversation.getApp();
|
||||||
String modelId = aiApp.getModelId();
|
String modelId = aiApp.getModelId();
|
||||||
AssertUtils.assertNotEmpty("请先选择模型", modelId);
|
AssertUtils.assertNotEmpty("请先选择模型", modelId);
|
||||||
// AI应用提示词
|
// AI应用提示词
|
||||||
String prompt = aiApp.getPrompt();
|
String prompt = aiApp.getPrompt();
|
||||||
|
|
||||||
String username = "jeecg";
|
|
||||||
try {
|
|
||||||
HttpServletRequest req = SpringContextUtils.getHttpServletRequest();
|
|
||||||
username = JwtUtil.getUserNameByToken(req);
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error(e.getMessage());
|
|
||||||
}
|
|
||||||
//将变量中的题试题替换并追加
|
|
||||||
if(oConvertUtils.isObjectNotEmpty(aiApp.getVariables())) {
|
|
||||||
prompt = airagVariableService.additionalPrompt(username, aiApp);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (oConvertUtils.isNotEmpty(prompt)) {
|
if (oConvertUtils.isNotEmpty(prompt)) {
|
||||||
appendMessage(messages, new SystemMessage(prompt), chatConversation, topicId);
|
appendMessage(messages, new SystemMessage(prompt), chatConversation, topicId);
|
||||||
}
|
}
|
||||||
@ -1128,9 +943,6 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
|||||||
if (metadata.containsKey("maxTokens")) {
|
if (metadata.containsKey("maxTokens")) {
|
||||||
aiChatParams.setMaxTokens(metadata.getInteger("maxTokens"));
|
aiChatParams.setMaxTokens(metadata.getInteger("maxTokens"));
|
||||||
}
|
}
|
||||||
if (metadata.containsKey(FlowConsts.FLOW_NODE_OPTION_TIME_OUT)) {
|
|
||||||
aiChatParams.setTimeout(oConvertUtils.getInt(metadata.getInteger(FlowConsts.FLOW_NODE_OPTION_TIME_OUT), 300));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1153,69 +965,15 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//流程不为空,构建插件
|
|
||||||
if(oConvertUtils.isNotEmpty(flowId)){
|
|
||||||
Map<String, Object> result = airagFlowPluginService.getFlowsToPlugin(flowId);
|
|
||||||
this.addPluginToParams(aiChatParams, result);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设置网络搜索参数(如果前端传递了)
|
// 设置网络搜索参数(如果前端传递了)
|
||||||
if (sendParams != null && oConvertUtils.isObjectNotEmpty(sendParams.getEnableSearch())) {
|
if (sendParams != null && oConvertUtils.isObjectNotEmpty(sendParams.getEnableSearch())) {
|
||||||
aiChatParams.setEnableSearch(sendParams.getEnableSearch());
|
aiChatParams.setEnableSearch(sendParams.getEnableSearch());
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置深度思考参数(如果前端传递了)
|
|
||||||
if (sendParams != null && oConvertUtils.isObjectNotEmpty(sendParams.getEnableThink())) {
|
|
||||||
aiChatParams.setReturnThinking(sendParams.getEnableThink());
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设置记忆库的插件
|
|
||||||
if(sendParams != null && oConvertUtils.isNotEmpty(memoryId)){
|
|
||||||
//开启记忆
|
|
||||||
if(null == aiApp.getIzOpenMemory() || AiAppConsts.IZ_OPEN_MEMORY.equals(aiApp.getIzOpenMemory())){
|
|
||||||
Map<String, Object> pluginMemory = airagKnowledgeService.getPluginMemory(memoryId);
|
|
||||||
this.addPluginToParams(aiChatParams, pluginMemory);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//设置变量的插件
|
|
||||||
// 添加系统级工具:变量更新
|
|
||||||
if (oConvertUtils.isNotEmpty(aiApp.getId())) {
|
|
||||||
airagVariableService.addUpdateVariableTool(aiApp,username,aiChatParams);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 打印流程耗时日志
|
// 打印流程耗时日志
|
||||||
printChatDuration(requestId, "构造应用自定义参数完成");
|
printChatDuration(requestId, "构造应用自定义参数完成");
|
||||||
// 发消息
|
// 发消息
|
||||||
//update-begin---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
sendWithDefault(requestId, chatConversation, topicId, modelId, messages, aiChatParams);
|
||||||
sendWithDefault(requestId, chatConversation, topicId, modelId, messages, aiChatParams, sendParams.getSessionType());
|
|
||||||
//update-end---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 添加插件到参数中
|
|
||||||
*
|
|
||||||
* @param aiChatParams
|
|
||||||
* @param result
|
|
||||||
*/
|
|
||||||
private void addPluginToParams(AIChatParams aiChatParams, Map<String, Object> result) {
|
|
||||||
if (result == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Map<ToolSpecification, ToolExecutor> flowsToPlugin = (Map<ToolSpecification, ToolExecutor>) result.get("pluginTool");
|
|
||||||
String pluginId = (String) result.get("pluginId");
|
|
||||||
if (aiChatParams.getTools() == null) {
|
|
||||||
aiChatParams.setTools(new HashMap<>());
|
|
||||||
}
|
|
||||||
if (flowsToPlugin != null) {
|
|
||||||
aiChatParams.getTools().putAll(flowsToPlugin);
|
|
||||||
}
|
|
||||||
if (aiChatParams.getPluginIds() == null) {
|
|
||||||
aiChatParams.setPluginIds(new ArrayList<>());
|
|
||||||
}
|
|
||||||
if (oConvertUtils.isNotEmpty(pluginId)) {
|
|
||||||
aiChatParams.getPluginIds().add(pluginId);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1226,12 +984,11 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
|||||||
* @param topicId
|
* @param topicId
|
||||||
* @param modelId
|
* @param modelId
|
||||||
* @param messages
|
* @param messages
|
||||||
* @param sessionType
|
|
||||||
* @return
|
* @return
|
||||||
* @author chenrui
|
* @author chenrui
|
||||||
* @date 2025/2/25 19:24
|
* @date 2025/2/25 19:24
|
||||||
*/
|
*/
|
||||||
private void sendWithDefault(String requestId, ChatConversation chatConversation, String topicId, String modelId, List<ChatMessage> messages, AIChatParams aiChatParams, String sessionType) {
|
private void sendWithDefault(String requestId, ChatConversation chatConversation, String topicId, String modelId, List<ChatMessage> messages, AIChatParams aiChatParams) {
|
||||||
// 调用ai聊天
|
// 调用ai聊天
|
||||||
if (null == aiChatParams) {
|
if (null == aiChatParams) {
|
||||||
aiChatParams = new AIChatParams();
|
aiChatParams = new AIChatParams();
|
||||||
@ -1240,16 +997,11 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
|||||||
if(chatConversation.getApp().getId().equals(AiAppConsts.DEFAULT_APP_ID)){
|
if(chatConversation.getApp().getId().equals(AiAppConsts.DEFAULT_APP_ID)){
|
||||||
aiChatParams.setTools(jeecgToolsProvider.getDefaultTools());
|
aiChatParams.setTools(jeecgToolsProvider.getDefaultTools());
|
||||||
}
|
}
|
||||||
if(CollectionUtils.isEmpty(aiChatParams.getKnowIds())){
|
aiChatParams.setKnowIds(chatConversation.getApp().getKnowIds());
|
||||||
aiChatParams.setKnowIds(chatConversation.getApp().getKnowIds());
|
|
||||||
} else {
|
|
||||||
aiChatParams.getKnowIds().addAll(chatConversation.getApp().getKnowIds());
|
|
||||||
}
|
|
||||||
aiChatParams.setMaxMsgNumber(oConvertUtils.getInt(chatConversation.getApp().getMsgNum(), 5));
|
aiChatParams.setMaxMsgNumber(oConvertUtils.getInt(chatConversation.getApp().getMsgNum(), 5));
|
||||||
aiChatParams.setCurrentHttpRequest(SpringContextUtils.getHttpServletRequest());
|
aiChatParams.setCurrentHttpRequest(SpringContextUtils.getHttpServletRequest());
|
||||||
|
aiChatParams.setReturnThinking(true);
|
||||||
HttpServletRequest httpRequest = SpringContextUtils.getHttpServletRequest();
|
HttpServletRequest httpRequest = SpringContextUtils.getHttpServletRequest();
|
||||||
// for [QQYUN-9234] MCP服务连接关闭 - 保存参数引用用于在回调中关闭MCP连接
|
|
||||||
final AIChatParams finalAiChatParams = aiChatParams;
|
|
||||||
TokenStream chatStream;
|
TokenStream chatStream;
|
||||||
try {
|
try {
|
||||||
// 打印流程耗时日志
|
// 打印流程耗时日志
|
||||||
@ -1261,8 +1013,6 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
|||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error(e.getMessage(), e);
|
log.error(e.getMessage(), e);
|
||||||
// for [QQYUN-9234] MCP服务连接关闭 - 异常时关闭MCP连接
|
|
||||||
finalAiChatParams.closeMcpConnections();
|
|
||||||
// sse
|
// sse
|
||||||
SseEmitter emitter = AiragLocalCache.get(AiragConsts.CACHE_TYPE_SSE, requestId);
|
SseEmitter emitter = AiragLocalCache.get(AiragConsts.CACHE_TYPE_SSE, requestId);
|
||||||
if (null == emitter) {
|
if (null == emitter) {
|
||||||
@ -1348,8 +1098,6 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
|||||||
// 打印流程耗时日志
|
// 打印流程耗时日志
|
||||||
printChatDuration(requestId, "LLM输出消息完成");
|
printChatDuration(requestId, "LLM输出消息完成");
|
||||||
AiragLocalCache.remove(AiragConsts.CACHE_TYPE_SSE_SEND_TIME, requestId);
|
AiragLocalCache.remove(AiragConsts.CACHE_TYPE_SSE_SEND_TIME, requestId);
|
||||||
// for [QQYUN-9234] MCP服务连接关闭 - 聊天完成时关闭MCP连接
|
|
||||||
finalAiChatParams.closeMcpConnections();
|
|
||||||
// 记录ai的回复
|
// 记录ai的回复
|
||||||
AiMessage aiMessage = responseMessage.aiMessage();
|
AiMessage aiMessage = responseMessage.aiMessage();
|
||||||
FinishReason finishReason = responseMessage.finishReason();
|
FinishReason finishReason = responseMessage.finishReason();
|
||||||
@ -1365,9 +1113,7 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
|||||||
EventData eventData = new EventData(requestId, null, EventData.EVENT_MESSAGE_END, chatConversation.getId(), topicId);
|
EventData eventData = new EventData(requestId, null, EventData.EVENT_MESSAGE_END, chatConversation.getId(), topicId);
|
||||||
appendMessage(messages, aiMessage, chatConversation, topicId);
|
appendMessage(messages, aiMessage, chatConversation, topicId);
|
||||||
// 保存会话
|
// 保存会话
|
||||||
//update-begin---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
saveChatConversation(chatConversation, false, httpRequest);
|
||||||
saveChatConversation(chatConversation, false, httpRequest, sessionType);
|
|
||||||
//update-end---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
|
||||||
closeSSE(emitter, eventData);
|
closeSSE(emitter, eventData);
|
||||||
} else if (FinishReason.LENGTH.equals(finishReason)) {
|
} else if (FinishReason.LENGTH.equals(finishReason)) {
|
||||||
// 上下文长度超过限制
|
// 上下文长度超过限制
|
||||||
@ -1391,8 +1137,6 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
|||||||
// 打印流程耗时日志
|
// 打印流程耗时日志
|
||||||
printChatDuration(requestId, "LLM输出消息异常");
|
printChatDuration(requestId, "LLM输出消息异常");
|
||||||
AiragLocalCache.remove(AiragConsts.CACHE_TYPE_SSE_SEND_TIME, requestId);
|
AiragLocalCache.remove(AiragConsts.CACHE_TYPE_SSE_SEND_TIME, requestId);
|
||||||
// for [QQYUN-9234] MCP服务连接关闭 - 聊天异常时关闭MCP连接
|
|
||||||
finalAiChatParams.closeMcpConnections();
|
|
||||||
// sse
|
// sse
|
||||||
SseEmitter emitter = AiragLocalCache.get(AiragConsts.CACHE_TYPE_SSE, requestId);
|
SseEmitter emitter = AiragLocalCache.get(AiragConsts.CACHE_TYPE_SSE, requestId);
|
||||||
if (null == emitter) {
|
if (null == emitter) {
|
||||||
@ -1457,7 +1201,7 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
|||||||
*/
|
*/
|
||||||
private static void sendMessage2Client(SseEmitter emitter, EventData eventData) {
|
private static void sendMessage2Client(SseEmitter emitter, EventData eventData) {
|
||||||
try {
|
try {
|
||||||
log.debug("发送消息:{}", eventData.getRequestId());
|
log.info("发送消息:{}", eventData.getRequestId());
|
||||||
String eventStr = JSONObject.toJSONString(eventData);
|
String eventStr = JSONObject.toJSONString(eventData);
|
||||||
log.debug("[AI应用]接收LLM返回消息:{}", eventStr);
|
log.debug("[AI应用]接收LLM返回消息:{}", eventStr);
|
||||||
emitter.send(SseEmitter.event().data(eventStr));
|
emitter.send(SseEmitter.event().data(eventStr));
|
||||||
@ -1507,9 +1251,7 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
|||||||
if (oConvertUtils.isEmpty(chatConversation.getId())) {
|
if (oConvertUtils.isEmpty(chatConversation.getId())) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
//update-begin---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
String key = getConversationCacheKey(chatConversation.getId(), null);
|
||||||
String key = getConversationCacheKey(chatConversation.getId(), null,"");
|
|
||||||
//update-end---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
|
||||||
if (oConvertUtils.isEmpty(key)) {
|
if (oConvertUtils.isEmpty(key)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -1539,13 +1281,10 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
|||||||
if (oConvertUtils.isNotEmpty(summaryTitle)) {
|
if (oConvertUtils.isNotEmpty(summaryTitle)) {
|
||||||
cachedConversation.setTitle(summaryTitle);
|
cachedConversation.setTitle(summaryTitle);
|
||||||
} else {
|
} else {
|
||||||
int maxLength = AiAppConsts.CONVERSATION_MAX_TITLE_LENGTH;
|
cachedConversation.setTitle(question.length() > 5 ? question.substring(0, 5) : question);
|
||||||
cachedConversation.setTitle(question.length() > maxLength ? question.substring(0, maxLength) : question);
|
|
||||||
}
|
}
|
||||||
//保存会话
|
//保存会话
|
||||||
//update-begin---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
saveChatConversation(cachedConversation);
|
||||||
saveChatConversation(cachedConversation,"");
|
|
||||||
//update-end---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -1590,296 +1329,4 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
|||||||
log.info("[AI-CHAT]{},requestId:{},耗时:{}s", message, requestId, (System.currentTimeMillis() - beginTime) / 1000);
|
log.info("[AI-CHAT]{},requestId:{},耗时:{}s", message, requestId, (System.currentTimeMillis() - beginTime) / 1000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 根据会话类型获取会话信息
|
|
||||||
*
|
|
||||||
* @param sessionType
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public Result<?> getConversationsByType(String sessionType) {
|
|
||||||
String key = getConversationDirCacheKey(null);
|
|
||||||
key = key + ":" + sessionType + ":*";
|
|
||||||
List<String> keys = redisUtil.scan(key);
|
|
||||||
// 如果键集合为空,返回空列表
|
|
||||||
if (keys.isEmpty()) {
|
|
||||||
return Result.ok(Collections.emptyList());
|
|
||||||
}
|
|
||||||
|
|
||||||
// 遍历键集合,获取对应的 ChatConversation 对象
|
|
||||||
List<ChatConversation> conversations = new ArrayList<>();
|
|
||||||
for (Object k : keys) {
|
|
||||||
ChatConversation conversation = (ChatConversation) redisTemplate.boundValueOps(k).get();
|
|
||||||
|
|
||||||
if (conversation != null) {
|
|
||||||
AiragApp app = conversation.getApp();
|
|
||||||
if (null == app) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
conversation.setApp(null);
|
|
||||||
conversation.setMessages(null);
|
|
||||||
conversations.add(conversation);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 对会话列表按创建时间降序排序
|
|
||||||
conversations.sort((o1, o2) -> {
|
|
||||||
Date date1 = o1.getCreateTime();
|
|
||||||
Date date2 = o2.getCreateTime();
|
|
||||||
if (date1 == null && date2 == null) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
if (date1 == null) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
if (date2 == null) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
return date2.compareTo(date1);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 返回结果
|
|
||||||
return Result.ok(conversations);
|
|
||||||
}
|
|
||||||
|
|
||||||
//================================================= begin 【QQYUN-14269】【AI】支持变量 ========================================
|
|
||||||
/**
|
|
||||||
* 初始化变量(仅不存在时设置)
|
|
||||||
*/
|
|
||||||
private void saveVariables(AiragApp app) {
|
|
||||||
if(null == app){
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if(!AiAppConsts.IZ_OPEN_MEMORY.equals(app.getIzOpenMemory())){
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (oConvertUtils.isObjectNotEmpty(app.getVariables())) {
|
|
||||||
// 变量替换
|
|
||||||
String username = "jeecg";
|
|
||||||
try {
|
|
||||||
HttpServletRequest req = SpringContextUtils.getHttpServletRequest();
|
|
||||||
username = JwtUtil.getUserNameByToken(req);
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error(e.getMessage());
|
|
||||||
}
|
|
||||||
if (oConvertUtils.isNotEmpty(username) && oConvertUtils.isNotEmpty(app.getId())) {
|
|
||||||
String variables = app.getVariables();
|
|
||||||
JSONArray objects = JSONArray.parseArray(variables);
|
|
||||||
for (int i = 0; i < objects.size(); i++) {
|
|
||||||
JSONObject jsonObject = objects.getJSONObject(i);
|
|
||||||
String name = jsonObject.getString("name");
|
|
||||||
String defaultValue = jsonObject.getString("defaultValue");
|
|
||||||
if (oConvertUtils.isNotEmpty(name)) {
|
|
||||||
airagVariableService.initVariable(username, app.getId(), name, defaultValue);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//================================================= end 【QQYUN-14269】【AI】支持变量 ========================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ai海报生成
|
|
||||||
*
|
|
||||||
* @param chatSendParams
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public String genAiPoster(ChatSendParams chatSendParams) {
|
|
||||||
AssertUtils.assertNotEmpty("请选择绘画模型", chatSendParams.getDrawModelId());
|
|
||||||
AssertUtils.assertNotEmpty("请填写提示词", chatSendParams.getContent());
|
|
||||||
AIChatParams aiChatParams = new AIChatParams();
|
|
||||||
if(oConvertUtils.isNotEmpty(chatSendParams.getImageSize())){
|
|
||||||
aiChatParams.setImageSize(chatSendParams.getImageSize());
|
|
||||||
}
|
|
||||||
String image= chatSendParams.getImageUrl();
|
|
||||||
List<Map<String, Object>> imageList = new ArrayList<>();
|
|
||||||
if(oConvertUtils.isEmpty(image)) {
|
|
||||||
//生成图片
|
|
||||||
imageList = aiChatHandler.imageGenerate(chatSendParams.getDrawModelId(), chatSendParams.getContent(), aiChatParams);
|
|
||||||
} else {
|
|
||||||
//图生图
|
|
||||||
imageList = aiChatHandler.imageEdit(chatSendParams.getDrawModelId(), chatSendParams.getContent(), Arrays.asList(image.split(SymbolConstant.COMMA)), aiChatParams);
|
|
||||||
}
|
|
||||||
return imageList.stream().map(this::uploadImage).collect(Collectors.joining("\n"));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 上传图片
|
|
||||||
*
|
|
||||||
* @param map
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
private String uploadImage(Map<String, Object> map) {
|
|
||||||
if (null == map || map.isEmpty()) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
String type = String.valueOf(map.get("type"));
|
|
||||||
String value = String.valueOf(map.get("value"));
|
|
||||||
byte[] data = new byte[1024];
|
|
||||||
// 判断是否是base64
|
|
||||||
if ("base64".equals(type)) {
|
|
||||||
if(value.startsWith("data:image")){
|
|
||||||
value = value.substring(value.indexOf(",") + 1);
|
|
||||||
}
|
|
||||||
data = Base64.getDecoder().decode(value);
|
|
||||||
} else {
|
|
||||||
//下载网络图片
|
|
||||||
InputStream inputStream = FileDownloadUtils.getDownInputStream(value, "");
|
|
||||||
if (inputStream != null) {
|
|
||||||
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
|
|
||||||
byte[] inpByte = new byte[1024]; // 1KB缓冲区
|
|
||||||
int nRead;
|
|
||||||
while ((nRead = inputStream.read(inpByte, 0, data.length)) != -1) {
|
|
||||||
buffer.write(inpByte, 0, nRead);
|
|
||||||
}
|
|
||||||
data = buffer.toByteArray();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (data != null) {
|
|
||||||
Path path = jeecgBaseConfig.getPath();
|
|
||||||
String bizPath = "chat";
|
|
||||||
String url = CommonUtils.uploadOnlineImage(data, path.getUpload(), bizPath, jeecgBaseConfig.getUploadType());
|
|
||||||
if("local".equals(jeecgBaseConfig.getUploadType())){
|
|
||||||
url = "#{domainURL}/" + url;
|
|
||||||
}
|
|
||||||
return url;
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("上传图片失败", e);
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
//================================================= begin【QQYUN-14261】【AI】AI助手,支持多模态能力- 文档========================================
|
|
||||||
/**
|
|
||||||
* 构建文件内容
|
|
||||||
*
|
|
||||||
* @param content
|
|
||||||
* @param files
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
private String buildContentWithFiles(String content, List<String> files) {
|
|
||||||
String filesText = parseFilesToText(files);
|
|
||||||
if (oConvertUtils.isEmpty(content)) {
|
|
||||||
content = "请基于我提供的附件内容回答问题。";
|
|
||||||
}else{
|
|
||||||
content = content + "\n\n请基于我提供的附件内容回答问题。";
|
|
||||||
}
|
|
||||||
if (oConvertUtils.isNotEmpty(filesText)) {
|
|
||||||
if (oConvertUtils.isNotEmpty(content)) {
|
|
||||||
content = content + "\n\n" + filesText;
|
|
||||||
} else {
|
|
||||||
content = filesText;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return content;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 将文件转换成text
|
|
||||||
*
|
|
||||||
* @param files
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
private String parseFilesToText(List<String> files) {
|
|
||||||
if (com.baomidou.mybatisplus.core.toolkit.CollectionUtils.isEmpty(files)) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
StringBuilder sb = new StringBuilder();
|
|
||||||
TikaDocumentParser parser = new TikaDocumentParser(AutoDetectParser::new, null, null, null);
|
|
||||||
int parsedCount = 0;
|
|
||||||
for (String fileRef : files) {
|
|
||||||
if (parsedCount >= LLMConsts.CHAT_FILE_MAX_COUNT) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (oConvertUtils.isEmpty(fileRef)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
String fileRefWithoutQuery = fileRef;
|
|
||||||
if (fileRefWithoutQuery.contains("?")) {
|
|
||||||
fileRefWithoutQuery = fileRefWithoutQuery.substring(0, fileRefWithoutQuery.indexOf("?"));
|
|
||||||
}
|
|
||||||
String fileName = FilenameUtils.getName(fileRefWithoutQuery);
|
|
||||||
String ext = FilenameUtils.getExtension(fileName);
|
|
||||||
if (oConvertUtils.isEmpty(ext) || !LLMConsts.CHAT_FILE_EXT_WHITELIST.contains(ext.toLowerCase())) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
File file = ensureLocalFile(fileRef, fileName);
|
|
||||||
if (file == null || !file.exists() || !file.isFile()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
Document document = parser.parse(file);
|
|
||||||
if (document == null || oConvertUtils.isEmpty(document.text())) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
String text = document.text().trim();
|
|
||||||
if (text.length() > LLMConsts.CHAT_FILE_TEXT_MAX_LENGTH) {
|
|
||||||
text = text.substring(0, LLMConsts.CHAT_FILE_TEXT_MAX_LENGTH);
|
|
||||||
}
|
|
||||||
sb.append("附件[").append(fileName).append("]内容:\n").append(text).append("\n\n");
|
|
||||||
parsedCount++;
|
|
||||||
if (sb.length() > LLMConsts.CHAT_FILE_TEXT_MAX_LENGTH) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.warn("附件解析失败: {}, {}", fileRef, e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return sb.toString().trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取本地文件
|
|
||||||
*
|
|
||||||
* @param fileRef
|
|
||||||
* @param fileName
|
|
||||||
* @return
|
|
||||||
* @throws IOException
|
|
||||||
*/
|
|
||||||
private File ensureLocalFile(String fileRef, String fileName) {
|
|
||||||
String uploadpath = jeecgBaseConfig.getPath().getUpload();
|
|
||||||
if (LLMConsts.WEB_PATTERN.matcher(fileRef).matches()) {
|
|
||||||
String tempDir = uploadpath + File.separator + "chat" + File.separator + UUID.randomUUID() + File.separator;
|
|
||||||
File dir = new File(tempDir);
|
|
||||||
if (!dir.exists() && !dir.mkdirs()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
String tempFilePath = tempDir + fileName;
|
|
||||||
FileDownloadUtils.download2DiskFromNet(fileRef, tempFilePath);
|
|
||||||
return new File(tempFilePath);
|
|
||||||
}
|
|
||||||
return new File(uploadpath + File.separator + fileRef);
|
|
||||||
}
|
|
||||||
//================================================= end【QQYUN-14261】【AI】AI助手,支持多模态能力- 文档========================================
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ai创作
|
|
||||||
*
|
|
||||||
* @param aiWriteGenerateVo
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public SseEmitter genAiWriter(AiWriteGenerateVo aiWriteGenerateVo) {
|
|
||||||
String activeMode = "compose";
|
|
||||||
ChatSendParams sendParams = new ChatSendParams();
|
|
||||||
sendParams.setAppId(AiAppConsts.WRITER_APP_ID);
|
|
||||||
String content = "";
|
|
||||||
//写作
|
|
||||||
if (activeMode.equals(aiWriteGenerateVo.getActiveMode())) {
|
|
||||||
content = StrUtil.format(Prompts.AI_WRITER_PROMPT, aiWriteGenerateVo.getPrompt(), aiWriteGenerateVo.getFormat(), aiWriteGenerateVo.getTone(), aiWriteGenerateVo.getLanguage(), aiWriteGenerateVo.getLength());
|
|
||||||
} else {
|
|
||||||
//回复
|
|
||||||
content = StrUtil.format(Prompts.AI_REPLY_PROMPT, aiWriteGenerateVo.getPrompt(), aiWriteGenerateVo.getOriginalContent(), aiWriteGenerateVo.getFormat(), aiWriteGenerateVo.getTone(), aiWriteGenerateVo.getLanguage(), aiWriteGenerateVo.getLength());
|
|
||||||
}
|
|
||||||
sendParams.setContent(content);
|
|
||||||
sendParams.setIzSaveSession(false);
|
|
||||||
return this.send(sendParams);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -1,194 +0,0 @@
|
|||||||
package org.jeecg.modules.airag.app.service.impl;
|
|
||||||
|
|
||||||
import com.alibaba.fastjson.JSONArray;
|
|
||||||
import com.alibaba.fastjson.JSONObject;
|
|
||||||
import dev.langchain4j.agent.tool.ToolSpecification;
|
|
||||||
import dev.langchain4j.model.chat.request.json.JsonObjectSchema;
|
|
||||||
import dev.langchain4j.service.tool.ToolExecutor;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.jeecg.common.util.SpringContextUtils;
|
|
||||||
import org.jeecg.common.util.oConvertUtils;
|
|
||||||
import org.jeecg.modules.airag.app.consts.AiAppConsts;
|
|
||||||
import org.jeecg.modules.airag.app.entity.AiragApp;
|
|
||||||
import org.jeecg.modules.airag.app.service.IAiragVariableService;
|
|
||||||
import org.jeecg.modules.airag.app.vo.AppVariableVo;
|
|
||||||
import org.jeecg.modules.airag.common.handler.AIChatParams;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.data.redis.core.RedisTemplate;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @Description: AI应用变量服务实现
|
|
||||||
* @Author: jeecg-boot
|
|
||||||
* @Date: 2025-02-26
|
|
||||||
* @Version: V1.0
|
|
||||||
*/
|
|
||||||
@Service
|
|
||||||
@Slf4j
|
|
||||||
public class AiragVariableServiceImpl implements IAiragVariableService {
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private RedisTemplate redisTemplate;
|
|
||||||
|
|
||||||
private static final String CACHE_PREFIX = "airag:app:var:";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 初始化变量(仅不存在时设置)
|
|
||||||
*
|
|
||||||
* @param username
|
|
||||||
* @param appId
|
|
||||||
* @param name
|
|
||||||
* @param defaultValue
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void initVariable(String username, String appId, String name, String defaultValue) {
|
|
||||||
if (oConvertUtils.isEmpty(username) || oConvertUtils.isEmpty(appId) || oConvertUtils.isEmpty(name)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
String key = CACHE_PREFIX + appId + ":" + username;
|
|
||||||
redisTemplate.opsForHash().putIfAbsent(key, name, defaultValue != null ? defaultValue : "");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 追加提示词
|
|
||||||
*
|
|
||||||
* @param username
|
|
||||||
* @param app
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public String additionalPrompt(String username, AiragApp app) {
|
|
||||||
String memoryPrompt = app.getMemoryPrompt();
|
|
||||||
String prompt = app.getPrompt();
|
|
||||||
|
|
||||||
if (oConvertUtils.isEmpty(memoryPrompt)) {
|
|
||||||
return prompt;
|
|
||||||
}
|
|
||||||
String variablesStr = app.getVariables();
|
|
||||||
if (oConvertUtils.isEmpty(variablesStr)) {
|
|
||||||
return prompt;
|
|
||||||
}
|
|
||||||
|
|
||||||
List<AppVariableVo> variableList = JSONArray.parseArray(variablesStr, AppVariableVo.class);
|
|
||||||
if (variableList == null || variableList.isEmpty()) {
|
|
||||||
return prompt;
|
|
||||||
}
|
|
||||||
|
|
||||||
String key = CACHE_PREFIX + app.getId() + ":" + username;
|
|
||||||
Map<Object, Object> savedValues = redisTemplate.opsForHash().entries(key);
|
|
||||||
|
|
||||||
for (AppVariableVo variable : variableList) {
|
|
||||||
if (variable.getEnable() != null && !variable.getEnable()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
String name = variable.getName();
|
|
||||||
String value = variable.getDefaultValue();
|
|
||||||
|
|
||||||
// 优先使用Redis中的值
|
|
||||||
if (savedValues.containsKey(name)) {
|
|
||||||
Object savedVal = savedValues.get(name);
|
|
||||||
if (savedVal != null) {
|
|
||||||
value = String.valueOf(savedVal);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value == null) {
|
|
||||||
value = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
// 替换 {{name}}
|
|
||||||
memoryPrompt = memoryPrompt.replace("{{" + name + "}}", value);
|
|
||||||
}
|
|
||||||
return prompt + "\n" + memoryPrompt;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新变量值
|
|
||||||
*
|
|
||||||
* @param userId
|
|
||||||
* @param appId
|
|
||||||
* @param name
|
|
||||||
* @param value
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void updateVariable(String userId, String appId, String name, String value) {
|
|
||||||
if (oConvertUtils.isEmpty(userId) || oConvertUtils.isEmpty(appId) || oConvertUtils.isEmpty(name)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
String key = CACHE_PREFIX + appId + ":" + userId;
|
|
||||||
redisTemplate.opsForHash().put(key, name, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 添加变量更新工具
|
|
||||||
*
|
|
||||||
* @param params
|
|
||||||
* @param aiApp
|
|
||||||
* @param username
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void addUpdateVariableTool(AiragApp aiApp, String username, AIChatParams params) {
|
|
||||||
if (params.getTools() == null) {
|
|
||||||
params.setTools(new HashMap<>());
|
|
||||||
}
|
|
||||||
if (!AiAppConsts.IZ_OPEN_MEMORY.equals(aiApp.getIzOpenMemory())) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// 构建变量描述信息
|
|
||||||
String variablesStr = aiApp.getVariables();
|
|
||||||
List<AppVariableVo> variableList = null;
|
|
||||||
if (oConvertUtils.isNotEmpty(variablesStr)) {
|
|
||||||
variableList = JSONArray.parseArray(variablesStr, AppVariableVo.class);
|
|
||||||
}
|
|
||||||
|
|
||||||
//工具描述
|
|
||||||
StringBuilder descriptionBuilder = new StringBuilder("更新应用变量的值。仅当检测到变量的新值与当前值不一致时调用。如果已调用过或值未变,请勿重复调用。");
|
|
||||||
if (variableList != null && !variableList.isEmpty()) {
|
|
||||||
descriptionBuilder.append("\n\n可用变量列表:");
|
|
||||||
for (AppVariableVo var : variableList) {
|
|
||||||
if (var.getEnable() != null && !var.getEnable()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
descriptionBuilder.append("\n- ").append(var.getName());
|
|
||||||
if (oConvertUtils.isNotEmpty(var.getDescription())) {
|
|
||||||
descriptionBuilder.append(": ").append(var.getDescription());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
descriptionBuilder.append("\n\n注意:variableName必须是上述列表中的名称之一。");
|
|
||||||
}
|
|
||||||
|
|
||||||
//构建更新变量的工具
|
|
||||||
ToolSpecification spec = ToolSpecification.builder()
|
|
||||||
.name("update_variable")
|
|
||||||
.description(descriptionBuilder.toString())
|
|
||||||
.parameters(JsonObjectSchema.builder()
|
|
||||||
.addStringProperty("variableName", "变量名称")
|
|
||||||
.addStringProperty("value", "变量值")
|
|
||||||
.required("variableName", "value")
|
|
||||||
.build())
|
|
||||||
.build();
|
|
||||||
|
|
||||||
//监听工具的调用
|
|
||||||
ToolExecutor executor = (toolExecutionRequest, memoryId) -> {
|
|
||||||
try {
|
|
||||||
JSONObject args = JSONObject.parseObject(toolExecutionRequest.arguments());
|
|
||||||
String name = args.getString("variableName");
|
|
||||||
String value = args.getString("value");
|
|
||||||
IAiragVariableService variableService = SpringContextUtils.getBean(IAiragVariableService.class);
|
|
||||||
//更新变量值
|
|
||||||
variableService.updateVariable(username, aiApp.getId(), name, value);
|
|
||||||
return "变量 " + name + " 已更新为: " + value;
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("更新变量失败", e);
|
|
||||||
return "更新变量失败: " + e.getMessage();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
params.getTools().put(spec, executor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,48 +0,0 @@
|
|||||||
package org.jeecg.modules.airag.app.vo;
|
|
||||||
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @Description: ai写作生成实体类
|
|
||||||
*
|
|
||||||
* @author: wangshuai
|
|
||||||
* @date: 2026/1/12 15:59
|
|
||||||
*/
|
|
||||||
@Data
|
|
||||||
public class AiWriteGenerateVo {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 写作类型
|
|
||||||
*/
|
|
||||||
private String activeMode;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 写作内容提示
|
|
||||||
*/
|
|
||||||
private String prompt;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 原文
|
|
||||||
*/
|
|
||||||
private String originalContent;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 长度
|
|
||||||
*/
|
|
||||||
private String length;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 格式
|
|
||||||
*/
|
|
||||||
private String format;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 语气
|
|
||||||
*/
|
|
||||||
private String tone;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 语言
|
|
||||||
*/
|
|
||||||
private String language;
|
|
||||||
}
|
|
||||||
@ -1,46 +0,0 @@
|
|||||||
package org.jeecg.modules.airag.app.vo;
|
|
||||||
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
import java.io.Serializable;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @Description: 应用变量配置
|
|
||||||
* @Author: jeecg-boot
|
|
||||||
* @Date: 2025-02-26
|
|
||||||
* @Version: V1.0
|
|
||||||
*/
|
|
||||||
@Data
|
|
||||||
public class AppVariableVo implements Serializable {
|
|
||||||
private static final long serialVersionUID = 1L;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 变量名
|
|
||||||
*/
|
|
||||||
private String name;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 描述
|
|
||||||
*/
|
|
||||||
private String description;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 默认值
|
|
||||||
*/
|
|
||||||
private String defaultValue;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 是否启用
|
|
||||||
*/
|
|
||||||
private Boolean enable;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 动作
|
|
||||||
*/
|
|
||||||
private String action;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 排序
|
|
||||||
*/
|
|
||||||
private Integer orderNum;
|
|
||||||
}
|
|
||||||
@ -47,14 +47,4 @@ public class ChatConversation {
|
|||||||
* for [issues/8545]新建AI应用的时候只能选择没有自定义参数的AI流程
|
* for [issues/8545]新建AI应用的时候只能选择没有自定义参数的AI流程
|
||||||
*/
|
*/
|
||||||
private Map<String, Object> flowInputs;
|
private Map<String, Object> flowInputs;
|
||||||
|
|
||||||
/**
|
|
||||||
* portal 应用门户
|
|
||||||
*/
|
|
||||||
private String sessionType;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 是否保存会话
|
|
||||||
*/
|
|
||||||
private Boolean izSaveSession;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -47,11 +47,6 @@ public class ChatSendParams {
|
|||||||
*/
|
*/
|
||||||
private List<String> images;
|
private List<String> images;
|
||||||
|
|
||||||
/**
|
|
||||||
* 文件列表
|
|
||||||
*/
|
|
||||||
private List<String> files;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 工作流额外入参配置
|
* 工作流额外入参配置
|
||||||
* key: 参数field, value: 参数值
|
* key: 参数field, value: 参数值
|
||||||
@ -64,39 +59,4 @@ public class ChatSendParams {
|
|||||||
*/
|
*/
|
||||||
private Boolean enableSearch;
|
private Boolean enableSearch;
|
||||||
|
|
||||||
/**
|
|
||||||
* 是否开启深度思考
|
|
||||||
*/
|
|
||||||
private Boolean enableThink;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 会话类型: portal 应用门户
|
|
||||||
*/
|
|
||||||
private String sessionType;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 是否开启生成绘画
|
|
||||||
*/
|
|
||||||
private Boolean enableDraw;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 绘画模型的id
|
|
||||||
*/
|
|
||||||
private String drawModelId;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 图片尺寸
|
|
||||||
*/
|
|
||||||
private String imageSize;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 一张图片
|
|
||||||
*/
|
|
||||||
private String imageUrl;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 是否保存会话
|
|
||||||
*/
|
|
||||||
private Boolean izSaveSession;
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
package org.jeecg.modules.airag.demo;
|
package org.jeecg.modules.airag.demo;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.jeecg.modules.airag.flow.component.enhance.IAiRagEnhanceJava;
|
import org.jeecg.modules.airag.flow.component.enhance.IAiRagEnhanceJava;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
@ -12,15 +11,12 @@ import java.util.Map;
|
|||||||
* @Author: chenrui
|
* @Author: chenrui
|
||||||
* @Date: 2025/3/6 11:42
|
* @Date: 2025/3/6 11:42
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
|
||||||
@Component("testAiragEnhance")
|
@Component("testAiragEnhance")
|
||||||
public class TestAiragEnhance implements IAiRagEnhanceJava {
|
public class TestAiragEnhance implements IAiRagEnhanceJava {
|
||||||
@Override
|
@Override
|
||||||
public Map<String, Object> process(Map<String, Object> inputParams) {
|
public Map<String, Object> process(Map<String, Object> inputParams) {
|
||||||
Object arg1 = inputParams.get("arg1");
|
Object arg1 = inputParams.get("arg1");
|
||||||
Object arg2 = inputParams.get("arg2");
|
Object arg2 = inputParams.get("arg2");
|
||||||
Object index = inputParams.get("index");
|
|
||||||
log.info("arg1={}, arg2={}, index={}", arg1, arg2, index);
|
|
||||||
return Collections.singletonMap("result",arg1.toString()+"java拼接"+arg2.toString());
|
return Collections.singletonMap("result",arg1.toString()+"java拼接"+arg2.toString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,209 +0,0 @@
|
|||||||
package org.jeecg.modules.airag.llm.consts;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @Description: 流程插件常量
|
|
||||||
*
|
|
||||||
* @author: wangshuai
|
|
||||||
* @date: 2025/12/23 19:37
|
|
||||||
*/
|
|
||||||
public interface FlowPluginContent {
|
|
||||||
/**
|
|
||||||
* 名称
|
|
||||||
*/
|
|
||||||
String NAME = "name";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 描述
|
|
||||||
*/
|
|
||||||
String DESCRIPTION = "description";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 响应
|
|
||||||
*/
|
|
||||||
String RESPONSES = "responses";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 类型
|
|
||||||
*/
|
|
||||||
String TYPE = "type";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 参数
|
|
||||||
*/
|
|
||||||
String PARAMETERS = "parameters";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 是否必须
|
|
||||||
*/
|
|
||||||
String REQUIRED = "required";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 默认值
|
|
||||||
*/
|
|
||||||
String DEFAULT_VALUE = "defaultValue";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 路径
|
|
||||||
*/
|
|
||||||
String PATH = "path";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 方法
|
|
||||||
*/
|
|
||||||
String METHOD = "method";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 位置
|
|
||||||
*/
|
|
||||||
String LOCATION = "location";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 认证类型
|
|
||||||
*/
|
|
||||||
String AUTH_TYPE = "authType";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* token参数名称
|
|
||||||
*/
|
|
||||||
String TOKEN_PARAM_NAME = "tokenParamName";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* token参数值
|
|
||||||
*/
|
|
||||||
String TOKEN_PARAM_VALUE = "tokenParamValue";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* token
|
|
||||||
*/
|
|
||||||
String TOKEN = "token";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Path位置
|
|
||||||
*/
|
|
||||||
String LOCATION_PATH = "Path";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Header位置
|
|
||||||
*/
|
|
||||||
String LOCATION_HEADER = "Header";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Query位置
|
|
||||||
*/
|
|
||||||
String LOCATION_QUERY = "Query";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Body位置
|
|
||||||
*/
|
|
||||||
String LOCATION_BODY = "Body";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Form-Data位置
|
|
||||||
*/
|
|
||||||
String LOCATION_FORM_DATA = "Form-Data";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* String类型
|
|
||||||
*/
|
|
||||||
String TYPE_STRING = "String";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* string类型
|
|
||||||
*/
|
|
||||||
String TYPE_STRING_LOWER = "string";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Number类型
|
|
||||||
*/
|
|
||||||
String TYPE_NUMBER = "Number";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* number类型
|
|
||||||
*/
|
|
||||||
String TYPE_NUMBER_LOWER = "number";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Integer类型
|
|
||||||
*/
|
|
||||||
String TYPE_INTEGER = "Integer";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* integer类型
|
|
||||||
*/
|
|
||||||
String TYPE_INTEGER_LOWER = "integer";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Boolean类型
|
|
||||||
*/
|
|
||||||
String TYPE_BOOLEAN = "Boolean";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* boolean类型
|
|
||||||
*/
|
|
||||||
String TYPE_BOOLEAN_LOWER = "boolean";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 工具数量
|
|
||||||
*/
|
|
||||||
String TOOL_COUNT = "tool_count";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 是否启用
|
|
||||||
*/
|
|
||||||
String ENABLED = "enabled";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 输入
|
|
||||||
*/
|
|
||||||
String INPUTS = "inputs";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 输出
|
|
||||||
*/
|
|
||||||
String OUTPUTS = "outputs";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* POST请求
|
|
||||||
*/
|
|
||||||
String POST = "POST";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* token名称
|
|
||||||
*/
|
|
||||||
String X_ACCESS_TOKEN = "X-Access-Token";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 插件名称
|
|
||||||
*/
|
|
||||||
String PLUGIN_NAME = "流程调用";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 插件描述
|
|
||||||
*/
|
|
||||||
String PLUGIN_DESC = "调用工作流";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 插件请求地址
|
|
||||||
*/
|
|
||||||
String PLUGIN_REQUEST_URL = "/airag/flow/plugin/run/";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 记忆库插件名称
|
|
||||||
*/
|
|
||||||
String PLUGIN_MEMORY_NAME = "记忆库";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 记忆库插件描述
|
|
||||||
*/
|
|
||||||
String PLUGIN_MEMORY_DESC = "用于记录长期记忆";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 添加记忆路径
|
|
||||||
*/
|
|
||||||
String PLUGIN_MEMORY_ADD_PATH = "/airag/knowledge/plugin/add";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 查询记忆路径
|
|
||||||
*/
|
|
||||||
String PLUGIN_MEMORY_QUERY_PATH = "/airag/knowledge/plugin/query";
|
|
||||||
}
|
|
||||||
@ -1,8 +1,5 @@
|
|||||||
package org.jeecg.modules.airag.llm.consts;
|
package org.jeecg.modules.airag.llm.consts;
|
||||||
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -38,11 +35,6 @@ public class LLMConsts {
|
|||||||
*/
|
*/
|
||||||
public static final String MODEL_TYPE_LLM = "LLM";
|
public static final String MODEL_TYPE_LLM = "LLM";
|
||||||
|
|
||||||
/**
|
|
||||||
* 模型类型: 图像生成
|
|
||||||
*/
|
|
||||||
public static final String MODEL_TYPE_IMAGE = "IMAGE";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 向量模型:默认维度
|
* 向量模型:默认维度
|
||||||
*/
|
*/
|
||||||
@ -93,29 +85,4 @@ public class LLMConsts {
|
|||||||
*/
|
*/
|
||||||
public static final String DEEPSEEK_REASONER = "deepseek-reasoner";
|
public static final String DEEPSEEK_REASONER = "deepseek-reasoner";
|
||||||
|
|
||||||
/**
|
|
||||||
* 知识库类型:知识库
|
|
||||||
*/
|
|
||||||
public static final String KNOWLEDGE_TYPE_KNOWLEDGE = "knowledge";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 知识库类型:记忆库
|
|
||||||
*/
|
|
||||||
public static final String KNOWLEDGE_TYPE_MEMORY = "memory";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 支持文件的后缀
|
|
||||||
*/
|
|
||||||
public static final Set<String> CHAT_FILE_EXT_WHITELIST = new HashSet<>(Arrays.asList("txt", "pdf", "docx", "doc", "pptx", "ppt", "xlsx", "xls", "md"));
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 文件内容最大长度
|
|
||||||
*/
|
|
||||||
public static final int CHAT_FILE_TEXT_MAX_LENGTH = 20000;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 上传文件对打数量
|
|
||||||
*/
|
|
||||||
public static final int CHAT_FILE_MAX_COUNT = 3;
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user