mirror of
https://github.com/jeecgboot/JeecgBoot.git
synced 2026-01-23 03:27:26 +08:00
Compare commits
86 Commits
v3.8.0last
...
v3.7.3spri
| Author | SHA1 | Date | |
|---|---|---|---|
| 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,3 +13,4 @@ os_del.cmd
|
|||||||
os_del_doc.cmd
|
os_del_doc.cmd
|
||||||
.svn
|
.svn
|
||||||
derby.log
|
derby.log
|
||||||
|
*.log
|
||||||
164
README-AI.md
164
README-AI.md
@ -1,164 +0,0 @@
|
|||||||
AIGC应用平台介绍
|
|
||||||
===============
|
|
||||||
|
|
||||||
一个全栈式 AI 开发平台,旨在帮助开发者快速构建和部署个性化的 AI 应用。
|
|
||||||
|
|
||||||
> JDK说明:AI流程编排引擎暂时不支持jdk21,所以目前只能使用jdk8或者jdk17启动项目。
|
|
||||||
|
|
||||||
|
|
||||||
JeecgBoot平台的AIGC功能模块,是一套类似`Dify`的`AIGC应用开发平台`+`知识库问答`,是一款基于LLM大语言模型AI应用平台和 RAG 的知识库问答系统。
|
|
||||||
其直观的界面结合了 AI 流程编排、RAG 管道、知识库管理、模型管理、对接向量库、实时运行可观察等,让您可以快速从原型到生产,拥有AI服务能力。
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### AI视频介绍
|
|
||||||
|
|
||||||
[](https://www.bilibili.com/video/BV1zmd7YFE4w)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#### Dify `VS` JEECG AI
|
|
||||||
|
|
||||||
> JEECG AI与Dify相比,在多个方面展现出显著的优势,特别是在文档处理、格式和图片保持方面。以下是一些具体的优点:
|
|
||||||
> - Markdown文档库导入:
|
|
||||||
> JEECG AI允许用户直接导入整个Markdown文档库,这不仅保留markdown格式,还支持图片的导入,确保文档内容的完整性和可视化效果。
|
|
||||||
> - 对话回复格式美观:
|
|
||||||
> 在对话过程中,JEECG AI能够保持回复内容的原格式,也不丢失图片,使得输出的文章更加美观,不会出现格式错乱的情况,还支持图片的渲染。
|
|
||||||
> - PDF文档导入与格式转换:
|
|
||||||
> JEECG AI在处理PDF文档时,能够更好地保持原始格式和图片,确保转换后的内容与原始文档一致。这个功能在许多AI产品中表现不佳,而JEECG AI在这方面做出了显著的优化
|
|
||||||
|
|
||||||
|
|
||||||
| 功能 | Dify | Jeecg AI |
|
|
||||||
|------------|------------------|-----------------------------------------|
|
|
||||||
| AI工作流 | 有 | 有 |
|
|
||||||
| RAG 管道向量搜索 | 有 | 有 |
|
|
||||||
| AI模型管理 | 有 | 有 |
|
|
||||||
| AI应用管理 | 有 | 有 |
|
|
||||||
| AI知识库 | 有 | 有 |
|
|
||||||
| 产品方向 | 一款独立的 LLM 应用开发平台 | 低代码与AIGC应用二者结合的平台 |
|
|
||||||
| 业务集成 | 业务集成能力弱 | 更方便与业务系统集成,调用系统接口和逻辑更加方便 |
|
|
||||||
| AI业务流 | 侧重AI逻辑流程 | AI流程编排作为低代码的业务引擎,用户可以通过AI流程配置各种业务流和AI流程 |
|
|
||||||
| 实现语言 | python + react | JAVA + vue3 |
|
|
||||||
| 上传markdown文档库(支持图片) | 不支持 | 支持 |
|
|
||||||
| AI对话支持发图和展示图片 | 支持 | 支持 |
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### 安装向量库 pgvector
|
|
||||||
|
|
||||||
- https://help.jeecg.com/aigc/config
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## 功能特点
|
|
||||||
|
|
||||||
- AI流程: 提供强大的AI流程设计器引擎,支持编排 AI 工作过程,满足复杂业务场景,支持画布上构建和实时运行查看 AI流程运行情况。
|
|
||||||
- AI流程即服务: 通过AI流程编排你需要的智能体,结合AI+自定义开发节点 实现功能性 API,让你瞬间拥有各种智能体API。
|
|
||||||
- AI助手对话功能: 集成 ChatGPT、Deepseek、智普、私有大模型 等 AI 模型,提供智能对话和生成式 AI 功能,深度与知识库结合提供更精准的知识。
|
|
||||||
- RAG 功能: 涵盖从文档摄入到检索的所有内容,支持从 PDF、PPT 和其他常见文档格式中提取文本,支持检索增强生成(RAG),将未训练数据与 AI 模型集成,提升智能交互能力。
|
|
||||||
- AI 知识库: 通过导入文档或已有问答对进行训练,让 AI 模型能根据文档以交互式对话方式回答问题。
|
|
||||||
- 模型管理:支持对接各种大模型,包括本地私有大模型(Deepseek/ Llama 3 / Qwen 2 等)、国内公共大模型(通义千问 / 腾讯混元 / 字节豆包 / 百度千帆 / 智谱 AI / Kimi 等)和国外公共大模型(OpenAI / Claude / Gemini 等);
|
|
||||||
- 无缝嵌入:Iframe一键嵌入,支持将AI聊天助手快速嵌入到第三方系统,让系统快速拥有智能问答能力,提高用户满意度。
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#### 在线体验
|
|
||||||
|
|
||||||
- JeecgBoot演示: https://boot3.jeecg.com
|
|
||||||
- 敲敲云在线搭建AI知识库:https://app.qiaoqiaoyun.com
|
|
||||||
|
|
||||||
|
|
||||||
## 技术交流
|
|
||||||
|
|
||||||
- 开发文档:https://help.jeecg.com/aigc
|
|
||||||
- QQ群:716488839
|
|
||||||
|
|
||||||
|
|
||||||
## 功能列表
|
|
||||||
|
|
||||||
- AI应用管理(普通应用、高级流程应用)
|
|
||||||
- AI模型管理
|
|
||||||
- AI知识库
|
|
||||||
- AI应用平台(普通、对接AI流程)
|
|
||||||
- AI流程编排
|
|
||||||
- AI聊天支持嵌入第三方
|
|
||||||
- AI向量库对接
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## 支持AI模型
|
|
||||||
|
|
||||||
| AI大模型 | 支持 |
|
|
||||||
|---------------| --- |
|
|
||||||
| DeepSeek | √ |
|
|
||||||
| ChatGTP | √ |
|
|
||||||
| Qwq | √ |
|
|
||||||
| 智库 | √ |
|
|
||||||
| Ollama本地搭建大模型 | √ |
|
|
||||||
| 等等。。 | √ |
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## AIGC能做什么
|
|
||||||
|
|
||||||
AIGC模块是一个基于AI的自动化流程编排工具和聊天应用搭建平台,它可以帮助用户快速生成AI流程接口和聊天应用,提高效率。
|
|
||||||
以下是一些具体的应用场景和示例:
|
|
||||||
|
|
||||||
- 你可能需要一个翻译接口,可以通过AI流程编排搭建出来。
|
|
||||||
- 你可能需要一个接口转换工具,可以通过AI流程编排搭建出来。(比如:jimureport所需要接口返回格式与你的系统不同,你通过AI接口实现自动转换)
|
|
||||||
- 你可能需要一个聊天机器人,可以通过AI流程编排搭建出来。
|
|
||||||
- 你可能需要一个自动化流程,可以通过AI流程编排搭建出来。
|
|
||||||
- 你可能需要一个自动化处理文件的流程,可以通过AI流程结合python脚本实现操作电脑,文件等。
|
|
||||||
|
|
||||||
|
|
||||||
## AI应用平台功能展示
|
|
||||||
|
|
||||||
AI模型列表
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
选择AI模型,配置你的参数
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
|
|
||||||
AI知识库支持手工录入文本,导入pdf\\word\\excel等文档,支持问答对训练
|
|
||||||
|
|
||||||

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

|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
AI流程,提供强大的AI流程设计器引擎,支持编排 AI 工作过程,满足复杂业务场景,支持画布上构建和实时运行查看 AI流程运行情况。
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
|
|
||||||
目前支持的节点有:开始、结束、AI知识库节点、AI节点、分类节点、分支节点、JAVA节点、脚本节点、子流程节点、http请求节点、直接回复节点等节点
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
节点项配置
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
在线运行看结果
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
|
|
||||||
AI应用配置,支持AI流程配置和简单的AI配置
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
可以关联多个知识库,右侧是AI智能回复,你可以搭建自己的智能体,比如搭建一个 “诗词达人” “翻译助手”
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
可以将创建的聊天应用,集成到第三方系统中
|
|
||||||
|
|
||||||

|
|
||||||
47
README-EN.md
47
README-EN.md
@ -7,12 +7,12 @@
|
|||||||
JEECG BOOT AI Low Code Platform
|
JEECG BOOT AI Low Code Platform
|
||||||
===============
|
===============
|
||||||
|
|
||||||
Current version: 3.8.0 (Release date: 2025-04-18)
|
Current version: 3.7.3 (Release date: 2025-02-10)
|
||||||
|
|
||||||
|
|
||||||
[](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)
|
||||||
|
|
||||||
@ -21,7 +21,7 @@ Current version: 3.8.0 (Release date: 2025-04-18)
|
|||||||
Project introduction
|
Project introduction
|
||||||
-----------------------------------
|
-----------------------------------
|
||||||
|
|
||||||
<h3 align="center">Java AI Low Code Platform</h3>
|
<h3 align="center">Java AI Low Code Platform for Enterprise web applications</h3>
|
||||||
|
|
||||||
JeecgBoot is a `AI low code platform` based on code `generators`! Front and back end separation architecture SpringBoot2.x, SpringCloud, Ant Design&Vue, Mybatis plus, Shiro, JWT, support for microservices. The powerful code generator makes the front and back end of the code generation, low code development! JeecgBoot leads a new low-code development paradigm (OnlineCoding-> Code Generator -> Manual MERGE) that helps resolve 70% of the duplication in Java projects and makes development more business-focused. Not only can quickly improve efficiency, save research and development costs, but also do not lose flexibility!
|
JeecgBoot is a `AI low code platform` based on code `generators`! Front and back end separation architecture SpringBoot2.x, SpringCloud, Ant Design&Vue, Mybatis plus, Shiro, JWT, support for microservices. The powerful code generator makes the front and back end of the code generation, low code development! JeecgBoot leads a new low-code development paradigm (OnlineCoding-> Code Generator -> Manual MERGE) that helps resolve 70% of the duplication in Java projects and makes development more business-focused. Not only can quickly improve efficiency, save research and development costs, but also do not lose flexibility!
|
||||||
|
|
||||||
@ -37,7 +37,7 @@ AI Empowering Low-Code: Currently, JeecgBoot supports AI large models such as Ch
|
|||||||
Technical support
|
Technical support
|
||||||
-----------------------------------
|
-----------------------------------
|
||||||
|
|
||||||
Problems or bugs in use can be found in [Making on the Issues](https://github.com/jeecgboot/JeecgBoot/issues/new?template=bug_report.md)
|
Problems or bugs in use can be found in [Making on the Issues](https://github.com/jeecgboot/JeecgBoot/issues/new)
|
||||||
|
|
||||||
|
|
||||||
##### Project description
|
##### Project description
|
||||||
@ -49,11 +49,6 @@ Problems or bugs in use can be found in [Making on the Issues](https://github.co
|
|||||||
| `jeecg-uniapp` | [APP development framework, a code multi terminal adaptation, and support APP, small program, H5](https://github.com/jeecgboot/jeecg-uniapp) |
|
| `jeecg-uniapp` | [APP development framework, a code multi terminal adaptation, and support APP, small program, H5](https://github.com/jeecgboot/jeecg-uniapp) |
|
||||||
|
|
||||||
|
|
||||||
### Video Introduction
|
|
||||||
|
|
||||||
[](https://www.bilibili.com/video/BV1Nk4y1o7Qc)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Download other source code
|
Download other source code
|
||||||
-----------------------------------
|
-----------------------------------
|
||||||
@ -69,8 +64,8 @@ Jeecg-Boot AI low code platform can be applied in the development of any J2EE pr
|
|||||||
Starts the project
|
Starts the project
|
||||||
-----------------------------------
|
-----------------------------------
|
||||||
|
|
||||||
- [IDEA Quick start](https://help.jeecg.com/java/setup/idea/startup)
|
- [IDEA Quick start](https://help.jeecg.com/java/setup/idea/startup.html)
|
||||||
- [Docker Quick start](https://help.jeecg.com/java/docker/quick)
|
- [Docker Quick start](https://help.jeecg.com/java/docker/quick.html)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -79,9 +74,9 @@ Technical documentation
|
|||||||
|
|
||||||
- Website: [http://www.jeecg.com](http://www.jeecg.com)
|
- Website: [http://www.jeecg.com](http://www.jeecg.com)
|
||||||
- Demo : [OnlineDemo](http://boot3.jeecg.com) | [APP](http://jeecg.com/appIndex)
|
- Demo : [OnlineDemo](http://boot3.jeecg.com) | [APP](http://jeecg.com/appIndex)
|
||||||
- Doc: [DocumentCenter](http://help.jeecg.com) | [AI Config](https://help.jeecg.com/java/ai/aichat)
|
- Doc: [DocumentCenter](http://help.jeecg.com) | [AI Config](https://help.jeecg.com/java/ai/aichat.html)
|
||||||
- Newbie guide: [Quick start](http://www.jeecg.com/doc/quickstart) | [Q&A ](http://www.jeecg.com/doc/qa) | [1 minute experience](https://my.oschina.net/jeecg/blog/3083313)
|
- Newbie guide: [Quick start](http://www.jeecg.com/doc/quickstart) | [Q&A ](http://www.jeecg.com/doc/qa) | [1 minute experience](https://my.oschina.net/jeecg/blog/3083313)
|
||||||
- QQ group : ⑩716488839、⑨808791225
|
- QQ group : ⑩716488839、⑨808791225、⑧825232878、⑦791696430、⑥730954414(full)、683903138(full)、⑤860162132(full)、④774126647(full)、③816531124(full)、②769925425(full)、①284271917(full)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -181,7 +176,7 @@ Technical Architecture:
|
|||||||
|
|
||||||
#### Development Environment
|
#### Development Environment
|
||||||
|
|
||||||
- Language: Java Default Jdk17(support jdk8、jdk21)
|
- Language: Java 8+ (17)
|
||||||
|
|
||||||
- IDE(JAVA) : IDEA (lombok plug-in must be installed)
|
- IDE(JAVA) : IDEA (lombok plug-in must be installed)
|
||||||
|
|
||||||
@ -198,17 +193,17 @@ Technical Architecture:
|
|||||||
|
|
||||||
- Basic framework: Spring Boot 2.7.18
|
- Basic framework: Spring Boot 2.7.18
|
||||||
|
|
||||||
- Microservice framework: Spring Cloud Alibaba 2021.0.6.2
|
- Microservice framework: Spring Cloud Alibaba 2021.0.1.0
|
||||||
|
|
||||||
- Persistence layer framework: MybatisPlus 3.5.3.2
|
- Persistence layer framework: MybatisPlus 3.5.3.2
|
||||||
|
|
||||||
- Report tool: JimuReport 1.9.5
|
- Report tool: JimuReport 1.9.3
|
||||||
|
|
||||||
- Security framework: Apache Shiro 1.13.0, Jwt 4.5.0
|
- Security framework: Apache Shiro 1.12.0, Jwt 3.11.0
|
||||||
|
|
||||||
- Microservice technology stack: Spring Cloud Alibaba, Nacos, Gateway, Sentinel, Skywalking
|
- Microservice technology stack: Spring Cloud Alibaba, Nacos, Gateway, Sentinel, Skywalking
|
||||||
|
|
||||||
- Database connection pool: Alibaba Druid 1.1.24
|
- Database connection pool: Alibaba Druid 1.1.22
|
||||||
|
|
||||||
- Log printing: logback
|
- Log printing: logback
|
||||||
|
|
||||||
@ -247,16 +242,8 @@ Technical Architecture:
|
|||||||
| --- | --- |
|
| --- | --- |
|
||||||
| DeepSeek | √ |
|
| DeepSeek | √ |
|
||||||
| ChatGPT | √ |
|
| ChatGPT | √ |
|
||||||
| Qwq | √ |
|
|
||||||
| 智库 | √ |
|
|
||||||
| Ollama本地搭建大模型 | √ |
|
|
||||||
| 等等。。 | √ |
|
|
||||||
|
|
||||||
|
|
||||||
AI Config: https://help.jeecg.com/java/ai/aichat
|
|
||||||
|
|
||||||
AI APP: https://help.jeecg.com/aigc
|
|
||||||
|
|
||||||
|
AI Config: https://help.jeecg.com/java/ai/aichat.html
|
||||||
|
|
||||||
## Microservice solutions
|
## Microservice solutions
|
||||||
|
|
||||||
@ -268,7 +255,7 @@ AI APP: https://help.jeecg.com/aigc
|
|||||||
- 6. Distributed files Minio and Alioss √
|
- 6. Distributed files Minio and Alioss √
|
||||||
- 7. Unified permission control
|
- 7. Unified permission control
|
||||||
- 8. Service monitoring SpringBootAdmin√
|
- 8. Service monitoring SpringBootAdmin√
|
||||||
- 9. link tracking Skywalking [reference document](https://help.jeecg.com/java/springcloud/super/skywarking)
|
- 9. link tracking Skywalking [reference document](https://help.jeecg.com/java/springcloud/super/skywarking.html)
|
||||||
- 10. Messaging middleware RabbitMQ √
|
- 10. Messaging middleware RabbitMQ √
|
||||||
- 11. Distributed task xxl-job √
|
- 11. Distributed task xxl-job √
|
||||||
- 12. Distributed Transaction Seata
|
- 12. Distributed Transaction Seata
|
||||||
@ -285,8 +272,8 @@ AI APP: https://help.jeecg.com/aigc
|
|||||||

|

|
||||||
|
|
||||||
### quick start
|
### quick start
|
||||||
- Microservice Development: [Monomer upgrade to microservice](https://help.jeecg.com/java/springcloud/switchcloud/monomer)
|
- Microservice Development: [Monomer upgrade to microservice](https://help.jeecg.com/java/springcloud/switchcloud/monomer.html)
|
||||||
- [Docker starts the micro-service background](https://help.jeecg.com/java/docker/springcloud)
|
- [Docker starts the micro-service background](https://help.jeecg.com/java/docker/springcloud.html)
|
||||||
|
|
||||||
|
|
||||||
### Effect of system
|
### Effect of system
|
||||||
|
|||||||
155
README.md
155
README.md
@ -2,12 +2,12 @@
|
|||||||
JeecgBoot AI低代码平台
|
JeecgBoot AI低代码平台
|
||||||
===============
|
===============
|
||||||
|
|
||||||
当前最新版本: 3.8.0(发布日期:2025-04-18)
|
当前最新版本: 3.7.3(发布日期:2025-02-10)
|
||||||
|
|
||||||
|
|
||||||
[](https://github.com/jeecgboot/JeecgBoot/blob/master/LICENSE)
|
[](https://github.com/jeecgboot/JeecgBoot/blob/master/LICENSE)
|
||||||
[](http://guojusoft.com)
|
[](http://guojusoft.com)
|
||||||
[](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)
|
||||||
|
|
||||||
@ -16,55 +16,32 @@ JeecgBoot AI低代码平台
|
|||||||
项目介绍
|
项目介绍
|
||||||
-----------------------------------
|
-----------------------------------
|
||||||
|
|
||||||
<h3 align="center">Java AI Low Code Platform</h3>
|
<h3 align="center">Java AI Low Code Platform for Enterprise web applications</h3>
|
||||||
|
|
||||||
JeecgBoot是一款基于AIGC和低代码引擎的AI低代码平台,旨在帮助开发者快速实现低代码开发和构建、部署个性化的 AI 应用。
|
JeecgBoot 是一款基于`BPM`和`代码生成器`的 AI低代码平台!前后端分离架构 SpringBoot2.x/3.x,SpringCloud,Ant Design Vue3,Mybatis-plus,Shiro,JWT,支持微服务、多租户;支持 AI 大模型 DeepSeek 和 ChatGPT、Ollama本地模型; 强大的代码生成器让前后端代码一键生成,无需写任何代码! JeecgBoot 引领 AI 低代码开发模式(AI生成-> OnlineCoding-> 代码生成器-> 手工MERGE), 帮助解决Java项目80%的重复工作,让开发更多关注业务。既能快速提高效率,节省成本,同时又不失灵活性!AIGC能力:AI对话助手、AI建表、AI写文章、AI流程编排、AI知识库问答等等.
|
||||||
前后端分离架构Ant Design&Vue3,SpringBoot,SpringCloud Alibaba,Mybatis-plus,Shiro,强大的代码生成器让前后端代码一键生成,无需写任何代码!
|
|
||||||
成套AI大模型功能: AI模型管理、AI应用、知识库、AI流程编排、AI对话助手等;
|
|
||||||
引领AI低代码开发模式: AIGC生成->OnlineCoding-> 代码生成-> 手工MERGE, 帮助Java项目解决80%的重复工作,让开发更多关注业务,快速提高效率 节省成本,同时又不失灵活性!
|
|
||||||
|
|
||||||
|
JeecgBoot 提供了一系列 `AI能力` `低代码模块`,实现在线开发`真正的零代码`:Online表单开发、Online报表、报表配置能力、在线图表设计、仪表盘设计、大屏设计、移动配置能力、表单设计器、在线设计流程、流程自动化配置、插件能力(可插拔)、AI对话助手,AI建表、AI写文章、AI流程编排、AI知识库问答、AI赋能低代码等等!
|
||||||
|
|
||||||
JeecgBoot 提供了一系列 `低代码能力`,实现`真正的零代码`在线开发:Online表单开发、Online报表、复杂报表设计、打印设计、在线图表设计、仪表盘设计、大屏设计、移动图表能力、表单设计器、在线设计流程、流程自动化配置、插件能力(可插拔)
|
|
||||||
|
|
||||||
`AI赋能低代码:` 目前提供了AI应用、AI模型管理、AI流程编排、AI对话助手,AI建表、AI写文章、AI知识库问答、AI字段建议等功能;支持各种AI大模型ChatGPT、DeepSeek、Ollama、智普、千问等.
|
|
||||||
|
|
||||||
`JEECG宗旨是:` 简单功能由OnlineCoding配置实现,做到`零代码开发`;复杂功能由代码生成器生成进行手工Merge 实现`低代码开发`,既保证了`智能`又兼顾`灵活`;实现了低代码开发的同时又支持灵活编码,解决了当前低代码产品普遍不灵活的弊端!
|
`JEECG宗旨是:` 简单功能由OnlineCoding配置实现,做到`零代码开发`;复杂功能由代码生成器生成进行手工Merge 实现`低代码开发`,既保证了`智能`又兼顾`灵活`;实现了低代码开发的同时又支持灵活编码,解决了当前低代码产品普遍不灵活的弊端!
|
||||||
|
|
||||||
`JEECG业务流程:` 采用工作流来实现、扩展出任务接口,供开发编写业务逻辑,表单提供多种解决方案: 表单设计器、online配置表单、编码表单。同时实现了流程与表单的分离设计(松耦合)、并支持任务节点灵活配置,既保证了公司流程的保密性,又减少了开发人员的工作量。
|
`JEECG业务流程:` 采用工作流来实现、扩展出任务接口,供开发编写业务逻辑,表单提供多种解决方案: 表单设计器、online配置表单、编码表单。同时实现了流程与表单的分离设计(松耦合)、并支持任务节点灵活配置,既保证了公司流程的保密性,又减少了开发人员的工作量。
|
||||||
|
|
||||||
|
`AI赋能低代码:` 目前JeecgBoot支持AI大模型`ChatGPT`和`DeepSeek`,现在最新版默认使用`DeepSeek`,速度更快质量更高。目前提供了AI对话助手、AI建表、AI报表、AI写文章、AI流程编排、AI知识库问答等功能。
|
||||||
|
|
||||||
### 视频介绍
|
|
||||||
|
|
||||||
[](https://www.bilibili.com/video/BV1Nk4y1o7Qc)
|
|
||||||
|
|
||||||
|
|
||||||
适用项目
|
适用项目
|
||||||
-----------------------------------
|
-----------------------------------
|
||||||
JeecgBoot AI低代码平台,可以应用在任何J2EE项目的开发中,支持信创国产化。尤其适合SAAS项目、企业信息管理系统(MIS)、内部办公系统(OA)、企业资源计划系统(ERP)、客户关系管理系统(CRM)等,其半智能手工Merge的开发方式,可以显著提高开发效率70%以上,极大降低开发成本。
|
JeecgBoot AI低代码平台,可以应用在任何J2EE项目的开发中,支持信创国产化(默认适配达梦和人大金仓)。尤其适合SAAS项目、企业信息管理系统(MIS)、内部办公系统(OA)、企业资源计划系统(ERP)、客户关系管理系统(CRM)等,其半智能手工Merge的开发方式,可以显著提高开发效率70%以上,极大降低开发成本。
|
||||||
又是一个全栈式 AI 开发平台,快速帮助企业构建和部署个性化的 AI 应用。
|
|
||||||
|
|
||||||
|
|
||||||
信创国产化
|
#### 项目说明
|
||||||
-----------------------------------
|
|
||||||
JeecgBoot 是一个开源低代码开发平台,支持全信创环境。它兼容多种国产操作系统和数据库,包括:
|
|
||||||
|
|
||||||
- 操作系统:国产麒麟、银河麒麟等国产系统几乎都是基于 Linux 内核,因此它们具有良好的兼容性。
|
|
||||||
- 数据库:达梦、人大金仓、TiDB , [转库文档](https://my.oschina.net/jeecg/blog/4905722)
|
|
||||||
- 中间件:东方通 TongWeb、TongRDS,宝兰德 AppServer、CacheDB, [信创配置文档](https://help.jeecg.com/java/tongweb-deploy/)
|
|
||||||
|
|
||||||
通过这些适配,JeecgBoot 为使用国产软件和硬件的用户提供了高效的开发解决方案。
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
项目说明
|
|
||||||
-----------------------------------
|
|
||||||
|
|
||||||
| 项目名 | 说明 |
|
| 项目名 | 说明 |
|
||||||
|--------------------|------------------------|
|
|--------------------|------------------------|
|
||||||
| `jeecg-boot` | 后端源码JAVA(SpringBoot微服务架构) |
|
| `jeecg-boot` | 后端源码JAVA(SpringBoot微服务架构) |
|
||||||
| `jeecgboot-vue3` | 前端源码VUE3(vue3+vite6+ts最新技术栈) |
|
| `jeecgboot-vue3` | 前端源码VUE3(vue3+vite6+ts最新技术栈) |
|
||||||
| `JeecgUniapp` | [配套APP框架](https://github.com/jeecgboot/JeecgUniapp) 适配多个终端,支持APP、小程序、H5 |
|
| `jeecg-uniapp` | [配套APP框架](https://github.com/jeecgboot/jeecg-uniapp) 适配多个终端,支持APP、小程序、H5 |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -72,78 +49,39 @@ JeecgBoot 是一个开源低代码开发平台,支持全信创环境。它兼
|
|||||||
-----------------------------------
|
-----------------------------------
|
||||||
|
|
||||||
- 官方网站: [http://www.jeecg.com](http://www.jeecg.com)
|
- 官方网站: [http://www.jeecg.com](http://www.jeecg.com)
|
||||||
- 在线演示 : [平台演示](http://boot3.jeecg.com) | [APP演示](http://jeecg.com/appIndex) | [体验低代码](https://jeecg.blog.csdn.net/article/details/106079007) | [体验零代码](https://app.qiaoqiaoyun.com/myapps/index)
|
- 在线演示 : [在线演示](http://boot3.jeecg.com) | [APP演示](http://jeecg.com/appIndex)
|
||||||
- 开发文档: [文档中心](https://help.jeecg.com) | [AIGC大模块](https://help.jeecg.com/aigc)
|
- 快速体验: [一分钟体验低代码](https://jeecg.blog.csdn.net/article/details/106079007?spm=1001.2014.3001.5502 "一分钟体验零代码") | [在线体验零代码](https://app.qiaoqiaoyun.com/myapps/index "在线体验零代码")
|
||||||
- 新手指南: [快速入门](http://www.jeecg.com/doc/quickstart) | [入门视频](http://jeecg.com/doc/video) | [如何反馈问题](https://github.com/jeecgboot/JeecgBoot/issues/new?template=bug_report.md)
|
- 开发文档: [文档中心](https://help.jeecg.com) | [AI集成配置(支持DeepSeek)](https://help.jeecg.com/java/ai/aichat.html)
|
||||||
|
- 反馈问题: [在Github上提Issues](https://github.com/jeecgboot/JeecgBoot/issues/new)
|
||||||
|
- 新手指南: [快速入门](http://www.jeecg.com/doc/quickstart) | [入门视频](http://jeecg.com/doc/video)
|
||||||
- QQ交流群 : ⑩716488839、⑨808791225(满)、其他(满)
|
- QQ交流群 : ⑩716488839、⑨808791225(满)、其他(满)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
启动项目
|
启动项目
|
||||||
-----------------------------------
|
-----------------------------------
|
||||||
|
|
||||||
- [IDEA启动前后端项目](https://help.jeecg.com/java/setup/idea/startup)
|
- [IDEA启动前后端项目](https://help.jeecg.com/java/setup/idea/startup.html)
|
||||||
- [Docker一键启动前后端](https://help.jeecg.com/java/docker/quick)
|
- [Docker一键启动前后端](https://help.jeecg.com/java/docker/quick.html)
|
||||||
|
|
||||||
|
|
||||||
|
AIGC功能清单
|
||||||
AIGC应用平台介绍
|
|
||||||
-----------------------------------
|
-----------------------------------
|
||||||
|
|
||||||
JeecgBoot 平台的AIGC功能模块,是一套类似`Dify`的`AIGC应用开发平台`+`知识库问答`,是一款基于LLM大语言模型AI应用平台和 RAG 的知识库问答系统。
|
- AI对聊天助手
|
||||||
其直观的界面结合了 AI 流程编排、RAG 管道、知识库管理、模型管理、对接向量库、实时运行可观察等,让您可以快速从原型到生产,拥有AI服务能力。
|
|
||||||
|
|
||||||
> JDK说明:AI流程编排引擎暂时不支持jdk21,所以目前只能使用jdk8或者jdk17启动项目。
|
|
||||||
|
|
||||||
- [AIGC专题介绍页](README-AI.md)
|
|
||||||
- [AIGC开发文档](https://help.jeecg.com/aigc)
|
|
||||||
- [配置向量库PGVector](https://help.jeecg.com/aigc/config)
|
|
||||||
|
|
||||||
|
|
||||||
##### AI视频介绍
|
|
||||||
|
|
||||||
[](https://www.bilibili.com/video/BV1zmd7YFE4w)
|
|
||||||
|
|
||||||
|
|
||||||
##### 在线体验
|
|
||||||
|
|
||||||
- JeecgBoot演示: https://boot3.jeecg.com
|
|
||||||
- 敲敲云在线搭建AI知识库:https://app.qiaoqiaoyun.com
|
|
||||||
|
|
||||||
##### Dify `VS` JEECG AI
|
|
||||||
|
|
||||||
> JEECG AI与Dify相比,在多个方面展现出显著的优势,特别是在文档处理、格式和图片保持方面。以下是一些具体的优点:
|
|
||||||
> - Markdown文档库导入:
|
|
||||||
> JEECG AI允许用户直接导入整个Markdown文档库,这不仅保留markdown格式,还支持图片的导入,确保文档内容的完整性和可视化效果。
|
|
||||||
> - 对话回复格式美观:
|
|
||||||
> 在对话过程中,JEECG AI能够保持回复内容的原格式,也不丢失图片,使得输出的文章更加美观,不会出现格式错乱的情况,还支持图片的渲染。
|
|
||||||
> - PDF文档导入与格式转换:
|
|
||||||
> JEECG AI在处理PDF文档时,能够更好地保持原始格式和图片,确保转换后的内容与原始文档一致。这个功能在许多AI产品中表现不佳,而JEECG AI在这方面做出了显著的优化
|
|
||||||
|
|
||||||
##### 功能大模块
|
|
||||||
|
|
||||||
- AI应用开发平台
|
|
||||||
- AI知识库系统
|
|
||||||
- AI大模型管理
|
|
||||||
- AI流程编排
|
|
||||||
- AI对话支持图片
|
|
||||||
- AI对话助手(智能问答)
|
|
||||||
- AI建表(Online表单)
|
- AI建表(Online表单)
|
||||||
- AI写文章(CMS)
|
- AI写文章(CMS)
|
||||||
- AI表单字段建议(表单设计器)
|
- AI表单建议(表单设计器)
|
||||||
|
- AI流程编排(研发中)
|
||||||
##### AI大模型支持
|
- AI知识库问答系统(研发中)
|
||||||
|
- AI应用开发平台(研发中)
|
||||||
| AI大模型 | 支持 |
|
- AI聊天窗口支持嵌入第三方(研发中)
|
||||||
| --- | --- |
|
|
||||||
| DeepSeek | √ |
|
|
||||||
| ChatGTP | √ |
|
|
||||||
| Qwq | √ |
|
|
||||||
| 智库 | √ |
|
|
||||||
| Ollama本地模型 | √ |
|
|
||||||
| 等等。。 | √ |
|
|
||||||
|
|
||||||
|
|
||||||
|
<b>关注公众号了解官方动态</b>
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
技术架构:
|
技术架构:
|
||||||
@ -152,15 +90,15 @@ JeecgBoot 平台的AIGC功能模块,是一套类似`Dify`的`AIGC应用开发
|
|||||||
#### 后端
|
#### 后端
|
||||||
|
|
||||||
- IDE建议: IDEA (必须安装lombok插件 )
|
- IDE建议: IDEA (必须安装lombok插件 )
|
||||||
- 语言:Java 默认jdk17(支持jdk8、jdk21)
|
- 语言:Java 8+ (支持17)
|
||||||
- 依赖管理:Maven
|
- 依赖管理:Maven
|
||||||
- 基础框架:Spring Boot 2.7.18
|
- 基础框架:Spring Boot 2.7.18
|
||||||
- 微服务框架: Spring Cloud Alibaba 2021.0.6.2
|
- 微服务框架: Spring Cloud Alibaba 2021.0.1.0
|
||||||
- 持久层框架:MybatisPlus 3.5.3.2
|
- 持久层框架:MybatisPlus 3.5.3.2
|
||||||
- 报表工具: JimuReport 1.9.5
|
- 报表工具: JimuReport 1.9.3
|
||||||
- 安全框架:Apache Shiro 1.13.0,Jwt 4.5.0
|
- 安全框架:Apache Shiro 1.12.0,Jwt 3.11.0
|
||||||
- 微服务技术栈:Spring Cloud Alibaba、Nacos、Gateway、Sentinel、Skywalking
|
- 微服务技术栈:Spring Cloud Alibaba、Nacos、Gateway、Sentinel、Skywalking
|
||||||
- 数据库连接池:阿里巴巴Druid 1.1.24
|
- 数据库连接池:阿里巴巴Druid 1.1.22
|
||||||
- AI大模型:支持 `ChatGPT` `DeepSeek`切换
|
- AI大模型:支持 `ChatGPT` `DeepSeek`切换
|
||||||
- 日志打印:logback
|
- 日志打印:logback
|
||||||
- 缓存:Redis
|
- 缓存:Redis
|
||||||
@ -186,9 +124,7 @@ JeecgBoot 平台的AIGC功能模块,是一套类似`Dify`的`AIGC应用开发
|
|||||||
` ( 因为Vite6 需要 Node.js 18 / 20+ )`
|
` ( 因为Vite6 需要 Node.js 18 / 20+ )`
|
||||||
|
|
||||||
|
|
||||||
#### 平台支持数据库
|
#### 支持库
|
||||||
|
|
||||||
> jeecgboot平台支持以下数据库,默认我们只提供mysql脚本,其他数据库可以参考[转库文档](https://my.oschina.net/jeecg/blog/4905722)自己转。
|
|
||||||
|
|
||||||
| 数据库 | 支持 |
|
| 数据库 | 支持 |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
@ -197,11 +133,20 @@ JeecgBoot 平台的AIGC功能模块,是一套类似`Dify`的`AIGC应用开发
|
|||||||
| Sqlserver2017 | √ |
|
| Sqlserver2017 | √ |
|
||||||
| PostgreSQL | √ |
|
| PostgreSQL | √ |
|
||||||
| MariaDB | √ |
|
| MariaDB | √ |
|
||||||
|
| MariaDB | √ |
|
||||||
| 达梦 | √ |
|
| 达梦 | √ |
|
||||||
| 人大金仓 | √ |
|
| 人大金仓 | √ |
|
||||||
| TiDB | √ |
|
| TiDB | √ |
|
||||||
| kingbase8 | √ |
|
|
||||||
|
|
||||||
|
#### 支持AI大模型
|
||||||
|
|
||||||
|
| AI大模型 | 支持 |
|
||||||
|
| --- | --- |
|
||||||
|
| DeepSeek | √ |
|
||||||
|
| ChatGTP | √ |
|
||||||
|
| Ollama本地搭建大模型 | √ |
|
||||||
|
|
||||||
|
AI集成文档: https://help.jeecg.com/java/ai/aichat.html
|
||||||
|
|
||||||
|
|
||||||
## 微服务解决方案
|
## 微服务解决方案
|
||||||
@ -215,7 +160,7 @@ JeecgBoot 平台的AIGC功能模块,是一套类似`Dify`的`AIGC应用开发
|
|||||||
- 6、分布式文件 Minio、阿里OSS √
|
- 6、分布式文件 Minio、阿里OSS √
|
||||||
- 7、统一权限控制 JWT + Shiro √
|
- 7、统一权限控制 JWT + Shiro √
|
||||||
- 8、服务监控 SpringBootAdmin√
|
- 8、服务监控 SpringBootAdmin√
|
||||||
- 9、链路跟踪 Skywalking [参考文档](https://help.jeecg.com/java/springcloud/super/skywarking)
|
- 9、链路跟踪 Skywalking [参考文档](https://help.jeecg.com/java/springcloud/super/skywarking.html)
|
||||||
- 10、消息中间件 RabbitMQ √
|
- 10、消息中间件 RabbitMQ √
|
||||||
- 11、分布式任务 xxl-job √
|
- 11、分布式任务 xxl-job √
|
||||||
- 12、分布式事务 Seata
|
- 12、分布式事务 Seata
|
||||||
@ -227,8 +172,8 @@ JeecgBoot 平台的AIGC功能模块,是一套类似`Dify`的`AIGC应用开发
|
|||||||
|
|
||||||
#### 微服务方式启动
|
#### 微服务方式启动
|
||||||
|
|
||||||
- [单体快速切换微服务](https://help.jeecg.com/java/springcloud/switchcloud/monomer)
|
- [单体快速切换微服务](https://help.jeecg.com/java/springcloud/switchcloud/monomer.html)
|
||||||
- [Docker一键启动微服务前后端](https://help.jeecg.com/java/docker/quickcloud)
|
- [Docker一键启动微服务前后端](https://help.jeecg.com/java/docker/quickcloud.html)
|
||||||
|
|
||||||
|
|
||||||
#### 微服务架构图
|
#### 微服务架构图
|
||||||
@ -309,10 +254,10 @@ JeecgBoot 平台的AIGC功能模块,是一套类似`Dify`的`AIGC应用开发
|
|||||||
│ ├─AI对话助手
|
│ ├─AI对话助手
|
||||||
│ ├─AI建表
|
│ ├─AI建表
|
||||||
│ ├─AI写文章
|
│ ├─AI写文章
|
||||||
│ ├─AI流程编排
|
│ ├─AI流程编排(研发中)
|
||||||
│ ├─AI知识库问答系统
|
│ ├─AI知识库问答系统(研发中)
|
||||||
│ ├─AI应用开发平台
|
│ ├─AI应用开发平台(研发中)
|
||||||
│ ├─AI聊天窗口支持嵌入第三方
|
│ ├─AI聊天窗口支持嵌入第三方(研发中)
|
||||||
├─Online在线开发(低代码)
|
├─Online在线开发(低代码)
|
||||||
│ ├─Online在线表单
|
│ ├─Online在线表单
|
||||||
│ ├─Online代码生成器
|
│ ├─Online代码生成器
|
||||||
|
|||||||
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
|
||||||
@ -2,12 +2,12 @@
|
|||||||
JeecgBoot 低代码开发平台
|
JeecgBoot 低代码开发平台
|
||||||
===============
|
===============
|
||||||
|
|
||||||
当前最新版本: 3.8.0(发布日期:2025-05-16)
|
当前最新版本: 3.7.3(发布日期:2025-02-10)
|
||||||
|
|
||||||
|
|
||||||
[](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)
|
||||||
|
|
||||||
@ -44,14 +44,14 @@ JeecgBoot 是一款基于代码生成器的`低代码开发平台`!前后端
|
|||||||
启动项目
|
启动项目
|
||||||
-----------------------------------
|
-----------------------------------
|
||||||
|
|
||||||
- [IDEA启动前后端项目](https://help.jeecg.com/java/setup/idea/startup)
|
- [IDEA启动前后端项目](https://help.jeecg.com/java/setup/idea/startup.html)
|
||||||
- [Docker一键启动前后端](https://help.jeecg.com/java/docker/quick)
|
- [Docker一键启动前后端](https://help.jeecg.com/java/docker/quick.html)
|
||||||
|
|
||||||
|
|
||||||
微服务启动
|
微服务启动
|
||||||
-----------------------------------
|
-----------------------------------
|
||||||
- [单体快速切换微服务](https://help.jeecg.com/java/springcloud/switchcloud/monomer)
|
- [单体快速切换微服务](https://help.jeecg.com/java/springcloud/switchcloud/monomer.html)
|
||||||
- [Docker启动微服务后台](https://help.jeecg.com/java/docker/springcloud)
|
- [Docker启动微服务后台](https://help.jeecg.com/java/docker/springcloud.html)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -66,10 +66,10 @@ JeecgBoot 是一款基于代码生成器的`低代码开发平台`!前后端
|
|||||||
- 基础框架:Spring Boot 2.7.18
|
- 基础框架:Spring Boot 2.7.18
|
||||||
- 微服务框架: Spring Cloud Alibaba 2021.0.1.0
|
- 微服务框架: Spring Cloud Alibaba 2021.0.1.0
|
||||||
- 持久层框架:MybatisPlus 3.5.3.2
|
- 持久层框架:MybatisPlus 3.5.3.2
|
||||||
- 报表工具: JimuReport 1.9.4
|
- 报表工具: JimuReport 1.8.1
|
||||||
- 安全框架:Apache Shiro 1.12.0,Jwt 3.11.0
|
- 安全框架:Apache Shiro 1.12.0,Jwt 3.11.0
|
||||||
- 微服务技术栈:Spring Cloud Alibaba、Nacos、Gateway、Sentinel、Skywalking
|
- 微服务技术栈:Spring Cloud Alibaba、Nacos、Gateway、Sentinel、Skywalking
|
||||||
- 数据库连接池:阿里巴巴Druid 1.1.24
|
- 数据库连接池:阿里巴巴Druid 1.1.22
|
||||||
- 日志打印:logback
|
- 日志打印:logback
|
||||||
- 缓存:Redis
|
- 缓存:Redis
|
||||||
- 其他:autopoi, fastjson,poi,Swagger-ui,quartz, lombok(简化代码)等。
|
- 其他:autopoi, fastjson,poi,Swagger-ui,quartz, lombok(简化代码)等。
|
||||||
@ -113,7 +113,7 @@ JeecgBoot 是一款基于代码生成器的`低代码开发平台`!前后端
|
|||||||
- 6、分布式文件 Minio、阿里OSS √
|
- 6、分布式文件 Minio、阿里OSS √
|
||||||
- 7、统一权限控制 JWT + Shiro √
|
- 7、统一权限控制 JWT + Shiro √
|
||||||
- 8、服务监控 SpringBootAdmin√
|
- 8、服务监控 SpringBootAdmin√
|
||||||
- 9、链路跟踪 Skywalking [参考文档](https://help.jeecg.com/java/springcloud/super/skywarking)
|
- 9、链路跟踪 Skywalking [参考文档](https://help.jeecg.com/java/springcloud/super/skywarking.html)
|
||||||
- 10、消息中间件 RabbitMQ √
|
- 10、消息中间件 RabbitMQ √
|
||||||
- 11、分布式任务 xxl-job √
|
- 11、分布式任务 xxl-job √
|
||||||
- 12、分布式事务 Seata
|
- 12、分布式事务 Seata
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -1,170 +0,0 @@
|
|||||||
/*
|
|
||||||
Navicat Premium Data Transfer
|
|
||||||
|
|
||||||
Source Server : mysql5.7
|
|
||||||
Source Server Type : MySQL
|
|
||||||
Source Server Version : 50738 (5.7.38)
|
|
||||||
Source Host : 127.0.0.1:3306
|
|
||||||
Source Schema : jeecg-boot
|
|
||||||
|
|
||||||
Target Server Type : MySQL
|
|
||||||
Target Server Version : 50738 (5.7.38)
|
|
||||||
File Encoding : 65001
|
|
||||||
|
|
||||||
Date: 15/05/2025 10:18:36
|
|
||||||
*/
|
|
||||||
|
|
||||||
SET NAMES utf8mb4;
|
|
||||||
SET FOREIGN_KEY_CHECKS = 0;
|
|
||||||
|
|
||||||
-- ----------------------------
|
|
||||||
-- Table structure for open_api
|
|
||||||
-- ----------------------------
|
|
||||||
DROP TABLE IF EXISTS `open_api`;
|
|
||||||
CREATE TABLE `open_api` (
|
|
||||||
`id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
|
|
||||||
`name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '接口名称',
|
|
||||||
`request_method` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '请求方法',
|
|
||||||
`request_url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '接口地址',
|
|
||||||
`black_list` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT 'IP 黑名单',
|
|
||||||
`body` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '请求体内容',
|
|
||||||
`origin_url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '原始地址',
|
|
||||||
`status` int(10) NULL DEFAULT NULL COMMENT '状态',
|
|
||||||
`del_flag` int(10) NULL DEFAULT NULL COMMENT '删除标识',
|
|
||||||
`create_by` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '创建人',
|
|
||||||
`create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
|
|
||||||
`update_by` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '修改人',
|
|
||||||
`update_time` datetime NULL DEFAULT NULL COMMENT '修改时间',
|
|
||||||
`headers_json` json NULL COMMENT '请求头json',
|
|
||||||
`params_json` json NULL COMMENT '请求参数json',
|
|
||||||
PRIMARY KEY (`id`) USING BTREE
|
|
||||||
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '接口表' ROW_FORMAT = DYNAMIC;
|
|
||||||
|
|
||||||
-- ----------------------------
|
|
||||||
-- Records of open_api
|
|
||||||
-- ----------------------------
|
|
||||||
INSERT INTO `open_api` VALUES ('1922132683346649090', '根据部门查询用户', 'GET', 'TEwcXBlr', NULL, NULL, '/sys/user/queryUserByDepId', 1, 0, 'admin', '2025-05-13 11:31:58', 'admin', '2025-05-15 10:10:01', '[]', '[{\"id\": \"row_24\", \"note\": \"\", \"paramKey\": \"id\", \"required\": \"1\", \"defaultValue\": \"\"}]');
|
|
||||||
|
|
||||||
-- ----------------------------
|
|
||||||
-- Table structure for open_api_auth
|
|
||||||
-- ----------------------------
|
|
||||||
DROP TABLE IF EXISTS `open_api_auth`;
|
|
||||||
CREATE TABLE `open_api_auth` (
|
|
||||||
`id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
|
|
||||||
`name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '授权名称',
|
|
||||||
`ak` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT 'AK',
|
|
||||||
`sk` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT 'SK',
|
|
||||||
`create_by` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '创建人',
|
|
||||||
`create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
|
|
||||||
`update_by` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '修改人',
|
|
||||||
`update_time` datetime NULL DEFAULT NULL COMMENT '修改时间',
|
|
||||||
`system_user_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '关联系统用户名',
|
|
||||||
PRIMARY KEY (`id`) USING BTREE
|
|
||||||
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '权限表' ROW_FORMAT = DYNAMIC;
|
|
||||||
|
|
||||||
-- ----------------------------
|
|
||||||
-- Records of open_api_auth
|
|
||||||
-- ----------------------------
|
|
||||||
INSERT INTO `open_api_auth` VALUES ('1922164194775056386', 'scott', 'ak-pFjyNHWRsJEFWlu6', '4hV5dBrZtmGAtPdbA5yseaeKRYNpzGsS', 'admin', '2025-05-13 13:37:11', NULL, NULL, 'e9ca23d68d884d4ebb19d07889727dae');
|
|
||||||
|
|
||||||
-- ----------------------------
|
|
||||||
-- Table structure for open_api_log
|
|
||||||
-- ----------------------------
|
|
||||||
DROP TABLE IF EXISTS `open_api_log`;
|
|
||||||
CREATE TABLE `open_api_log` (
|
|
||||||
`id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
|
|
||||||
`api_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '接口ID',
|
|
||||||
`call_auth_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '调用ID',
|
|
||||||
`call_time` datetime NULL DEFAULT NULL COMMENT '调用时间',
|
|
||||||
`used_time` bigint(20) NULL DEFAULT NULL COMMENT '耗时',
|
|
||||||
`response_time` datetime NULL DEFAULT NULL COMMENT '响应时间',
|
|
||||||
PRIMARY KEY (`id`) USING BTREE
|
|
||||||
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '调用记录表' ROW_FORMAT = DYNAMIC;
|
|
||||||
|
|
||||||
-- ----------------------------
|
|
||||||
-- Records of open_api_log
|
|
||||||
-- ----------------------------
|
|
||||||
INSERT INTO `open_api_log` VALUES ('1922175238557913090', '1922132683346649090', '1922164194775056386', '2025-05-13 14:21:04', 94, '2025-05-13 14:21:04');
|
|
||||||
INSERT INTO `open_api_log` VALUES ('1922175436256432130', '1922132683346649090', '1922164194775056386', '2025-05-13 14:21:51', 38, '2025-05-13 14:21:51');
|
|
||||||
INSERT INTO `open_api_log` VALUES ('1922175487921868802', '1922132683346649090', '1922164194775056386', '2025-05-13 14:22:03', 31, '2025-05-13 14:22:03');
|
|
||||||
INSERT INTO `open_api_log` VALUES ('1922176033789562883', '1922132683346649090', '1922164194775056386', '2025-05-13 14:24:13', 27, '2025-05-13 14:24:13');
|
|
||||||
INSERT INTO `open_api_log` VALUES ('1922176583943835650', '1922132683346649090', '1922164194775056386', '2025-05-13 14:26:25', 39, '2025-05-13 14:26:25');
|
|
||||||
INSERT INTO `open_api_log` VALUES ('1922177249969934337', '1922132683346649090', '1922164194775056386', '2025-05-13 14:28:08', 55250, '2025-05-13 14:29:03');
|
|
||||||
INSERT INTO `open_api_log` VALUES ('1922180212645941249', '1922132683346649090', '1922164194775056386', '2025-05-13 14:40:46', 4162, '2025-05-13 14:40:50');
|
|
||||||
INSERT INTO `open_api_log` VALUES ('1922180441692688385', '1922132683346649090', '1922164194775056386', '2025-05-13 14:41:11', 33346, '2025-05-13 14:41:44');
|
|
||||||
INSERT INTO `open_api_log` VALUES ('1922180521686454273', '1922132683346649090', '1922164194775056386', '2025-05-13 14:42:00', 3570, '2025-05-13 14:42:03');
|
|
||||||
INSERT INTO `open_api_log` VALUES ('1922180965825499138', '1922132683346649090', '1922164194775056386', '2025-05-13 14:42:10', 99211, '2025-05-13 14:43:49');
|
|
||||||
INSERT INTO `open_api_log` VALUES ('1922181034515615746', '1922132683346649090', '1922164194775056386', '2025-05-13 14:43:52', 14005, '2025-05-13 14:44:06');
|
|
||||||
INSERT INTO `open_api_log` VALUES ('1922183171307982850', '1922132683346649090', '1922164194775056386', '2025-05-13 14:52:15', 19834, '2025-05-13 14:52:35');
|
|
||||||
INSERT INTO `open_api_log` VALUES ('1922184177068523521', '1922132683346649090', '1922164194775056386', '2025-05-13 14:56:34', 748, '2025-05-13 14:56:35');
|
|
||||||
INSERT INTO `open_api_log` VALUES ('1922184729043107841', '1922132683346649090', '1922164194775056386', '2025-05-13 14:58:46', 1031, '2025-05-13 14:58:47');
|
|
||||||
INSERT INTO `open_api_log` VALUES ('1922184806453182465', '1922132683346649090', '1922164194775056386', '2025-05-13 14:59:05', 68, '2025-05-13 14:59:05');
|
|
||||||
INSERT INTO `open_api_log` VALUES ('1922184918382379009', '1922132683346649090', '1922164194775056386', '2025-05-13 14:59:10', 22155, '2025-05-13 14:59:32');
|
|
||||||
INSERT INTO `open_api_log` VALUES ('1922185292635844610', '1922132683346649090', '1922164194775056386', '2025-05-13 15:00:55', 6267, '2025-05-13 15:01:01');
|
|
||||||
INSERT INTO `open_api_log` VALUES ('1922186002672791554', '1922132683346649090', '1922164194775056386', '2025-05-13 15:03:23', 27554, '2025-05-13 15:03:50');
|
|
||||||
INSERT INTO `open_api_log` VALUES ('1922187506582425601', '1922132683346649090', '1922164194775056386', '2025-05-13 15:09:45', 3464, '2025-05-13 15:09:49');
|
|
||||||
INSERT INTO `open_api_log` VALUES ('1922187586597163011', '1922132683346649090', '1922164194775056386', '2025-05-13 15:10:08', 82, '2025-05-13 15:10:08');
|
|
||||||
INSERT INTO `open_api_log` VALUES ('1922187924741951490', '1922132683346649090', '1922164194775056386', '2025-05-13 15:10:49', 39590, '2025-05-13 15:11:28');
|
|
||||||
INSERT INTO `open_api_log` VALUES ('1922188138710261761', '1922132683346649090', '1922164194775056386', '2025-05-13 15:12:19', 758, '2025-05-13 15:12:19');
|
|
||||||
INSERT INTO `open_api_log` VALUES ('1922188290661507073', '1922132683346649090', '1922164194775056386', '2025-05-13 15:12:29', 26527, '2025-05-13 15:12:56');
|
|
||||||
INSERT INTO `open_api_log` VALUES ('1922189701755424769', '1922132683346649090', '1922164194775056386', '2025-05-13 15:18:28', 3619, '2025-05-13 15:18:32');
|
|
||||||
INSERT INTO `open_api_log` VALUES ('1922190076784803841', '1922132683346649090', '1922164194775056386', '2025-05-13 15:20:01', 741, '2025-05-13 15:20:02');
|
|
||||||
INSERT INTO `open_api_log` VALUES ('1922836671113101313', '1922132683346649090', '1922164194775056386', '2025-05-15 10:09:21', 186, '2025-05-15 10:09:22');
|
|
||||||
INSERT INTO `open_api_log` VALUES ('1922836856287428610', '1922132683346649090', '1922164194775056386', '2025-05-15 10:10:06', 145, '2025-05-15 10:10:06');
|
|
||||||
|
|
||||||
-- ----------------------------
|
|
||||||
-- Table structure for open_api_permission
|
|
||||||
-- ----------------------------
|
|
||||||
DROP TABLE IF EXISTS `open_api_permission`;
|
|
||||||
CREATE TABLE `open_api_permission` (
|
|
||||||
`id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
|
|
||||||
`api_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '接口ID',
|
|
||||||
`api_auth_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '认证ID',
|
|
||||||
`create_by` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '创建人',
|
|
||||||
`create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
|
|
||||||
`update_by` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '更新人',
|
|
||||||
`update_time` datetime NULL DEFAULT NULL COMMENT '更新时间',
|
|
||||||
PRIMARY KEY (`id`) USING BTREE
|
|
||||||
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'openapi授权' ROW_FORMAT = DYNAMIC;
|
|
||||||
|
|
||||||
-- ----------------------------
|
|
||||||
-- Records of open_api_permission
|
|
||||||
-- ----------------------------
|
|
||||||
INSERT INTO `open_api_permission` VALUES ('1922164225875820545', '1922132683346649090', '1922164194775056386', 'admin', '2025-05-13 13:37:18', NULL, NULL);
|
|
||||||
|
|
||||||
SET FOREIGN_KEY_CHECKS = 1;
|
|
||||||
|
|
||||||
|
|
||||||
INSERT INTO sys_permission (id, parent_id, name, url, component, is_route, component_name, redirect, menu_type, perms, perms_type, sort_no, always_show, icon, is_leaf, keep_alive, hidden, hide_tab, description, create_by, create_time, update_by, update_time, del_flag, rule_flag, status, internal_or_external) VALUES ('1917957565728198657', '1922109301837606914', '接口文档', '/openapi/SwaggerUI', 'openapi/SwaggerUI', 1, '', null, 1, null, '0', 1, 0, null, 1, 0, 0, 0, null, 'admin', '2025-05-01 23:01:32', 'admin', '2025-05-13 09:59:46', 0, 0, null, 0);
|
|
||||||
INSERT INTO sys_permission (id, parent_id, name, url, component, is_route, component_name, redirect, menu_type, perms, perms_type, sort_no, always_show, icon, is_leaf, keep_alive, hidden, hide_tab, description, create_by, create_time, update_by, update_time, del_flag, rule_flag, status, internal_or_external) VALUES ('1922109301837606914', '', 'OpenApi管理', '/openapi', 'layouts/RouteView', 1, '', null, 0, null, '0', 12.1, 0, 'ant-design:swap-outlined', 0, 0, 0, 0, null, 'admin', '2025-05-13 09:59:03', 'admin', '2025-05-13 10:02:43', 0, 0, null, 0);
|
|
||||||
INSERT INTO sys_permission (id, parent_id, name, url, component, is_route, component_name, redirect, menu_type, perms, perms_type, sort_no, always_show, icon, is_leaf, keep_alive, hidden, hide_tab, description, create_by, create_time, update_by, update_time, del_flag, rule_flag, status, internal_or_external) VALUES ('2025050104193340030', '1922109301837606914', '接口管理', '/openapi/openApiList', 'openapi/OpenApiList', 1, null, null, 1, null, '1', 0, 0, null, 0, 0, 0, 0, null, 'admin', '2025-05-01 16:19:03', 'admin', '2025-05-13 09:59:24', 0, 0, '1', 0);
|
|
||||||
INSERT INTO sys_permission (id, parent_id, name, url, component, is_route, component_name, redirect, menu_type, perms, perms_type, sort_no, always_show, icon, is_leaf, keep_alive, hidden, hide_tab, description, create_by, create_time, update_by, update_time, del_flag, rule_flag, status, internal_or_external) VALUES ('2025050104193350031', '2025050104193340030', '添加接口管理', null, null, 0, null, null, 2, 'openapi:open_api:add', '1', null, 0, null, 1, 0, 0, 0, null, 'admin', '2025-05-01 16:19:03', null, null, 0, 0, '1', 0);
|
|
||||||
INSERT INTO sys_permission (id, parent_id, name, url, component, is_route, component_name, redirect, menu_type, perms, perms_type, sort_no, always_show, icon, is_leaf, keep_alive, hidden, hide_tab, description, create_by, create_time, update_by, update_time, del_flag, rule_flag, status, internal_or_external) VALUES ('2025050104193350032', '2025050104193340030', '编辑接口管理', null, null, 0, null, null, 2, 'openapi:open_api:edit', '1', null, 0, null, 1, 0, 0, 0, null, 'admin', '2025-05-01 16:19:03', null, null, 0, 0, '1', 0);
|
|
||||||
INSERT INTO sys_permission (id, parent_id, name, url, component, is_route, component_name, redirect, menu_type, perms, perms_type, sort_no, always_show, icon, is_leaf, keep_alive, hidden, hide_tab, description, create_by, create_time, update_by, update_time, del_flag, rule_flag, status, internal_or_external) VALUES ('2025050104193350033', '2025050104193340030', '删除接口管理', null, null, 0, null, null, 2, 'openapi:open_api:delete', '1', null, 0, null, 1, 0, 0, 0, null, 'admin', '2025-05-01 16:19:03', null, null, 0, 0, '1', 0);
|
|
||||||
INSERT INTO sys_permission (id, parent_id, name, url, component, is_route, component_name, redirect, menu_type, perms, perms_type, sort_no, always_show, icon, is_leaf, keep_alive, hidden, hide_tab, description, create_by, create_time, update_by, update_time, del_flag, rule_flag, status, internal_or_external) VALUES ('2025050104193350034', '2025050104193340030', '批量删除接口管理', null, null, 0, null, null, 2, 'openapi:open_api:deleteBatch', '1', null, 0, null, 1, 0, 0, 0, null, 'admin', '2025-05-01 16:19:03', null, null, 0, 0, '1', 0);
|
|
||||||
INSERT INTO sys_permission (id, parent_id, name, url, component, is_route, component_name, redirect, menu_type, perms, perms_type, sort_no, always_show, icon, is_leaf, keep_alive, hidden, hide_tab, description, create_by, create_time, update_by, update_time, del_flag, rule_flag, status, internal_or_external) VALUES ('2025050104193350035', '2025050104193340030', '导出excel_接口管理', null, null, 0, null, null, 2, 'openapi:open_api:exportXls', '1', null, 0, null, 1, 0, 0, 0, null, 'admin', '2025-05-01 16:19:03', null, null, 0, 0, '1', 0);
|
|
||||||
INSERT INTO sys_permission (id, parent_id, name, url, component, is_route, component_name, redirect, menu_type, perms, perms_type, sort_no, always_show, icon, is_leaf, keep_alive, hidden, hide_tab, description, create_by, create_time, update_by, update_time, del_flag, rule_flag, status, internal_or_external) VALUES ('2025050104193350036', '2025050104193340030', '导入excel_接口管理', null, null, 0, null, null, 2, 'openapi:open_api:importExcel', '1', null, 0, null, 1, 0, 0, 0, null, 'admin', '2025-05-01 16:19:03', null, null, 0, 0, '1', 0);
|
|
||||||
INSERT INTO sys_permission (id, parent_id, name, url, component, is_route, component_name, redirect, menu_type, perms, perms_type, sort_no, always_show, icon, is_leaf, keep_alive, hidden, hide_tab, description, create_by, create_time, update_by, update_time, del_flag, rule_flag, status, internal_or_external) VALUES ('2025050105554940200', '1922109301837606914', '授权管理', '/openapi/openApiAuthList', 'openapi/OpenApiAuthList', 1, null, null, 1, null, '1', 0, 0, null, 0, 0, 0, 0, null, 'admin', '2025-05-01 17:55:20', 'admin', '2025-05-13 09:59:35', 0, 0, '1', 0);
|
|
||||||
INSERT INTO sys_permission (id, parent_id, name, url, component, is_route, component_name, redirect, menu_type, perms, perms_type, sort_no, always_show, icon, is_leaf, keep_alive, hidden, hide_tab, description, create_by, create_time, update_by, update_time, del_flag, rule_flag, status, internal_or_external) VALUES ('2025050105554940201', '2025050105554940200', '添加授权管理', null, null, 0, null, null, 2, 'openapi:open_api_auth:add', '1', null, 0, null, 1, 0, 0, 0, null, 'admin', '2025-05-01 17:55:20', null, null, 0, 0, '1', 0);
|
|
||||||
INSERT INTO sys_permission (id, parent_id, name, url, component, is_route, component_name, redirect, menu_type, perms, perms_type, sort_no, always_show, icon, is_leaf, keep_alive, hidden, hide_tab, description, create_by, create_time, update_by, update_time, del_flag, rule_flag, status, internal_or_external) VALUES ('2025050105554940202', '2025050105554940200', '编辑授权管理', null, null, 0, null, null, 2, 'openapi:open_api_auth:edit', '1', null, 0, null, 1, 0, 0, 0, null, 'admin', '2025-05-01 17:55:20', null, null, 0, 0, '1', 0);
|
|
||||||
INSERT INTO sys_permission (id, parent_id, name, url, component, is_route, component_name, redirect, menu_type, perms, perms_type, sort_no, always_show, icon, is_leaf, keep_alive, hidden, hide_tab, description, create_by, create_time, update_by, update_time, del_flag, rule_flag, status, internal_or_external) VALUES ('2025050105554940203', '2025050105554940200', '删除授权管理', null, null, 0, null, null, 2, 'openapi:open_api_auth:delete', '1', null, 0, null, 1, 0, 0, 0, null, 'admin', '2025-05-01 17:55:20', null, null, 0, 0, '1', 0);
|
|
||||||
INSERT INTO sys_permission (id, parent_id, name, url, component, is_route, component_name, redirect, menu_type, perms, perms_type, sort_no, always_show, icon, is_leaf, keep_alive, hidden, hide_tab, description, create_by, create_time, update_by, update_time, del_flag, rule_flag, status, internal_or_external) VALUES ('2025050105554940204', '2025050105554940200', '批量删除授权管理', null, null, 0, null, null, 2, 'openapi:open_api_auth:deleteBatch', '1', null, 0, null, 1, 0, 0, 0, null, 'admin', '2025-05-01 17:55:20', null, null, 0, 0, '1', 0);
|
|
||||||
INSERT INTO sys_permission (id, parent_id, name, url, component, is_route, component_name, redirect, menu_type, perms, perms_type, sort_no, always_show, icon, is_leaf, keep_alive, hidden, hide_tab, description, create_by, create_time, update_by, update_time, del_flag, rule_flag, status, internal_or_external) VALUES ('2025050105554940205', '2025050105554940200', '导出excel_授权管理', null, null, 0, null, null, 2, 'openapi:open_api_auth:exportXls', '1', null, 0, null, 1, 0, 0, 0, null, 'admin', '2025-05-01 17:55:20', null, null, 0, 0, '1', 0);
|
|
||||||
INSERT INTO sys_permission (id, parent_id, name, url, component, is_route, component_name, redirect, menu_type, perms, perms_type, sort_no, always_show, icon, is_leaf, keep_alive, hidden, hide_tab, description, create_by, create_time, update_by, update_time, del_flag, rule_flag, status, internal_or_external) VALUES ('2025050105554940206', '2025050105554940200', '导入excel_授权管理', null, null, 0, null, null, 2, 'openapi:open_api_auth:importExcel', '1', null, 0, null, 1, 0, 0, 0, null, 'admin', '2025-05-01 17:55:20', null, null, 0, 0, '1', 0);
|
|
||||||
|
|
||||||
INSERT INTO sys_role_permission (id, role_id, permission_id, data_rule_ids, operate_date, operate_ip) VALUES ('1917957659860963330', 'f6817f48af4fb3af11b9e8bf182f618b', '1917957565728198657', null, '2025-05-01 23:01:55', '0:0:0:0:0:0:0:1');
|
|
||||||
INSERT INTO sys_role_permission (id, role_id, permission_id, data_rule_ids, operate_date, operate_ip) VALUES ('1922109760551858178', 'f6817f48af4fb3af11b9e8bf182f618b', '1922109301837606914', null, '2025-05-13 10:00:53', '0:0:0:0:0:0:0:1');
|
|
||||||
INSERT INTO sys_role_permission (id, role_id, permission_id, data_rule_ids, operate_date, operate_ip) VALUES ('1917857071739539457', 'f6817f48af4fb3af11b9e8bf182f618b', '2025050104193340030', null, '2025-05-01 16:22:13', '0:0:0:0:0:0:0:1');
|
|
||||||
INSERT INTO sys_role_permission (id, role_id, permission_id, data_rule_ids, operate_date, operate_ip) VALUES ('1917857071806648321', 'f6817f48af4fb3af11b9e8bf182f618b', '2025050104193350031', null, '2025-05-01 16:22:13', '0:0:0:0:0:0:0:1');
|
|
||||||
INSERT INTO sys_role_permission (id, role_id, permission_id, data_rule_ids, operate_date, operate_ip) VALUES ('1917857071806648322', 'f6817f48af4fb3af11b9e8bf182f618b', '2025050104193350032', null, '2025-05-01 16:22:13', '0:0:0:0:0:0:0:1');
|
|
||||||
INSERT INTO sys_role_permission (id, role_id, permission_id, data_rule_ids, operate_date, operate_ip) VALUES ('1917857071806648323', 'f6817f48af4fb3af11b9e8bf182f618b', '2025050104193350033', null, '2025-05-01 16:22:13', '0:0:0:0:0:0:0:1');
|
|
||||||
INSERT INTO sys_role_permission (id, role_id, permission_id, data_rule_ids, operate_date, operate_ip) VALUES ('1917857071806648324', 'f6817f48af4fb3af11b9e8bf182f618b', '2025050104193350034', null, '2025-05-01 16:22:13', '0:0:0:0:0:0:0:1');
|
|
||||||
INSERT INTO sys_role_permission (id, role_id, permission_id, data_rule_ids, operate_date, operate_ip) VALUES ('1917857071806648325', 'f6817f48af4fb3af11b9e8bf182f618b', '2025050104193350035', null, '2025-05-01 16:22:13', '0:0:0:0:0:0:0:1');
|
|
||||||
INSERT INTO sys_role_permission (id, role_id, permission_id, data_rule_ids, operate_date, operate_ip) VALUES ('1917857071806648326', 'f6817f48af4fb3af11b9e8bf182f618b', '2025050104193350036', null, '2025-05-01 16:22:13', '0:0:0:0:0:0:0:1');
|
|
||||||
INSERT INTO sys_role_permission (id, role_id, permission_id, data_rule_ids, operate_date, operate_ip) VALUES ('1917881149426864129', 'f6817f48af4fb3af11b9e8bf182f618b', '2025050105554940200', null, '2025-05-01 17:57:53', '0:0:0:0:0:0:0:1');
|
|
||||||
INSERT INTO sys_role_permission (id, role_id, permission_id, data_rule_ids, operate_date, operate_ip) VALUES ('1917881149431058436', 'f6817f48af4fb3af11b9e8bf182f618b', '2025050105554940203', null, '2025-05-01 17:57:53', '0:0:0:0:0:0:0:1');
|
|
||||||
INSERT INTO sys_role_permission (id, role_id, permission_id, data_rule_ids, operate_date, operate_ip) VALUES ('1917881149431058437', 'f6817f48af4fb3af11b9e8bf182f618b', '2025050105554940204', null, '2025-05-01 17:57:53', '0:0:0:0:0:0:0:1');
|
|
||||||
INSERT INTO sys_role_permission (id, role_id, permission_id, data_rule_ids, operate_date, operate_ip) VALUES ('1917881149431058438', 'f6817f48af4fb3af11b9e8bf182f618b', '2025050105554940205', null, '2025-05-01 17:57:53', '0:0:0:0:0:0:0:1');
|
|
||||||
INSERT INTO sys_role_permission (id, role_id, permission_id, data_rule_ids, operate_date, operate_ip) VALUES ('1917881149431058439', 'f6817f48af4fb3af11b9e8bf182f618b', '2025050105554940206', null, '2025-05-01 17:57:53', '0:0:0:0:0:0:0:1');
|
|
||||||
INSERT INTO sys_role_permission (id, role_id, permission_id, data_rule_ids, operate_date, operate_ip) VALUES ('1917957659860963330', 'f6817f48af4fb3af11b9e8bf182f618b', '1917957565728198657', null, '2025-05-01 23:01:55', '0:0:0:0:0:0:0:1');
|
|
||||||
INSERT INTO sys_role_permission (id, role_id, permission_id, data_rule_ids, operate_date, operate_ip) VALUES ('1922109760551858178', 'f6817f48af4fb3af11b9e8bf182f618b', '1922109301837606914', null, '2025-05-13 10:00:53', '0:0:0:0:0:0:0:1');
|
|
||||||
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,11 +4,15 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>org.jeecgframework.boot</groupId>
|
<groupId>org.jeecgframework.boot</groupId>
|
||||||
<artifactId>jeecg-boot-parent</artifactId>
|
<artifactId>jeecg-boot-parent</artifactId>
|
||||||
<version>3.8.1</version>
|
<version>3.7.3</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>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<spring-boot.version>3.1.5</spring-boot.version>
|
||||||
|
</properties>
|
||||||
|
|
||||||
<repositories>
|
<repositories>
|
||||||
<repository>
|
<repository>
|
||||||
<id>aliyun</id>
|
<id>aliyun</id>
|
||||||
@ -108,18 +112,7 @@
|
|||||||
<!-- mybatis-plus -->
|
<!-- mybatis-plus -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.baomidou</groupId>
|
<groupId>com.baomidou</groupId>
|
||||||
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
|
<artifactId>mybatis-plus-boot-starter</artifactId>
|
||||||
<version>${mybatis-plus.version}</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.baomidou</groupId>
|
|
||||||
<artifactId>mybatis-plus-jsqlparser-4.9</artifactId>
|
|
||||||
<version>${mybatis-plus.version}</version>
|
|
||||||
</dependency>
|
|
||||||
<!-- minidao -->
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.baomidou</groupId>
|
|
||||||
<artifactId>mybatis-plus-jsqlparser-4.9</artifactId>
|
|
||||||
<version>${mybatis-plus.version}</version>
|
<version>${mybatis-plus.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
@ -185,6 +178,7 @@
|
|||||||
<version>${dm8.version}</version>
|
<version>${dm8.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
|
||||||
<!-- Quartz定时任务 -->
|
<!-- Quartz定时任务 -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
@ -198,88 +192,26 @@
|
|||||||
<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>
|
|
||||||
</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>
|
||||||
|
|
||||||
|
<!-- knife4j -->
|
||||||
<!-- <dependency>
|
<dependency>
|
||||||
<groupId>com.github.xiaoymin</groupId>
|
<groupId>com.github.xiaoymin</groupId>
|
||||||
<artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
|
<artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
|
||||||
<version>${knife4j-spring-boot-starter.version}</version>
|
<version>${knife4j-spring-boot-starter.version}</version>
|
||||||
</dependency>-->
|
|
||||||
<!-- knife4j 升级springboot3.4.5报错 -->
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.github.xiaoymin</groupId>
|
|
||||||
<artifactId>knife4j-openapi3-ui</artifactId>
|
|
||||||
<version>${knife4j-spring-boot-starter.version}</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.springdoc</groupId>
|
|
||||||
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
|
|
||||||
<version>2.7.0</version>
|
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- 代码生成器 -->
|
<!-- 代码生成器 -->
|
||||||
@ -288,16 +220,6 @@
|
|||||||
<groupId>org.jeecgframework.boot</groupId>
|
<groupId>org.jeecgframework.boot</groupId>
|
||||||
<artifactId>codegenerate</artifactId>
|
<artifactId>codegenerate</artifactId>
|
||||||
<version>${codegenerate.version}</version>
|
<version>${codegenerate.version}</version>
|
||||||
<exclusions>
|
|
||||||
<exclusion>
|
|
||||||
<artifactId>commons-io</artifactId>
|
|
||||||
<groupId>commons-io</groupId>
|
|
||||||
</exclusion>
|
|
||||||
<exclusion>
|
|
||||||
<artifactId>mysql-connector-java</artifactId>
|
|
||||||
<groupId>mysql</groupId>
|
|
||||||
</exclusion>
|
|
||||||
</exclusions>
|
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- AutoPoi Excel工具类-->
|
<!-- AutoPoi Excel工具类-->
|
||||||
@ -327,12 +249,6 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.minio</groupId>
|
<groupId>io.minio</groupId>
|
||||||
<artifactId>minio</artifactId>
|
<artifactId>minio</artifactId>
|
||||||
<exclusions>
|
|
||||||
<exclusion>
|
|
||||||
<artifactId>checker-qual</artifactId>
|
|
||||||
<groupId>org.checkerframework</groupId>
|
|
||||||
</exclusion>
|
|
||||||
</exclusions>
|
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- 阿里云短信 -->
|
<!-- 阿里云短信 -->
|
||||||
@ -384,15 +300,11 @@
|
|||||||
<groupId>cn.hutool</groupId>
|
<groupId>cn.hutool</groupId>
|
||||||
<artifactId>hutool-crypto</artifactId>
|
<artifactId>hutool-crypto</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- chatgpt -->
|
<!-- chatgpt -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.jeecgframework.boot</groupId>
|
<groupId>org.jeecgframework.boot</groupId>
|
||||||
<artifactId>jeecg-boot-starter3-chatgpt</artifactId>
|
<artifactId>jeecg-boot-starter3-chatgpt</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
<!-- minidao -->
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.jeecgframework.boot3</groupId>
|
|
||||||
<artifactId>minidao-spring-boot-starter-jsqlparser-4.9</artifactId>
|
|
||||||
</dependency>
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</project>
|
</project>
|
||||||
@ -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.system.vo.*;
|
import org.jeecg.common.system.vo.*;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@ -64,6 +65,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字典表的 翻译
|
||||||
@ -144,4 +152,31 @@ public interface CommonAPI {
|
|||||||
List<DictModel> translateDictFromTableByKeys(String table, String text, String code, String keys, String dataSource);
|
List<DictModel> translateDictFromTableByKeys(String table, String text, String code, String keys, String dataSource);
|
||||||
//update-end---author:chenrui ---date:20231221 for:[issues/#5643]解决分布式下表字典跨库无法查询问题------------
|
//update-end---author:chenrui ---date:20231221 for:[issues/#5643]解决分布式下表字典跨库无法查询问题------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 登录加载系统字典
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
Map<String,List<DictModel>> queryAllDictItems();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询SysDepart集合
|
||||||
|
* @param userId
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
List<SysDepartModel> queryUserDeparts(String userId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据用户名设置部门ID
|
||||||
|
* @param username
|
||||||
|
* @param orgCode
|
||||||
|
*/
|
||||||
|
void updateUserDepart(String username,String orgCode,Integer loginTenantId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置登录租户
|
||||||
|
* @param username
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
JSONObject setLoginTenant(String username);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,7 +90,7 @@ 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缓存时间:3600秒即一小时 */
|
// /** Token缓存时间:3600秒即一小时 */
|
||||||
// int TOKEN_EXPIRE_TIME = 3600;
|
// int TOKEN_EXPIRE_TIME = 3600;
|
||||||
|
|
||||||
|
|||||||
@ -1,21 +0,0 @@
|
|||||||
package org.jeecg.common.exception;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* jeecgboot断言异常
|
|
||||||
* for [QQYUN-10990]AIRAG
|
|
||||||
* @author chenrui
|
|
||||||
* @date 2025/2/14 14:31
|
|
||||||
*/
|
|
||||||
public class JeecgBootAssertException extends JeecgBootException {
|
|
||||||
private static final long serialVersionUID = 1L;
|
|
||||||
|
|
||||||
|
|
||||||
public JeecgBootAssertException(String message) {
|
|
||||||
super(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
public JeecgBootAssertException(String message, int errCode) {
|
|
||||||
super(message, errCode);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -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,10 +22,10 @@ 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.web.HttpRequestMethodNotSupportedException;
|
import org.springframework.web.HttpRequestMethodNotSupportedException;
|
||||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
|
||||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||||
@ -35,7 +33,6 @@ import org.springframework.web.multipart.MaxUploadSizeExceededException;
|
|||||||
import org.springframework.web.servlet.NoHandlerFoundException;
|
import org.springframework.web.servlet.NoHandlerFoundException;
|
||||||
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 异常处理器
|
* 异常处理器
|
||||||
@ -50,11 +47,22 @@ public class JeecgBootExceptionHandler {
|
|||||||
@Resource
|
@Resource
|
||||||
BaseCommonService baseCommonService;
|
BaseCommonService baseCommonService;
|
||||||
|
|
||||||
@ExceptionHandler(MethodArgumentNotValidException.class)
|
/**
|
||||||
public Result<?> handleValidationExceptions(MethodArgumentNotValidException e) {
|
* 验证码错误异常
|
||||||
|
*/
|
||||||
|
|
||||||
|
@ExceptionHandler(JeecgCaptchaException.class)
|
||||||
|
@ResponseStatus(HttpStatus.OK)
|
||||||
|
public Result<?> handleJeecgCaptchaException(JeecgCaptchaException e) {
|
||||||
log.error(e.getMessage(), e);
|
log.error(e.getMessage(), e);
|
||||||
addSysLog(e);
|
return Result.error(e.getCode(), e.getMessage());
|
||||||
return Result.error("校验失败!" + e.getBindingResult().getAllErrors().stream().map(ObjectError::getDefaultMessage).collect(Collectors.joining(",")));
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(AuthenticationException.class)
|
||||||
|
@ResponseStatus(HttpStatus.OK)
|
||||||
|
public Result<?> handleJeecgCaptchaException(AuthenticationException e) {
|
||||||
|
log.error(e.getMessage(), e);
|
||||||
|
return Result.error(401, e.getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -111,8 +119,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,17 +1,18 @@
|
|||||||
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;
|
||||||
import com.baomidou.mybatisplus.extension.service.IService;
|
import com.baomidou.mybatisplus.extension.service.IService;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.commons.beanutils.PropertyUtils;
|
import org.apache.commons.beanutils.PropertyUtils;
|
||||||
import org.apache.shiro.SecurityUtils;
|
|
||||||
import org.jeecg.common.api.vo.Result;
|
import org.jeecg.common.api.vo.Result;
|
||||||
import org.jeecg.common.system.query.QueryGenerator;
|
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;
|
||||||
@ -19,6 +20,7 @@ import org.jeecgframework.poi.excel.entity.ImportParams;
|
|||||||
import org.jeecgframework.poi.excel.entity.enmus.ExcelType;
|
import org.jeecgframework.poi.excel.entity.enmus.ExcelType;
|
||||||
import org.jeecgframework.poi.excel.view.JeecgEntityExcelView;
|
import org.jeecgframework.poi.excel.view.JeecgEntityExcelView;
|
||||||
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;
|
||||||
@ -51,7 +53,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");
|
||||||
@ -89,7 +91,7 @@ public class JeecgController<T, S extends IService<T>> {
|
|||||||
protected ModelAndView exportXlsSheet(HttpServletRequest request, T object, Class<T> clazz, String title,String exportFields,Integer pageNum) {
|
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);
|
||||||
|
|||||||
@ -2,7 +2,6 @@ package org.jeecg.common.system.base.entity;
|
|||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
|
||||||
import org.jeecgframework.poi.excel.annotation.Excel;
|
import org.jeecgframework.poi.excel.annotation.Excel;
|
||||||
import org.springframework.format.annotation.DateTimeFormat;
|
import org.springframework.format.annotation.DateTimeFormat;
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
@ -45,6 +63,8 @@ public class JwtUtil {
|
|||||||
public static final long EXPIRE_TIME = (7 * 12) * 60 * 60 * 1000;
|
public static final long EXPIRE_TIME = (7 * 12) * 60 * 60 * 1000;
|
||||||
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
|
||||||
@ -80,10 +100,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.error(e.getMessage(), e);
|
log.error(e.getMessage(), e);
|
||||||
@ -107,17 +126,25 @@ public class JwtUtil {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 生成签名,5min后过期
|
* 生成token
|
||||||
*
|
*
|
||||||
* @param username 用户名
|
* @param username 用户名
|
||||||
* @param secret 用户的密码
|
* @param secret 用户的密码
|
||||||
* @return 加密的token
|
* @return 加密的token
|
||||||
*/
|
*/
|
||||||
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);
|
||||||
|
SelfAuthenticationToken selfAuthenticationToken = new SelfAuthenticationToken(client, additionalParameter);
|
||||||
|
selfAuthenticationToken.setAuthenticated(true);
|
||||||
|
OAuth2AccessTokenAuthenticationToken accessToken = (OAuth2AccessTokenAuthenticationToken) selfAuthenticationProvider.authenticate(selfAuthenticationToken);
|
||||||
|
return accessToken.getAccessToken().getTokenValue();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -182,7 +209,7 @@ public class JwtUtil {
|
|||||||
//2.通过shiro获取登录用户信息
|
//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());
|
||||||
}
|
}
|
||||||
@ -202,12 +229,10 @@ public class JwtUtil {
|
|||||||
}
|
}
|
||||||
//update-begin---author:chenrui ---date:20250107 for:[QQYUN-10785]数据权限,查看自己拥有部门的权限中存在问题 #7288------------
|
//update-begin---author:chenrui ---date:20250107 for:[QQYUN-10785]数据权限,查看自己拥有部门的权限中存在问题 #7288------------
|
||||||
// 是否存在字符串标志
|
// 是否存在字符串标志
|
||||||
boolean multiStr;
|
boolean multiStr = false;
|
||||||
if(oConvertUtils.isNotEmpty(key) && key.trim().matches("^\\[\\w+]$")){
|
if(oConvertUtils.isNotEmpty(key) && key.trim().matches("^\\[\\w+]$")){
|
||||||
key = key.substring(1,key.length()-1);
|
key = key.substring(1,key.length()-1);
|
||||||
multiStr = true;
|
multiStr = true;
|
||||||
} else {
|
|
||||||
multiStr = false;
|
|
||||||
}
|
}
|
||||||
//update-end---author:chenrui ---date:20250107 for:[QQYUN-10785]数据权限,查看自己拥有部门的权限中存在问题 #7288------------
|
//update-end---author:chenrui ---date:20250107 for:[QQYUN-10785]数据权限,查看自己拥有部门的权限中存在问题 #7288------------
|
||||||
//替换为当前系统时间(年月日)
|
//替换为当前系统时间(年月日)
|
||||||
@ -291,15 +316,7 @@ public class JwtUtil {
|
|||||||
//update-begin---author:chenrui ---date:20250107 for:[QQYUN-10785]数据权限,查看自己拥有部门的权限中存在问题 #7288------------
|
//update-begin---author:chenrui ---date:20250107 for:[QQYUN-10785]数据权限,查看自己拥有部门的权限中存在问题 #7288------------
|
||||||
returnValue = user.getSysMultiOrgCode().stream()
|
returnValue = user.getSysMultiOrgCode().stream()
|
||||||
.filter(Objects::nonNull)
|
.filter(Objects::nonNull)
|
||||||
//update-begin---author:chenrui ---date:20250224 for:[issues/7288]数据权限,查看自己拥有部门的权限中存在问题 #7288------------
|
.map(orgCode -> "'" + orgCode + "'")
|
||||||
.map(orgCode -> {
|
|
||||||
if (multiStr) {
|
|
||||||
return "'" + orgCode + "'";
|
|
||||||
} else {
|
|
||||||
return orgCode;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
//update-end---author:chenrui ---date:20250224 for:[issues/7288]数据权限,查看自己拥有部门的权限中存在问题 #7288------------
|
|
||||||
.collect(Collectors.joining(", "));
|
.collect(Collectors.joining(", "));
|
||||||
//update-end---author:chenrui ---date:20250107 for:[QQYUN-10785]数据权限,查看自己拥有部门的权限中存在问题 #7288------------
|
//update-end---author:chenrui ---date:20250107 for:[QQYUN-10785]数据权限,查看自己拥有部门的权限中存在问题 #7288------------
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
*/
|
*/
|
||||||
@ -138,4 +145,29 @@ public class LoginUser {
|
|||||||
/**设备id uniapp推送用*/
|
/**设备id uniapp推送用*/
|
||||||
private String clientId;
|
private String clientId;
|
||||||
|
|
||||||
|
@SensitiveField
|
||||||
|
private String salt;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
// 重新构建对象过滤一些敏感字段
|
||||||
|
LoginUser loginUser = new LoginUser();
|
||||||
|
loginUser.setId(id);
|
||||||
|
loginUser.setUsername(username);
|
||||||
|
loginUser.setRealname(realname);
|
||||||
|
loginUser.setOrgCode(orgCode);
|
||||||
|
loginUser.setSex(sex);
|
||||||
|
loginUser.setEmail(email);
|
||||||
|
loginUser.setPhone(phone);
|
||||||
|
loginUser.setDelFlag(delFlag);
|
||||||
|
loginUser.setStatus(status);
|
||||||
|
loginUser.setActivitiSync(activitiSync);
|
||||||
|
loginUser.setUserIdentity(userIdentity);
|
||||||
|
loginUser.setDepartIds(departIds);
|
||||||
|
loginUser.setPost(post);
|
||||||
|
loginUser.setTelephone(telephone);
|
||||||
|
loginUser.setRelTenantIds(relTenantIds);
|
||||||
|
loginUser.setClientId(clientId);
|
||||||
|
return JSON.toJSONString(loginUser);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,239 +0,0 @@
|
|||||||
package org.jeecg.common.util;
|
|
||||||
|
|
||||||
|
|
||||||
import org.jeecg.common.exception.JeecgBootAssertException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 断言检查工具
|
|
||||||
* for for [QQYUN-10990]AIRAG
|
|
||||||
* @author chenrui
|
|
||||||
* @date 2017-06-22 10:05:56
|
|
||||||
*/
|
|
||||||
public class AssertUtils {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 确保对象为空,如果不为空抛出异常
|
|
||||||
*
|
|
||||||
* @param msg
|
|
||||||
* @param obj
|
|
||||||
* @throws JeecgBootAssertException
|
|
||||||
* @author chenrui
|
|
||||||
* @date 2017-06-22 10:05:56
|
|
||||||
*/
|
|
||||||
public static void assertEmpty(String msg, Object obj) {
|
|
||||||
if (oConvertUtils.isObjectNotEmpty(obj)) {
|
|
||||||
throw new JeecgBootAssertException(msg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 确保对象不为空,如果为空抛出异常
|
|
||||||
*
|
|
||||||
* @param msg
|
|
||||||
* @param obj
|
|
||||||
* @throws JeecgBootAssertException
|
|
||||||
* @author chenrui
|
|
||||||
* @date 2017-06-22 10:05:56
|
|
||||||
*/
|
|
||||||
public static void assertNotEmpty(String msg, Object obj) {
|
|
||||||
if (oConvertUtils.isObjectEmpty(obj)) {
|
|
||||||
throw new JeecgBootAssertException(msg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 验证对象是否相同
|
|
||||||
*
|
|
||||||
* @param message
|
|
||||||
* @param expected
|
|
||||||
* @param actual
|
|
||||||
* @author chenrui
|
|
||||||
* @date 2018/9/12 15:45
|
|
||||||
*/
|
|
||||||
public static void assertEquals(String message, Object expected,
|
|
||||||
Object actual) {
|
|
||||||
if (oConvertUtils.isEqual(expected, actual)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
throw new JeecgBootAssertException(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 验证不相同
|
|
||||||
*
|
|
||||||
* @param message
|
|
||||||
* @param expected
|
|
||||||
* @param actual
|
|
||||||
* @author chenrui
|
|
||||||
* @date 2018/9/12 15:45
|
|
||||||
*/
|
|
||||||
public static void assertNotEquals(String message, Object expected,
|
|
||||||
Object actual) {
|
|
||||||
if (oConvertUtils.isEqual(expected, actual)) {
|
|
||||||
throw new JeecgBootAssertException(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 验证是否相等
|
|
||||||
*
|
|
||||||
* @param message
|
|
||||||
* @param expected
|
|
||||||
* @param actual
|
|
||||||
* @author chenrui
|
|
||||||
* @date 2018/9/12 15:45
|
|
||||||
*/
|
|
||||||
public static void assertSame(String message, Object expected,
|
|
||||||
Object actual) {
|
|
||||||
if (expected == actual) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
throw new JeecgBootAssertException(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 验证不相等
|
|
||||||
*
|
|
||||||
* @param message
|
|
||||||
* @param unexpected
|
|
||||||
* @param actual
|
|
||||||
* @author chenrui
|
|
||||||
* @date 2018/9/12 15:45
|
|
||||||
*/
|
|
||||||
public static void assertNotSame(String message, Object unexpected,
|
|
||||||
Object actual) {
|
|
||||||
if (unexpected == actual) {
|
|
||||||
throw new JeecgBootAssertException(message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 验证是否为真
|
|
||||||
*
|
|
||||||
* @param message
|
|
||||||
* @param condition
|
|
||||||
*/
|
|
||||||
public static void assertTrue(String message, boolean condition) {
|
|
||||||
if (!condition) {
|
|
||||||
throw new JeecgBootAssertException(message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 验证 condition是否为false
|
|
||||||
*
|
|
||||||
* @param message
|
|
||||||
* @param condition
|
|
||||||
*/
|
|
||||||
public static void assertFalse(String message, boolean condition) {
|
|
||||||
assertTrue(message, !condition);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 验证是否存在
|
|
||||||
*
|
|
||||||
* @param message
|
|
||||||
* @param obj
|
|
||||||
* @param objs
|
|
||||||
* @param <T>
|
|
||||||
* @throws JeecgBootAssertException
|
|
||||||
* @author chenrui
|
|
||||||
* @date 2018/1/31 22:14
|
|
||||||
*/
|
|
||||||
public static <T> void assertIn(String message, T obj, T... objs) {
|
|
||||||
assertNotEmpty(message, obj);
|
|
||||||
assertNotEmpty(message, objs);
|
|
||||||
if (!oConvertUtils.isIn(obj, objs)) {
|
|
||||||
throw new JeecgBootAssertException(message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 验证是否不存在
|
|
||||||
*
|
|
||||||
* @param message
|
|
||||||
* @param obj
|
|
||||||
* @param objs
|
|
||||||
* @param <T>
|
|
||||||
* @throws JeecgBootAssertException
|
|
||||||
* @author chenrui
|
|
||||||
* @date 2018/1/31 22:14
|
|
||||||
*/
|
|
||||||
|
|
||||||
public static <T> void assertNotIn(String message, T obj, T... objs) {
|
|
||||||
assertNotEmpty(message, obj);
|
|
||||||
assertNotEmpty(message, objs);
|
|
||||||
if (oConvertUtils.isIn(obj, objs)) {
|
|
||||||
throw new JeecgBootAssertException(message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 确保src大于des
|
|
||||||
*
|
|
||||||
* @param message
|
|
||||||
* @param src
|
|
||||||
* @param des
|
|
||||||
* @author chenrui
|
|
||||||
* @date 2018/9/19 15:30
|
|
||||||
*/
|
|
||||||
public static void assertGt(String message, Number src, Number des) {
|
|
||||||
if (oConvertUtils.isGt(src, des)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
throw new JeecgBootAssertException(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 确保src大于等于des
|
|
||||||
*
|
|
||||||
* @param message
|
|
||||||
* @param src
|
|
||||||
* @param des
|
|
||||||
* @author chenrui
|
|
||||||
* @date 2018/9/19 15:30
|
|
||||||
*/
|
|
||||||
public static void assertGe(String message, Number src, Number des) {
|
|
||||||
if (oConvertUtils.isGe(src, des)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
throw new JeecgBootAssertException(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 确保src小于des
|
|
||||||
*
|
|
||||||
* @param message
|
|
||||||
* @param src
|
|
||||||
* @param des
|
|
||||||
* @author chenrui
|
|
||||||
* @date 2018/9/19 15:30
|
|
||||||
*/
|
|
||||||
public static void assertLt(String message, Number src, Number des) {
|
|
||||||
if (oConvertUtils.isGe(src, des)) {
|
|
||||||
throw new JeecgBootAssertException(message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 确保src小于等于des
|
|
||||||
*
|
|
||||||
* @param message
|
|
||||||
* @param src
|
|
||||||
* @param des
|
|
||||||
* @author chenrui
|
|
||||||
* @date 2018/9/19 15:30
|
|
||||||
*/
|
|
||||||
public static void assertLe(String message, Number src, Number des) {
|
|
||||||
if (oConvertUtils.isGt(src, des)) {
|
|
||||||
throw new JeecgBootAssertException(message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -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
|
||||||
@ -106,8 +105,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("用户不存在!");
|
||||||
}
|
}
|
||||||
@ -158,10 +157,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,9 +1,9 @@
|
|||||||
package org.jeecg.common.util.encryption;
|
package org.jeecg.common.util.encryption;
|
||||||
|
|
||||||
import org.apache.shiro.lang.codec.Base64;
|
|
||||||
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.util.Base64;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @Description: AES 加密
|
* @Description: AES 加密
|
||||||
@ -48,7 +48,7 @@ public class AesEncryptUtil {
|
|||||||
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) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
@ -66,7 +66,7 @@ public class AesEncryptUtil {
|
|||||||
*/
|
*/
|
||||||
public static String desEncrypt(String data, String key, String iv) throws Exception {
|
public static String desEncrypt(String data, String key, String iv) throws Exception {
|
||||||
//update-begin-author:taoyan date:2022-5-23 for:VUEN-1084 【vue3】online表单测试发现的新问题 6、解密报错 ---解码失败应该把异常抛出去,在外面处理
|
//update-begin-author:taoyan date:2022-5-23 for:VUEN-1084 【vue3】online表单测试发现的新问题 6、解密报错 ---解码失败应该把异常抛出去,在外面处理
|
||||||
byte[] encrypted1 = Base64.decode(data);
|
byte[] encrypted1 = Base64.getDecoder().decode(data);
|
||||||
|
|
||||||
Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
|
Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
|
||||||
SecretKeySpec keyspec = new SecretKeySpec(key.getBytes(), "AES");
|
SecretKeySpec keyspec = new SecretKeySpec(key.getBytes(), "AES");
|
||||||
|
|||||||
@ -42,7 +42,6 @@ public class SsrfFileTypeFilter {
|
|||||||
FILE_TYPE_WHITE_LIST.add("pdf");
|
FILE_TYPE_WHITE_LIST.add("pdf");
|
||||||
FILE_TYPE_WHITE_LIST.add("csv");
|
FILE_TYPE_WHITE_LIST.add("csv");
|
||||||
// FILE_TYPE_WHITE_LIST.add("xml");
|
// FILE_TYPE_WHITE_LIST.add("xml");
|
||||||
FILE_TYPE_WHITE_LIST.add("md");
|
|
||||||
|
|
||||||
//音视频文件
|
//音视频文件
|
||||||
FILE_TYPE_WHITE_LIST.add("mp4");
|
FILE_TYPE_WHITE_LIST.add("mp4");
|
||||||
@ -66,10 +65,6 @@ public class SsrfFileTypeFilter {
|
|||||||
FILE_TYPE_WHITE_LIST.add("apk");
|
FILE_TYPE_WHITE_LIST.add("apk");
|
||||||
FILE_TYPE_WHITE_LIST.add("wgt");
|
FILE_TYPE_WHITE_LIST.add("wgt");
|
||||||
|
|
||||||
//幻灯片文件后缀
|
|
||||||
FILE_TYPE_WHITE_LIST.add("ppt");
|
|
||||||
FILE_TYPE_WHITE_LIST.add("pptx");
|
|
||||||
|
|
||||||
//设置禁止文件的头部标记
|
//设置禁止文件的头部标记
|
||||||
FILE_TYPE_MAP.put("3c25402070616765206c", "jsp");
|
FILE_TYPE_MAP.put("3c25402070616765206c", "jsp");
|
||||||
FILE_TYPE_MAP.put("3c3f7068700a0a2f2a2a0a202a205048", "php");
|
FILE_TYPE_MAP.put("3c3f7068700a0a2f2a2a0a202a205048", "php");
|
||||||
|
|||||||
@ -13,7 +13,6 @@ import jakarta.servlet.http.HttpServletRequest;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.UnsupportedEncodingException;
|
import java.io.UnsupportedEncodingException;
|
||||||
import java.lang.reflect.Array;
|
|
||||||
import java.lang.reflect.Field;
|
import java.lang.reflect.Field;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
@ -464,7 +463,7 @@ public class oConvertUtils {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<String> childs = childArray.toJavaList(String.class);
|
String[] childs = (String[]) childArray.toArray();
|
||||||
for (String v : childs) {
|
for (String v : childs) {
|
||||||
if (!isIn(v, all)) {
|
if (!isIn(v, all)) {
|
||||||
return false;
|
return false;
|
||||||
@ -1030,108 +1029,4 @@ public class oConvertUtils {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 判断对象是否为空 <br/>
|
|
||||||
* 支持各种类型的对象
|
|
||||||
* for for [QQYUN-10990]AIRAG
|
|
||||||
* @param obj
|
|
||||||
* @return
|
|
||||||
* @author chenrui
|
|
||||||
* @date 2025/2/13 18:34
|
|
||||||
*/
|
|
||||||
public static boolean isObjectEmpty(Object obj) {
|
|
||||||
if (null == obj) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (obj instanceof CharSequence) {
|
|
||||||
return isEmpty(obj);
|
|
||||||
} else if (obj instanceof Map) {
|
|
||||||
return ((Map<?, ?>) obj).isEmpty();
|
|
||||||
} else if (obj instanceof Iterable) {
|
|
||||||
return isObjectEmpty(((Iterable<?>) obj).iterator());
|
|
||||||
} else if (obj instanceof Iterator) {
|
|
||||||
return !((Iterator<?>) obj).hasNext();
|
|
||||||
} else if (isArray(obj)) {
|
|
||||||
return 0 == Array.getLength(obj);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* iterator 是否为空
|
|
||||||
* for for [QQYUN-10990]AIRAG
|
|
||||||
* @param iterator Iterator对象
|
|
||||||
* @return 是否为空
|
|
||||||
*/
|
|
||||||
public static boolean isEmptyIterator(Iterator<?> iterator) {
|
|
||||||
return null == iterator || false == iterator.hasNext();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 判断对象是否不为空
|
|
||||||
* for for [QQYUN-10990]AIRAG
|
|
||||||
* @param object
|
|
||||||
* @return
|
|
||||||
* @author chenrui
|
|
||||||
* @date 2025/2/13 18:35
|
|
||||||
*/
|
|
||||||
public static boolean isObjectNotEmpty(Object object) {
|
|
||||||
return !isObjectEmpty(object);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 如果src大于des返回true
|
|
||||||
* for [QQYUN-10990]AIRAG
|
|
||||||
* @param src
|
|
||||||
* @param des
|
|
||||||
* @return
|
|
||||||
* @author: chenrui
|
|
||||||
* @date: 2018/9/19 15:30
|
|
||||||
*/
|
|
||||||
public static boolean isGt(Number src, Number des) {
|
|
||||||
if (null == src || null == des) {
|
|
||||||
throw new IllegalArgumentException("参数不能为空");
|
|
||||||
}
|
|
||||||
if (src.doubleValue() > des.doubleValue()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 如果src大于等于des返回true
|
|
||||||
* for [QQYUN-10990]AIRAG
|
|
||||||
* @param src
|
|
||||||
* @param des
|
|
||||||
* @return
|
|
||||||
* @author: chenrui
|
|
||||||
* @date: 2018/9/19 15:30
|
|
||||||
*/
|
|
||||||
public static boolean isGe(Number src, Number des) {
|
|
||||||
if (null == src || null == des) {
|
|
||||||
throw new IllegalArgumentException("参数不能为空");
|
|
||||||
}
|
|
||||||
if (src.doubleValue() < des.doubleValue()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 判断是否存在
|
|
||||||
* for [QQYUN-10990]AIRAG
|
|
||||||
* @param obj
|
|
||||||
* @param objs
|
|
||||||
* @param <T>
|
|
||||||
* @return
|
|
||||||
* @author chenrui
|
|
||||||
* @date 2020/9/12 15:50
|
|
||||||
*/
|
|
||||||
public static <T> boolean isIn(T obj, T... objs) {
|
|
||||||
return isIn(obj, objs);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,33 @@
|
|||||||
|
package org.jeecg.common.util.sqlInjection;
|
||||||
|
|
||||||
|
import net.sf.jsqlparser.parser.CCJSqlParserDefaultVisitor;
|
||||||
|
import net.sf.jsqlparser.parser.SimpleNode;
|
||||||
|
import net.sf.jsqlparser.statement.select.UnionOp;
|
||||||
|
import org.jeecg.common.exception.JeecgSqlInjectionException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 基于抽象语法树(AST)的注入攻击分析实现
|
||||||
|
*
|
||||||
|
* @author guyadong
|
||||||
|
*/
|
||||||
|
public class InjectionAstNodeVisitor extends CCJSqlParserDefaultVisitor {
|
||||||
|
public InjectionAstNodeVisitor() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理禁止联合查询
|
||||||
|
*
|
||||||
|
* @param node
|
||||||
|
* @param data
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public Object visit(SimpleNode node, Object data) {
|
||||||
|
Object value = node.jjtGetValue();
|
||||||
|
if (value instanceof UnionOp) {
|
||||||
|
throw new JeecgSqlInjectionException("DISABLE UNION");
|
||||||
|
}
|
||||||
|
return super.visit(node, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -0,0 +1,172 @@
|
|||||||
|
package org.jeecg.common.util.sqlInjection;
|
||||||
|
|
||||||
|
|
||||||
|
import net.sf.jsqlparser.expression.BinaryExpression;
|
||||||
|
import net.sf.jsqlparser.expression.Expression;
|
||||||
|
import net.sf.jsqlparser.expression.Function;
|
||||||
|
import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
|
||||||
|
import net.sf.jsqlparser.expression.operators.conditional.OrExpression;
|
||||||
|
import net.sf.jsqlparser.expression.operators.relational.ComparisonOperator;
|
||||||
|
import net.sf.jsqlparser.schema.Column;
|
||||||
|
import net.sf.jsqlparser.statement.select.Join;
|
||||||
|
import net.sf.jsqlparser.statement.select.OrderByElement;
|
||||||
|
import net.sf.jsqlparser.statement.select.PlainSelect;
|
||||||
|
import net.sf.jsqlparser.statement.select.SelectItem;
|
||||||
|
import net.sf.jsqlparser.statement.select.SubSelect;
|
||||||
|
import net.sf.jsqlparser.statement.select.WithItem;
|
||||||
|
import net.sf.jsqlparser.util.TablesNamesFinder;
|
||||||
|
import org.jeecg.common.exception.JeecgSqlInjectionException;
|
||||||
|
import org.jeecg.common.util.sqlInjection.parse.ConstAnalyzer;
|
||||||
|
import org.jeecg.common.util.sqlInjection.parse.ParserSupport;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 基于SQL语法对象的SQL注入攻击分析实现
|
||||||
|
*
|
||||||
|
* @author guyadong
|
||||||
|
*/
|
||||||
|
public class InjectionSyntaxObjectAnalyzer extends TablesNamesFinder {
|
||||||
|
/**
|
||||||
|
* 危险函数名
|
||||||
|
*/
|
||||||
|
private static final String DANGROUS_FUNCTIONS = "(sleep|benchmark|extractvalue|updatexml|ST_LatFromGeoHash|ST_LongFromGeoHash|GTID_SUBSET|GTID_SUBTRACT|floor|ST_Pointfromgeohash"
|
||||||
|
+ "|geometrycollection|multipoint|polygon|multipolygon|linestring|multilinestring)";
|
||||||
|
|
||||||
|
private static ThreadLocal<Boolean> disableSubselect = new ThreadLocal<Boolean>() {
|
||||||
|
@Override
|
||||||
|
protected Boolean initialValue() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
private ConstAnalyzer constAnalyzer = new ConstAnalyzer();
|
||||||
|
|
||||||
|
public InjectionSyntaxObjectAnalyzer() {
|
||||||
|
super();
|
||||||
|
init(true);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visitBinaryExpression(BinaryExpression binaryExpression) {
|
||||||
|
if (binaryExpression instanceof ComparisonOperator) {
|
||||||
|
if (isConst(binaryExpression.getLeftExpression()) && isConst(binaryExpression.getRightExpression())) {
|
||||||
|
/** 禁用恒等式 */
|
||||||
|
throw new JeecgSqlInjectionException("DISABLE IDENTICAL EQUATION " + binaryExpression);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
super.visitBinaryExpression(binaryExpression);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(AndExpression andExpression) {
|
||||||
|
super.visit(andExpression);
|
||||||
|
checkConstExpress(andExpression.getLeftExpression());
|
||||||
|
checkConstExpress(andExpression.getRightExpression());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(OrExpression orExpression) {
|
||||||
|
super.visit(orExpression);
|
||||||
|
checkConstExpress(orExpression.getLeftExpression());
|
||||||
|
checkConstExpress(orExpression.getRightExpression());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(Function function) {
|
||||||
|
if (function.getName().matches(DANGROUS_FUNCTIONS)) {
|
||||||
|
/** 禁用危险函数 */
|
||||||
|
throw new JeecgSqlInjectionException("DANGROUS FUNCTION: " + function.getName());
|
||||||
|
}
|
||||||
|
super.visit(function);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(WithItem withItem) {
|
||||||
|
try {
|
||||||
|
/** 允许 WITH 语句中的子查询 */
|
||||||
|
disableSubselect.set(false);
|
||||||
|
super.visit(withItem);
|
||||||
|
} finally {
|
||||||
|
disableSubselect.set(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(SubSelect subSelect) {
|
||||||
|
try {
|
||||||
|
/** 允许语句中的子查询 */
|
||||||
|
disableSubselect.set(false);
|
||||||
|
super.visit(subSelect);
|
||||||
|
} finally {
|
||||||
|
disableSubselect.set(true);
|
||||||
|
}
|
||||||
|
// if (disableSubselect.get()) {
|
||||||
|
// // 禁用子查询
|
||||||
|
// throw new JeecgSqlInjectionException("DISABLE subselect " + subSelect);
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(Column tableColumn) {
|
||||||
|
if (ParserSupport.isBoolean(tableColumn)) {
|
||||||
|
throw new JeecgSqlInjectionException("DISABLE CONST BOOL " + tableColumn);
|
||||||
|
}
|
||||||
|
super.visit(tableColumn);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(PlainSelect plainSelect) {
|
||||||
|
if (plainSelect.getSelectItems() != null) {
|
||||||
|
for (SelectItem item : plainSelect.getSelectItems()) {
|
||||||
|
item.accept(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (plainSelect.getFromItem() != null) {
|
||||||
|
plainSelect.getFromItem().accept(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (plainSelect.getJoins() != null) {
|
||||||
|
for (Join join : plainSelect.getJoins()) {
|
||||||
|
join.getRightItem().accept(this);
|
||||||
|
for (Expression e : join.getOnExpressions()) {
|
||||||
|
e.accept(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (plainSelect.getWhere() != null) {
|
||||||
|
plainSelect.getWhere().accept(this);
|
||||||
|
checkConstExpress(plainSelect.getWhere());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (plainSelect.getHaving() != null) {
|
||||||
|
plainSelect.getHaving().accept(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (plainSelect.getOracleHierarchical() != null) {
|
||||||
|
plainSelect.getOracleHierarchical().accept(this);
|
||||||
|
}
|
||||||
|
if (plainSelect.getOrderByElements() != null) {
|
||||||
|
for (OrderByElement orderByElement : plainSelect.getOrderByElements()) {
|
||||||
|
orderByElement.getExpression().accept(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (plainSelect.getGroupBy() != null) {
|
||||||
|
for (Expression expression : plainSelect.getGroupBy().getGroupByExpressionList().getExpressions()) {
|
||||||
|
expression.accept(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isConst(Expression expression) {
|
||||||
|
return constAnalyzer.isConstExpression(expression);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkConstExpress(Expression expression) {
|
||||||
|
if (constAnalyzer.isConstExpression(expression)) {
|
||||||
|
/** 禁用常量表达式 */
|
||||||
|
throw new JeecgSqlInjectionException("DISABLE CONST EXPRESSION " + expression);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -0,0 +1,65 @@
|
|||||||
|
package org.jeecg.common.util.sqlInjection;
|
||||||
|
|
||||||
|
import org.jeecg.common.exception.JeecgSqlInjectionException;
|
||||||
|
import org.jeecg.common.util.sqlInjection.parse.ParserSupport;
|
||||||
|
;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SQL注入攻击分析器
|
||||||
|
*
|
||||||
|
* @author guyadong
|
||||||
|
* 参考:
|
||||||
|
* https://blog.csdn.net/10km/article/details/127767358
|
||||||
|
* https://gitee.com/l0km/sql2java/tree/dev/sql2java-manager/src/main/java/gu/sql2java/parser
|
||||||
|
*/
|
||||||
|
public class SqlInjectionAnalyzer {
|
||||||
|
|
||||||
|
//启用/关闭注入攻击检查
|
||||||
|
private boolean injectCheckEnable = true;
|
||||||
|
//防止SQL注入攻击分析实现
|
||||||
|
private final InjectionSyntaxObjectAnalyzer injectionChecker;
|
||||||
|
private final InjectionAstNodeVisitor injectionVisitor;
|
||||||
|
|
||||||
|
public SqlInjectionAnalyzer() {
|
||||||
|
this.injectionChecker = new InjectionSyntaxObjectAnalyzer();
|
||||||
|
this.injectionVisitor = new InjectionAstNodeVisitor();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启用/关闭注入攻击检查,默认启动
|
||||||
|
*
|
||||||
|
* @param enable
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public SqlInjectionAnalyzer injectCheckEnable(boolean enable) {
|
||||||
|
injectCheckEnable = enable;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 对解析后的SQL对象执行注入攻击分析,有注入攻击的危险则抛出异常{@link JeecgSqlInjectionException}
|
||||||
|
*
|
||||||
|
* @param sqlParserInfo
|
||||||
|
* @throws JeecgSqlInjectionException
|
||||||
|
*/
|
||||||
|
public ParserSupport.SqlParserInfo injectAnalyse(ParserSupport.SqlParserInfo sqlParserInfo) throws JeecgSqlInjectionException {
|
||||||
|
if (null != sqlParserInfo && injectCheckEnable) {
|
||||||
|
/** SQL注入攻击检查 */
|
||||||
|
sqlParserInfo.statement.accept(injectionChecker);
|
||||||
|
sqlParserInfo.simpleNode.jjtAccept(injectionVisitor, null);
|
||||||
|
}
|
||||||
|
return sqlParserInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* sql校验
|
||||||
|
*/
|
||||||
|
public static void checkSql(String sql,boolean check){
|
||||||
|
SqlInjectionAnalyzer sqlInjectionAnalyzer = new SqlInjectionAnalyzer();
|
||||||
|
sqlInjectionAnalyzer.injectCheckEnable(check);
|
||||||
|
ParserSupport.SqlParserInfo sqlParserInfo = ParserSupport.parse0(sql, null,null);
|
||||||
|
sqlInjectionAnalyzer.injectAnalyse(sqlParserInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -0,0 +1,569 @@
|
|||||||
|
package org.jeecg.common.util.sqlInjection.parse;
|
||||||
|
|
||||||
|
import net.sf.jsqlparser.expression.*;
|
||||||
|
import net.sf.jsqlparser.expression.operators.arithmetic.Addition;
|
||||||
|
import net.sf.jsqlparser.expression.operators.arithmetic.BitwiseAnd;
|
||||||
|
import net.sf.jsqlparser.expression.operators.arithmetic.BitwiseLeftShift;
|
||||||
|
import net.sf.jsqlparser.expression.operators.arithmetic.BitwiseOr;
|
||||||
|
import net.sf.jsqlparser.expression.operators.arithmetic.BitwiseRightShift;
|
||||||
|
import net.sf.jsqlparser.expression.operators.arithmetic.BitwiseXor;
|
||||||
|
import net.sf.jsqlparser.expression.operators.arithmetic.Concat;
|
||||||
|
import net.sf.jsqlparser.expression.operators.arithmetic.Division;
|
||||||
|
import net.sf.jsqlparser.expression.operators.arithmetic.IntegerDivision;
|
||||||
|
import net.sf.jsqlparser.expression.operators.arithmetic.Modulo;
|
||||||
|
import net.sf.jsqlparser.expression.operators.arithmetic.Multiplication;
|
||||||
|
import net.sf.jsqlparser.expression.operators.arithmetic.Subtraction;
|
||||||
|
import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
|
||||||
|
import net.sf.jsqlparser.expression.operators.conditional.OrExpression;
|
||||||
|
import net.sf.jsqlparser.expression.operators.conditional.XorExpression;
|
||||||
|
import net.sf.jsqlparser.expression.operators.relational.Between;
|
||||||
|
import net.sf.jsqlparser.expression.operators.relational.EqualsTo;
|
||||||
|
import net.sf.jsqlparser.expression.operators.relational.ExistsExpression;
|
||||||
|
import net.sf.jsqlparser.expression.operators.relational.ExpressionList;
|
||||||
|
import net.sf.jsqlparser.expression.operators.relational.FullTextSearch;
|
||||||
|
import net.sf.jsqlparser.expression.operators.relational.GeometryDistance;
|
||||||
|
import net.sf.jsqlparser.expression.operators.relational.GreaterThan;
|
||||||
|
import net.sf.jsqlparser.expression.operators.relational.GreaterThanEquals;
|
||||||
|
import net.sf.jsqlparser.expression.operators.relational.InExpression;
|
||||||
|
import net.sf.jsqlparser.expression.operators.relational.IsBooleanExpression;
|
||||||
|
import net.sf.jsqlparser.expression.operators.relational.IsDistinctExpression;
|
||||||
|
import net.sf.jsqlparser.expression.operators.relational.IsNullExpression;
|
||||||
|
import net.sf.jsqlparser.expression.operators.relational.ItemsListVisitor;
|
||||||
|
import net.sf.jsqlparser.expression.operators.relational.JsonOperator;
|
||||||
|
import net.sf.jsqlparser.expression.operators.relational.LikeExpression;
|
||||||
|
import net.sf.jsqlparser.expression.operators.relational.Matches;
|
||||||
|
import net.sf.jsqlparser.expression.operators.relational.MinorThan;
|
||||||
|
import net.sf.jsqlparser.expression.operators.relational.MinorThanEquals;
|
||||||
|
import net.sf.jsqlparser.expression.operators.relational.MultiExpressionList;
|
||||||
|
import net.sf.jsqlparser.expression.operators.relational.NamedExpressionList;
|
||||||
|
import net.sf.jsqlparser.expression.operators.relational.NotEqualsTo;
|
||||||
|
import net.sf.jsqlparser.expression.operators.relational.RegExpMatchOperator;
|
||||||
|
import net.sf.jsqlparser.expression.operators.relational.RegExpMySQLOperator;
|
||||||
|
import net.sf.jsqlparser.expression.operators.relational.SimilarToExpression;
|
||||||
|
import net.sf.jsqlparser.schema.Column;
|
||||||
|
import net.sf.jsqlparser.statement.select.AllColumns;
|
||||||
|
import net.sf.jsqlparser.statement.select.AllTableColumns;
|
||||||
|
import net.sf.jsqlparser.statement.select.OrderByElement;
|
||||||
|
import net.sf.jsqlparser.statement.select.SubSelect;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断表达是否为常量的分析器
|
||||||
|
*
|
||||||
|
* @author guyadong
|
||||||
|
*/
|
||||||
|
public class ConstAnalyzer implements ExpressionVisitor, ItemsListVisitor {
|
||||||
|
|
||||||
|
private static ThreadLocal<Boolean> constFlag = new ThreadLocal<Boolean>() {
|
||||||
|
@Override
|
||||||
|
protected Boolean initialValue() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(NullValue value) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(Function function) {
|
||||||
|
constFlag.set(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(SignedExpression expr) {
|
||||||
|
expr.getExpression().accept(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(JdbcParameter parameter) {
|
||||||
|
constFlag.set(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(JdbcNamedParameter parameter) {
|
||||||
|
constFlag.set(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(DoubleValue value) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(LongValue value) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(DateValue value) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(TimeValue value) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(TimestampValue value) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(Parenthesis parenthesis) {
|
||||||
|
parenthesis.getExpression().accept(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(StringValue value) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(Addition expr) {
|
||||||
|
visitBinaryExpression(expr);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(Division expr) {
|
||||||
|
visitBinaryExpression(expr);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(IntegerDivision expr) {
|
||||||
|
visitBinaryExpression(expr);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(Multiplication expr) {
|
||||||
|
visitBinaryExpression(expr);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(Subtraction expr) {
|
||||||
|
visitBinaryExpression(expr);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(AndExpression expr) {
|
||||||
|
visitBinaryExpression(expr);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(OrExpression expr) {
|
||||||
|
visitBinaryExpression(expr);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(XorExpression expr) {
|
||||||
|
visitBinaryExpression(expr);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(Between expr) {
|
||||||
|
expr.getLeftExpression().accept(this);
|
||||||
|
expr.getBetweenExpressionStart().accept(this);
|
||||||
|
expr.getBetweenExpressionEnd().accept(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用于处理 OverlapsCondition 类型的表达式
|
||||||
|
* @param overlapsCondition
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void visit(OverlapsCondition overlapsCondition) {
|
||||||
|
constFlag.set(false);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 用于处理 SafeCastExpression 类型的表达式。
|
||||||
|
* @param safeCastExpression
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void visit(SafeCastExpression safeCastExpression) {
|
||||||
|
constFlag.set(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(EqualsTo expr) {
|
||||||
|
visitBinaryExpression(expr);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(GreaterThan expr) {
|
||||||
|
visitBinaryExpression(expr);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(GreaterThanEquals expr) {
|
||||||
|
visitBinaryExpression(expr);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(InExpression expr) {
|
||||||
|
if (expr.getLeftExpression() != null) {
|
||||||
|
expr.getLeftExpression().accept(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(IsNullExpression expr) {
|
||||||
|
expr.getLeftExpression().accept(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(FullTextSearch expr) {
|
||||||
|
constFlag.set(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(IsBooleanExpression expr) {
|
||||||
|
expr.getLeftExpression().accept(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(LikeExpression expr) {
|
||||||
|
visitBinaryExpression(expr);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(MinorThan expr) {
|
||||||
|
visitBinaryExpression(expr);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(MinorThanEquals expr) {
|
||||||
|
visitBinaryExpression(expr);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(NotEqualsTo expr) {
|
||||||
|
visitBinaryExpression(expr);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(Column column) {
|
||||||
|
if (!ParserSupport.isBoolean(column)) {
|
||||||
|
constFlag.set(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(SubSelect subSelect) {
|
||||||
|
constFlag.set(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(CaseExpression expr) {
|
||||||
|
constFlag.set(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(WhenClause expr) {
|
||||||
|
constFlag.set(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(ExistsExpression expr) {
|
||||||
|
constFlag.set(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(AnyComparisonExpression expr) {
|
||||||
|
constFlag.set(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(Concat expr) {
|
||||||
|
visitBinaryExpression(expr);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(Matches expr) {
|
||||||
|
visitBinaryExpression(expr);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(BitwiseAnd expr) {
|
||||||
|
visitBinaryExpression(expr);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(BitwiseOr expr) {
|
||||||
|
visitBinaryExpression(expr);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(BitwiseXor expr) {
|
||||||
|
visitBinaryExpression(expr);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(CastExpression expr) {
|
||||||
|
expr.getLeftExpression().accept(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(TryCastExpression expr) {
|
||||||
|
constFlag.set(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(Modulo expr) {
|
||||||
|
visitBinaryExpression(expr);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(AnalyticExpression expr) {
|
||||||
|
constFlag.set(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(ExtractExpression expr) {
|
||||||
|
expr.getExpression().accept(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(IntervalExpression expr) {
|
||||||
|
constFlag.set(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(OracleHierarchicalExpression expr) {
|
||||||
|
constFlag.set(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(RegExpMatchOperator expr) {
|
||||||
|
visitBinaryExpression(expr);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(ExpressionList expressionList) {
|
||||||
|
for (Expression expr : expressionList.getExpressions()) {
|
||||||
|
expr.accept(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(NamedExpressionList namedExpressionList) {
|
||||||
|
for (Expression expr : namedExpressionList.getExpressions()) {
|
||||||
|
expr.accept(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(MultiExpressionList multiExprList) {
|
||||||
|
for (ExpressionList list : multiExprList.getExpressionLists()) {
|
||||||
|
visit(list);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(NotExpression notExpr) {
|
||||||
|
notExpr.getExpression().accept(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(BitwiseRightShift expr) {
|
||||||
|
visitBinaryExpression(expr);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(BitwiseLeftShift expr) {
|
||||||
|
visitBinaryExpression(expr);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void visitBinaryExpression(BinaryExpression expr) {
|
||||||
|
expr.getLeftExpression().accept(this);
|
||||||
|
expr.getRightExpression().accept(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(JsonExpression jsonExpr) {
|
||||||
|
jsonExpr.getExpression().accept(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(JsonOperator expr) {
|
||||||
|
visitBinaryExpression(expr);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(RegExpMySQLOperator expr) {
|
||||||
|
visitBinaryExpression(expr);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(UserVariable var) {
|
||||||
|
constFlag.set(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(NumericBind bind) {
|
||||||
|
constFlag.set(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(KeepExpression expr) {
|
||||||
|
for (OrderByElement element : expr.getOrderByElements()) {
|
||||||
|
element.getExpression().accept(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(MySQLGroupConcat groupConcat) {
|
||||||
|
constFlag.set(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(ValueListExpression valueListExpression) {
|
||||||
|
for (Expression expr : valueListExpression.getExpressionList().getExpressions()) {
|
||||||
|
expr.accept(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(AllColumns allColumns) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(AllTableColumns allTableColumns) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(AllValue allValue) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(IsDistinctExpression isDistinctExpression) {
|
||||||
|
visitBinaryExpression(isDistinctExpression);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(RowGetExpression rowGetExpression) {
|
||||||
|
rowGetExpression.getExpression().accept(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(HexValue hexValue) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(OracleHint hint) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(TimeKeyExpression timeKeyExpression) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(DateTimeLiteralExpression literal) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(NextValExpression nextVal) {
|
||||||
|
constFlag.set(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(CollateExpression col) {
|
||||||
|
constFlag.set(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(SimilarToExpression expr) {
|
||||||
|
visitBinaryExpression(expr);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(ArrayExpression array) {
|
||||||
|
array.getObjExpression().accept(this);
|
||||||
|
if (array.getIndexExpression() != null) {
|
||||||
|
array.getIndexExpression().accept(this);
|
||||||
|
}
|
||||||
|
if (array.getStartIndexExpression() != null) {
|
||||||
|
array.getStartIndexExpression().accept(this);
|
||||||
|
}
|
||||||
|
if (array.getStopIndexExpression() != null) {
|
||||||
|
array.getStopIndexExpression().accept(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(ArrayConstructor aThis) {
|
||||||
|
for (Expression expression : aThis.getExpressions()) {
|
||||||
|
expression.accept(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(VariableAssignment var) {
|
||||||
|
constFlag.set(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(XMLSerializeExpr expr) {
|
||||||
|
constFlag.set(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(TimezoneExpression expr) {
|
||||||
|
expr.getLeftExpression().accept(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(JsonAggregateFunction expression) {
|
||||||
|
Expression expr = expression.getExpression();
|
||||||
|
if (expr != null) {
|
||||||
|
expr.accept(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
expr = expression.getFilterExpression();
|
||||||
|
if (expr != null) {
|
||||||
|
expr.accept(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(JsonFunction expression) {
|
||||||
|
for (JsonFunctionExpression expr : expression.getExpressions()) {
|
||||||
|
expr.getExpression().accept(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(ConnectByRootOperator connectByRootOperator) {
|
||||||
|
constFlag.set(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(OracleNamedFunctionParameter oracleNamedFunctionParameter) {
|
||||||
|
constFlag.set(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(GeometryDistance geometryDistance) {
|
||||||
|
visitBinaryExpression(geometryDistance);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(RowConstructor rowConstructor) {
|
||||||
|
constFlag.set(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isConstExpression(Expression expression) {
|
||||||
|
if (null != expression) {
|
||||||
|
constFlag.set(true);
|
||||||
|
expression.accept(this);
|
||||||
|
return constFlag.get();
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -0,0 +1,177 @@
|
|||||||
|
package org.jeecg.common.util.sqlInjection.parse;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import net.sf.jsqlparser.JSQLParserException;
|
||||||
|
import net.sf.jsqlparser.parser.*;
|
||||||
|
import net.sf.jsqlparser.schema.Column;
|
||||||
|
import net.sf.jsqlparser.statement.Statement;
|
||||||
|
import net.sf.jsqlparser.statement.select.PlainSelect;
|
||||||
|
import net.sf.jsqlparser.statement.select.Select;
|
||||||
|
import net.sf.jsqlparser.statement.select.SelectBody;
|
||||||
|
|
||||||
|
import static com.google.common.base.Preconditions.checkArgument;
|
||||||
|
import static com.google.common.base.Preconditions.checkNotNull;
|
||||||
|
|
||||||
|
import java.lang.reflect.InvocationTargetException;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
import com.google.common.base.Throwables;
|
||||||
|
import org.jeecg.common.exception.JeecgBootException;
|
||||||
|
import org.jeecg.common.exception.JeecgSqlInjectionException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析sql支持
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
public class ParserSupport {
|
||||||
|
/**
|
||||||
|
* 解析SELECT SQL语句,解析失败或非SELECT语句则抛出异常
|
||||||
|
*
|
||||||
|
* @param sql
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public static Select parseSelect(String sql) {
|
||||||
|
Statement stmt;
|
||||||
|
try {
|
||||||
|
stmt = CCJSqlParserUtil.parse(checkNotNull(sql, "sql is null"));
|
||||||
|
} catch (JSQLParserException e) {
|
||||||
|
throw new JeecgBootException(e);
|
||||||
|
}
|
||||||
|
checkArgument(stmt instanceof Select, "%s is not SELECT statment", sql);
|
||||||
|
Select select = (Select) stmt;
|
||||||
|
SelectBody selectBody = select.getSelectBody();
|
||||||
|
// 暂时只支持简单的SELECT xxxx FROM ....语句不支持复杂语句如WITH
|
||||||
|
checkArgument(selectBody instanceof PlainSelect, "ONLY SUPPORT plain select statement %s", sql);
|
||||||
|
return (Select) stmt;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析SELECT SQL语句,解析失败或非SELECT语句则
|
||||||
|
*
|
||||||
|
* @param sql
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public static Select parseSelectUnchecked(String sql) {
|
||||||
|
try {
|
||||||
|
return parseSelect(sql);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 实现SQL语句解析,解析成功则返回解析后的{@link Statement},
|
||||||
|
* 并通过{@code visitor}参数提供基于AST(抽象语法树)的遍历所有节点的能力。
|
||||||
|
*
|
||||||
|
* @param sql SQL语句
|
||||||
|
* @param visitor 遍历所有节点的{@link SimpleNodeVisitor}接口实例,为{@code null}忽略
|
||||||
|
* @param sqlSyntaxNormalizer SQL语句分析转换器,为{@code null}忽略
|
||||||
|
* @throws JSQLParserException 输入的SQL语句有语法错误
|
||||||
|
* @see #parse0(String, CCJSqlParserVisitor, SqlSyntaxNormalizer)
|
||||||
|
*/
|
||||||
|
public static Statement parse(String sql, CCJSqlParserVisitor visitor, SqlSyntaxNormalizer sqlSyntaxNormalizer) throws JSQLParserException {
|
||||||
|
return parse0(sql, visitor, sqlSyntaxNormalizer).statement;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 参照{@link CCJSqlParserUtil#parseAST(String)}和{@link CCJSqlParserUtil#parse(String)}实现SQL语句解析,
|
||||||
|
* 解析成功则返回解析后的{@link SqlParserInfo}对象,
|
||||||
|
* 并通过{@code visitor}参数提供基于AST(抽象语法树)的遍历所有节点的能力。
|
||||||
|
*
|
||||||
|
* @param sql SQL语句
|
||||||
|
* @param visitor 遍历所有节点的{@link SimpleNodeVisitor}接口实例,为{@code null}忽略
|
||||||
|
* @param sqlSyntaxAnalyzer SQL语句分析转换器,为{@code null}忽略
|
||||||
|
* @throws JSQLParserException 输入的SQL语句有语法错误
|
||||||
|
* @see net.sf.jsqlparser.parser.Node#jjtAccept(SimpleNodeVisitor, Object)
|
||||||
|
*/
|
||||||
|
public static SqlParserInfo parse0(String sql, CCJSqlParserVisitor visitor, SqlSyntaxNormalizer sqlSyntaxAnalyzer) throws JeecgSqlInjectionException {
|
||||||
|
|
||||||
|
//检查是否非select开头,暂不支持
|
||||||
|
if(!sql.toLowerCase().trim().startsWith("select ")) {
|
||||||
|
log.warn("传入sql 非select开头,不支持非select开头的语句解析!");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
//检查是否存储过程,暂不支持
|
||||||
|
if(sql.toLowerCase().trim().startsWith("call ")){
|
||||||
|
log.warn("传入call 开头存储过程,不支持存储过程解析!");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
//检查特殊语义的特殊字符,目前检查冒号、$、#三种特殊语义字符
|
||||||
|
String specialCharacters = "[:$#]";
|
||||||
|
Pattern pattern = Pattern.compile(specialCharacters);
|
||||||
|
Matcher matcher = pattern.matcher(sql);
|
||||||
|
if (matcher.find()) {
|
||||||
|
sql = sql.replaceAll("[:$#]", "@");
|
||||||
|
}
|
||||||
|
|
||||||
|
checkArgument(null != sql, "sql is null");
|
||||||
|
boolean allowComplexParsing = CCJSqlParserUtil.getNestingDepth(sql) <= CCJSqlParserUtil.ALLOWED_NESTING_DEPTH;
|
||||||
|
|
||||||
|
CCJSqlParser parser = CCJSqlParserUtil.newParser(sql).withAllowComplexParsing(allowComplexParsing);
|
||||||
|
Statement stmt;
|
||||||
|
try {
|
||||||
|
stmt = parser.Statement();
|
||||||
|
} catch (Exception ex) {
|
||||||
|
log.error("请注意,SQL语法可能存在问题---> {}", ex.getMessage());
|
||||||
|
throw new JeecgSqlInjectionException("请注意,SQL语法可能存在问题:"+sql);
|
||||||
|
}
|
||||||
|
if (null != visitor) {
|
||||||
|
parser.getASTRoot().jjtAccept(visitor, null);
|
||||||
|
}
|
||||||
|
if (null != sqlSyntaxAnalyzer) {
|
||||||
|
stmt.accept(sqlSyntaxAnalyzer.resetChanged());
|
||||||
|
}
|
||||||
|
return new SqlParserInfo(stmt.toString(), stmt, (SimpleNode) parser.getASTRoot());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 调用{@link CCJSqlParser}解析SQL语句部件返回解析生成的对象,如{@code 'ORDER BY id DESC'}
|
||||||
|
*
|
||||||
|
* @param <T>
|
||||||
|
* @param input
|
||||||
|
* @param method 指定调用的{@link CCJSqlParser}解析方法
|
||||||
|
* @param targetType 返回的解析对象类型
|
||||||
|
* @return
|
||||||
|
* @since 3.18.3
|
||||||
|
*/
|
||||||
|
public static <T> T parseComponent(String input, String method, Class<T> targetType) {
|
||||||
|
try {
|
||||||
|
CCJSqlParser parser = new CCJSqlParser(new StringProvider(input));
|
||||||
|
try {
|
||||||
|
return checkNotNull(targetType, "targetType is null").cast(parser.getClass().getMethod(method).invoke(parser));
|
||||||
|
} catch (InvocationTargetException e) {
|
||||||
|
Throwables.throwIfUnchecked(e.getTargetException());
|
||||||
|
throw new RuntimeException(e.getTargetException());
|
||||||
|
}
|
||||||
|
} catch (IllegalAccessException | NoSuchMethodException | SecurityException e) {
|
||||||
|
Throwables.throwIfUnchecked(e);
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 如果{@link Column}没有定义table,且字段名为true/false(不区分大小写)则视为布尔常量
|
||||||
|
*
|
||||||
|
* @param column
|
||||||
|
*/
|
||||||
|
public static boolean isBoolean(Column column) {
|
||||||
|
return null != column && null == column.getTable() &&
|
||||||
|
Pattern.compile("(true|false)", Pattern.CASE_INSENSITIVE).matcher(column.getColumnName()).matches();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class SqlParserInfo {
|
||||||
|
public String nativeSql;
|
||||||
|
public Statement statement;
|
||||||
|
public SimpleNode simpleNode;
|
||||||
|
|
||||||
|
SqlParserInfo(String nativeSql, Statement statement, SimpleNode simpleNode) {
|
||||||
|
this.nativeSql = nativeSql;
|
||||||
|
this.statement = statement;
|
||||||
|
this.simpleNode = simpleNode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -0,0 +1,37 @@
|
|||||||
|
package org.jeecg.common.util.sqlInjection.parse;
|
||||||
|
|
||||||
|
import net.sf.jsqlparser.util.TablesNamesFinder;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SQL语句分析转换器基类<br>
|
||||||
|
* 基于SQL语法对象实现对SQL的修改
|
||||||
|
* (暂时用不到)
|
||||||
|
*
|
||||||
|
* @author guyadong
|
||||||
|
* @since 3.17.0
|
||||||
|
*/
|
||||||
|
public class SqlSyntaxNormalizer extends TablesNamesFinder {
|
||||||
|
protected static final ThreadLocal<Boolean> changed = new ThreadLocal<>();
|
||||||
|
|
||||||
|
public SqlSyntaxNormalizer() {
|
||||||
|
super();
|
||||||
|
init(true);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 语句改变返回{@code true},否则返回{@code false}
|
||||||
|
*/
|
||||||
|
public boolean changed() {
|
||||||
|
return Boolean.TRUE.equals(changed.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 复位线程局部变量{@link #changed}状态
|
||||||
|
*/
|
||||||
|
public SqlSyntaxNormalizer resetChanged() {
|
||||||
|
changed.remove();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -1,255 +1,255 @@
|
|||||||
//package org.jeecg.common.util.sqlparse;
|
package org.jeecg.common.util.sqlparse;
|
||||||
//
|
|
||||||
//import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
//import net.sf.jsqlparser.JSQLParserException;
|
import net.sf.jsqlparser.JSQLParserException;
|
||||||
//import net.sf.jsqlparser.expression.*;
|
import net.sf.jsqlparser.expression.*;
|
||||||
//import net.sf.jsqlparser.parser.CCJSqlParserManager;
|
import net.sf.jsqlparser.parser.CCJSqlParserManager;
|
||||||
//import net.sf.jsqlparser.schema.Column;
|
import net.sf.jsqlparser.schema.Column;
|
||||||
//import net.sf.jsqlparser.schema.Table;
|
import net.sf.jsqlparser.schema.Table;
|
||||||
//import net.sf.jsqlparser.statement.Statement;
|
import net.sf.jsqlparser.statement.Statement;
|
||||||
//import net.sf.jsqlparser.statement.select.*;
|
import net.sf.jsqlparser.statement.select.*;
|
||||||
//import org.jeecg.common.exception.JeecgBootException;
|
import org.jeecg.common.exception.JeecgBootException;
|
||||||
//import org.jeecg.common.util.sqlparse.vo.SelectSqlInfo;
|
import org.jeecg.common.util.sqlparse.vo.SelectSqlInfo;
|
||||||
//
|
|
||||||
//import java.io.StringReader;
|
import java.io.StringReader;
|
||||||
//import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
//import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
//import java.util.List;
|
import java.util.List;
|
||||||
//import java.util.Map;
|
import java.util.Map;
|
||||||
//
|
|
||||||
///**
|
/**
|
||||||
// * 解析所有表名和字段的类
|
* 解析所有表名和字段的类
|
||||||
// */
|
*/
|
||||||
//@Slf4j
|
@Slf4j
|
||||||
//public class JSqlParserAllTableManager {
|
public class JSqlParserAllTableManager {
|
||||||
//
|
|
||||||
// private final String sql;
|
private final String sql;
|
||||||
// private final Map<String, SelectSqlInfo> allTableMap = new HashMap<>();
|
private final Map<String, SelectSqlInfo> allTableMap = new HashMap<>();
|
||||||
// /**
|
/**
|
||||||
// * 别名对应实际表名
|
* 别名对应实际表名
|
||||||
// */
|
*/
|
||||||
// private final Map<String, String> tableAliasMap = new HashMap<>();
|
private final Map<String, String> tableAliasMap = new HashMap<>();
|
||||||
//
|
|
||||||
// /**
|
/**
|
||||||
// * 解析后的sql
|
* 解析后的sql
|
||||||
// */
|
*/
|
||||||
// private String parsedSql = null;
|
private String parsedSql = null;
|
||||||
//
|
|
||||||
// JSqlParserAllTableManager(String selectSql) {
|
JSqlParserAllTableManager(String selectSql) {
|
||||||
// this.sql = selectSql;
|
this.sql = selectSql;
|
||||||
// }
|
}
|
||||||
//
|
|
||||||
// /**
|
/**
|
||||||
// * 开始解析
|
* 开始解析
|
||||||
// *
|
*
|
||||||
// * @return
|
* @return
|
||||||
// * @throws JSQLParserException
|
* @throws JSQLParserException
|
||||||
// */
|
*/
|
||||||
// public Map<String, SelectSqlInfo> parse() throws JSQLParserException {
|
public Map<String, SelectSqlInfo> parse() throws JSQLParserException {
|
||||||
// // 1. 创建解析器
|
// 1. 创建解析器
|
||||||
// CCJSqlParserManager mgr = new CCJSqlParserManager();
|
CCJSqlParserManager mgr = new CCJSqlParserManager();
|
||||||
// // 2. 使用解析器解析sql生成具有层次结构的java类
|
// 2. 使用解析器解析sql生成具有层次结构的java类
|
||||||
// Statement stmt = mgr.parse(new StringReader(this.sql));
|
Statement stmt = mgr.parse(new StringReader(this.sql));
|
||||||
// if (stmt instanceof Select) {
|
if (stmt instanceof Select) {
|
||||||
// Select selectStatement = (Select) stmt;
|
Select selectStatement = (Select) stmt;
|
||||||
// SelectBody selectBody = selectStatement.getSelectBody();
|
SelectBody selectBody = selectStatement.getSelectBody();
|
||||||
// this.parsedSql = selectBody.toString();
|
this.parsedSql = selectBody.toString();
|
||||||
// // 3. 解析select查询sql的信息
|
// 3. 解析select查询sql的信息
|
||||||
// if (selectBody instanceof PlainSelect) {
|
if (selectBody instanceof PlainSelect) {
|
||||||
// PlainSelect plainSelect = (PlainSelect) selectBody;
|
PlainSelect plainSelect = (PlainSelect) selectBody;
|
||||||
// // 4. 合并 fromItems
|
// 4. 合并 fromItems
|
||||||
// List<FromItem> fromItems = new ArrayList<>();
|
List<FromItem> fromItems = new ArrayList<>();
|
||||||
// fromItems.add(plainSelect.getFromItem());
|
fromItems.add(plainSelect.getFromItem());
|
||||||
// // 4.1 处理join的表
|
// 4.1 处理join的表
|
||||||
// List<Join> joins = plainSelect.getJoins();
|
List<Join> joins = plainSelect.getJoins();
|
||||||
// if (joins != null) {
|
if (joins != null) {
|
||||||
// joins.forEach(join -> fromItems.add(join.getRightItem()));
|
joins.forEach(join -> fromItems.add(join.getRightItem()));
|
||||||
// }
|
}
|
||||||
// // 5. 处理 fromItems
|
// 5. 处理 fromItems
|
||||||
// for (FromItem fromItem : fromItems) {
|
for (FromItem fromItem : fromItems) {
|
||||||
// // 5.1 通过表名的方式from
|
// 5.1 通过表名的方式from
|
||||||
// if (fromItem instanceof Table) {
|
if (fromItem instanceof Table) {
|
||||||
// this.addSqlInfoByTable((Table) fromItem);
|
this.addSqlInfoByTable((Table) fromItem);
|
||||||
// }
|
}
|
||||||
// // 5.2 通过子查询的方式from
|
// 5.2 通过子查询的方式from
|
||||||
// else if (fromItem instanceof SubSelect) {
|
else if (fromItem instanceof SubSelect) {
|
||||||
// this.handleSubSelect((SubSelect) fromItem);
|
this.handleSubSelect((SubSelect) fromItem);
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
// // 6. 解析 selectFields
|
// 6. 解析 selectFields
|
||||||
// List<SelectItem> selectItems = plainSelect.getSelectItems();
|
List<SelectItem> selectItems = plainSelect.getSelectItems();
|
||||||
// for (SelectItem selectItem : selectItems) {
|
for (SelectItem selectItem : selectItems) {
|
||||||
// // 6.1 查询的是全部字段
|
// 6.1 查询的是全部字段
|
||||||
// if (selectItem instanceof AllColumns) {
|
if (selectItem instanceof AllColumns) {
|
||||||
// // 当 selectItem 为 AllColumns 时,fromItem 必定为 Table
|
// 当 selectItem 为 AllColumns 时,fromItem 必定为 Table
|
||||||
// String tableName = plainSelect.getFromItem(Table.class).getName();
|
String tableName = plainSelect.getFromItem(Table.class).getName();
|
||||||
// // 此处必定不为空,因为在解析 fromItem 时,已经将表名添加到 allTableMap 中
|
// 此处必定不为空,因为在解析 fromItem 时,已经将表名添加到 allTableMap 中
|
||||||
// SelectSqlInfo sqlInfo = this.allTableMap.get(tableName);
|
SelectSqlInfo sqlInfo = this.allTableMap.get(tableName);
|
||||||
// assert sqlInfo != null;
|
assert sqlInfo != null;
|
||||||
// // 设置为查询全部字段
|
// 设置为查询全部字段
|
||||||
// sqlInfo.setSelectAll(true);
|
sqlInfo.setSelectAll(true);
|
||||||
// sqlInfo.setSelectFields(null);
|
sqlInfo.setSelectFields(null);
|
||||||
// sqlInfo.setRealSelectFields(null);
|
sqlInfo.setRealSelectFields(null);
|
||||||
// }
|
}
|
||||||
// // 6.2 查询的是带表别名( u.* )的全部字段
|
// 6.2 查询的是带表别名( u.* )的全部字段
|
||||||
// else if (selectItem instanceof AllTableColumns) {
|
else if (selectItem instanceof AllTableColumns) {
|
||||||
// AllTableColumns allTableColumns = (AllTableColumns) selectItem;
|
AllTableColumns allTableColumns = (AllTableColumns) selectItem;
|
||||||
// String aliasName = allTableColumns.getTable().getName();
|
String aliasName = allTableColumns.getTable().getName();
|
||||||
// // 通过别名获取表名
|
// 通过别名获取表名
|
||||||
// String tableName = this.tableAliasMap.get(aliasName);
|
String tableName = this.tableAliasMap.get(aliasName);
|
||||||
// if (tableName == null) {
|
if (tableName == null) {
|
||||||
// tableName = aliasName;
|
tableName = aliasName;
|
||||||
// }
|
}
|
||||||
// SelectSqlInfo sqlInfo = this.allTableMap.get(tableName);
|
SelectSqlInfo sqlInfo = this.allTableMap.get(tableName);
|
||||||
// // 如果此处为空,则说明该字段是通过子查询获取的,所以可以不处理,只有实际表才需要处理
|
// 如果此处为空,则说明该字段是通过子查询获取的,所以可以不处理,只有实际表才需要处理
|
||||||
// if (sqlInfo != null) {
|
if (sqlInfo != null) {
|
||||||
// // 设置为查询全部字段
|
// 设置为查询全部字段
|
||||||
// sqlInfo.setSelectAll(true);
|
sqlInfo.setSelectAll(true);
|
||||||
// sqlInfo.setSelectFields(null);
|
sqlInfo.setSelectFields(null);
|
||||||
// sqlInfo.setRealSelectFields(null);
|
sqlInfo.setRealSelectFields(null);
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
// // 6.3 各种字段表达式处理
|
// 6.3 各种字段表达式处理
|
||||||
// else if (selectItem instanceof SelectExpressionItem) {
|
else if (selectItem instanceof SelectExpressionItem) {
|
||||||
// SelectExpressionItem selectExpressionItem = (SelectExpressionItem) selectItem;
|
SelectExpressionItem selectExpressionItem = (SelectExpressionItem) selectItem;
|
||||||
// Expression expression = selectExpressionItem.getExpression();
|
Expression expression = selectExpressionItem.getExpression();
|
||||||
// Alias alias = selectExpressionItem.getAlias();
|
Alias alias = selectExpressionItem.getAlias();
|
||||||
// this.handleExpression(expression, alias, plainSelect.getFromItem());
|
this.handleExpression(expression, alias, plainSelect.getFromItem());
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
// } else {
|
} else {
|
||||||
// log.warn("暂时尚未处理该类型的 SelectBody: {}", selectBody.getClass().getName());
|
log.warn("暂时尚未处理该类型的 SelectBody: {}", selectBody.getClass().getName());
|
||||||
// throw new JeecgBootException("暂时尚未处理该类型的 SelectBody");
|
throw new JeecgBootException("暂时尚未处理该类型的 SelectBody");
|
||||||
// }
|
}
|
||||||
// } else {
|
} else {
|
||||||
// // 非 select 查询sql,不做处理
|
// 非 select 查询sql,不做处理
|
||||||
// throw new JeecgBootException("非 select 查询sql,不做处理");
|
throw new JeecgBootException("非 select 查询sql,不做处理");
|
||||||
// }
|
}
|
||||||
// return this.allTableMap;
|
return this.allTableMap;
|
||||||
// }
|
}
|
||||||
//
|
|
||||||
// /**
|
/**
|
||||||
// * 处理子查询
|
* 处理子查询
|
||||||
// *
|
*
|
||||||
// * @param subSelect
|
* @param subSelect
|
||||||
// */
|
*/
|
||||||
// private void handleSubSelect(SubSelect subSelect) {
|
private void handleSubSelect(SubSelect subSelect) {
|
||||||
// try {
|
try {
|
||||||
// String subSelectSql = subSelect.getSelectBody().toString();
|
String subSelectSql = subSelect.getSelectBody().toString();
|
||||||
// // 递归调用解析
|
// 递归调用解析
|
||||||
// Map<String, SelectSqlInfo> map = JSqlParserUtils.parseAllSelectTable(subSelectSql);
|
Map<String, SelectSqlInfo> map = JSqlParserUtils.parseAllSelectTable(subSelectSql);
|
||||||
// if (map != null) {
|
if (map != null) {
|
||||||
// this.assignMap(map);
|
this.assignMap(map);
|
||||||
// }
|
}
|
||||||
// } catch (Exception e) {
|
} catch (Exception e) {
|
||||||
// log.error("解析子查询出错", e);
|
log.error("解析子查询出错", e);
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
//
|
|
||||||
// /**
|
/**
|
||||||
// * 处理查询字段表达式
|
* 处理查询字段表达式
|
||||||
// *
|
*
|
||||||
// * @param expression
|
* @param expression
|
||||||
// */
|
*/
|
||||||
// private void handleExpression(Expression expression, Alias alias, FromItem fromItem) {
|
private void handleExpression(Expression expression, Alias alias, FromItem fromItem) {
|
||||||
// // 处理函数式字段 CONCAT(name,'(',age,')')
|
// 处理函数式字段 CONCAT(name,'(',age,')')
|
||||||
// if (expression instanceof Function) {
|
if (expression instanceof Function) {
|
||||||
// Function functionExp = (Function) expression;
|
Function functionExp = (Function) expression;
|
||||||
// List<Expression> expressions = functionExp.getParameters().getExpressions();
|
List<Expression> expressions = functionExp.getParameters().getExpressions();
|
||||||
// for (Expression expItem : expressions) {
|
for (Expression expItem : expressions) {
|
||||||
// this.handleExpression(expItem, null, fromItem);
|
this.handleExpression(expItem, null, fromItem);
|
||||||
// }
|
}
|
||||||
// return;
|
return;
|
||||||
// }
|
}
|
||||||
// // 处理字段上的子查询
|
// 处理字段上的子查询
|
||||||
// if (expression instanceof SubSelect) {
|
if (expression instanceof SubSelect) {
|
||||||
// this.handleSubSelect((SubSelect) expression);
|
this.handleSubSelect((SubSelect) expression);
|
||||||
// return;
|
return;
|
||||||
// }
|
}
|
||||||
// // 不处理字面量
|
// 不处理字面量
|
||||||
// if (expression instanceof StringValue ||
|
if (expression instanceof StringValue ||
|
||||||
// expression instanceof NullValue ||
|
expression instanceof NullValue ||
|
||||||
// expression instanceof LongValue ||
|
expression instanceof LongValue ||
|
||||||
// expression instanceof DoubleValue ||
|
expression instanceof DoubleValue ||
|
||||||
// expression instanceof HexValue ||
|
expression instanceof HexValue ||
|
||||||
// expression instanceof DateValue ||
|
expression instanceof DateValue ||
|
||||||
// expression instanceof TimestampValue ||
|
expression instanceof TimestampValue ||
|
||||||
// expression instanceof TimeValue
|
expression instanceof TimeValue
|
||||||
// ) {
|
) {
|
||||||
// return;
|
return;
|
||||||
// }
|
}
|
||||||
//
|
|
||||||
// // 处理字段
|
// 处理字段
|
||||||
// if (expression instanceof Column) {
|
if (expression instanceof Column) {
|
||||||
// Column column = (Column) expression;
|
Column column = (Column) expression;
|
||||||
// // 查询字段名
|
// 查询字段名
|
||||||
// String fieldName = column.getColumnName();
|
String fieldName = column.getColumnName();
|
||||||
// String aliasName = fieldName;
|
String aliasName = fieldName;
|
||||||
// if (alias != null) {
|
if (alias != null) {
|
||||||
// aliasName = alias.getName();
|
aliasName = alias.getName();
|
||||||
// }
|
}
|
||||||
// String tableName;
|
String tableName;
|
||||||
// if (column.getTable() != null) {
|
if (column.getTable() != null) {
|
||||||
// // 通过列的表名获取 sqlInfo
|
// 通过列的表名获取 sqlInfo
|
||||||
// // 例如 user.name,这里的 tableName 就是 user
|
// 例如 user.name,这里的 tableName 就是 user
|
||||||
// tableName = column.getTable().getName();
|
tableName = column.getTable().getName();
|
||||||
// // 有可能是别名,需要转换为真实表名
|
// 有可能是别名,需要转换为真实表名
|
||||||
// if (this.tableAliasMap.get(tableName) != null) {
|
if (this.tableAliasMap.get(tableName) != null) {
|
||||||
// tableName = this.tableAliasMap.get(tableName);
|
tableName = this.tableAliasMap.get(tableName);
|
||||||
// }
|
}
|
||||||
// } else {
|
} else {
|
||||||
// // 当column的table为空时,说明是 fromItem 中的字段
|
// 当column的table为空时,说明是 fromItem 中的字段
|
||||||
// tableName = ((Table) fromItem).getName();
|
tableName = ((Table) fromItem).getName();
|
||||||
// }
|
}
|
||||||
// SelectSqlInfo $sqlInfo = this.allTableMap.get(tableName);
|
SelectSqlInfo $sqlInfo = this.allTableMap.get(tableName);
|
||||||
// if ($sqlInfo != null) {
|
if ($sqlInfo != null) {
|
||||||
// $sqlInfo.addSelectField(aliasName, fieldName);
|
$sqlInfo.addSelectField(aliasName, fieldName);
|
||||||
// } else {
|
} else {
|
||||||
// log.warn("发生意外情况,未找到表名为 {} 的 SelectSqlInfo", tableName);
|
log.warn("发生意外情况,未找到表名为 {} 的 SelectSqlInfo", tableName);
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
//
|
|
||||||
// /**
|
/**
|
||||||
// * 根据表名添加sqlInfo
|
* 根据表名添加sqlInfo
|
||||||
// *
|
*
|
||||||
// * @param table
|
* @param table
|
||||||
// */
|
*/
|
||||||
// private void addSqlInfoByTable(Table table) {
|
private void addSqlInfoByTable(Table table) {
|
||||||
// String tableName = table.getName();
|
String tableName = table.getName();
|
||||||
// // 解析 aliasName
|
// 解析 aliasName
|
||||||
// if (table.getAlias() != null) {
|
if (table.getAlias() != null) {
|
||||||
// this.tableAliasMap.put(table.getAlias().getName(), tableName);
|
this.tableAliasMap.put(table.getAlias().getName(), tableName);
|
||||||
// }
|
}
|
||||||
// SelectSqlInfo sqlInfo = new SelectSqlInfo(this.parsedSql);
|
SelectSqlInfo sqlInfo = new SelectSqlInfo(this.parsedSql);
|
||||||
// sqlInfo.setFromTableName(table.getName());
|
sqlInfo.setFromTableName(table.getName());
|
||||||
// this.allTableMap.put(sqlInfo.getFromTableName(), sqlInfo);
|
this.allTableMap.put(sqlInfo.getFromTableName(), sqlInfo);
|
||||||
// }
|
}
|
||||||
//
|
|
||||||
// /**
|
/**
|
||||||
// * 合并map
|
* 合并map
|
||||||
// *
|
*
|
||||||
// * @param source
|
* @param source
|
||||||
// */
|
*/
|
||||||
// private void assignMap(Map<String, SelectSqlInfo> source) {
|
private void assignMap(Map<String, SelectSqlInfo> source) {
|
||||||
// for (Map.Entry<String, SelectSqlInfo> entry : source.entrySet()) {
|
for (Map.Entry<String, SelectSqlInfo> entry : source.entrySet()) {
|
||||||
// SelectSqlInfo sqlInfo = this.allTableMap.get(entry.getKey());
|
SelectSqlInfo sqlInfo = this.allTableMap.get(entry.getKey());
|
||||||
// if (sqlInfo == null) {
|
if (sqlInfo == null) {
|
||||||
// this.allTableMap.put(entry.getKey(), entry.getValue());
|
this.allTableMap.put(entry.getKey(), entry.getValue());
|
||||||
// } else {
|
} else {
|
||||||
// // 合并
|
// 合并
|
||||||
// if (sqlInfo.getSelectFields() == null) {
|
if (sqlInfo.getSelectFields() == null) {
|
||||||
// sqlInfo.setSelectFields(entry.getValue().getSelectFields());
|
sqlInfo.setSelectFields(entry.getValue().getSelectFields());
|
||||||
// } else {
|
} else {
|
||||||
// sqlInfo.getSelectFields().addAll(entry.getValue().getSelectFields());
|
sqlInfo.getSelectFields().addAll(entry.getValue().getSelectFields());
|
||||||
// }
|
}
|
||||||
// if (sqlInfo.getRealSelectFields() == null) {
|
if (sqlInfo.getRealSelectFields() == null) {
|
||||||
// sqlInfo.setRealSelectFields(entry.getValue().getRealSelectFields());
|
sqlInfo.setRealSelectFields(entry.getValue().getRealSelectFields());
|
||||||
// } else {
|
} else {
|
||||||
// sqlInfo.getRealSelectFields().addAll(entry.getValue().getRealSelectFields());
|
sqlInfo.getRealSelectFields().addAll(entry.getValue().getRealSelectFields());
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
//
|
|
||||||
//}
|
}
|
||||||
|
|||||||
@ -1,190 +1,190 @@
|
|||||||
//package org.jeecg.common.util.sqlparse;
|
package org.jeecg.common.util.sqlparse;
|
||||||
//
|
|
||||||
//import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
//import net.sf.jsqlparser.JSQLParserException;
|
import net.sf.jsqlparser.JSQLParserException;
|
||||||
//import net.sf.jsqlparser.expression.*;
|
import net.sf.jsqlparser.expression.*;
|
||||||
//import net.sf.jsqlparser.parser.CCJSqlParserManager;
|
import net.sf.jsqlparser.parser.CCJSqlParserManager;
|
||||||
//import net.sf.jsqlparser.schema.Column;
|
import net.sf.jsqlparser.schema.Column;
|
||||||
//import net.sf.jsqlparser.schema.Table;
|
import net.sf.jsqlparser.schema.Table;
|
||||||
//import net.sf.jsqlparser.statement.Statement;
|
import net.sf.jsqlparser.statement.Statement;
|
||||||
//import net.sf.jsqlparser.statement.select.*;
|
import net.sf.jsqlparser.statement.select.*;
|
||||||
//import org.jeecg.common.exception.JeecgBootException;
|
import org.jeecg.common.exception.JeecgBootException;
|
||||||
//import org.jeecg.common.util.oConvertUtils;
|
import org.jeecg.common.util.oConvertUtils;
|
||||||
//import org.jeecg.common.util.sqlparse.vo.SelectSqlInfo;
|
import org.jeecg.common.util.sqlparse.vo.SelectSqlInfo;
|
||||||
//
|
|
||||||
//import java.io.StringReader;
|
import java.io.StringReader;
|
||||||
//import java.util.List;
|
import java.util.List;
|
||||||
//import java.util.Map;
|
import java.util.Map;
|
||||||
//
|
|
||||||
//@Slf4j
|
@Slf4j
|
||||||
//public class JSqlParserUtils {
|
public class JSqlParserUtils {
|
||||||
//
|
|
||||||
// /**
|
/**
|
||||||
// * 解析 查询(select)sql的信息,
|
* 解析 查询(select)sql的信息,
|
||||||
// * 此方法会展开所有子查询到一个map里,
|
* 此方法会展开所有子查询到一个map里,
|
||||||
// * key只存真实的表名,如果查询的没有真实的表名,则会被忽略。
|
* key只存真实的表名,如果查询的没有真实的表名,则会被忽略。
|
||||||
// * value只存真实的字段名,如果查询的没有真实的字段名,则会被忽略。
|
* value只存真实的字段名,如果查询的没有真实的字段名,则会被忽略。
|
||||||
// * <p>
|
* <p>
|
||||||
// * 例如:SELECT a.*,d.age,(SELECT count(1) FROM sys_depart) AS count FROM (SELECT username AS foo, realname FROM sys_user) a, demo d
|
* 例如:SELECT a.*,d.age,(SELECT count(1) FROM sys_depart) AS count FROM (SELECT username AS foo, realname FROM sys_user) a, demo d
|
||||||
// * 解析后的结果为:{sys_user=[username, realname], demo=[age], sys_depart=[]}
|
* 解析后的结果为:{sys_user=[username, realname], demo=[age], sys_depart=[]}
|
||||||
// *
|
*
|
||||||
// * @param selectSql
|
* @param selectSql
|
||||||
// * @return
|
* @return
|
||||||
// */
|
*/
|
||||||
// public static Map<String, SelectSqlInfo> parseAllSelectTable(String selectSql) throws JSQLParserException {
|
public static Map<String, SelectSqlInfo> parseAllSelectTable(String selectSql) throws JSQLParserException {
|
||||||
// if (oConvertUtils.isEmpty(selectSql)) {
|
if (oConvertUtils.isEmpty(selectSql)) {
|
||||||
// return null;
|
return null;
|
||||||
// }
|
}
|
||||||
// // log.info("解析查询Sql:{}", selectSql);
|
// log.info("解析查询Sql:{}", selectSql);
|
||||||
// JSqlParserAllTableManager allTableManager = new JSqlParserAllTableManager(selectSql);
|
JSqlParserAllTableManager allTableManager = new JSqlParserAllTableManager(selectSql);
|
||||||
// return allTableManager.parse();
|
return allTableManager.parse();
|
||||||
// }
|
}
|
||||||
//
|
|
||||||
// /**
|
/**
|
||||||
// * 解析 查询(select)sql的信息,子查询嵌套
|
* 解析 查询(select)sql的信息,子查询嵌套
|
||||||
// *
|
*
|
||||||
// * @param selectSql
|
* @param selectSql
|
||||||
// * @return
|
* @return
|
||||||
// */
|
*/
|
||||||
// public static SelectSqlInfo parseSelectSqlInfo(String selectSql) throws JSQLParserException {
|
public static SelectSqlInfo parseSelectSqlInfo(String selectSql) throws JSQLParserException {
|
||||||
// if (oConvertUtils.isEmpty(selectSql)) {
|
if (oConvertUtils.isEmpty(selectSql)) {
|
||||||
// return null;
|
return null;
|
||||||
// }
|
}
|
||||||
// // log.info("解析查询Sql:{}", selectSql);
|
// log.info("解析查询Sql:{}", selectSql);
|
||||||
// // 使用 JSqlParer 解析sql
|
// 使用 JSqlParer 解析sql
|
||||||
// // 1、创建解析器
|
// 1、创建解析器
|
||||||
// CCJSqlParserManager mgr = new CCJSqlParserManager();
|
CCJSqlParserManager mgr = new CCJSqlParserManager();
|
||||||
// // 2、使用解析器解析sql生成具有层次结构的java类
|
// 2、使用解析器解析sql生成具有层次结构的java类
|
||||||
// Statement stmt = mgr.parse(new StringReader(selectSql));
|
Statement stmt = mgr.parse(new StringReader(selectSql));
|
||||||
// if (stmt instanceof Select) {
|
if (stmt instanceof Select) {
|
||||||
// Select selectStatement = (Select) stmt;
|
Select selectStatement = (Select) stmt;
|
||||||
// // 3、解析select查询sql的信息
|
// 3、解析select查询sql的信息
|
||||||
// return JSqlParserUtils.parseBySelectBody(selectStatement.getSelectBody());
|
return JSqlParserUtils.parseBySelectBody(selectStatement.getSelectBody());
|
||||||
// } else {
|
} else {
|
||||||
// // 非 select 查询sql,不做处理
|
// 非 select 查询sql,不做处理
|
||||||
// throw new JeecgBootException("非 select 查询sql,不做处理");
|
throw new JeecgBootException("非 select 查询sql,不做处理");
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
//
|
|
||||||
// /**
|
/**
|
||||||
// * 解析 select 查询sql的信息
|
* 解析 select 查询sql的信息
|
||||||
// *
|
*
|
||||||
// * @param selectBody
|
* @param selectBody
|
||||||
// * @return
|
* @return
|
||||||
// */
|
*/
|
||||||
// private static SelectSqlInfo parseBySelectBody(SelectBody selectBody) {
|
private static SelectSqlInfo parseBySelectBody(SelectBody selectBody) {
|
||||||
// // 判断是否使用了union等操作
|
// 判断是否使用了union等操作
|
||||||
// if (selectBody instanceof SetOperationList) {
|
if (selectBody instanceof SetOperationList) {
|
||||||
// // 如果使用了union等操作,则只解析第一个查询
|
// 如果使用了union等操作,则只解析第一个查询
|
||||||
// List<SelectBody> selectBodyList = ((SetOperationList) selectBody).getSelects();
|
List<SelectBody> selectBodyList = ((SetOperationList) selectBody).getSelects();
|
||||||
// return JSqlParserUtils.parseBySelectBody(selectBodyList.get(0));
|
return JSqlParserUtils.parseBySelectBody(selectBodyList.get(0));
|
||||||
// }
|
}
|
||||||
// // 简单的select查询
|
// 简单的select查询
|
||||||
// if (selectBody instanceof PlainSelect) {
|
if (selectBody instanceof PlainSelect) {
|
||||||
// SelectSqlInfo sqlInfo = new SelectSqlInfo(selectBody);
|
SelectSqlInfo sqlInfo = new SelectSqlInfo(selectBody);
|
||||||
// PlainSelect plainSelect = (PlainSelect) selectBody;
|
PlainSelect plainSelect = (PlainSelect) selectBody;
|
||||||
// FromItem fromItem = plainSelect.getFromItem();
|
FromItem fromItem = plainSelect.getFromItem();
|
||||||
// // 解析 aliasName
|
// 解析 aliasName
|
||||||
// if (fromItem.getAlias() != null) {
|
if (fromItem.getAlias() != null) {
|
||||||
// sqlInfo.setFromTableAliasName(fromItem.getAlias().getName());
|
sqlInfo.setFromTableAliasName(fromItem.getAlias().getName());
|
||||||
// }
|
}
|
||||||
// // 解析 表名
|
// 解析 表名
|
||||||
// if (fromItem instanceof Table) {
|
if (fromItem instanceof Table) {
|
||||||
// // 通过表名的方式from
|
// 通过表名的方式from
|
||||||
// Table fromTable = (Table) fromItem;
|
Table fromTable = (Table) fromItem;
|
||||||
// sqlInfo.setFromTableName(fromTable.getName());
|
sqlInfo.setFromTableName(fromTable.getName());
|
||||||
// } else if (fromItem instanceof SubSelect) {
|
} else if (fromItem instanceof SubSelect) {
|
||||||
// // 通过子查询的方式from
|
// 通过子查询的方式from
|
||||||
// SubSelect fromSubSelect = (SubSelect) fromItem;
|
SubSelect fromSubSelect = (SubSelect) fromItem;
|
||||||
// SelectSqlInfo subSqlInfo = JSqlParserUtils.parseBySelectBody(fromSubSelect.getSelectBody());
|
SelectSqlInfo subSqlInfo = JSqlParserUtils.parseBySelectBody(fromSubSelect.getSelectBody());
|
||||||
// sqlInfo.setFromSubSelect(subSqlInfo);
|
sqlInfo.setFromSubSelect(subSqlInfo);
|
||||||
// }
|
}
|
||||||
// // 解析 selectFields
|
// 解析 selectFields
|
||||||
// List<SelectItem> selectItems = plainSelect.getSelectItems();
|
List<SelectItem> selectItems = plainSelect.getSelectItems();
|
||||||
// for (SelectItem selectItem : selectItems) {
|
for (SelectItem selectItem : selectItems) {
|
||||||
// if (selectItem instanceof AllColumns || selectItem instanceof AllTableColumns) {
|
if (selectItem instanceof AllColumns || selectItem instanceof AllTableColumns) {
|
||||||
// // 全部字段
|
// 全部字段
|
||||||
// sqlInfo.setSelectAll(true);
|
sqlInfo.setSelectAll(true);
|
||||||
// sqlInfo.setSelectFields(null);
|
sqlInfo.setSelectFields(null);
|
||||||
// sqlInfo.setRealSelectFields(null);
|
sqlInfo.setRealSelectFields(null);
|
||||||
// break;
|
break;
|
||||||
// } else if (selectItem instanceof SelectExpressionItem) {
|
} else if (selectItem instanceof SelectExpressionItem) {
|
||||||
// // 获取单个查询字段名
|
// 获取单个查询字段名
|
||||||
// SelectExpressionItem selectExpressionItem = (SelectExpressionItem) selectItem;
|
SelectExpressionItem selectExpressionItem = (SelectExpressionItem) selectItem;
|
||||||
// Expression expression = selectExpressionItem.getExpression();
|
Expression expression = selectExpressionItem.getExpression();
|
||||||
// Alias alias = selectExpressionItem.getAlias();
|
Alias alias = selectExpressionItem.getAlias();
|
||||||
// JSqlParserUtils.handleExpression(sqlInfo, expression, alias);
|
JSqlParserUtils.handleExpression(sqlInfo, expression, alias);
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
// return sqlInfo;
|
return sqlInfo;
|
||||||
// } else {
|
} else {
|
||||||
// log.warn("暂时尚未处理该类型的 SelectBody: {}", selectBody.getClass().getName());
|
log.warn("暂时尚未处理该类型的 SelectBody: {}", selectBody.getClass().getName());
|
||||||
// throw new JeecgBootException("暂时尚未处理该类型的 SelectBody");
|
throw new JeecgBootException("暂时尚未处理该类型的 SelectBody");
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
//
|
|
||||||
// /**
|
/**
|
||||||
// * 处理查询字段表达式
|
* 处理查询字段表达式
|
||||||
// *
|
*
|
||||||
// * @param sqlInfo
|
* @param sqlInfo
|
||||||
// * @param expression
|
* @param expression
|
||||||
// * @param alias 是否有别名,无传null
|
* @param alias 是否有别名,无传null
|
||||||
// */
|
*/
|
||||||
// private static void handleExpression(SelectSqlInfo sqlInfo, Expression expression, Alias alias) {
|
private static void handleExpression(SelectSqlInfo sqlInfo, Expression expression, Alias alias) {
|
||||||
// // 处理函数式字段 CONCAT(name,'(',age,')')
|
// 处理函数式字段 CONCAT(name,'(',age,')')
|
||||||
// if (expression instanceof Function) {
|
if (expression instanceof Function) {
|
||||||
// JSqlParserUtils.handleFunctionExpression((Function) expression, sqlInfo);
|
JSqlParserUtils.handleFunctionExpression((Function) expression, sqlInfo);
|
||||||
// return;
|
return;
|
||||||
// }
|
}
|
||||||
// // 处理字段上的子查询
|
// 处理字段上的子查询
|
||||||
// if (expression instanceof SubSelect) {
|
if (expression instanceof SubSelect) {
|
||||||
// SubSelect subSelect = (SubSelect) expression;
|
SubSelect subSelect = (SubSelect) expression;
|
||||||
// SelectSqlInfo subSqlInfo = JSqlParserUtils.parseBySelectBody(subSelect.getSelectBody());
|
SelectSqlInfo subSqlInfo = JSqlParserUtils.parseBySelectBody(subSelect.getSelectBody());
|
||||||
// // 注:字段上的子查询,必须只查询一个字段,否则会报错,所以可以放心合并
|
// 注:字段上的子查询,必须只查询一个字段,否则会报错,所以可以放心合并
|
||||||
// sqlInfo.getSelectFields().addAll(subSqlInfo.getSelectFields());
|
sqlInfo.getSelectFields().addAll(subSqlInfo.getSelectFields());
|
||||||
// sqlInfo.getRealSelectFields().addAll(subSqlInfo.getAllRealSelectFields());
|
sqlInfo.getRealSelectFields().addAll(subSqlInfo.getAllRealSelectFields());
|
||||||
// return;
|
return;
|
||||||
// }
|
}
|
||||||
// // 不处理字面量
|
// 不处理字面量
|
||||||
// if (expression instanceof StringValue ||
|
if (expression instanceof StringValue ||
|
||||||
// expression instanceof NullValue ||
|
expression instanceof NullValue ||
|
||||||
// expression instanceof LongValue ||
|
expression instanceof LongValue ||
|
||||||
// expression instanceof DoubleValue ||
|
expression instanceof DoubleValue ||
|
||||||
// expression instanceof HexValue ||
|
expression instanceof HexValue ||
|
||||||
// expression instanceof DateValue ||
|
expression instanceof DateValue ||
|
||||||
// expression instanceof TimestampValue ||
|
expression instanceof TimestampValue ||
|
||||||
// expression instanceof TimeValue
|
expression instanceof TimeValue
|
||||||
// ) {
|
) {
|
||||||
// return;
|
return;
|
||||||
// }
|
}
|
||||||
//
|
|
||||||
// // 查询字段名
|
// 查询字段名
|
||||||
// String selectField = expression.toString();
|
String selectField = expression.toString();
|
||||||
// // 实际查询字段名
|
// 实际查询字段名
|
||||||
// String realSelectField = selectField;
|
String realSelectField = selectField;
|
||||||
// // 判断是否有别名
|
// 判断是否有别名
|
||||||
// if (alias != null) {
|
if (alias != null) {
|
||||||
// selectField = alias.getName();
|
selectField = alias.getName();
|
||||||
// }
|
}
|
||||||
// // 获取真实字段名
|
// 获取真实字段名
|
||||||
// if (expression instanceof Column) {
|
if (expression instanceof Column) {
|
||||||
// Column column = (Column) expression;
|
Column column = (Column) expression;
|
||||||
// realSelectField = column.getColumnName();
|
realSelectField = column.getColumnName();
|
||||||
// }
|
}
|
||||||
// sqlInfo.addSelectField(selectField, realSelectField);
|
sqlInfo.addSelectField(selectField, realSelectField);
|
||||||
// }
|
}
|
||||||
//
|
|
||||||
// /**
|
/**
|
||||||
// * 处理函数式字段
|
* 处理函数式字段
|
||||||
// *
|
*
|
||||||
// * @param functionExp
|
* @param functionExp
|
||||||
// * @param sqlInfo
|
* @param sqlInfo
|
||||||
// */
|
*/
|
||||||
// private static void handleFunctionExpression(Function functionExp, SelectSqlInfo sqlInfo) {
|
private static void handleFunctionExpression(Function functionExp, SelectSqlInfo sqlInfo) {
|
||||||
// List<Expression> expressions = functionExp.getParameters().getExpressions();
|
List<Expression> expressions = functionExp.getParameters().getExpressions();
|
||||||
// for (Expression expression : expressions) {
|
for (Expression expression : expressions) {
|
||||||
// JSqlParserUtils.handleExpression(sqlInfo, expression, null);
|
JSqlParserUtils.handleExpression(sqlInfo, expression, null);
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
//
|
|
||||||
//}
|
}
|
||||||
|
|||||||
@ -1,101 +1,101 @@
|
|||||||
//package org.jeecg.common.util.sqlparse.vo;
|
package org.jeecg.common.util.sqlparse.vo;
|
||||||
//
|
|
||||||
//import lombok.Data;
|
import lombok.Data;
|
||||||
//import net.sf.jsqlparser.statement.select.SelectBody;
|
import net.sf.jsqlparser.statement.select.SelectBody;
|
||||||
//
|
|
||||||
//import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
//import java.util.Set;
|
import java.util.Set;
|
||||||
//
|
|
||||||
///**
|
/**
|
||||||
// * select 查询 sql 的信息
|
* select 查询 sql 的信息
|
||||||
// */
|
*/
|
||||||
//@Data
|
@Data
|
||||||
//public class SelectSqlInfo {
|
public class SelectSqlInfo {
|
||||||
//
|
|
||||||
// /**
|
/**
|
||||||
// * 查询的表名,如果是子查询,则此处为null
|
* 查询的表名,如果是子查询,则此处为null
|
||||||
// */
|
*/
|
||||||
// private String fromTableName;
|
private String fromTableName;
|
||||||
// /**
|
/**
|
||||||
// * 表别名
|
* 表别名
|
||||||
// */
|
*/
|
||||||
// private String fromTableAliasName;
|
private String fromTableAliasName;
|
||||||
// /**
|
/**
|
||||||
// * 通过子查询获取的表信息,例如:select name from (select * from user) u
|
* 通过子查询获取的表信息,例如:select name from (select * from user) u
|
||||||
// * 如果不是子查询,则为null
|
* 如果不是子查询,则为null
|
||||||
// */
|
*/
|
||||||
// private SelectSqlInfo fromSubSelect;
|
private SelectSqlInfo fromSubSelect;
|
||||||
// /**
|
/**
|
||||||
// * 查询的字段集合,如果是 * 则为null,如果设了别名则为别名
|
* 查询的字段集合,如果是 * 则为null,如果设了别名则为别名
|
||||||
// */
|
*/
|
||||||
// private Set<String> selectFields;
|
private Set<String> selectFields;
|
||||||
// /**
|
/**
|
||||||
// * 真实的查询字段集合,如果是 * 则为null,如果设了别名则为原始字段名
|
* 真实的查询字段集合,如果是 * 则为null,如果设了别名则为原始字段名
|
||||||
// */
|
*/
|
||||||
// private Set<String> realSelectFields;
|
private Set<String> realSelectFields;
|
||||||
// /**
|
/**
|
||||||
// * 是否是查询所有字段
|
* 是否是查询所有字段
|
||||||
// */
|
*/
|
||||||
// private boolean selectAll;
|
private boolean selectAll;
|
||||||
//
|
|
||||||
// /**
|
/**
|
||||||
// * 解析之后的 SQL (关键字都是大写)
|
* 解析之后的 SQL (关键字都是大写)
|
||||||
// */
|
*/
|
||||||
// private final String parsedSql;
|
private final String parsedSql;
|
||||||
//
|
|
||||||
// public SelectSqlInfo(String parsedSql) {
|
public SelectSqlInfo(String parsedSql) {
|
||||||
// this.parsedSql = parsedSql;
|
this.parsedSql = parsedSql;
|
||||||
// }
|
}
|
||||||
//
|
|
||||||
// public SelectSqlInfo(SelectBody selectBody) {
|
public SelectSqlInfo(SelectBody selectBody) {
|
||||||
// this.parsedSql = selectBody.toString();
|
this.parsedSql = selectBody.toString();
|
||||||
// }
|
}
|
||||||
//
|
|
||||||
// public void addSelectField(String selectField, String realSelectField) {
|
public void addSelectField(String selectField, String realSelectField) {
|
||||||
// if (this.selectFields == null) {
|
if (this.selectFields == null) {
|
||||||
// this.selectFields = new HashSet<>();
|
this.selectFields = new HashSet<>();
|
||||||
// }
|
}
|
||||||
// if (this.realSelectFields == null) {
|
if (this.realSelectFields == null) {
|
||||||
// this.realSelectFields = new HashSet<>();
|
this.realSelectFields = new HashSet<>();
|
||||||
// }
|
}
|
||||||
// this.selectFields.add(selectField);
|
this.selectFields.add(selectField);
|
||||||
// this.realSelectFields.add(realSelectField);
|
this.realSelectFields.add(realSelectField);
|
||||||
// }
|
}
|
||||||
//
|
|
||||||
// /**
|
/**
|
||||||
// * 获取所有字段,包括子查询里的。
|
* 获取所有字段,包括子查询里的。
|
||||||
// *
|
*
|
||||||
// * @return
|
* @return
|
||||||
// */
|
*/
|
||||||
// public Set<String> getAllRealSelectFields() {
|
public Set<String> getAllRealSelectFields() {
|
||||||
// Set<String> fields = new HashSet<>();
|
Set<String> fields = new HashSet<>();
|
||||||
// // 递归获取所有字段,起个直观的方法名为:
|
// 递归获取所有字段,起个直观的方法名为:
|
||||||
// this.recursiveGetAllFields(this, fields);
|
this.recursiveGetAllFields(this, fields);
|
||||||
// return fields;
|
return fields;
|
||||||
// }
|
}
|
||||||
//
|
|
||||||
// /**
|
/**
|
||||||
// * 递归获取所有字段
|
* 递归获取所有字段
|
||||||
// */
|
*/
|
||||||
// private void recursiveGetAllFields(SelectSqlInfo sqlInfo, Set<String> fields) {
|
private void recursiveGetAllFields(SelectSqlInfo sqlInfo, Set<String> fields) {
|
||||||
// if (!sqlInfo.isSelectAll() && sqlInfo.getRealSelectFields() != null) {
|
if (!sqlInfo.isSelectAll() && sqlInfo.getRealSelectFields() != null) {
|
||||||
// fields.addAll(sqlInfo.getRealSelectFields());
|
fields.addAll(sqlInfo.getRealSelectFields());
|
||||||
// }
|
}
|
||||||
// if (sqlInfo.getFromSubSelect() != null) {
|
if (sqlInfo.getFromSubSelect() != null) {
|
||||||
// recursiveGetAllFields(sqlInfo.getFromSubSelect(), fields);
|
recursiveGetAllFields(sqlInfo.getFromSubSelect(), fields);
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
//
|
|
||||||
// @Override
|
@Override
|
||||||
// public String toString() {
|
public String toString() {
|
||||||
// return "SelectSqlInfo{" +
|
return "SelectSqlInfo{" +
|
||||||
// "fromTableName='" + fromTableName + '\'' +
|
"fromTableName='" + fromTableName + '\'' +
|
||||||
// ", fromSubSelect=" + fromSubSelect +
|
", fromSubSelect=" + fromSubSelect +
|
||||||
// ", aliasName='" + fromTableAliasName + '\'' +
|
", aliasName='" + fromTableAliasName + '\'' +
|
||||||
// ", selectFields=" + selectFields +
|
", selectFields=" + selectFields +
|
||||||
// ", realSelectFields=" + realSelectFields +
|
", realSelectFields=" + realSelectFields +
|
||||||
// ", selectAll=" + selectAll +
|
", selectAll=" + selectAll +
|
||||||
// "}";
|
"}";
|
||||||
// }
|
}
|
||||||
//
|
|
||||||
//}
|
}
|
||||||
|
|||||||
@ -1,10 +1,7 @@
|
|||||||
package org.jeecg.config;
|
package org.jeecg.config;
|
||||||
|
|
||||||
import org.jeecg.config.vo.*;
|
import org.jeecg.config.vo.*;
|
||||||
import org.springframework.beans.factory.config.BeanDefinition;
|
|
||||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
import org.springframework.context.annotation.Bean;
|
|
||||||
import org.springframework.context.annotation.Role;
|
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
|
||||||
@ -14,17 +11,12 @@ import org.springframework.stereotype.Component;
|
|||||||
*/
|
*/
|
||||||
@Component("jeecgBaseConfig")
|
@Component("jeecgBaseConfig")
|
||||||
@ConfigurationProperties(prefix = "jeecg")
|
@ConfigurationProperties(prefix = "jeecg")
|
||||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
|
||||||
public class JeecgBaseConfig {
|
public class JeecgBaseConfig {
|
||||||
/**
|
/**
|
||||||
* 签名密钥串(字典等敏感接口)
|
* 签名密钥串(字典等敏感接口)
|
||||||
* @TODO 降低使用成本加的默认值,实际以 yml配置 为准
|
* @TODO 降低使用成本加的默认值,实际以 yml配置 为准
|
||||||
*/
|
*/
|
||||||
private String signatureSecret = "dd05f1c54d63749eda95f9fa6d49v442a";
|
private String signatureSecret = "dd05f1c54d63749eda95f9fa6d49v442a";
|
||||||
/**
|
|
||||||
* 自定义后台资源前缀,解决表单设计器无法通过前端nginx转发访问
|
|
||||||
*/
|
|
||||||
private String customResourcePrefixPath;
|
|
||||||
/**
|
/**
|
||||||
* 需要加强校验的接口清单
|
* 需要加强校验的接口清单
|
||||||
*/
|
*/
|
||||||
@ -40,10 +32,6 @@ public class JeecgBaseConfig {
|
|||||||
*/
|
*/
|
||||||
private Firewall firewall;
|
private Firewall firewall;
|
||||||
|
|
||||||
/**
|
|
||||||
* shiro拦截排除
|
|
||||||
*/
|
|
||||||
private Shiro shiro;
|
|
||||||
/**
|
/**
|
||||||
* 上传文件配置
|
* 上传文件配置
|
||||||
*/
|
*/
|
||||||
@ -76,14 +64,6 @@ public class JeecgBaseConfig {
|
|||||||
*/
|
*/
|
||||||
private BaiduApi baiduApi;
|
private BaiduApi baiduApi;
|
||||||
|
|
||||||
public String getCustomResourcePrefixPath() {
|
|
||||||
return customResourcePrefixPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setCustomResourcePrefixPath(String customResourcePrefixPath) {
|
|
||||||
this.customResourcePrefixPath = customResourcePrefixPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Elasticsearch getElasticsearch() {
|
public Elasticsearch getElasticsearch() {
|
||||||
return elasticsearch;
|
return elasticsearch;
|
||||||
}
|
}
|
||||||
@ -108,14 +88,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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,8 +1,9 @@
|
|||||||
//package org.jeecg.config;
|
//package org.jeecg.config;
|
||||||
//
|
//
|
||||||
//
|
//
|
||||||
//import com.github.xiaoymin.knife4j.spring.annotations.EnableKnife4j;
|
//import io.swagger.annotations.ApiOperation;
|
||||||
//import org.jeecg.common.constant.CommonConstant;
|
//import org.jeecg.common.constant.CommonConstant;
|
||||||
|
//import org.jeecg.config.mybatis.MybatisPlusSaasConfig;
|
||||||
//import org.springframework.beans.BeansException;
|
//import org.springframework.beans.BeansException;
|
||||||
//import org.springframework.beans.factory.config.BeanPostProcessor;
|
//import org.springframework.beans.factory.config.BeanPostProcessor;
|
||||||
//import org.springframework.context.annotation.Bean;
|
//import org.springframework.context.annotation.Bean;
|
||||||
@ -18,15 +19,13 @@
|
|||||||
//import springfox.documentation.builders.ParameterBuilder;
|
//import springfox.documentation.builders.ParameterBuilder;
|
||||||
//import springfox.documentation.builders.PathSelectors;
|
//import springfox.documentation.builders.PathSelectors;
|
||||||
//import springfox.documentation.builders.RequestHandlerSelectors;
|
//import springfox.documentation.builders.RequestHandlerSelectors;
|
||||||
//import springfox.documentation.oas.annotations.EnableOpenApi;
|
|
||||||
//import springfox.documentation.schema.ModelRef;
|
//import springfox.documentation.schema.ModelRef;
|
||||||
//import springfox.documentation.service.*;
|
//import springfox.documentation.service.*;
|
||||||
//import springfox.documentation.spi.DocumentationType;
|
//import springfox.documentation.spi.DocumentationType;
|
||||||
//import springfox.documentation.spi.service.contexts.SecurityContext;
|
//import springfox.documentation.spi.service.contexts.SecurityContext;
|
||||||
//import springfox.documentation.spring.web.plugins.Docket;
|
//import springfox.documentation.spring.web.plugins.Docket;
|
||||||
//import springfox.documentation.spring.web.plugins.WebFluxRequestHandlerProvider;
|
|
||||||
//import springfox.documentation.spring.web.plugins.WebMvcRequestHandlerProvider;
|
//import springfox.documentation.spring.web.plugins.WebMvcRequestHandlerProvider;
|
||||||
//import springfox.documentation.swagger2.annotations.EnableSwagger2;
|
//import springfox.documentation.swagger2.annotations.EnableSwagger2WebMvc;
|
||||||
//
|
//
|
||||||
//import java.lang.reflect.Field;
|
//import java.lang.reflect.Field;
|
||||||
//import java.util.ArrayList;
|
//import java.util.ArrayList;
|
||||||
@ -38,8 +37,7 @@
|
|||||||
// * @Author scott
|
// * @Author scott
|
||||||
// */
|
// */
|
||||||
//@Configuration
|
//@Configuration
|
||||||
//@EnableSwagger2 //开启 Swagger2
|
//@EnableSwagger2WebMvc
|
||||||
//@EnableKnife4j //开启 knife4j,可以不写
|
|
||||||
//@Import(BeanValidatorPluginsConfiguration.class)
|
//@Import(BeanValidatorPluginsConfiguration.class)
|
||||||
//public class Swagger2Config implements WebMvcConfigurer {
|
//public class Swagger2Config implements WebMvcConfigurer {
|
||||||
//
|
//
|
||||||
@ -97,6 +95,14 @@
|
|||||||
// List<Parameter> pars = new ArrayList<>();
|
// List<Parameter> pars = new ArrayList<>();
|
||||||
// tokenPar.name(CommonConstant.X_ACCESS_TOKEN).description("token").modelRef(new ModelRef("string")).parameterType("header").required(false).build();
|
// tokenPar.name(CommonConstant.X_ACCESS_TOKEN).description("token").modelRef(new ModelRef("string")).parameterType("header").required(false).build();
|
||||||
// pars.add(tokenPar.build());
|
// pars.add(tokenPar.build());
|
||||||
|
// //update-begin-author:liusq---date:2024-08-15--for: 开启多租户时,全局参数增加租户id
|
||||||
|
// if(MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL){
|
||||||
|
// ParameterBuilder tenantPar = new ParameterBuilder();
|
||||||
|
// tenantPar.name(CommonConstant.TENANT_ID).description("租户ID").modelRef(new ModelRef("string")).parameterType("header").required(false).build();
|
||||||
|
// pars.add(tenantPar.build());
|
||||||
|
// }
|
||||||
|
// //update-end-author:liusq---date:2024-08-15--for: 开启多租户时,全局参数增加租户id
|
||||||
|
//
|
||||||
// return pars;
|
// return pars;
|
||||||
// }
|
// }
|
||||||
//
|
//
|
||||||
@ -151,7 +157,7 @@
|
|||||||
//
|
//
|
||||||
// @Override
|
// @Override
|
||||||
// public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
|
// public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
|
||||||
// if (bean instanceof WebMvcRequestHandlerProvider || bean instanceof WebFluxRequestHandlerProvider) {
|
// if (bean instanceof WebMvcRequestHandlerProvider) {
|
||||||
// customizeSpringfoxHandlerMappings(getHandlerMappings(bean));
|
// customizeSpringfoxHandlerMappings(getHandlerMappings(bean));
|
||||||
// }
|
// }
|
||||||
// return bean;
|
// return bean;
|
||||||
|
|||||||
@ -3,45 +3,21 @@ package org.jeecg.config;
|
|||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.models.Components;
|
import io.swagger.v3.oas.models.Components;
|
||||||
import io.swagger.v3.oas.models.OpenAPI;
|
import io.swagger.v3.oas.models.OpenAPI;
|
||||||
|
import io.swagger.v3.oas.models.Paths;
|
||||||
import io.swagger.v3.oas.models.info.Contact;
|
import io.swagger.v3.oas.models.info.Contact;
|
||||||
import io.swagger.v3.oas.models.info.Info;
|
import io.swagger.v3.oas.models.info.Info;
|
||||||
import io.swagger.v3.oas.models.info.License;
|
import io.swagger.v3.oas.models.info.License;
|
||||||
import io.swagger.v3.oas.models.security.SecurityRequirement;
|
import io.swagger.v3.oas.models.security.SecurityRequirement;
|
||||||
import io.swagger.v3.oas.models.security.SecurityScheme;
|
import io.swagger.v3.oas.models.security.SecurityScheme;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.jeecg.common.constant.CommonConstant;
|
import org.jeecg.common.constant.CommonConstant;
|
||||||
import org.springdoc.core.customizers.GlobalOpenApiCustomizer;
|
import org.springdoc.core.models.GroupedOpenApi;
|
||||||
import org.springdoc.core.filters.GlobalOpenApiMethodFilter;
|
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.context.annotation.PropertySource;
|
|
||||||
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
|
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
|
||||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||||
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @author eightmonth
|
|
||||||
*/
|
|
||||||
@Slf4j
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@PropertySource("classpath:config/default-spring-doc.properties")
|
|
||||||
public class Swagger3Config implements WebMvcConfigurer {
|
public class Swagger3Config implements WebMvcConfigurer {
|
||||||
// 定义不需要注入安全要求的路径集合
|
|
||||||
Set<String> excludedPaths = new HashSet<>(Arrays.asList(
|
|
||||||
"/sys/randomImage/{key}",
|
|
||||||
"/sys/login",
|
|
||||||
"/sys/phoneLogin",
|
|
||||||
"/sys/mLogin",
|
|
||||||
"/sys/sms",
|
|
||||||
"/sys/cas/client/validateLogin",
|
|
||||||
"/test/jeecgDemo/demo3",
|
|
||||||
"/sys/thirdLogin/**",
|
|
||||||
"/sys/user/register"
|
|
||||||
));
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* 显示swagger-ui.html文档展示页,还必须注入swagger资源:
|
* 显示swagger-ui.html文档展示页,还必须注入swagger资源:
|
||||||
@ -56,33 +32,15 @@ public class Swagger3Config implements WebMvcConfigurer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public GlobalOpenApiMethodFilter globalOpenApiMethodFilter() {
|
public GroupedOpenApi swaggerOpenApi() {
|
||||||
return method -> method.isAnnotationPresent(Operation.class);
|
return GroupedOpenApi.builder()
|
||||||
}
|
.group("default")
|
||||||
|
.packagesToScan("org.jeecg")
|
||||||
@Bean
|
// 剔除以下几个包路径的接口生成文档
|
||||||
public GlobalOpenApiCustomizer globalOpenApiCustomizer() {
|
.packagesToExclude("org.jeecg.modules.drag", "org.jeecg.modules.online", "org.jeecg.modules.jmreport")
|
||||||
return openApi -> {
|
// 加了Operation注解的方法,才生成接口文档
|
||||||
// 全局添加鉴权参数
|
.addOpenApiMethodFilter(method -> method.isAnnotationPresent(Operation.class))
|
||||||
if (openApi.getPaths() != null) {
|
.build();
|
||||||
openApi.getPaths().forEach((path, pathItem) -> {
|
|
||||||
//log.debug("path: {}", path);
|
|
||||||
// 检查当前路径是否在排除列表中
|
|
||||||
boolean isExcluded = excludedPaths.stream().anyMatch(excludedPath ->
|
|
||||||
excludedPath.equals(path) ||
|
|
||||||
(excludedPath.endsWith("**") && path.startsWith(excludedPath.substring(0, excludedPath.length() - 2)))
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!isExcluded) {
|
|
||||||
// 接口添加鉴权参数
|
|
||||||
pathItem.readOperations()
|
|
||||||
.forEach(operation ->
|
|
||||||
operation.addSecurityItem(new SecurityRequirement().addList(CommonConstant.X_ACCESS_TOKEN))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@ -90,13 +48,12 @@ 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.8.0")
|
.version("1.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")
|
||||||
.license(new License().name("Apache 2.0").url("http://www.apache.org/licenses/LICENSE-2.0.html")))
|
.license(new License().name("Apache 2.0").url("http://www.apache.org/licenses/LICENSE-2.0.html"))
|
||||||
.addSecurityItem(new SecurityRequirement().addList(CommonConstant.X_ACCESS_TOKEN))
|
);
|
||||||
.components(new Components().addSecuritySchemes(CommonConstant.X_ACCESS_TOKEN,
|
|
||||||
new SecurityScheme().name(CommonConstant.X_ACCESS_TOKEN).type(SecurityScheme.Type.HTTP)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -10,7 +10,7 @@ import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
|
|||||||
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
|
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
|
||||||
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
|
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
|
||||||
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
|
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
|
||||||
import io.micrometer.prometheusmetrics.PrometheusMeterRegistry;
|
import io.micrometer.prometheus.PrometheusMeterRegistry;
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
import org.springframework.beans.factory.InitializingBean;
|
import org.springframework.beans.factory.InitializingBean;
|
||||||
import org.springframework.beans.factory.ObjectProvider;
|
import org.springframework.beans.factory.ObjectProvider;
|
||||||
@ -18,13 +18,11 @@ import org.springframework.beans.factory.annotation.Autowired;
|
|||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.beans.factory.config.BeanPostProcessor;
|
import org.springframework.beans.factory.config.BeanPostProcessor;
|
||||||
import org.springframework.boot.actuate.web.exchanges.InMemoryHttpExchangeRepository;
|
import org.springframework.boot.actuate.web.exchanges.InMemoryHttpExchangeRepository;
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
|
||||||
import org.springframework.boot.autoconfigure.jackson.JacksonProperties;
|
import org.springframework.boot.autoconfigure.jackson.JacksonProperties;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Conditional;
|
import org.springframework.context.annotation.Conditional;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.context.annotation.Primary;
|
import org.springframework.context.annotation.Primary;
|
||||||
import org.springframework.http.CacheControl;
|
|
||||||
import org.springframework.http.converter.HttpMessageConverter;
|
import org.springframework.http.converter.HttpMessageConverter;
|
||||||
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
|
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
|
||||||
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
|
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
|
||||||
@ -42,7 +40,6 @@ import java.time.LocalDateTime;
|
|||||||
import java.time.LocalTime;
|
import java.time.LocalTime;
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Spring Boot 2.0 解决跨域问题
|
* Spring Boot 2.0 解决跨域问题
|
||||||
@ -73,8 +70,6 @@ public class WebMvcConfiguration implements WebMvcConfigurer {
|
|||||||
.addResourceLocations("file:" + jeecgBaseConfig.getPath().getWebapp() + "//");
|
.addResourceLocations("file:" + jeecgBaseConfig.getPath().getWebapp() + "//");
|
||||||
}
|
}
|
||||||
resourceHandlerRegistration.addResourceLocations(staticLocations.split(","));
|
resourceHandlerRegistration.addResourceLocations(staticLocations.split(","));
|
||||||
// 设置缓存控制标头 Cache-Control有效期为30天
|
|
||||||
resourceHandlerRegistration.setCacheControl(CacheControl.maxAge(30, TimeUnit.DAYS));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -152,7 +147,6 @@ public class WebMvcConfiguration implements WebMvcConfigurer {
|
|||||||
* 解决metrics端点不显示jvm信息的问题(zyf)
|
* 解决metrics端点不显示jvm信息的问题(zyf)
|
||||||
*/
|
*/
|
||||||
@Bean
|
@Bean
|
||||||
@ConditionalOnBean(name = "meterRegistryPostProcessor")
|
|
||||||
InitializingBean forcePrometheusPostProcessor(BeanPostProcessor meterRegistryPostProcessor) {
|
InitializingBean forcePrometheusPostProcessor(BeanPostProcessor meterRegistryPostProcessor) {
|
||||||
return () -> meterRegistryPostProcessor.postProcessAfterInitialization(prometheusMeterRegistry, "");
|
return () -> meterRegistryPostProcessor.postProcessAfterInitialization(prometheusMeterRegistry, "");
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,7 +2,6 @@ package org.jeecg.config.firewall.interceptor;
|
|||||||
|
|
||||||
import com.alibaba.fastjson.JSON;
|
import com.alibaba.fastjson.JSON;
|
||||||
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.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;
|
||||||
@ -11,6 +10,7 @@ import org.jeecg.common.system.vo.LoginUser;
|
|||||||
import org.jeecg.common.util.CommonUtils;
|
import org.jeecg.common.util.CommonUtils;
|
||||||
import org.jeecg.common.util.SpringContextUtils;
|
import org.jeecg.common.util.SpringContextUtils;
|
||||||
import org.jeecg.config.JeecgBaseConfig;
|
import org.jeecg.config.JeecgBaseConfig;
|
||||||
|
import org.jeecg.config.security.utils.SecureUtil;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.web.servlet.HandlerInterceptor;
|
import org.springframework.web.servlet.HandlerInterceptor;
|
||||||
|
|
||||||
@ -63,13 +63,10 @@ public class LowCodeModeInterceptor implements HandlerInterceptor {
|
|||||||
if (jeecgBaseConfig.getFirewall()!=null && LowCodeModeInterceptor.LOW_CODE_MODE_PROD.equals(jeecgBaseConfig.getFirewall().getLowCodeMode())) {
|
if (jeecgBaseConfig.getFirewall()!=null && LowCodeModeInterceptor.LOW_CODE_MODE_PROD.equals(jeecgBaseConfig.getFirewall().getLowCodeMode())) {
|
||||||
String requestURI = request.getRequestURI().substring(request.getContextPath().length());
|
String requestURI = request.getRequestURI().substring(request.getContextPath().length());
|
||||||
log.info("低代码模式,拦截请求路径:" + requestURI);
|
log.info("低代码模式,拦截请求路径:" + requestURI);
|
||||||
LoginUser loginUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();
|
LoginUser loginUser = SecureUtil.currentUser();
|
||||||
Set<String> hasRoles = null;
|
Set<String> hasRoles = null;
|
||||||
if (loginUser == null) {
|
if (loginUser == null) {
|
||||||
loginUser = commonAPI.getUserByName(JwtUtil.getUserNameByToken(SpringContextUtils.getHttpServletRequest()));
|
loginUser = commonAPI.getUserByName(JwtUtil.getUserNameByToken(SpringContextUtils.getHttpServletRequest()));
|
||||||
}
|
|
||||||
|
|
||||||
if (loginUser != null) {
|
|
||||||
//当前登录人拥有的角色
|
//当前登录人拥有的角色
|
||||||
hasRoles = commonAPI.queryUserRolesById(loginUser.getId());
|
hasRoles = commonAPI.queryUserRolesById(loginUser.getId());
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
@ -192,7 +192,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;
|
||||||
|
|||||||
@ -6,8 +6,6 @@ import java.util.List;
|
|||||||
import cn.hutool.core.util.ObjectUtil;
|
import cn.hutool.core.util.ObjectUtil;
|
||||||
import com.baomidou.mybatisplus.extension.plugins.inner.DynamicTableNameInnerInterceptor;
|
import com.baomidou.mybatisplus.extension.plugins.inner.DynamicTableNameInnerInterceptor;
|
||||||
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
|
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
|
||||||
import net.sf.jsqlparser.expression.Expression;
|
|
||||||
import net.sf.jsqlparser.expression.LongValue;
|
|
||||||
import org.jeecg.common.config.TenantContext;
|
import org.jeecg.common.config.TenantContext;
|
||||||
import org.jeecg.common.constant.CommonConstant;
|
import org.jeecg.common.constant.CommonConstant;
|
||||||
import org.jeecg.common.constant.TenantConstant;
|
import org.jeecg.common.constant.TenantConstant;
|
||||||
@ -23,6 +21,8 @@ import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler;
|
|||||||
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
|
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
|
||||||
import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
|
import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
|
||||||
|
|
||||||
|
import net.sf.jsqlparser.expression.Expression;
|
||||||
|
import net.sf.jsqlparser.expression.LongValue;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 单数据源配置(jeecg.datasource.open = false时生效)
|
* 单数据源配置(jeecg.datasource.open = false时生效)
|
||||||
@ -60,18 +60,7 @@ public class MybatisPlusSaasConfig {
|
|||||||
TENANT_TABLE.add("sys_category");
|
TENANT_TABLE.add("sys_category");
|
||||||
TENANT_TABLE.add("sys_data_source");
|
TENANT_TABLE.add("sys_data_source");
|
||||||
TENANT_TABLE.add("sys_position");
|
TENANT_TABLE.add("sys_position");
|
||||||
//b-2.仪表盘
|
//TENANT_TABLE.add("sys_announcement");
|
||||||
TENANT_TABLE.add("onl_drag_page");
|
|
||||||
TENANT_TABLE.add("onl_drag_dataset_head");
|
|
||||||
TENANT_TABLE.add("jimu_report_data_source");
|
|
||||||
TENANT_TABLE.add("jimu_report");
|
|
||||||
TENANT_TABLE.add("jimu_dict");
|
|
||||||
//b-4.AIRAG
|
|
||||||
TENANT_TABLE.add("airag_app");
|
|
||||||
TENANT_TABLE.add("airag_flow");
|
|
||||||
TENANT_TABLE.add("airag_knowledge");
|
|
||||||
TENANT_TABLE.add("airag_knowledge_doc");
|
|
||||||
TENANT_TABLE.add("airag_model");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//2.示例测试
|
//2.示例测试
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
package org.jeecg.config.oss;
|
package org.jeecg.config.oss;
|
||||||
|
|
||||||
import jakarta.annotation.PostConstruct;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.jeecg.common.constant.CommonConstant;
|
import org.jeecg.common.constant.CommonConstant;
|
||||||
import org.jeecg.common.constant.SymbolConstant;
|
import org.jeecg.common.constant.SymbolConstant;
|
||||||
@ -27,7 +26,7 @@ public class MinioConfig {
|
|||||||
@Value(value = "${jeecg.minio.bucketName}")
|
@Value(value = "${jeecg.minio.bucketName}")
|
||||||
private String bucketName;
|
private String bucketName;
|
||||||
|
|
||||||
@PostConstruct
|
@Bean
|
||||||
public void initMinio(){
|
public void initMinio(){
|
||||||
if(!minioUrl.startsWith(CommonConstant.STR_HTTP)){
|
if(!minioUrl.startsWith(CommonConstant.STR_HTTP)){
|
||||||
minioUrl = "http://" + minioUrl;
|
minioUrl = "http://" + minioUrl;
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
package org.jeecg.config.oss;
|
package org.jeecg.config.oss;
|
||||||
|
|
||||||
import jakarta.annotation.PostConstruct;
|
|
||||||
import org.jeecg.common.util.oss.OssBootUtil;
|
import org.jeecg.common.util.oss.OssBootUtil;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
@ -27,7 +26,7 @@ public class OssConfiguration {
|
|||||||
private String staticDomain;
|
private String staticDomain;
|
||||||
|
|
||||||
|
|
||||||
@PostConstruct
|
@Bean
|
||||||
public void initOssBootConfiguration() {
|
public void initOssBootConfiguration() {
|
||||||
OssBootUtil.setEndPoint(endpoint);
|
OssBootUtil.setEndPoint(endpoint);
|
||||||
OssBootUtil.setAccessKeyId(accessKeyId);
|
OssBootUtil.setAccessKeyId(accessKeyId);
|
||||||
|
|||||||
@ -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,45 @@
|
|||||||
|
package org.jeecg.config.security;
|
||||||
|
|
||||||
|
import io.undertow.servlet.spec.HttpServletRequestImpl;
|
||||||
|
import io.undertow.util.HttpString;
|
||||||
|
import jakarta.servlet.FilterChain;
|
||||||
|
import jakarta.servlet.ServletException;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import org.springframework.core.annotation.Order;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
import org.springframework.web.filter.OncePerRequestFilter;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 复制仪盘表请求query体携带的token
|
||||||
|
* @author eightmonth
|
||||||
|
* @date 2024/7/3 14:04
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
@Order(value = Integer.MIN_VALUE)
|
||||||
|
public class CopyTokenFilter extends OncePerRequestFilter {
|
||||||
|
@Override
|
||||||
|
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
|
||||||
|
// 以下为undertow定制代码,如切换其它servlet容器,需要同步更换
|
||||||
|
HttpServletRequestImpl undertowRequest = (HttpServletRequestImpl) request;
|
||||||
|
String token = request.getHeader("Authorization");
|
||||||
|
if (StringUtils.hasText(token)) {
|
||||||
|
undertowRequest.getExchange().getRequestHeaders().remove("Authorization");
|
||||||
|
undertowRequest.getExchange().getRequestHeaders().add(new HttpString("Authorization"), "bearer " + token);
|
||||||
|
} else {
|
||||||
|
String bearerToken = request.getParameter("token");
|
||||||
|
String headerBearerToken = request.getHeader("X-Access-Token");
|
||||||
|
if (StringUtils.hasText(bearerToken)) {
|
||||||
|
undertowRequest.getExchange().getRequestHeaders().add(new HttpString("Authorization"), "bearer " + bearerToken);
|
||||||
|
} else if (StringUtils.hasText(headerBearerToken)) {
|
||||||
|
undertowRequest.getExchange().getRequestHeaders().add(new HttpString("Authorization"), "bearer " + headerBearerToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
filterChain.doFilter(undertowRequest, response);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@ -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,245 @@
|
|||||||
|
package org.jeecg.config.security;
|
||||||
|
|
||||||
|
import com.nimbusds.jose.jwk.Curve;
|
||||||
|
import com.nimbusds.jose.jwk.ECKey;
|
||||||
|
import com.nimbusds.jose.jwk.JWKSet;
|
||||||
|
import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
|
||||||
|
import com.nimbusds.jose.jwk.source.JWKSource;
|
||||||
|
import com.nimbusds.jose.proc.SecurityContext;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.SneakyThrows;
|
||||||
|
import org.jeecg.config.security.app.AppGrantAuthenticationConvert;
|
||||||
|
import org.jeecg.config.security.app.AppGrantAuthenticationProvider;
|
||||||
|
import org.jeecg.config.security.password.PasswordGrantAuthenticationConvert;
|
||||||
|
import org.jeecg.config.security.password.PasswordGrantAuthenticationProvider;
|
||||||
|
import org.jeecg.config.security.phone.PhoneGrantAuthenticationConvert;
|
||||||
|
import org.jeecg.config.security.phone.PhoneGrantAuthenticationProvider;
|
||||||
|
import org.jeecg.config.security.social.SocialGrantAuthenticationConvert;
|
||||||
|
import org.jeecg.config.security.social.SocialGrantAuthenticationProvider;
|
||||||
|
import org.jeecg.config.shiro.ignore.InMemoryIgnoreAuth;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.core.annotation.Order;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.jdbc.core.JdbcTemplate;
|
||||||
|
import org.springframework.security.config.Customizer;
|
||||||
|
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
|
||||||
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||||
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||||
|
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||||
|
import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer;
|
||||||
|
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
|
import org.springframework.security.oauth2.jwt.JwtDecoder;
|
||||||
|
import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.token.*;
|
||||||
|
import org.springframework.security.web.SecurityFilterChain;
|
||||||
|
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
|
||||||
|
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||||
|
import org.springframework.security.web.util.matcher.MediaTypeRequestMatcher;
|
||||||
|
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||||
|
import org.springframework.util.CollectionUtils;
|
||||||
|
import org.springframework.web.cors.CorsConfiguration;
|
||||||
|
|
||||||
|
import java.security.KeyPair;
|
||||||
|
import java.security.KeyPairGenerator;
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
import java.security.interfaces.ECPrivateKey;
|
||||||
|
import java.security.interfaces.ECPublicKey;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* spring authorization server核心配置
|
||||||
|
* @author eightmonth@qq.com
|
||||||
|
* @date 2024/1/2 9:29
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
@EnableWebSecurity
|
||||||
|
@EnableMethodSecurity
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class SecurityConfig {
|
||||||
|
|
||||||
|
private JdbcTemplate jdbcTemplate;
|
||||||
|
private OAuth2AuthorizationService authorizationService;
|
||||||
|
private JeecgAuthenticationConvert jeecgAuthenticationConvert;
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@Order(1)
|
||||||
|
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http)
|
||||||
|
throws Exception {
|
||||||
|
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
|
||||||
|
// 注册自定义登录类型
|
||||||
|
http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
|
||||||
|
.tokenEndpoint(tokenEndpoint -> tokenEndpoint.accessTokenRequestConverter(new PasswordGrantAuthenticationConvert())
|
||||||
|
.authenticationProvider(new PasswordGrantAuthenticationProvider(authorizationService, tokenGenerator())))
|
||||||
|
.tokenEndpoint(tokenEndpoint -> tokenEndpoint.accessTokenRequestConverter(new PhoneGrantAuthenticationConvert())
|
||||||
|
.authenticationProvider(new PhoneGrantAuthenticationProvider(authorizationService, tokenGenerator())))
|
||||||
|
.tokenEndpoint(tokenEndpoint -> tokenEndpoint.accessTokenRequestConverter(new AppGrantAuthenticationConvert())
|
||||||
|
.authenticationProvider(new AppGrantAuthenticationProvider(authorizationService, tokenGenerator())))
|
||||||
|
.tokenEndpoint(tokenEndpoint -> tokenEndpoint.accessTokenRequestConverter(new SocialGrantAuthenticationConvert())
|
||||||
|
.authenticationProvider(new SocialGrantAuthenticationProvider(authorizationService, tokenGenerator())))
|
||||||
|
//开启OpenID Connect 1.0(其中oidc为OpenID Connect的缩写)。 访问 /.well-known/openid-configuration即可获取认证信息
|
||||||
|
.oidc(Customizer.withDefaults());
|
||||||
|
http
|
||||||
|
//将需要认证的请求,重定向到login页面行登录认证。
|
||||||
|
.exceptionHandling((exceptions) -> exceptions
|
||||||
|
.defaultAuthenticationEntryPointFor(
|
||||||
|
new LoginUrlAuthenticationEntryPoint("/sys/login"),
|
||||||
|
new MediaTypeRequestMatcher(MediaType.TEXT_HTML)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return http.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@Order(2)
|
||||||
|
public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http)
|
||||||
|
throws Exception {
|
||||||
|
http
|
||||||
|
//设置所有请求都需要认证,未认证的请求都被重定向到login页面进行登录
|
||||||
|
.authorizeHttpRequests((authorize) -> authorize
|
||||||
|
.requestMatchers(InMemoryIgnoreAuth.get().stream().map(AntPathRequestMatcher::antMatcher).toList().toArray(new AntPathRequestMatcher[0])).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/sys/cas/client/validateLogin")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/sys/randomImage/**")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/sys/checkCaptcha")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/sys/login")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/sys/mLogin")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/sys/logout")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/sys/thirdLogin/**")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/sys/getEncryptedString")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/sys/sms")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/sys/phoneLogin")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/sys/user/checkOnlyUser")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/sys/user/register")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/sys/user/phoneVerification")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/sys/user/passwordChange")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/auth/2step-code")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/sys/common/static/**")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/sys/common/pdf/**")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/generic/**")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/sys/getLoginQrcode/**")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/sys/getQrcodeToken/**")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/sys/checkAuth")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/doc.html")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/**/*.js")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/**/*.css")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/**/*.html")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/**/*.svg")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/**/*.pdf")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/**/*.jpg")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/**/*.png")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/**/*.gif")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/**/*.ico")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/**/*.ttf")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/**/*.woff")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/**/*.woff2")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/druid/**")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/swagger-ui.html")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/swagger**/**")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/webjars/**")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/v3/**")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/WW_verify*")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/sys/annountCement/show/**")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/jmreport/**")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/**/*.js.map")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/**/*.css.map")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/drag/list")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/drag/view")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/drag/onlDragDatasetHead/getLoginUser")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/drag/page/queryById")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/drag/onlDragDatasetHead/getAllChartData")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/drag/onlDragDatasetHead/getTotalData")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/drag/mock/json/**")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/test/bigScreen/**")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/bigscreen/template1/**")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/bigscreen/template1/**")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/websocket/**")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/newsWebsocket/**")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/vxeSocket/**")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/test/seata/**")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/error")).permitAll()
|
||||||
|
.anyRequest().authenticated()
|
||||||
|
)
|
||||||
|
.headers(headers -> headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable))
|
||||||
|
.cors(cors -> cors
|
||||||
|
.configurationSource(req -> {
|
||||||
|
CorsConfiguration config = new CorsConfiguration();
|
||||||
|
config.applyPermitDefaultValues();
|
||||||
|
config.setAllowedMethods(Arrays.asList("HEAD", "GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"));
|
||||||
|
return config;
|
||||||
|
}))
|
||||||
|
.csrf(AbstractHttpConfigurer::disable)
|
||||||
|
.oauth2ResourceServer(oauth2 -> oauth2.jwt(jwt -> jwt.jwtAuthenticationConverter(jeecgAuthenticationConvert)));
|
||||||
|
return http.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数据库保存注册客户端信息
|
||||||
|
*/
|
||||||
|
@Bean
|
||||||
|
public RegisteredClientRepository registeredClientRepository() {
|
||||||
|
return new JdbcRegisteredClientRepository(jdbcTemplate);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*配置 JWK,为JWT(id_token)提供加密密钥,用于加密/解密或签名/验签
|
||||||
|
* JWK详细见:https://datatracker.ietf.org/doc/html/draft-ietf-jose-json-web-key-41
|
||||||
|
*/
|
||||||
|
@Bean
|
||||||
|
@SneakyThrows
|
||||||
|
public JWKSource<SecurityContext> jwkSource() {
|
||||||
|
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("EC");
|
||||||
|
// 如果不设置secureRandom,会存在一个问题,当应用重启后,原有的token将会全部失效,因为重启的keyPair与之前已经不同
|
||||||
|
SecureRandom secureRandom = SecureRandom.getInstance("SHA1PRNG");
|
||||||
|
// 重要!生产环境需要修改!
|
||||||
|
secureRandom.setSeed("jeecg".getBytes());
|
||||||
|
keyPairGenerator.initialize(256, secureRandom);
|
||||||
|
KeyPair keyPair = keyPairGenerator.generateKeyPair();
|
||||||
|
ECPublicKey publicKey = (ECPublicKey) keyPair.getPublic();
|
||||||
|
ECPrivateKey privateKey = (ECPrivateKey) keyPair.getPrivate();
|
||||||
|
|
||||||
|
ECKey jwk = new ECKey.Builder(Curve.P_256, publicKey)
|
||||||
|
.privateKey(privateKey)
|
||||||
|
.keyID("jeecg")
|
||||||
|
.build();
|
||||||
|
JWKSet jwkSet = new JWKSet(jwk);
|
||||||
|
return new ImmutableJWKSet<>(jwkSet);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public PasswordEncoder passwordEncoder() {
|
||||||
|
return NoOpPasswordEncoder.getInstance();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 配置jwt解析器
|
||||||
|
*/
|
||||||
|
@Bean
|
||||||
|
public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
|
||||||
|
return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*配置token生成器
|
||||||
|
*/
|
||||||
|
@Bean
|
||||||
|
OAuth2TokenGenerator<?> tokenGenerator() {
|
||||||
|
JwtGenerator jwtGenerator = new JwtGenerator(new NimbusJwtEncoder(jwkSource()));
|
||||||
|
OAuth2AccessTokenGenerator accessTokenGenerator = new OAuth2AccessTokenGenerator();
|
||||||
|
OAuth2RefreshTokenGenerator refreshTokenGenerator = new OAuth2RefreshTokenGenerator();
|
||||||
|
return new DelegatingOAuth2TokenGenerator(
|
||||||
|
new JeecgOAuth2AccessTokenGenerator(new NimbusJwtEncoder(jwkSource())),
|
||||||
|
new OAuth2RefreshTokenGenerator()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -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,23 @@
|
|||||||
|
package org.jeecg.config.security.utils;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson2.JSONObject;
|
||||||
|
import org.jeecg.common.system.vo.LoginUser;
|
||||||
|
import org.jeecg.common.util.SpringContextUtils;
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 认证信息工具类
|
||||||
|
* @author EightMonth
|
||||||
|
* @date 2024/1/10 17:03
|
||||||
|
*/
|
||||||
|
public class SecureUtil {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通过当前认证信息获取用户信息
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public static LoginUser currentUser() {
|
||||||
|
String name = SecurityContextHolder.getContext().getAuthentication().getName();
|
||||||
|
return JSONObject.parseObject(name, LoginUser.class);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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,382 +0,0 @@
|
|||||||
package org.jeecg.config.shiro;
|
|
||||||
|
|
||||||
import jakarta.annotation.Resource;
|
|
||||||
import jakarta.servlet.DispatcherType;
|
|
||||||
import jakarta.servlet.Filter;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
|
|
||||||
import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
|
|
||||||
import org.apache.shiro.mgt.DefaultSubjectDAO;
|
|
||||||
import org.apache.shiro.mgt.SecurityManager;
|
|
||||||
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
|
|
||||||
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
|
|
||||||
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
|
|
||||||
import org.apache.shiro.spring.web.ShiroUrlPathHelper;
|
|
||||||
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
|
|
||||||
import org.crazycake.shiro.*;
|
|
||||||
import org.jeecg.common.constant.CommonConstant;
|
|
||||||
import org.jeecg.common.util.oConvertUtils;
|
|
||||||
import org.jeecg.config.JeecgBaseConfig;
|
|
||||||
import org.jeecg.config.shiro.filters.CustomShiroFilterFactoryBean;
|
|
||||||
import org.jeecg.config.shiro.filters.JwtFilter;
|
|
||||||
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.beans.factory.annotation.Qualifier;
|
|
||||||
import org.springframework.beans.factory.config.BeanDefinition;
|
|
||||||
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
|
|
||||||
import org.springframework.boot.web.servlet.FilterRegistrationBean;
|
|
||||||
import org.springframework.context.annotation.*;
|
|
||||||
import org.springframework.core.env.Environment;
|
|
||||||
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
|
|
||||||
import org.springframework.util.CollectionUtils;
|
|
||||||
import org.springframework.util.StringUtils;
|
|
||||||
import org.springframework.web.filter.DelegatingFilterProxy;
|
|
||||||
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
|
|
||||||
import redis.clients.jedis.HostAndPort;
|
|
||||||
import redis.clients.jedis.JedisCluster;
|
|
||||||
|
|
||||||
import java.util.*;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @author: Scott
|
|
||||||
* @date: 2018/2/7
|
|
||||||
* @description: shiro 配置类
|
|
||||||
*/
|
|
||||||
|
|
||||||
@Slf4j
|
|
||||||
@Configuration
|
|
||||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
|
||||||
public class ShiroConfig {
|
|
||||||
|
|
||||||
@Resource
|
|
||||||
private LettuceConnectionFactory lettuceConnectionFactory;
|
|
||||||
@Autowired
|
|
||||||
private Environment env;
|
|
||||||
@Resource
|
|
||||||
private JeecgBaseConfig jeecgBaseConfig;
|
|
||||||
@Autowired(required = false)
|
|
||||||
private RedisProperties redisProperties;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Filter Chain定义说明
|
|
||||||
*
|
|
||||||
* 1、一个URL可以配置多个Filter,使用逗号分隔
|
|
||||||
* 2、当设置多个过滤器时,全部验证通过,才视为通过
|
|
||||||
* 3、部分过滤器可指定参数,如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"); //授权接口排除
|
|
||||||
|
|
||||||
|
|
||||||
//update-begin--Author:scott Date:20221116 for:排除静态资源后缀
|
|
||||||
filterChainDefinitionMap.put("/", "anon");
|
|
||||||
filterChainDefinitionMap.put("/doc.html", "anon");
|
|
||||||
filterChainDefinitionMap.put("/**/*.js", "anon");
|
|
||||||
filterChainDefinitionMap.put("/**/*.css", "anon");
|
|
||||||
filterChainDefinitionMap.put("/**/*.html", "anon");
|
|
||||||
filterChainDefinitionMap.put("/**/*.svg", "anon");
|
|
||||||
filterChainDefinitionMap.put("/**/*.pdf", "anon");
|
|
||||||
filterChainDefinitionMap.put("/**/*.jpg", "anon");
|
|
||||||
filterChainDefinitionMap.put("/**/*.png", "anon");
|
|
||||||
filterChainDefinitionMap.put("/**/*.gif", "anon");
|
|
||||||
filterChainDefinitionMap.put("/**/*.ico", "anon");
|
|
||||||
filterChainDefinitionMap.put("/**/*.ttf", "anon");
|
|
||||||
filterChainDefinitionMap.put("/**/*.woff", "anon");
|
|
||||||
filterChainDefinitionMap.put("/**/*.woff2", "anon");
|
|
||||||
filterChainDefinitionMap.put("/**/*.glb", "anon");
|
|
||||||
filterChainDefinitionMap.put("/**/*.wasm", "anon");
|
|
||||||
//update-end--Author:scott Date:20221116 for:排除静态资源后缀
|
|
||||||
|
|
||||||
filterChainDefinitionMap.put("/druid/**", "anon");
|
|
||||||
filterChainDefinitionMap.put("/swagger-ui.html", "anon");
|
|
||||||
filterChainDefinitionMap.put("/swagger**/**", "anon");
|
|
||||||
filterChainDefinitionMap.put("/webjars/**", "anon");
|
|
||||||
filterChainDefinitionMap.put("/v3/**", "anon");
|
|
||||||
|
|
||||||
// update-begin--Author:sunjianlei Date:20210510 for:排除消息通告查看详情页面(用于第三方APP)
|
|
||||||
filterChainDefinitionMap.put("/sys/annountCement/show/**", "anon");
|
|
||||||
// update-end--Author:sunjianlei Date:20210510 for:排除消息通告查看详情页面(用于第三方APP)
|
|
||||||
|
|
||||||
//积木报表排除
|
|
||||||
filterChainDefinitionMap.put("/jmreport/**", "anon");
|
|
||||||
filterChainDefinitionMap.put("/**/*.js.map", "anon");
|
|
||||||
filterChainDefinitionMap.put("/**/*.css.map", "anon");
|
|
||||||
|
|
||||||
//积木BI大屏和仪表盘排除
|
|
||||||
filterChainDefinitionMap.put("/drag/view", "anon");
|
|
||||||
filterChainDefinitionMap.put("/drag/page/queryById", "anon");
|
|
||||||
filterChainDefinitionMap.put("/drag/page/addVisitsNumber", "anon");
|
|
||||||
filterChainDefinitionMap.put("/drag/page/queryTemplateList", "anon");
|
|
||||||
filterChainDefinitionMap.put("/drag/share/view/**", "anon");
|
|
||||||
filterChainDefinitionMap.put("/drag/onlDragDatasetHead/getAllChartData", "anon");
|
|
||||||
filterChainDefinitionMap.put("/drag/onlDragDatasetHead/getTotalData", "anon");
|
|
||||||
filterChainDefinitionMap.put("/drag/mock/json/**", "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无痕刷新示例
|
|
||||||
|
|
||||||
//性能监控——安全隐患泄露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;
|
|
||||||
}
|
|
||||||
|
|
||||||
//update-begin---author:chenrui ---date:20240126 for:【QQYUN-7932】AI助手------------
|
|
||||||
|
|
||||||
/**
|
|
||||||
* spring过滤装饰器 <br/>
|
|
||||||
* 因为shiro的filter不支持异步请求,导致所有的异步请求都会报错. <br/>
|
|
||||||
* 所以需要用spring的FilterRegistrationBean再代理一下shiro的filter.为他扩展异步支持. <br/>
|
|
||||||
* 后续所有异步的接口都需要再这里增加registration.addUrlPatterns("/xxx/xxx");
|
|
||||||
* @return
|
|
||||||
* @author chenrui
|
|
||||||
* @date 2024/12/3 19:49
|
|
||||||
*/
|
|
||||||
@Bean
|
|
||||||
public FilterRegistrationBean shiroFilterRegistration() {
|
|
||||||
FilterRegistrationBean registration = new FilterRegistrationBean();
|
|
||||||
registration.setFilter(new DelegatingFilterProxy("shiroFilterFactoryBean"));
|
|
||||||
registration.setEnabled(true);
|
|
||||||
//update-begin---author:chenrui ---date:20241202 for:[issues/7491]运行时间好长,效率慢 ------------
|
|
||||||
registration.addUrlPatterns("/test/ai/chat/send");
|
|
||||||
//update-end---author:chenrui ---date:20241202 for:[issues/7491]运行时间好长,效率慢 ------------
|
|
||||||
registration.addUrlPatterns("/airag/flow/run");
|
|
||||||
registration.addUrlPatterns("/airag/flow/debug");
|
|
||||||
registration.addUrlPatterns("/airag/chat/send");
|
|
||||||
registration.addUrlPatterns("/airag/app/debug");
|
|
||||||
//支持异步
|
|
||||||
registration.setAsyncSupported(true);
|
|
||||||
registration.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ASYNC);
|
|
||||||
return registration;
|
|
||||||
}
|
|
||||||
//update-end---author:chenrui ---date:20240126 for:【QQYUN-7932】AI助手------------
|
|
||||||
|
|
||||||
@Bean("securityManager")
|
|
||||||
public DefaultWebSecurityManager securityManager(ShiroRealm myRealm) {
|
|
||||||
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
|
|
||||||
securityManager.setRealm(myRealm);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* 关闭shiro自带的session,详情见文档
|
|
||||||
* http://shiro.apache.org/session-management.html#SessionManagement-
|
|
||||||
* StatelessApplications%28Sessionless%29
|
|
||||||
*/
|
|
||||||
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
|
|
||||||
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
|
|
||||||
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
|
|
||||||
subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
|
|
||||||
securityManager.setSubjectDAO(subjectDAO);
|
|
||||||
//自定义缓存实现,使用redis
|
|
||||||
securityManager.setCacheManager(redisCacheManager());
|
|
||||||
return securityManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 下面的代码是添加注解支持
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
@Bean
|
|
||||||
@DependsOn("lifecycleBeanPostProcessor")
|
|
||||||
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
|
|
||||||
DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
|
|
||||||
defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
|
|
||||||
/**
|
|
||||||
* 解决重复代理问题 github#994
|
|
||||||
* 添加前缀判断 不匹配 任何Advisor
|
|
||||||
*/
|
|
||||||
defaultAdvisorAutoProxyCreator.setUsePrefix(true);
|
|
||||||
defaultAdvisorAutoProxyCreator.setAdvisorBeanNamePrefix("_no_advisor");
|
|
||||||
return defaultAdvisorAutoProxyCreator;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
public static LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
|
|
||||||
return new LifecycleBeanPostProcessor();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
|
|
||||||
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
|
|
||||||
advisor.setSecurityManager(securityManager);
|
|
||||||
return advisor;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* cacheManager 缓存 redis实现
|
|
||||||
* 使用的是shiro-redis开源插件
|
|
||||||
*
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
public RedisCacheManager redisCacheManager() {
|
|
||||||
log.info("===============(1)创建缓存管理器RedisCacheManager");
|
|
||||||
RedisCacheManager redisCacheManager = new RedisCacheManager();
|
|
||||||
redisCacheManager.setRedisManager(redisManager());
|
|
||||||
//redis中针对不同用户缓存(此处的id需要对应user实体中的id字段,用于唯一标识)
|
|
||||||
redisCacheManager.setPrincipalIdFieldName("id");
|
|
||||||
//用户权限信息缓存时间
|
|
||||||
redisCacheManager.setExpire(200000);
|
|
||||||
return redisCacheManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* RedisConfig在项目starter项目中
|
|
||||||
* jeecg-boot-starter-github\jeecg-boot-common\src\main\java\org\jeecg\common\modules\redis\config\RedisConfig.java
|
|
||||||
*
|
|
||||||
* 配置shiro redisManager
|
|
||||||
* 使用的是shiro-redis开源插件
|
|
||||||
*
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
@Bean
|
|
||||||
public IRedisManager redisManager() {
|
|
||||||
log.info("===============(2)创建RedisManager,连接Redis..");
|
|
||||||
IRedisManager manager;
|
|
||||||
// sentinel cluster redis(【issues/5569】shiro集成 redis 不支持 sentinel 方式部署的redis集群 #5569)
|
|
||||||
if (Objects.nonNull(redisProperties)
|
|
||||||
&& Objects.nonNull(redisProperties.getSentinel())
|
|
||||||
&& !CollectionUtils.isEmpty(redisProperties.getSentinel().getNodes())) {
|
|
||||||
RedisSentinelManager sentinelManager = new RedisSentinelManager();
|
|
||||||
sentinelManager.setMasterName(redisProperties.getSentinel().getMaster());
|
|
||||||
sentinelManager.setHost(String.join(",", redisProperties.getSentinel().getNodes()));
|
|
||||||
sentinelManager.setPassword(redisProperties.getPassword());
|
|
||||||
sentinelManager.setDatabase(redisProperties.getDatabase());
|
|
||||||
|
|
||||||
return sentinelManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
// redis 单机支持,在集群为空,或者集群无机器时候使用 add by jzyadmin@163.com
|
|
||||||
if (lettuceConnectionFactory.getClusterConfiguration() == null || lettuceConnectionFactory.getClusterConfiguration().getClusterNodes().isEmpty()) {
|
|
||||||
RedisManager redisManager = new RedisManager();
|
|
||||||
redisManager.setHost(lettuceConnectionFactory.getHostName() + ":" + lettuceConnectionFactory.getPort());
|
|
||||||
//(lettuceConnectionFactory.getPort());
|
|
||||||
redisManager.setDatabase(lettuceConnectionFactory.getDatabase());
|
|
||||||
redisManager.setTimeout(0);
|
|
||||||
if (!StringUtils.isEmpty(lettuceConnectionFactory.getPassword())) {
|
|
||||||
redisManager.setPassword(lettuceConnectionFactory.getPassword());
|
|
||||||
}
|
|
||||||
manager = redisManager;
|
|
||||||
}else{
|
|
||||||
// redis集群支持,优先使用集群配置
|
|
||||||
RedisClusterManager redisManager = new RedisClusterManager();
|
|
||||||
Set<HostAndPort> portSet = new HashSet<>();
|
|
||||||
lettuceConnectionFactory.getClusterConfiguration().getClusterNodes().forEach(node -> portSet.add(new HostAndPort(node.getHost() , node.getPort())));
|
|
||||||
//update-begin--Author:scott Date:20210531 for:修改集群模式下未设置redis密码的bug issues/I3QNIC
|
|
||||||
if (oConvertUtils.isNotEmpty(lettuceConnectionFactory.getPassword())) {
|
|
||||||
JedisCluster jedisCluster = new JedisCluster(portSet, 2000, 2000, 5,
|
|
||||||
lettuceConnectionFactory.getPassword(), new GenericObjectPoolConfig());
|
|
||||||
redisManager.setPassword(lettuceConnectionFactory.getPassword());
|
|
||||||
redisManager.setJedisCluster(jedisCluster);
|
|
||||||
} else {
|
|
||||||
JedisCluster jedisCluster = new JedisCluster(portSet);
|
|
||||||
redisManager.setJedisCluster(jedisCluster);
|
|
||||||
}
|
|
||||||
//update-end--Author:scott Date:20210531 for:修改集群模式下未设置redis密码的bug issues/I3QNIC
|
|
||||||
manager = redisManager;
|
|
||||||
}
|
|
||||||
return manager;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 解决 ShiroRequestMappingConfig 获取 requestMappingHandlerMapping Bean 冲突
|
|
||||||
* spring-boot-autoconfigure:3.4.5 和 spring-boot-actuator-autoconfigure:3.4.5
|
|
||||||
*/
|
|
||||||
@Primary
|
|
||||||
@Bean
|
|
||||||
public RequestMappingHandlerMapping overridedRequestMappingHandlerMapping() {
|
|
||||||
RequestMappingHandlerMapping mapping = new RequestMappingHandlerMapping();
|
|
||||||
mapping.setUrlPathHelper(new ShiroUrlPathHelper());
|
|
||||||
return mapping;
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<String> rebuildUrl(String[] bases, String[] uris) {
|
|
||||||
List<String> urls = new ArrayList<>();
|
|
||||||
for (String base : bases) {
|
|
||||||
for (String uri : uris) {
|
|
||||||
urls.add(prefix(base)+prefix(uri));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return urls;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String prefix(String seg) {
|
|
||||||
return seg.startsWith("/") ? seg : "/"+seg;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,237 +0,0 @@
|
|||||||
package org.jeecg.config.shiro;
|
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.apache.shiro.authc.AuthenticationException;
|
|
||||||
import org.apache.shiro.authc.AuthenticationInfo;
|
|
||||||
import org.apache.shiro.authc.AuthenticationToken;
|
|
||||||
import org.apache.shiro.authc.SimpleAuthenticationInfo;
|
|
||||||
import org.apache.shiro.authz.AuthorizationInfo;
|
|
||||||
import org.apache.shiro.authz.SimpleAuthorizationInfo;
|
|
||||||
import org.apache.shiro.realm.AuthorizingRealm;
|
|
||||||
import org.apache.shiro.subject.PrincipalCollection;
|
|
||||||
import org.jeecg.common.api.CommonAPI;
|
|
||||||
import org.jeecg.common.config.TenantContext;
|
|
||||||
import org.jeecg.common.constant.CacheConstant;
|
|
||||||
import org.jeecg.common.constant.CommonConstant;
|
|
||||||
import org.jeecg.common.system.util.JwtUtil;
|
|
||||||
import org.jeecg.common.system.vo.LoginUser;
|
|
||||||
import org.jeecg.common.util.RedisUtil;
|
|
||||||
import org.jeecg.common.util.SpringContextUtils;
|
|
||||||
import org.jeecg.common.util.TokenUtils;
|
|
||||||
import org.jeecg.common.util.oConvertUtils;
|
|
||||||
import org.jeecg.config.mybatis.MybatisPlusSaasConfig;
|
|
||||||
import org.springframework.beans.factory.config.BeanDefinition;
|
|
||||||
import org.springframework.context.annotation.Lazy;
|
|
||||||
import org.springframework.context.annotation.Role;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
import jakarta.annotation.Resource;
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @Description: 用户登录鉴权和获取用户授权
|
|
||||||
* @Author: Scott
|
|
||||||
* @Date: 2019-4-23 8:13
|
|
||||||
* @Version: 1.1
|
|
||||||
*/
|
|
||||||
@Component
|
|
||||||
@Slf4j
|
|
||||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
|
||||||
public class ShiroRealm extends AuthorizingRealm {
|
|
||||||
@Lazy
|
|
||||||
@Resource
|
|
||||||
private CommonAPI commonApi;
|
|
||||||
|
|
||||||
@Lazy
|
|
||||||
@Resource
|
|
||||||
private RedisUtil redisUtil;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 必须重写此方法,不然Shiro会报错
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public boolean supports(AuthenticationToken token) {
|
|
||||||
return token instanceof JwtToken;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 权限信息认证(包括角色以及权限)是用户访问controller的时候才进行验证(redis存储的此处权限信息)
|
|
||||||
* 触发检测用户权限时才会调用此方法,例如checkRole,checkPermission
|
|
||||||
*
|
|
||||||
* @param principals 身份信息
|
|
||||||
* @return AuthorizationInfo 权限信息
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
|
|
||||||
log.debug("===============Shiro权限认证开始============ [ roles、permissions]==========");
|
|
||||||
String username = null;
|
|
||||||
String userId = null;
|
|
||||||
if (principals != null) {
|
|
||||||
LoginUser sysUser = (LoginUser) principals.getPrimaryPrincipal();
|
|
||||||
username = sysUser.getUsername();
|
|
||||||
userId = sysUser.getId();
|
|
||||||
}
|
|
||||||
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
|
|
||||||
|
|
||||||
// 设置用户拥有的角色集合,比如“admin,test”
|
|
||||||
Set<String> roleSet = commonApi.queryUserRolesById(userId);
|
|
||||||
//System.out.println(roleSet.toString());
|
|
||||||
info.setRoles(roleSet);
|
|
||||||
|
|
||||||
// 设置用户拥有的权限集合,比如“sys:role:add,sys:user:add”
|
|
||||||
Set<String> permissionSet = commonApi.queryUserAuths(userId);
|
|
||||||
info.addStringPermissions(permissionSet);
|
|
||||||
//System.out.println(permissionSet);
|
|
||||||
log.info("===============Shiro权限认证成功==============");
|
|
||||||
return info;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 用户信息认证是在用户进行登录的时候进行验证(不存redis)
|
|
||||||
* 也就是说验证用户输入的账号和密码是否正确,错误抛出异常
|
|
||||||
*
|
|
||||||
* @param auth 用户登录的账号密码信息
|
|
||||||
* @return 返回封装了用户信息的 AuthenticationInfo 实例
|
|
||||||
* @throws AuthenticationException
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException {
|
|
||||||
log.debug("===============Shiro身份认证开始============doGetAuthenticationInfo==========");
|
|
||||||
String token = (String) auth.getCredentials();
|
|
||||||
if (token == null) {
|
|
||||||
HttpServletRequest req = SpringContextUtils.getHttpServletRequest();
|
|
||||||
log.info("————————身份认证失败——————————IP地址: "+ oConvertUtils.getIpAddrByRequest(req) +",URL:"+req.getRequestURI());
|
|
||||||
throw new AuthenticationException("token为空!");
|
|
||||||
}
|
|
||||||
// 校验token有效性
|
|
||||||
LoginUser loginUser = null;
|
|
||||||
try {
|
|
||||||
loginUser = this.checkUserTokenIsEffect(token);
|
|
||||||
} catch (AuthenticationException e) {
|
|
||||||
JwtUtil.responseError(SpringContextUtils.getHttpServletResponse(),401,e.getMessage());
|
|
||||||
e.printStackTrace();
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return new SimpleAuthenticationInfo(loginUser, token, getName());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 校验token的有效性
|
|
||||||
*
|
|
||||||
* @param token
|
|
||||||
*/
|
|
||||||
public LoginUser checkUserTokenIsEffect(String token) throws AuthenticationException {
|
|
||||||
// 解密获得username,用于和数据库进行对比
|
|
||||||
String username = JwtUtil.getUsername(token);
|
|
||||||
if (username == null) {
|
|
||||||
throw new AuthenticationException("token非法无效!");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 查询用户信息
|
|
||||||
log.debug("———校验token是否有效————checkUserTokenIsEffect——————— "+ token);
|
|
||||||
LoginUser loginUser = TokenUtils.getLoginUser(username, commonApi, redisUtil);
|
|
||||||
//LoginUser loginUser = commonApi.getUserByName(username);
|
|
||||||
if (loginUser == null) {
|
|
||||||
throw new AuthenticationException("用户不存在!");
|
|
||||||
}
|
|
||||||
// 判断用户状态
|
|
||||||
if (loginUser.getStatus() != 1) {
|
|
||||||
throw new AuthenticationException("账号已被锁定,请联系管理员!");
|
|
||||||
}
|
|
||||||
// 校验token是否超时失效 & 或者账号密码是否错误
|
|
||||||
if (!jwtTokenRefresh(token, username, loginUser.getPassword())) {
|
|
||||||
throw new AuthenticationException(CommonConstant.TOKEN_IS_INVALID_MSG);
|
|
||||||
}
|
|
||||||
//update-begin-author:taoyan date:20210609 for:校验用户的tenant_id和前端传过来的是否一致
|
|
||||||
String userTenantIds = loginUser.getRelTenantIds();
|
|
||||||
if(MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL && oConvertUtils.isNotEmpty(userTenantIds)){
|
|
||||||
String contextTenantId = TenantContext.getTenant();
|
|
||||||
log.debug("登录租户:" + contextTenantId);
|
|
||||||
log.debug("用户拥有那些租户:" + userTenantIds);
|
|
||||||
//登录用户无租户,前端header中租户ID值为 0
|
|
||||||
String str ="0";
|
|
||||||
if(oConvertUtils.isNotEmpty(contextTenantId) && !str.equals(contextTenantId)){
|
|
||||||
//update-begin-author:taoyan date:20211227 for: /issues/I4O14W 用户租户信息变更判断漏洞
|
|
||||||
String[] arr = userTenantIds.split(",");
|
|
||||||
if(!oConvertUtils.isIn(contextTenantId, arr)){
|
|
||||||
boolean isAuthorization = false;
|
|
||||||
//========================================================================
|
|
||||||
// 查询用户信息(如果租户不匹配从数据库中重新查询一次用户信息)
|
|
||||||
String loginUserKey = CacheConstant.SYS_USERS_CACHE + "::" + username;
|
|
||||||
redisUtil.del(loginUserKey);
|
|
||||||
LoginUser loginUserFromDb = commonApi.getUserByName(username);
|
|
||||||
if (oConvertUtils.isNotEmpty(loginUserFromDb.getRelTenantIds())) {
|
|
||||||
String[] newArray = loginUserFromDb.getRelTenantIds().split(",");
|
|
||||||
if (oConvertUtils.isIn(contextTenantId, newArray)) {
|
|
||||||
isAuthorization = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//========================================================================
|
|
||||||
|
|
||||||
//*********************************************
|
|
||||||
if(!isAuthorization){
|
|
||||||
log.info("租户异常——登录租户:" + contextTenantId);
|
|
||||||
log.info("租户异常——用户拥有租户组:" + userTenantIds);
|
|
||||||
throw new AuthenticationException("登录租户授权变更,请重新登陆!");
|
|
||||||
}
|
|
||||||
//*********************************************
|
|
||||||
}
|
|
||||||
//update-end-author:taoyan date:20211227 for: /issues/I4O14W 用户租户信息变更判断漏洞
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//update-end-author:taoyan date:20210609 for:校验用户的tenant_id和前端传过来的是否一致
|
|
||||||
return loginUser;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* JWTToken刷新生命周期 (实现: 用户在线操作不掉线功能)
|
|
||||||
* 1、登录成功后将用户的JWT生成的Token作为k、v存储到cache缓存里面(这时候k、v值一样),缓存有效期设置为Jwt有效时间的2倍
|
|
||||||
* 2、当该用户再次请求时,通过JWTFilter层层校验之后会进入到doGetAuthenticationInfo进行身份验证
|
|
||||||
* 3、当该用户这次请求jwt生成的token值已经超时,但该token对应cache中的k还是存在,则表示该用户一直在操作只是JWT的token失效了,程序会给token对应的k映射的v值重新生成JWTToken并覆盖v值,该缓存生命周期重新计算
|
|
||||||
* 4、当该用户这次请求jwt在生成的token值已经超时,并在cache中不存在对应的k,则表示该用户账户空闲超时,返回用户信息已失效,请重新登录。
|
|
||||||
* 注意: 前端请求Header中设置Authorization保持不变,校验有效性以缓存中的token为准。
|
|
||||||
* 用户过期时间 = Jwt有效时间 * 2。
|
|
||||||
*
|
|
||||||
* @param userName
|
|
||||||
* @param passWord
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
public boolean jwtTokenRefresh(String token, String userName, String passWord) {
|
|
||||||
String cacheToken = String.valueOf(redisUtil.get(CommonConstant.PREFIX_USER_TOKEN + token));
|
|
||||||
if (oConvertUtils.isNotEmpty(cacheToken)) {
|
|
||||||
// 校验token有效性
|
|
||||||
if (!JwtUtil.verify(cacheToken, userName, passWord)) {
|
|
||||||
String newAuthorization = JwtUtil.sign(userName, passWord);
|
|
||||||
// 设置超时时间
|
|
||||||
redisUtil.set(CommonConstant.PREFIX_USER_TOKEN + token, newAuthorization);
|
|
||||||
redisUtil.expire(CommonConstant.PREFIX_USER_TOKEN + token, JwtUtil.EXPIRE_TIME *2 / 1000);
|
|
||||||
log.debug("——————————用户在线操作,更新token保证不掉线—————————jwtTokenRefresh——————— "+ token);
|
|
||||||
}
|
|
||||||
//update-begin--Author:scott Date:20191005 for:解决每次请求,都重写redis中 token缓存问题
|
|
||||||
// else {
|
|
||||||
// // 设置超时时间
|
|
||||||
// redisUtil.set(CommonConstant.PREFIX_USER_TOKEN + token, cacheToken);
|
|
||||||
// redisUtil.expire(CommonConstant.PREFIX_USER_TOKEN + token, JwtUtil.EXPIRE_TIME / 1000);
|
|
||||||
// }
|
|
||||||
//update-end--Author:scott Date:20191005 for:解决每次请求,都重写redis中 token缓存问题
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
//redis中不存在此TOEKN,说明token非法返回false
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 清除当前用户的权限认证缓存
|
|
||||||
*
|
|
||||||
* @param principals 权限信息
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void clearCache(PrincipalCollection principals) {
|
|
||||||
super.clearCache(principals);
|
|
||||||
//update-begin---author:scott ---date::2024-06-18 for:【TV360X-1320】分配权限必须退出重新登录才生效,造成很多用户困扰---
|
|
||||||
super.clearCachedAuthorizationInfo(principals);
|
|
||||||
//update-end---author:scott ---date::2024-06-18 for:【TV360X-1320】分配权限必须退出重新登录才生效,造成很多用户困扰---
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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,130 +0,0 @@
|
|||||||
package org.jeecg.config.shiro.filters;
|
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.apache.commons.lang.StringUtils;
|
|
||||||
import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;
|
|
||||||
import org.jeecg.common.config.TenantContext;
|
|
||||||
import org.jeecg.common.constant.CommonConstant;
|
|
||||||
import org.jeecg.common.system.util.JwtUtil;
|
|
||||||
import org.jeecg.common.util.oConvertUtils;
|
|
||||||
import org.jeecg.config.shiro.JwtToken;
|
|
||||||
import org.jeecg.config.shiro.ignore.InMemoryIgnoreAuth;
|
|
||||||
import org.springframework.http.HttpHeaders;
|
|
||||||
import org.springframework.http.HttpStatus;
|
|
||||||
import org.springframework.web.bind.annotation.RequestMethod;
|
|
||||||
|
|
||||||
import jakarta.servlet.ServletRequest;
|
|
||||||
import jakarta.servlet.ServletResponse;
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @Description: 鉴权登录拦截器
|
|
||||||
* @Author: Scott
|
|
||||||
* @Date: 2018/10/7
|
|
||||||
**/
|
|
||||||
@Slf4j
|
|
||||||
public class JwtFilter extends BasicHttpAuthenticationFilter {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 默认开启跨域设置(使用单体)
|
|
||||||
* 微服务情况下,此属性设置为false
|
|
||||||
*/
|
|
||||||
private boolean allowOrigin = true;
|
|
||||||
|
|
||||||
public JwtFilter(){}
|
|
||||||
public JwtFilter(boolean allowOrigin){
|
|
||||||
this.allowOrigin = allowOrigin;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 执行登录认证
|
|
||||||
*
|
|
||||||
* @param request
|
|
||||||
* @param response
|
|
||||||
* @param mappedValue
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
|
|
||||||
try {
|
|
||||||
// 判断当前路径是不是注解了@IngoreAuth路径,如果是,则放开验证
|
|
||||||
if (InMemoryIgnoreAuth.contains(((HttpServletRequest) request).getServletPath())) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
executeLogin(request, response);
|
|
||||||
return true;
|
|
||||||
} catch (Exception e) {
|
|
||||||
JwtUtil.responseError(response,401,CommonConstant.TOKEN_IS_INVALID_MSG);
|
|
||||||
return false;
|
|
||||||
//throw new AuthenticationException("Token失效,请重新登录", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
|
|
||||||
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
|
|
||||||
String token = httpServletRequest.getHeader(CommonConstant.X_ACCESS_TOKEN);
|
|
||||||
// update-begin--Author:lvdandan Date:20210105 for:JT-355 OA聊天添加token验证,获取token参数
|
|
||||||
if (oConvertUtils.isEmpty(token)) {
|
|
||||||
token = httpServletRequest.getParameter("token");
|
|
||||||
}
|
|
||||||
// update-end--Author:lvdandan Date:20210105 for:JT-355 OA聊天添加token验证,获取token参数
|
|
||||||
|
|
||||||
JwtToken jwtToken = new JwtToken(token);
|
|
||||||
// 提交给realm进行登入,如果错误他会抛出异常并被捕获
|
|
||||||
getSubject(request, response).login(jwtToken);
|
|
||||||
// 如果没有抛出异常则代表登入成功,返回true
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 对跨域提供支持
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
|
|
||||||
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
|
|
||||||
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
|
|
||||||
if(allowOrigin){
|
|
||||||
httpServletResponse.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, httpServletRequest.getHeader(HttpHeaders.ORIGIN));
|
|
||||||
// 允许客户端请求方法
|
|
||||||
httpServletResponse.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS, "GET,POST,OPTIONS,PUT,DELETE");
|
|
||||||
// 允许客户端提交的Header
|
|
||||||
String requestHeaders = httpServletRequest.getHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS);
|
|
||||||
if (StringUtils.isNotEmpty(requestHeaders)) {
|
|
||||||
httpServletResponse.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS, requestHeaders);
|
|
||||||
}
|
|
||||||
// 允许客户端携带凭证信息(是否允许发送Cookie)
|
|
||||||
httpServletResponse.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true");
|
|
||||||
}
|
|
||||||
// 跨域时会首先发送一个option请求,这里我们给option请求直接返回正常状态
|
|
||||||
if (RequestMethod.OPTIONS.name().equalsIgnoreCase(httpServletRequest.getMethod())) {
|
|
||||||
httpServletResponse.setStatus(HttpStatus.OK.value());
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
//update-begin-author:taoyan date:20200708 for:多租户用到
|
|
||||||
String tenantId = httpServletRequest.getHeader(CommonConstant.TENANT_ID);
|
|
||||||
TenantContext.setTenant(tenantId);
|
|
||||||
//update-end-author:taoyan date:20200708 for:多租户用到
|
|
||||||
|
|
||||||
return super.preHandle(request, response);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* JwtFilter中ThreadLocal需要及时清除 #3634
|
|
||||||
*
|
|
||||||
* @param request
|
|
||||||
* @param response
|
|
||||||
* @param exception
|
|
||||||
* @throws Exception
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void afterCompletion(ServletRequest request, ServletResponse response, Exception exception) throws Exception {
|
|
||||||
//log.info("------清空线程中多租户的ID={}------",TenantContext.getTenant());
|
|
||||||
TenantContext.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -91,10 +91,6 @@ public class IgnoreAuthPostProcessor implements InitializingBean {
|
|||||||
if (bases.length > 0) {
|
if (bases.length > 0) {
|
||||||
for (String base : bases) {
|
for (String base : bases) {
|
||||||
for (String uri : uris) {
|
for (String uri : uris) {
|
||||||
// 如果uri包含路径占位符, 则需要将其替换为*
|
|
||||||
if (uri.matches(".*\\{.*}.*")) {
|
|
||||||
uri = uri.replaceAll("\\{.*?}", "*");
|
|
||||||
}
|
|
||||||
urls.add(prefix(base) + prefix(uri));
|
urls.add(prefix(base) + prefix(uri));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,5 @@
|
|||||||
package org.jeecg.config.shiro.ignore;
|
package org.jeecg.config.shiro.ignore;
|
||||||
|
|
||||||
import org.springframework.util.AntPathMatcher;
|
|
||||||
import org.springframework.util.PathMatcher;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@ -14,7 +12,6 @@ import java.util.List;
|
|||||||
public class InMemoryIgnoreAuth {
|
public class InMemoryIgnoreAuth {
|
||||||
private static final List<String> IGNORE_AUTH_LIST = new ArrayList<>();
|
private static final List<String> IGNORE_AUTH_LIST = new ArrayList<>();
|
||||||
|
|
||||||
private static PathMatcher MATCHER = new AntPathMatcher();
|
|
||||||
public InMemoryIgnoreAuth() {}
|
public InMemoryIgnoreAuth() {}
|
||||||
|
|
||||||
public static void set(List<String> list) {
|
public static void set(List<String> list) {
|
||||||
@ -31,7 +28,7 @@ public class InMemoryIgnoreAuth {
|
|||||||
|
|
||||||
public static boolean contains(String url) {
|
public static boolean contains(String url) {
|
||||||
for (String ignoreAuth : IGNORE_AUTH_LIST) {
|
for (String ignoreAuth : IGNORE_AUTH_LIST) {
|
||||||
if(MATCHER.match(ignoreAuth,url)){
|
if (url.endsWith(ignoreAuth)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,2 +0,0 @@
|
|||||||
springdoc.auto-tag-classes: false
|
|
||||||
springdoc.packages-to-scan: org.jeecg
|
|
||||||
@ -0,0 +1,75 @@
|
|||||||
|
package org.jeecg.test.sqlinjection;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import net.sf.jsqlparser.JSQLParserException;
|
||||||
|
import org.jeecg.common.util.SqlInjectionUtil;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SQL注入攻击检查测试
|
||||||
|
* @author: liusq
|
||||||
|
* @date: 2023年09月08日
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
public class TestInjectWithSqlParser {
|
||||||
|
/**
|
||||||
|
* 注入测试
|
||||||
|
*
|
||||||
|
* @param sql
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
private boolean isExistSqlInject(String sql) {
|
||||||
|
try {
|
||||||
|
SqlInjectionUtil.specialFilterContentForOnlineReport(sql);
|
||||||
|
return false;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.info("===================================================");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void test() throws JSQLParserException {
|
||||||
|
//不存在sql注入
|
||||||
|
assertFalse(isExistSqlInject("select * from fm_time where dept_id=:sqlparamsmap.id and time=:sqlparamsmap.time"));
|
||||||
|
assertFalse(isExistSqlInject("select * from test"));
|
||||||
|
assertFalse(isExistSqlInject("select load_file(\"C:\\\\benben.txt\")"));
|
||||||
|
assertFalse(isExistSqlInject("WITH SUB1 AS (SELECT user FROM t1) SELECT * FROM T2 WHERE id > 123 "));
|
||||||
|
|
||||||
|
//存在sql注入
|
||||||
|
assertTrue(isExistSqlInject("or 1= 1 --"));
|
||||||
|
assertTrue(isExistSqlInject("select * from test where sleep(%23)"));
|
||||||
|
assertTrue(isExistSqlInject("select * from test where id=1 and multipoint((select * from(select * from(select user())a)b));"));
|
||||||
|
assertTrue(isExistSqlInject("select * from users;show databases;"));
|
||||||
|
assertTrue(isExistSqlInject("select * from dc_device where id=1 and length((select group_concat(table_name) from information_schema.tables where table_schema=database()))>13"));
|
||||||
|
assertTrue(isExistSqlInject("update user set name = '123'"));
|
||||||
|
assertTrue(isExistSqlInject("SELECT * FROM users WHERE username = 'admin' AND password = '123456' OR 1=1;--"));
|
||||||
|
assertTrue(isExistSqlInject("select * from users where id=1 and (select count(*) from information_schema.tables where table_schema='数据库名')>4 %23"));
|
||||||
|
assertTrue(isExistSqlInject("select * from dc_device where sleep(5) %23"));
|
||||||
|
assertTrue(isExistSqlInject("select * from dc_device where id in (select id from other)"));
|
||||||
|
assertTrue(isExistSqlInject("select * from dc_device where id in (select id from other)"));
|
||||||
|
assertTrue(isExistSqlInject("select * from dc_device where 2=2.0 or 2 != 4"));
|
||||||
|
assertTrue(isExistSqlInject("select * from dc_device where 1!=2.0"));
|
||||||
|
assertTrue(isExistSqlInject("select * from dc_device where id=floor(2.0)"));
|
||||||
|
assertTrue(isExistSqlInject("select * from dc_device where not true"));
|
||||||
|
assertTrue(isExistSqlInject("select * from dc_device where 1 or id > 0"));
|
||||||
|
assertTrue(isExistSqlInject("select * from dc_device where 'tom' or id > 0"));
|
||||||
|
assertTrue(isExistSqlInject("select * from dc_device where '-2.3' "));
|
||||||
|
assertTrue(isExistSqlInject("select * from dc_device where 2 "));
|
||||||
|
assertTrue(isExistSqlInject("select * from dc_device where (3+2) "));
|
||||||
|
assertTrue(isExistSqlInject("select * from dc_device where -1 IS TRUE"));
|
||||||
|
assertTrue(isExistSqlInject("select * from dc_device where 'hello' is null "));
|
||||||
|
assertTrue(isExistSqlInject("select * from dc_device where '2022-10-31' and id > 0"));
|
||||||
|
assertTrue(isExistSqlInject("select * from dc_device where id > 0 or 1!=2.0 "));
|
||||||
|
assertTrue(isExistSqlInject("select * from dc_device where id > 0 or 1 in (1,3,4) "));
|
||||||
|
assertTrue(isExistSqlInject("select * from dc_device UNION select name from other"));
|
||||||
|
assertTrue(isExistSqlInject("(SELECT 6240 FROM (SELECT(SLEEP(5))and 1=2)vidl)"));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@ -0,0 +1,50 @@
|
|||||||
|
package org.jeecg.test.sqlinjection;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import net.sf.jsqlparser.JSQLParserException;
|
||||||
|
import org.jeecg.common.util.SqlInjectionUtil;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SQL注入攻击检查测试
|
||||||
|
* @author: liusq
|
||||||
|
* @date: 2023年09月08日
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
public class TestSqlInjectForDict {
|
||||||
|
/**
|
||||||
|
* 注入测试
|
||||||
|
*
|
||||||
|
* @param sql
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
private boolean isExistSqlInject(String sql) {
|
||||||
|
try {
|
||||||
|
SqlInjectionUtil.specialFilterContentForDictSql(sql);
|
||||||
|
return false;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.info("===================================================");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void test() throws JSQLParserException {
|
||||||
|
//不存在sql注入
|
||||||
|
assertFalse(isExistSqlInject("sys_user,realname,id"));
|
||||||
|
assertFalse(isExistSqlInject("oa_officialdoc_organcode,organ_name,id"));
|
||||||
|
assertFalse(isExistSqlInject("onl_cgform_head where table_type!=3 and copy_type=0,table_txt,table_name"));
|
||||||
|
assertFalse(isExistSqlInject("onl_cgform_head where copy_type = 0,table_txt,table_name"));
|
||||||
|
|
||||||
|
//存在sql注入
|
||||||
|
assertTrue(isExistSqlInject("or 1= 1 --"));
|
||||||
|
assertTrue(isExistSqlInject("select * from test where sleep(%23)"));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@ -0,0 +1,60 @@
|
|||||||
|
package org.jeecg.test.sqlinjection;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import net.sf.jsqlparser.JSQLParserException;
|
||||||
|
import org.jeecg.common.util.SqlInjectionUtil;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SQL注入攻击检查测试
|
||||||
|
* @author: liusq
|
||||||
|
* @date: 2023年09月08日
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
public class TestSqlInjectForOnlineReport {
|
||||||
|
/**
|
||||||
|
* 注入测试
|
||||||
|
*
|
||||||
|
* @param sql
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
private boolean isExistSqlInject(String sql) {
|
||||||
|
try {
|
||||||
|
SqlInjectionUtil.specialFilterContentForOnlineReport(sql);
|
||||||
|
return false;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.info("===================================================");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void test() throws JSQLParserException {
|
||||||
|
//不存在sql注入
|
||||||
|
assertFalse(isExistSqlInject("select * from fm_time where dept_id=:sqlparamsmap.id and time=:sqlparamsmap.time"));
|
||||||
|
assertFalse(isExistSqlInject("select * from test"));
|
||||||
|
assertFalse(isExistSqlInject("select load_file(\"C:\\\\benben.txt\")"));
|
||||||
|
assertFalse(isExistSqlInject("select * from dc_device where id in (select id from other)"));
|
||||||
|
assertFalse(isExistSqlInject("select * from dc_device UNION select name from other"));
|
||||||
|
|
||||||
|
//存在sql注入
|
||||||
|
assertTrue(isExistSqlInject("(SELECT 6240 FROM (SELECT(SLEEP(5))and 1=2)vidl)"));
|
||||||
|
assertTrue(isExistSqlInject("or 1= 1 --"));
|
||||||
|
assertTrue(isExistSqlInject("select * from test where sleep(%23)"));
|
||||||
|
assertTrue(isExistSqlInject("select * from test where SLEEP(3)"));
|
||||||
|
assertTrue(isExistSqlInject("select * from test where id=1 and multipoint((select * from(select * from(select user())a)b));"));
|
||||||
|
assertTrue(isExistSqlInject("select * from users;show databases;"));
|
||||||
|
assertTrue(isExistSqlInject("select * from dc_device where id=1 and length((select group_concat(table_name) from information_schema.tables where table_schema=database()))>13"));
|
||||||
|
assertTrue(isExistSqlInject("update user set name = '123'"));
|
||||||
|
assertTrue(isExistSqlInject("SELECT * FROM users WHERE username = 'admin' AND password = '123456' OR 1=1;--"));
|
||||||
|
assertTrue(isExistSqlInject("select * from users where id=1 and (select count(*) from information_schema.tables where table_schema='数据库名')>4 %23"));
|
||||||
|
assertTrue(isExistSqlInject("select * from dc_device where sleep(5) %23"));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@ -0,0 +1,103 @@
|
|||||||
|
package org.jeecg.test.sqlinjection;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.toolkit.sql.SqlInjectionUtils;
|
||||||
|
import org.jeecg.common.util.SqlInjectionUtil;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Description: SQL注入测试类
|
||||||
|
* @author: scott
|
||||||
|
* @date: 2023年08月14日 9:55
|
||||||
|
*/
|
||||||
|
public class TestSqlInjection {
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表名带别名,同时有html编码字符
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void testSpecialSQL() {
|
||||||
|
String tableName = "sys_user t";
|
||||||
|
//解决使用参数tableName=sys_user t&复测,漏洞仍然存在
|
||||||
|
if (tableName.contains(" ")) {
|
||||||
|
tableName = tableName.substring(0, tableName.indexOf(" "));
|
||||||
|
}
|
||||||
|
//【issues/4393】 sys_user , (sys_user), sys_user%20, %60sys_user%60
|
||||||
|
String reg = "\\s+|\\(|\\)|`";
|
||||||
|
tableName = tableName.replaceAll(reg, "");
|
||||||
|
System.out.println(tableName);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试sql是否含sql注入风险
|
||||||
|
* <p>
|
||||||
|
* mybatis plus的方法
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void sqlInjectionCheck() {
|
||||||
|
String sql = "select * from sys_user";
|
||||||
|
System.out.println(SqlInjectionUtils.check(sql));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试sql是否有SLEEP风险
|
||||||
|
* <p>
|
||||||
|
* mybatisPlus的方法
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void sqlSleepCheck() {
|
||||||
|
SqlInjectionUtil.checkSqlAnnotation("(SELECT 6240 FROM (SELECT(SLEEP(5))and 1=2)vidl)");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试sql是否含sql注入风险
|
||||||
|
* <p>
|
||||||
|
* 自定义方法
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void sqlInjectionCheck2() {
|
||||||
|
String sql = "select * from sys_user";
|
||||||
|
SqlInjectionUtil.specialFilterContentForOnlineReport(sql);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 字段定义只能是是字母 数字 下划线的组合(不允许有空格、转义字符串等)
|
||||||
|
* <p>
|
||||||
|
* 判断字段名是否符合规范
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void testFieldSpecification() {
|
||||||
|
List<String> list = new ArrayList();
|
||||||
|
list.add("Hello World!");
|
||||||
|
list.add("Hello%20World!");
|
||||||
|
list.add("HelloWorld!");
|
||||||
|
list.add("Hello World");
|
||||||
|
list.add("age");
|
||||||
|
list.add("user_name");
|
||||||
|
list.add("user_name%20");
|
||||||
|
list.add("user_name%20 ");
|
||||||
|
|
||||||
|
for (String input : list) {
|
||||||
|
boolean containsSpecialChars = isValidString(input);
|
||||||
|
System.out.println("input:" + input + " ,包含空格和特殊字符: " + containsSpecialChars);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 字段定义只能是是字母 数字 下划线的组合(不允许有空格、转义字符串等)
|
||||||
|
*
|
||||||
|
* @param input
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
private static boolean isValidString(String input) {
|
||||||
|
Pattern pattern = Pattern.compile("^[a-zA-Z0-9_]+$");
|
||||||
|
return pattern.matcher(input).matches();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,109 @@
|
|||||||
|
package org.jeecg.test.sqlparse;
|
||||||
|
|
||||||
|
import net.sf.jsqlparser.JSQLParserException;
|
||||||
|
import org.jeecg.common.util.oConvertUtils;
|
||||||
|
import org.jeecg.common.util.sqlparse.JSqlParserUtils;
|
||||||
|
import org.jeecg.common.util.sqlparse.vo.SelectSqlInfo;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 针对 JSqlParserUtils 的单元测试
|
||||||
|
*/
|
||||||
|
public class JSqlParserUtilsTest {
|
||||||
|
|
||||||
|
private static final String[] sqlList = new String[]{
|
||||||
|
"select * from sys_user",
|
||||||
|
"select u.* from sys_user u",
|
||||||
|
"select u.*, c.name from sys_user u, demo c",
|
||||||
|
"select u.age, c.name from sys_user u, demo c",
|
||||||
|
"select sex, age, c.name from sys_user, demo c",
|
||||||
|
// 别名测试
|
||||||
|
"select username as realname from sys_user",
|
||||||
|
"select username as realname, u.realname as aaa, u.id bbb from sys_user u",
|
||||||
|
// 不存在真实地查询字段
|
||||||
|
"select count(1) from sys_user",
|
||||||
|
// 函数式字段
|
||||||
|
"select max(sex), id from sys_user",
|
||||||
|
// 复杂嵌套函数式字段
|
||||||
|
"select CONCAT(CONCAT(' _ ', sex), ' - ' , birthday) as info, id from sys_user",
|
||||||
|
// 更复杂的嵌套函数式字段
|
||||||
|
"select CONCAT(CONCAT(101,'_',NULL, DATE(create_time),'_',sex),' - ',birthday) as info, id from sys_user",
|
||||||
|
// 子查询SQL
|
||||||
|
"select u.name1 as name2 from (select username as name1 from sys_user) u",
|
||||||
|
// 多层嵌套子查询SQL
|
||||||
|
"select u2.name2 as name3 from (select u1.name1 as name2 from (select username as name1 from sys_user) u1) u2",
|
||||||
|
// 字段子查询SQL
|
||||||
|
"select id, (select username as name1 from sys_user u2 where u1.id = u2.id) as name2 from sys_user u1",
|
||||||
|
// 带条件的SQL(不解析where条件里的字段,但不影响解析查询字段)
|
||||||
|
"select username as name1 from sys_user where realname LIKE '%张%'",
|
||||||
|
// 多重复杂关联表查询解析,包含的表为:sys_user, sys_depart, sys_dict_item, demo
|
||||||
|
"" +
|
||||||
|
"SELECT " +
|
||||||
|
" u.*, d.age, sd.item_text AS sex, (SELECT count(sd.id) FROM sys_depart sd) AS count " +
|
||||||
|
"FROM " +
|
||||||
|
" (SELECT sd.username AS foo, sd.realname FROM sys_user sd) u, " +
|
||||||
|
" demo d " +
|
||||||
|
"LEFT JOIN sys_dict_item AS sd ON d.sex = sd.item_value " +
|
||||||
|
"WHERE sd.dict_id = '3d9a351be3436fbefb1307d4cfb49bf2'",
|
||||||
|
};
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testParseSelectSql() {
|
||||||
|
System.out.println("-----------------------------------------");
|
||||||
|
for (String sql : sqlList) {
|
||||||
|
System.out.println("待测试的sql:" + sql);
|
||||||
|
try {
|
||||||
|
// 解析所有的表名,key=表名,value=解析后的sql信息
|
||||||
|
Map<String, SelectSqlInfo> parsedMap = JSqlParserUtils.parseAllSelectTable(sql);
|
||||||
|
assert parsedMap != null;
|
||||||
|
for (Map.Entry<String, SelectSqlInfo> entry : parsedMap.entrySet()) {
|
||||||
|
System.out.println("表名:" + entry.getKey());
|
||||||
|
this.printSqlInfo(entry.getValue(), 1);
|
||||||
|
}
|
||||||
|
} catch (JSQLParserException e) {
|
||||||
|
System.out.println("SQL解析出现异常:" + e.getMessage());
|
||||||
|
}
|
||||||
|
System.out.println("-----------------------------------------");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void printSqlInfo(SelectSqlInfo sqlInfo, int level) {
|
||||||
|
String beforeStr = this.getBeforeStr(level);
|
||||||
|
if (sqlInfo.getFromTableName() == null) {
|
||||||
|
// 子查询
|
||||||
|
System.out.println(beforeStr + "子查询:" + sqlInfo.getFromSubSelect().getParsedSql());
|
||||||
|
this.printSqlInfo(sqlInfo.getFromSubSelect(), level + 1);
|
||||||
|
} else {
|
||||||
|
// 非子查询
|
||||||
|
System.out.println(beforeStr + "查询的表名:" + sqlInfo.getFromTableName());
|
||||||
|
}
|
||||||
|
if (oConvertUtils.isNotEmpty(sqlInfo.getFromTableAliasName())) {
|
||||||
|
System.out.println(beforeStr + "查询的表别名:" + sqlInfo.getFromTableAliasName());
|
||||||
|
}
|
||||||
|
if (sqlInfo.isSelectAll()) {
|
||||||
|
System.out.println(beforeStr + "查询的字段:*");
|
||||||
|
} else {
|
||||||
|
System.out.println(beforeStr + "查询的字段:" + sqlInfo.getSelectFields());
|
||||||
|
System.out.println(beforeStr + "真实的字段:" + sqlInfo.getRealSelectFields());
|
||||||
|
if (sqlInfo.getFromTableName() == null) {
|
||||||
|
System.out.println(beforeStr + "所有的字段(包括子查询):" + sqlInfo.getAllRealSelectFields());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 打印前缀,根据层级来打印
|
||||||
|
private String getBeforeStr(int level) {
|
||||||
|
if (level == 0) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
StringBuilder beforeStr = new StringBuilder();
|
||||||
|
for (int i = 0; i < level; i++) {
|
||||||
|
beforeStr.append(" ");
|
||||||
|
}
|
||||||
|
beforeStr.append("- ");
|
||||||
|
return beforeStr.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -1,12 +1,17 @@
|
|||||||
package org.jeecg.test.sqlparse;
|
package org.jeecg.test.sqlparse;
|
||||||
|
|
||||||
|
import net.sf.jsqlparser.JSQLParserException;
|
||||||
import org.jeecg.common.util.IpUtils;
|
import org.jeecg.common.util.IpUtils;
|
||||||
import org.jeecg.common.util.oConvertUtils;
|
import org.jeecg.common.util.oConvertUtils;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author: scott
|
* @author: scott
|
||||||
* @date: 2024年04月29日 16:48
|
* @date: 2024年04月29日 16:48
|
||||||
|
|||||||
@ -1,178 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
|
||||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
||||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
|
||||||
<modelVersion>4.0.0</modelVersion>
|
|
||||||
<parent>
|
|
||||||
<groupId>org.jeecgframework.boot</groupId>
|
|
||||||
<artifactId>jeecg-boot-module</artifactId>
|
|
||||||
<version>3.8.1</version>
|
|
||||||
</parent>
|
|
||||||
|
|
||||||
<artifactId>jeecg-boot-module-airag</artifactId>
|
|
||||||
|
|
||||||
<repositories>
|
|
||||||
<repository>
|
|
||||||
<id>aliyun</id>
|
|
||||||
<name>aliyun Repository</name>
|
|
||||||
<url>https://maven.aliyun.com/repository/public</url>
|
|
||||||
<snapshots>
|
|
||||||
<enabled>false</enabled>
|
|
||||||
</snapshots>
|
|
||||||
</repository>
|
|
||||||
<repository>
|
|
||||||
<id>jeecg</id>
|
|
||||||
<name>jeecg Repository</name>
|
|
||||||
<url>https://maven.jeecg.org/nexus/content/repositories/jeecg</url>
|
|
||||||
<snapshots>
|
|
||||||
<enabled>false</enabled>
|
|
||||||
</snapshots>
|
|
||||||
</repository>
|
|
||||||
</repositories>
|
|
||||||
|
|
||||||
<properties>
|
|
||||||
<kotlin.version>1.6.21</kotlin.version>
|
|
||||||
<liteflow.version>2.12.4.1</liteflow.version>
|
|
||||||
<langchain4j.version>0.35.0</langchain4j.version>
|
|
||||||
<apache-tika.version>2.9.1</apache-tika.version>
|
|
||||||
</properties>
|
|
||||||
|
|
||||||
<dependencies>
|
|
||||||
<!-- system单体 api-->
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.jeecgframework.boot</groupId>
|
|
||||||
<artifactId>jeecg-system-local-api</artifactId>
|
|
||||||
</dependency>
|
|
||||||
<!-- 微服务starter和system微服务 api
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.jeecgframework.boot</groupId>
|
|
||||||
<artifactId>jeecg-boot-starter-cloud</artifactId>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.jeecgframework.boot</groupId>
|
|
||||||
<artifactId>jeecg-system-cloud-api</artifactId>
|
|
||||||
</dependency>-->
|
|
||||||
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.jeecgframework.boot3</groupId>
|
|
||||||
<artifactId>jeecg-aiflow</artifactId>
|
|
||||||
<version>1.0.5</version>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<!-- aiflow 脚本依赖 -->
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.yomahub</groupId>
|
|
||||||
<artifactId>liteflow-script-graaljs</artifactId>
|
|
||||||
<version>${liteflow.version}</version>
|
|
||||||
<scope>runtime</scope>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.yomahub</groupId>
|
|
||||||
<artifactId>liteflow-script-groovy</artifactId>
|
|
||||||
<version>${liteflow.version}</version>
|
|
||||||
<scope>runtime</scope>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.yomahub</groupId>
|
|
||||||
<artifactId>liteflow-script-kotlin</artifactId>
|
|
||||||
<version>${liteflow.version}</version>
|
|
||||||
<exclusions>
|
|
||||||
<exclusion>
|
|
||||||
<groupId>org.jetbrains.kotlin</groupId>
|
|
||||||
<artifactId>kotlin-scripting-jsr223</artifactId>
|
|
||||||
</exclusion>
|
|
||||||
</exclusions>
|
|
||||||
<scope>runtime</scope>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.yomahub</groupId>
|
|
||||||
<artifactId>liteflow-script-aviator</artifactId>
|
|
||||||
<version>${liteflow.version}</version>
|
|
||||||
<scope>runtime</scope>
|
|
||||||
<exclusions>
|
|
||||||
<exclusion>
|
|
||||||
<artifactId>aviator</artifactId>
|
|
||||||
<groupId>com.googlecode.aviator</groupId>
|
|
||||||
</exclusion>
|
|
||||||
</exclusions>
|
|
||||||
</dependency>
|
|
||||||
<!-- aiflow 脚本依赖 -->
|
|
||||||
|
|
||||||
<!-- langChain4j model support -->
|
|
||||||
<dependency>
|
|
||||||
<groupId>dev.langchain4j</groupId>
|
|
||||||
<artifactId>langchain4j-ollama</artifactId>
|
|
||||||
<version>${langchain4j.version}</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>dev.langchain4j</groupId>
|
|
||||||
<artifactId>langchain4j-zhipu-ai</artifactId>
|
|
||||||
<version>${langchain4j.version}</version>
|
|
||||||
<exclusions>
|
|
||||||
<exclusion>
|
|
||||||
<artifactId>checker-qual</artifactId>
|
|
||||||
<groupId>org.checkerframework</groupId>
|
|
||||||
</exclusion>
|
|
||||||
<exclusion>
|
|
||||||
<artifactId>guava</artifactId>
|
|
||||||
<groupId>com.google.guava</groupId>
|
|
||||||
</exclusion>
|
|
||||||
</exclusions>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>dev.langchain4j</groupId>
|
|
||||||
<artifactId>langchain4j-qianfan</artifactId>
|
|
||||||
<version>${langchain4j.version}</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>dev.langchain4j</groupId>
|
|
||||||
<artifactId>langchain4j-dashscope</artifactId>
|
|
||||||
<version>${langchain4j.version}</version>
|
|
||||||
<exclusions>
|
|
||||||
<exclusion>
|
|
||||||
<groupId>org.slf4j</groupId>
|
|
||||||
<artifactId>slf4j-simple</artifactId>
|
|
||||||
</exclusion>
|
|
||||||
<exclusion>
|
|
||||||
<artifactId>okio</artifactId>
|
|
||||||
<groupId>com.squareup.okio</groupId>
|
|
||||||
</exclusion>
|
|
||||||
</exclusions>
|
|
||||||
</dependency>
|
|
||||||
<!-- langChain4j vextor support -->
|
|
||||||
<dependency>
|
|
||||||
<groupId>dev.langchain4j</groupId>
|
|
||||||
<artifactId>langchain4j-pgvector</artifactId>
|
|
||||||
<version>${langchain4j.version}</version>
|
|
||||||
</dependency>
|
|
||||||
<!-- langChain4j Document Parser -->
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.apache.tika</groupId>
|
|
||||||
<artifactId>tika-core</artifactId>
|
|
||||||
<version>${apache-tika.version}</version>
|
|
||||||
<exclusions>
|
|
||||||
<exclusion>
|
|
||||||
<artifactId>commons-io</artifactId>
|
|
||||||
<groupId>commons-io</groupId>
|
|
||||||
</exclusion>
|
|
||||||
</exclusions>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.apache.tika</groupId>
|
|
||||||
<artifactId>tika-parser-html-module</artifactId>
|
|
||||||
<version>${apache-tika.version}</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.apache.tika</groupId>
|
|
||||||
<artifactId>tika-parser-pdf-module</artifactId>
|
|
||||||
<version>${apache-tika.version}</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.apache.tika</groupId>
|
|
||||||
<artifactId>tika-parser-text-module</artifactId>
|
|
||||||
<version>${apache-tika.version}</version>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
</dependencies>
|
|
||||||
|
|
||||||
</project>
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
//package org.jeecg;
|
|
||||||
//
|
|
||||||
//import org.springframework.boot.SpringApplication;
|
|
||||||
//import org.springframework.boot.autoconfigure.SpringBootApplication;
|
|
||||||
//
|
|
||||||
//@SpringBootApplication
|
|
||||||
//public class JeecgAiRagApplication {
|
|
||||||
//
|
|
||||||
// public static void main(String[] args) {
|
|
||||||
// SpringApplication.run(JeecgAiRagApplication.class, args);
|
|
||||||
// }
|
|
||||||
//}
|
|
||||||
@ -1,37 +0,0 @@
|
|||||||
package org.jeecg.modules.airag.app.consts;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* AI应用常量类
|
|
||||||
*
|
|
||||||
* @author chenrui
|
|
||||||
* @date 2025/2/25 14:52
|
|
||||||
*/
|
|
||||||
public class AiAppConsts {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 状态:启用
|
|
||||||
*/
|
|
||||||
public static final String STATUS_ENABLE = "enable";
|
|
||||||
/**
|
|
||||||
* 状态:禁用
|
|
||||||
*/
|
|
||||||
public static final String STATUS_DISABLE = "disable";
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 默认应用id
|
|
||||||
*/
|
|
||||||
public static final String DEFAULT_APP_ID = "default";
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 应用类型:简单聊天
|
|
||||||
*/
|
|
||||||
public static final String APP_TYPE_CHAT_SIMPLE = "chatSimple";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 应用类型:聊天流(高级编排)
|
|
||||||
*/
|
|
||||||
public static final String APP_TYPE_CHAT_FLOW = "chatFLow";
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,107 +0,0 @@
|
|||||||
package org.jeecg.modules.airag.app.consts;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @Description: 提示词常量
|
|
||||||
* @Author: chenrui
|
|
||||||
* @Date: 2025/3/12 15:03
|
|
||||||
*/
|
|
||||||
public class Prompts {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 根据提示生成智能体提示词
|
|
||||||
*/
|
|
||||||
public static final String GENERATE_LLM_PROMPT = "# 角色\n" +
|
|
||||||
"你是一位专业且高效的AI提示词工程师,擅长根据用户多样化需求自动生成高质量的结构化提示词模板,具备全面而敏锐的分析能力和出色的创造力。\n" +
|
|
||||||
"## 要求:\n" +
|
|
||||||
"1. \"\"\"只输出提示词,不要输出多余解释\"\"\"\n" +
|
|
||||||
"2. \"\"\"不要在前后增加代码块的md语法.\"\"\"\n" +
|
|
||||||
"2. 贴合用户需求,描述智能助手的定位、能力、知识储备\n" +
|
|
||||||
"3. 提示词应清晰、精确、易于理解,在保持质量的同时,尽可能简洁\n" +
|
|
||||||
"4. 严格按照给定的流程和格式执行任务,确保输出规范准确。\n" +
|
|
||||||
"\n" +
|
|
||||||
"## 流程\n" +
|
|
||||||
"### 1: 需求分析\n" +
|
|
||||||
"1. 当用户描述需求时,严格运用SCQA框架确认核心要素,精准分析和联想:\"当前场景(Situation)是什么?主要矛盾(Complication)有哪些?需要解决的关键问题(Question)是?预期达成什么效果(Answer)?\"\n" +
|
|
||||||
"2. 通过5W1H细致分析和联想细节:\"目标受众(Who)?使用场景(Where/When)?具体要实现什么(What)?为什么需要这些特征(Why)?如何量化效果(How)?\"\n" +
|
|
||||||
"\n" +
|
|
||||||
"### 2: 框架选择\n" +
|
|
||||||
"根据需求从给定模板库中匹配最佳提示词类型:\n" +
|
|
||||||
"* 角色扮演型:\n" +
|
|
||||||
"```\n" +
|
|
||||||
"你将扮演一个人物角色<角色名称>,以下是关于这个角色的详细设定,请根据这些信息来构建你的回答。 \n" +
|
|
||||||
"\n" +
|
|
||||||
"**人物基本信息:**\n" +
|
|
||||||
"- 你是:<角色的名称、身份等基本介绍>\n" +
|
|
||||||
"- 人称:第一人称\n" +
|
|
||||||
"- 出身背景与上下文:<交代角色背景信息和上下文>\n" +
|
|
||||||
"**性格特点:**\n" +
|
|
||||||
"- <性格特点描述>\n" +
|
|
||||||
"**语言风格:**\n" +
|
|
||||||
"- <语言风格描述> \n" +
|
|
||||||
"**人际关系:**\n" +
|
|
||||||
"- <人际关系描述>\n" +
|
|
||||||
"**过往经历:**\n" +
|
|
||||||
"- <过往经历描述>\n" +
|
|
||||||
"**经典台词或口头禅:**\n" +
|
|
||||||
"补充信息: 即你可以将动作、神情语气、心理活动、故事背景放在()中来表示,为对话提供补充信息。\n" +
|
|
||||||
"- 台词1:<角色台词示例1> \n" +
|
|
||||||
"- 台词2:<角色台词示例2>\n" +
|
|
||||||
"- ...\n" +
|
|
||||||
"\n" +
|
|
||||||
"要求: \n" +
|
|
||||||
"- 要求1\n" +
|
|
||||||
"- 要求2\n" +
|
|
||||||
"- ... \n" +
|
|
||||||
"```\n" +
|
|
||||||
"* 多步骤型:\n" +
|
|
||||||
"```\n" +
|
|
||||||
"# 角色 \n" +
|
|
||||||
"你是<角色设定(比如:xx领域的专家)>\n" +
|
|
||||||
"你的目标是<希望模型执行什么任务,达成什么目标>\n" +
|
|
||||||
"\n" +
|
|
||||||
"{#以下可以采用先总括,再展开详细说明的方式,描述你希望智能体在每一个步骤如何进行工作,具体的工作步骤数量可以根据实际需求增删#}\n" +
|
|
||||||
"## 工作步骤 \n" +
|
|
||||||
"1. <工作流程1的一句话概括> \n" +
|
|
||||||
"2. <工作流程2的一句话概括> \n" +
|
|
||||||
"3. <工作流程3的一句话概括>\n" +
|
|
||||||
"\n" +
|
|
||||||
"### 第一步 <工作流程1标题> \n" +
|
|
||||||
"<工作流程步骤1的具体工作要求和举例说明,可以分点列出希望在本步骤做哪些事情,需要完成什么阶段性的工作目标>\n" +
|
|
||||||
"### 第二步 <工作流程2标题> \n" +
|
|
||||||
"<工作流程步骤2的具体工作要求和举例说明,可以分点列出希望在本步骤做哪些事情,需要完成什么阶段性的工作目标>\n" +
|
|
||||||
"### 第三步 <工作流程3标题>\n" +
|
|
||||||
"<工作流程步骤3的具体工作要求和举例说明,可以分点列出希望在本步骤做哪些事情,需要完成什么阶段性的工作目标>\n" +
|
|
||||||
"```\n" +
|
|
||||||
"* 限制性模板:\n" +
|
|
||||||
"```\n" +
|
|
||||||
"# 角色:<角色名称>\n" +
|
|
||||||
"<角色概述和主要职责的一句话描述>\n" +
|
|
||||||
"\n" +
|
|
||||||
"## 目标:\n" +
|
|
||||||
"<角色的工作目标,如果有多目标可以分点列出,但建议更聚焦1-2个目标>\n" +
|
|
||||||
"\n" +
|
|
||||||
"## 技能:\n" +
|
|
||||||
"1. <为了实现目标,角色需要具备的技能1>\n" +
|
|
||||||
"2. <为了实现目标,角色需要具备的技能2>\n" +
|
|
||||||
"3. <为了实现目标,角色需要具备的技能3>\n" +
|
|
||||||
"\n" +
|
|
||||||
"## 工作流:\n" +
|
|
||||||
"1. <描述角色工作流程的第一步>\n" +
|
|
||||||
"2. <描述角色工作流程的第二步>\n" +
|
|
||||||
"3. <描述角色工作流程的第三步>\n" +
|
|
||||||
"\n" +
|
|
||||||
"## 输出格式:\n" +
|
|
||||||
"<如果对角色的输出格式有特定要求,可以在这里强调并举例说明想要的输出格式>\n" +
|
|
||||||
"\n" +
|
|
||||||
"## 限制:\n" +
|
|
||||||
"- <描述角色在互动过程中需要遵循的限制条件1>\n" +
|
|
||||||
"- <描述角色在互动过程中需要遵循的限制条件2>\n" +
|
|
||||||
"- <描述角色在互动过程中需要遵循的限制条件3>\n" +
|
|
||||||
"```\n" +
|
|
||||||
"\n" +
|
|
||||||
"### 3: 生成优化\n" +
|
|
||||||
"1. 输出时自动添加三重保障机制:\n" +
|
|
||||||
" - 反幻觉校验:\"所有数据需标注来源,不确定信息用[需核实]标记\"\n" +
|
|
||||||
" - 风格校准器:\"对比[目标风格]与生成内容的余弦相似度,低于0.7时启动重写\"\n" +
|
|
||||||
" - 伦理审查模块:\"自动过滤涉及隐私/偏见/违法内容,替换为[合规表达]\"";
|
|
||||||
}
|
|
||||||
@ -1,156 +0,0 @@
|
|||||||
package org.jeecg.modules.airag.app.controller;
|
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
|
||||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
|
||||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.jeecg.common.api.vo.Result;
|
|
||||||
import org.jeecg.common.system.base.controller.JeecgController;
|
|
||||||
import org.jeecg.common.system.query.QueryGenerator;
|
|
||||||
import org.jeecg.common.util.AssertUtils;
|
|
||||||
import org.jeecg.config.shiro.IgnoreAuth;
|
|
||||||
import org.jeecg.modules.airag.app.consts.AiAppConsts;
|
|
||||||
import org.jeecg.modules.airag.app.entity.AiragApp;
|
|
||||||
import org.jeecg.modules.airag.app.service.IAiragAppService;
|
|
||||||
import org.jeecg.modules.airag.app.service.IAiragChatService;
|
|
||||||
import org.jeecg.modules.airag.app.vo.AppDebugParams;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.web.bind.annotation.*;
|
|
||||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
|
||||||
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
|
||||||
import java.util.Arrays;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @Description: AI应用
|
|
||||||
* @Author: jeecg-boot
|
|
||||||
* @Date: 2025-02-26
|
|
||||||
* @Version: V1.0
|
|
||||||
*/
|
|
||||||
@RestController
|
|
||||||
@RequestMapping("/airag/app")
|
|
||||||
@Slf4j
|
|
||||||
public class AiragAppController extends JeecgController<AiragApp, IAiragAppService> {
|
|
||||||
@Autowired
|
|
||||||
private IAiragAppService airagAppService;
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private IAiragChatService airagChatService;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 分页列表查询
|
|
||||||
*
|
|
||||||
* @param airagApp
|
|
||||||
* @param pageNo
|
|
||||||
* @param pageSize
|
|
||||||
* @param req
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
@GetMapping(value = "/list")
|
|
||||||
public Result<IPage<AiragApp>> queryPageList(AiragApp airagApp,
|
|
||||||
@RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo,
|
|
||||||
@RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize,
|
|
||||||
HttpServletRequest req) {
|
|
||||||
QueryWrapper<AiragApp> queryWrapper = QueryGenerator.initQueryWrapper(airagApp, req.getParameterMap());
|
|
||||||
Page<AiragApp> page = new Page<AiragApp>(pageNo, pageSize);
|
|
||||||
IPage<AiragApp> pageList = airagAppService.page(page, queryWrapper);
|
|
||||||
return Result.OK(pageList);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 新增或编辑
|
|
||||||
*
|
|
||||||
* @param airagApp
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
@RequestMapping(value = "/edit", method = {RequestMethod.PUT, RequestMethod.POST})
|
|
||||||
public Result<String> edit(@RequestBody AiragApp airagApp) {
|
|
||||||
AssertUtils.assertNotEmpty("参数异常", airagApp);
|
|
||||||
AssertUtils.assertNotEmpty("请输入应用名称", airagApp.getName());
|
|
||||||
AssertUtils.assertNotEmpty("请选择应用类型", airagApp.getType());
|
|
||||||
airagApp.setStatus(AiAppConsts.STATUS_ENABLE);
|
|
||||||
airagAppService.saveOrUpdate(airagApp);
|
|
||||||
return Result.OK("保存完成!", airagApp.getId());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 通过id删除
|
|
||||||
*
|
|
||||||
* @param id
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
@DeleteMapping(value = "/delete")
|
|
||||||
public Result<String> delete(@RequestParam(name = "id", required = true) String id) {
|
|
||||||
airagAppService.removeById(id);
|
|
||||||
return Result.OK("删除成功!");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 批量删除
|
|
||||||
*
|
|
||||||
* @param ids
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
@DeleteMapping(value = "/deleteBatch")
|
|
||||||
public Result<String> deleteBatch(@RequestParam(name = "ids", required = true) String ids) {
|
|
||||||
this.airagAppService.removeByIds(Arrays.asList(ids.split(",")));
|
|
||||||
return Result.OK("批量删除成功!");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 通过id查询
|
|
||||||
*
|
|
||||||
* @param id
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
@IgnoreAuth
|
|
||||||
@GetMapping(value = "/queryById")
|
|
||||||
public Result<AiragApp> queryById(@RequestParam(name = "id", required = true) String id) {
|
|
||||||
AiragApp airagApp = airagAppService.getById(id);
|
|
||||||
if (airagApp == null) {
|
|
||||||
return Result.error("未找到对应数据");
|
|
||||||
}
|
|
||||||
return Result.OK(airagApp);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 调试应用
|
|
||||||
*
|
|
||||||
* @param appDebugParams
|
|
||||||
* @return
|
|
||||||
* @author chenrui
|
|
||||||
* @date 2025/2/28 10:49
|
|
||||||
*/
|
|
||||||
@PostMapping(value = "/debug")
|
|
||||||
public SseEmitter debugApp(@RequestBody AppDebugParams appDebugParams) {
|
|
||||||
return airagChatService.debugApp(appDebugParams);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 根据需求生成提示词
|
|
||||||
*
|
|
||||||
* @param prompt
|
|
||||||
* @return
|
|
||||||
* @author chenrui
|
|
||||||
* @date 2025/3/12 15:30
|
|
||||||
*/
|
|
||||||
@GetMapping(value = "/prompt/generate")
|
|
||||||
public Result<?> generatePrompt(@RequestParam(name = "prompt", required = true) String prompt) {
|
|
||||||
return (Result<?>) airagAppService.generatePrompt(prompt,true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 根据需求生成提示词
|
|
||||||
*
|
|
||||||
* @param prompt
|
|
||||||
* @return
|
|
||||||
* @author chenrui
|
|
||||||
* @date 2025/3/12 15:30
|
|
||||||
*/
|
|
||||||
@PostMapping(value = "/prompt/generate")
|
|
||||||
public SseEmitter generatePromptSse(@RequestParam(name = "prompt", required = true) String prompt) {
|
|
||||||
return (SseEmitter) airagAppService.generatePrompt(prompt,false);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,144 +0,0 @@
|
|||||||
package org.jeecg.modules.airag.app.controller;
|
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.jeecg.common.api.vo.Result;
|
|
||||||
import org.jeecg.config.shiro.IgnoreAuth;
|
|
||||||
import org.jeecg.modules.airag.app.service.IAiragChatService;
|
|
||||||
import org.jeecg.modules.airag.app.vo.ChatConversation;
|
|
||||||
import org.jeecg.modules.airag.app.vo.ChatSendParams;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.web.bind.annotation.*;
|
|
||||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* airag应用-chat
|
|
||||||
*
|
|
||||||
* @Author: chenrui
|
|
||||||
* @Date: 2025-02-25 11:40
|
|
||||||
*/
|
|
||||||
@Slf4j
|
|
||||||
@RestController
|
|
||||||
@RequestMapping("/airag/chat")
|
|
||||||
public class AiragChatController {
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
IAiragChatService chatService;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 发送消息
|
|
||||||
*
|
|
||||||
* @return 返回一个Result对象,表示发送消息的结果
|
|
||||||
* @author chenrui
|
|
||||||
* @date 2025/2/25 11:42
|
|
||||||
*/
|
|
||||||
@IgnoreAuth
|
|
||||||
@PostMapping(value = "/send")
|
|
||||||
public SseEmitter send(@RequestBody ChatSendParams chatSendParams) {
|
|
||||||
return chatService.send(chatSendParams);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 发送消息 <br/>
|
|
||||||
* 兼容旧版浏览器
|
|
||||||
* @param content
|
|
||||||
* @param conversationId
|
|
||||||
* @param topicId
|
|
||||||
* @param appId
|
|
||||||
* @return
|
|
||||||
* @author chenrui
|
|
||||||
* @date 2025/2/25 18:13
|
|
||||||
*/
|
|
||||||
@GetMapping(value = "/send")
|
|
||||||
public SseEmitter sendByGet(@RequestParam("content") String content,
|
|
||||||
@RequestParam(value = "conversationId", required = false) String conversationId,
|
|
||||||
@RequestParam(value = "topicId", required = false) String topicId,
|
|
||||||
@RequestParam(value = "appId", required = false) String appId) {
|
|
||||||
ChatSendParams chatSendParams = new ChatSendParams(content, conversationId, topicId, appId);
|
|
||||||
return chatService.send(chatSendParams);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取所有对话
|
|
||||||
*
|
|
||||||
* @return 返回一个Result对象,包含所有对话的信息
|
|
||||||
* @author chenrui
|
|
||||||
* @date 2025/2/25 11:42
|
|
||||||
*/
|
|
||||||
@IgnoreAuth
|
|
||||||
@GetMapping(value = "/conversations")
|
|
||||||
public Result<?> getConversations(@RequestParam(value = "appId", required = false) String appId) {
|
|
||||||
return chatService.getConversations(appId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 删除会话
|
|
||||||
*
|
|
||||||
* @param id
|
|
||||||
* @return
|
|
||||||
* @author chenrui
|
|
||||||
* @date 2025/3/3 16:55
|
|
||||||
*/
|
|
||||||
@IgnoreAuth
|
|
||||||
@DeleteMapping(value = "/conversation/{id}")
|
|
||||||
public Result<?> deleteConversation(@PathVariable("id") String id) {
|
|
||||||
return chatService.deleteConversation(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新会话标题
|
|
||||||
*
|
|
||||||
* @param updateTitleParams
|
|
||||||
* @return
|
|
||||||
* @author chenrui
|
|
||||||
* @date 2025/3/3 16:55
|
|
||||||
*/
|
|
||||||
@IgnoreAuth
|
|
||||||
@PutMapping(value = "/conversation/update/title")
|
|
||||||
public Result<?> updateConversationTitle(@RequestBody ChatConversation updateTitleParams) {
|
|
||||||
return chatService.updateConversationTitle(updateTitleParams);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取消息
|
|
||||||
*
|
|
||||||
* @return 返回一个Result对象,包含消息的信息
|
|
||||||
* @author chenrui
|
|
||||||
* @date 2025/2/25 11:42
|
|
||||||
*/
|
|
||||||
@IgnoreAuth
|
|
||||||
@GetMapping(value = "/messages")
|
|
||||||
public Result<?> getMessages(@RequestParam(value = "conversationId", required = true) String conversationId) {
|
|
||||||
return chatService.getMessages(conversationId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 清空消息
|
|
||||||
*
|
|
||||||
* @return
|
|
||||||
* @author chenrui
|
|
||||||
* @date 2025/2/25 11:42
|
|
||||||
*/
|
|
||||||
@IgnoreAuth
|
|
||||||
@GetMapping(value = "/messages/clear/{conversationId}")
|
|
||||||
public Result<?> clearMessage(@PathVariable(value = "conversationId") String conversationId) {
|
|
||||||
return chatService.clearMessage(conversationId);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 根据请求ID停止某个请求的处理
|
|
||||||
*
|
|
||||||
* @param requestId 请求的唯一标识符,用于识别和停止特定的请求
|
|
||||||
* @return 返回一个Result对象,表示停止请求的结果
|
|
||||||
* @author chenrui
|
|
||||||
* @date 2025/2/25 11:42
|
|
||||||
*/
|
|
||||||
@IgnoreAuth
|
|
||||||
@GetMapping(value = "/stop/{requestId}")
|
|
||||||
public Result<?> stop(@PathVariable(name = "requestId", required = true) String requestId) {
|
|
||||||
return chatService.stop(requestId);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,190 +0,0 @@
|
|||||||
package org.jeecg.modules.airag.app.entity;
|
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.annotation.IdType;
|
|
||||||
import com.baomidou.mybatisplus.annotation.TableField;
|
|
||||||
import com.baomidou.mybatisplus.annotation.TableId;
|
|
||||||
import com.baomidou.mybatisplus.annotation.TableName;
|
|
||||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
|
||||||
import lombok.Data;
|
|
||||||
import lombok.EqualsAndHashCode;
|
|
||||||
import lombok.experimental.Accessors;
|
|
||||||
import org.jeecg.common.aspect.annotation.Dict;
|
|
||||||
import org.jeecg.common.util.oConvertUtils;
|
|
||||||
import org.jeecgframework.poi.excel.annotation.Excel;
|
|
||||||
import org.springframework.format.annotation.DateTimeFormat;
|
|
||||||
|
|
||||||
import java.io.Serializable;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @Description: AI应用
|
|
||||||
* @Author: jeecg-boot
|
|
||||||
* @Date: 2025-02-26
|
|
||||||
* @Version: V1.0
|
|
||||||
*/
|
|
||||||
@Data
|
|
||||||
@TableName("airag_app")
|
|
||||||
@Accessors(chain = true)
|
|
||||||
@EqualsAndHashCode(callSuper = false)
|
|
||||||
@Schema(description="AI应用")
|
|
||||||
public class AiragApp implements Serializable {
|
|
||||||
private static final long serialVersionUID = 1L;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 主键
|
|
||||||
*/
|
|
||||||
@TableId(type = IdType.ASSIGN_ID)
|
|
||||||
@Schema(description = "主键")
|
|
||||||
private String id;
|
|
||||||
/**
|
|
||||||
* 创建人
|
|
||||||
*/
|
|
||||||
@Schema(description = "创建人")
|
|
||||||
private String createBy;
|
|
||||||
/**
|
|
||||||
* 创建日期
|
|
||||||
*/
|
|
||||||
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
|
|
||||||
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
|
||||||
@Schema(description = "创建日期")
|
|
||||||
private java.util.Date createTime;
|
|
||||||
/**
|
|
||||||
* 更新人
|
|
||||||
*/
|
|
||||||
@Schema(description = "更新人")
|
|
||||||
private String updateBy;
|
|
||||||
/**
|
|
||||||
* 更新日期
|
|
||||||
*/
|
|
||||||
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
|
|
||||||
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
|
||||||
@Schema(description = "更新日期")
|
|
||||||
private java.util.Date updateTime;
|
|
||||||
/**
|
|
||||||
* 所属部门
|
|
||||||
*/
|
|
||||||
@Schema(description = "所属部门")
|
|
||||||
private String sysOrgCode;
|
|
||||||
/**
|
|
||||||
* 租户id
|
|
||||||
*/
|
|
||||||
@Excel(name = "租户id", width = 15)
|
|
||||||
@Schema(description = "租户id")
|
|
||||||
private String tenantId;
|
|
||||||
/**
|
|
||||||
* 应用名称
|
|
||||||
*/
|
|
||||||
@Excel(name = "应用名称", width = 15)
|
|
||||||
@Schema(description = "应用名称")
|
|
||||||
private String name;
|
|
||||||
/**
|
|
||||||
* 应用描述
|
|
||||||
*/
|
|
||||||
@Excel(name = "应用描述", width = 15)
|
|
||||||
@Schema(description = "应用描述")
|
|
||||||
private String descr;
|
|
||||||
/**
|
|
||||||
* 应用图标
|
|
||||||
*/
|
|
||||||
@Excel(name = "应用图标", width = 15)
|
|
||||||
@Schema(description = "应用图标")
|
|
||||||
private String icon;
|
|
||||||
/**
|
|
||||||
* 应用类型
|
|
||||||
*/
|
|
||||||
@Excel(name = "应用类型", width = 15, dicCode = "ai_app_type")
|
|
||||||
@Dict(dicCode = "ai_app_type")
|
|
||||||
@Schema(description = "应用类型")
|
|
||||||
private String type;
|
|
||||||
/**
|
|
||||||
* 开场白
|
|
||||||
*/
|
|
||||||
@Excel(name = "开场白", width = 15)
|
|
||||||
@Schema(description = "开场白")
|
|
||||||
private String prologue;
|
|
||||||
/**
|
|
||||||
* 预设问题
|
|
||||||
*/
|
|
||||||
@Excel(name = "预设问题", width = 15)
|
|
||||||
@Schema(description = "预设问题")
|
|
||||||
private String presetQuestion;
|
|
||||||
/**
|
|
||||||
* 提示词
|
|
||||||
*/
|
|
||||||
@Excel(name = "提示词", width = 15)
|
|
||||||
@Schema(description = "提示词")
|
|
||||||
private String prompt;
|
|
||||||
/**
|
|
||||||
* 模型配置
|
|
||||||
*/
|
|
||||||
@Excel(name = "模型配置", width = 15, dictTable = "airag_model where model_type = 'LLM' ", dicText = "name", dicCode = "id")
|
|
||||||
@Dict(dictTable = "airag_model where model_type = 'LLM' ", dicText = "name", dicCode = "id")
|
|
||||||
@Schema(description = "模型配置")
|
|
||||||
private String modelId;
|
|
||||||
/**
|
|
||||||
* 历史消息数
|
|
||||||
*/
|
|
||||||
@Excel(name = "历史消息数", width = 15)
|
|
||||||
@Schema(description = "历史消息数")
|
|
||||||
private Integer msgNum;
|
|
||||||
/**
|
|
||||||
* 知识库
|
|
||||||
*/
|
|
||||||
@Excel(name = "知识库", width = 15, dictTable = "airag_knowledge where status = 'enable'", dicText = "name", dicCode = "id")
|
|
||||||
@Dict(dictTable = "airag_knowledge where status = 'enable'", dicText = "name", dicCode = "id")
|
|
||||||
@Schema(description = "知识库")
|
|
||||||
private String knowledgeIds;
|
|
||||||
/**
|
|
||||||
* 流程
|
|
||||||
*/
|
|
||||||
@Excel(name = "流程", width = 15, dictTable = "airag_flow where status = 'enable' ", dicText = "name", dicCode = "id")
|
|
||||||
@Dict(dictTable = "airag_flow where status = 'enable' ", dicText = "name", dicCode = "id")
|
|
||||||
@Schema(description = "流程")
|
|
||||||
private String flowId;
|
|
||||||
/**
|
|
||||||
* 快捷指令
|
|
||||||
*/
|
|
||||||
@Excel(name = "快捷指令", width = 15)
|
|
||||||
@Schema(description = "快捷指令")
|
|
||||||
private String quickCommand;
|
|
||||||
/**
|
|
||||||
* 状态
|
|
||||||
*/
|
|
||||||
@Excel(name = "状态", width = 15)
|
|
||||||
@Schema(description = "状态")
|
|
||||||
private String status;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 元数据
|
|
||||||
*/
|
|
||||||
@Excel(name = "元数据", width = 15)
|
|
||||||
@Schema(description = "元数据")
|
|
||||||
private String metadata;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 知识库ids
|
|
||||||
*/
|
|
||||||
@TableField(exist = false)
|
|
||||||
private List<String> knowIds;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取知识库id
|
|
||||||
*
|
|
||||||
* @return
|
|
||||||
* @author chenrui
|
|
||||||
* @date 2025/2/28 11:45
|
|
||||||
*/
|
|
||||||
public List<String> getKnowIds() {
|
|
||||||
if (oConvertUtils.isNotEmpty(knowledgeIds)) {
|
|
||||||
String[] knowIds = knowledgeIds.split(",");
|
|
||||||
return Arrays.asList(knowIds);
|
|
||||||
} else {
|
|
||||||
return new ArrayList<>(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
package org.jeecg.modules.airag.app.mapper;
|
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
|
||||||
import org.jeecg.modules.airag.app.entity.AiragApp;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @Description: AI应用
|
|
||||||
* @Author: jeecg-boot
|
|
||||||
* @Date: 2025-02-26
|
|
||||||
* @Version: V1.0
|
|
||||||
*/
|
|
||||||
public interface AiragAppMapper extends BaseMapper<AiragApp> {
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
|
||||||
<mapper namespace="org.jeecg.modules.airag.app.mapper.AiragAppMapper">
|
|
||||||
|
|
||||||
</mapper>
|
|
||||||
@ -1,24 +0,0 @@
|
|||||||
package org.jeecg.modules.airag.app.service;
|
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.extension.service.IService;
|
|
||||||
import org.jeecg.common.api.vo.Result;
|
|
||||||
import org.jeecg.modules.airag.app.entity.AiragApp;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @Description: AI应用
|
|
||||||
* @Author: jeecg-boot
|
|
||||||
* @Date: 2025-02-26
|
|
||||||
* @Version: V1.0
|
|
||||||
*/
|
|
||||||
public interface IAiragAppService extends IService<AiragApp> {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 生成提示词
|
|
||||||
* @param prompt
|
|
||||||
* @return blocking 是否阻塞
|
|
||||||
* @return
|
|
||||||
* @author chenrui
|
|
||||||
* @date 2025/3/12 14:45
|
|
||||||
*/
|
|
||||||
Object generatePrompt(String prompt,boolean blocking);
|
|
||||||
}
|
|
||||||
@ -1,95 +0,0 @@
|
|||||||
package org.jeecg.modules.airag.app.service;
|
|
||||||
|
|
||||||
import org.jeecg.common.api.vo.Result;
|
|
||||||
import org.jeecg.modules.airag.app.vo.AppDebugParams;
|
|
||||||
import org.jeecg.modules.airag.app.vo.ChatConversation;
|
|
||||||
import org.jeecg.modules.airag.app.vo.ChatSendParams;
|
|
||||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ai聊天
|
|
||||||
*
|
|
||||||
* @author chenrui
|
|
||||||
* @date 2025/2/25 13:36
|
|
||||||
*/
|
|
||||||
public interface IAiragChatService {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 发送消息
|
|
||||||
*
|
|
||||||
* @param chatSendParams
|
|
||||||
* @return
|
|
||||||
* @author chenrui
|
|
||||||
* @date 2025/2/25 13:39
|
|
||||||
*/
|
|
||||||
SseEmitter send(ChatSendParams chatSendParams);
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 调试应用
|
|
||||||
*
|
|
||||||
* @param appDebugParams
|
|
||||||
* @return
|
|
||||||
* @author chenrui
|
|
||||||
* @date 2025/2/28 10:49
|
|
||||||
*/
|
|
||||||
SseEmitter debugApp(AppDebugParams appDebugParams);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 停止响应
|
|
||||||
*
|
|
||||||
* @param requestId
|
|
||||||
* @return
|
|
||||||
* @author chenrui
|
|
||||||
* @date 2025/2/25 17:17
|
|
||||||
*/
|
|
||||||
Result<?> stop(String requestId);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取所有对话
|
|
||||||
*
|
|
||||||
* @param appId
|
|
||||||
* @return
|
|
||||||
* @author chenrui
|
|
||||||
* @date 2025/2/26 14:48
|
|
||||||
*/
|
|
||||||
Result<?> getConversations(String appId);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取对话聊天记录
|
|
||||||
*
|
|
||||||
* @param conversationId
|
|
||||||
* @return
|
|
||||||
* @author chenrui
|
|
||||||
* @date 2025/2/26 15:16
|
|
||||||
*/
|
|
||||||
Result<?> getMessages(String conversationId);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 删除会话
|
|
||||||
*
|
|
||||||
* @param conversationId
|
|
||||||
* @return
|
|
||||||
* @author chenrui
|
|
||||||
* @date 2025/3/3 16:55
|
|
||||||
*/
|
|
||||||
Result<?> deleteConversation(String conversationId);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新会话标题
|
|
||||||
* @param updateTitleParams
|
|
||||||
* @return
|
|
||||||
* @author chenrui
|
|
||||||
* @date 2025/3/3 17:02
|
|
||||||
*/
|
|
||||||
Result<?> updateConversationTitle(ChatConversation updateTitleParams);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 清空消息
|
|
||||||
* @param conversationId
|
|
||||||
* @return
|
|
||||||
* @author chenrui
|
|
||||||
* @date 2025/3/3 19:49
|
|
||||||
*/
|
|
||||||
Result<?> clearMessage(String conversationId);
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user