mirror of
https://github.com/jeecgboot/JeecgBoot.git
synced 2025-12-08 17:12:28 +08:00
Compare commits
402 Commits
v3.8.2last
...
v3.8.3spri
| Author | SHA1 | Date | |
|---|---|---|---|
| d988a637b2 | |||
| 33cbc6e25b | |||
| d715c7a0ac | |||
| aca407e1ce | |||
| cfea79a187 | |||
| 7848d1fb33 | |||
| 91fa645878 | |||
| 5bcb649384 | |||
| c9fc948658 | |||
| b97d041e7f | |||
| 6492f2c99a | |||
| bf32385a06 | |||
| 6ef637c46f | |||
| bc6f336745 | |||
| 0d86df8e9e | |||
| 3db673b67d | |||
| 3ba5395d33 | |||
| e7eed37470 | |||
| 30ac3f7c72 | |||
| 03e6c97d80 | |||
| b9f6f6dc53 | |||
| 107e13c8af | |||
| 0512b41b2b | |||
| d6d880f887 | |||
| d02753ce50 | |||
| b0e974a418 | |||
| 388fa9b8c2 | |||
| bc04bd1433 | |||
| 35aba0784d | |||
| c3822ab702 | |||
| d4487356f0 | |||
| 0bfe5689cc | |||
| ae4363dc72 | |||
| 3e6c7651ee | |||
| c0ffd14b7a | |||
| 914875d6a1 | |||
| aa55c3a86c | |||
| 2298ee3eed | |||
| 2a8853b353 | |||
| b920c5b794 | |||
| d3fa38a9e6 | |||
| b0df78b06c | |||
| 6a6d29254b | |||
| 80749098bd | |||
| 19b7f2cb29 | |||
| 39f5c3a5be | |||
| 9ee3a36fbb | |||
| 8c5cf3a0d9 | |||
| 053552c123 | |||
| fc44deca83 | |||
| 5d5d9fc53d | |||
| 002bfe25f8 | |||
| 7cb2dc4fde | |||
| 9c244bd266 | |||
| ab151879b3 | |||
| 000ae1db30 | |||
| 63e066180d | |||
| 44c1079f87 | |||
| e825e0f912 | |||
| 132e89b0e1 | |||
| 881a637285 | |||
| 02e9f8984f | |||
| a4343fc2cb | |||
| 152e8c7aaa | |||
| d7dc81455d | |||
| aefdcd6315 | |||
| 1cf4054e76 | |||
| 7829cf18d7 | |||
| 69c3a9da9a | |||
| 4d34150479 | |||
| 29687c8908 | |||
| 2e93a92dde | |||
| d383f7458d | |||
| 700318e1c1 | |||
| d728d6b090 | |||
| 7abc2e4c9c | |||
| da2b0cc354 | |||
| a6751c22be | |||
| f087525a75 | |||
| 4f46213df6 | |||
| d76842ae07 | |||
| 8c64db46e5 | |||
| 81fb2ac3b2 | |||
| fa98817aeb | |||
| 1158977826 | |||
| 8b6def0ee3 | |||
| 39c0d5b3f5 | |||
| adeebee840 | |||
| 862aaa8632 | |||
| 6a11ff8a64 | |||
| 73059b8a53 | |||
| e377bf6990 | |||
| 434b42e9ed | |||
| f1ceb08e16 | |||
| d2eedacc85 | |||
| 8791384791 | |||
| b67770ff14 | |||
| 1dae808cf1 | |||
| 70d8353219 | |||
| 3e208de18e | |||
| c4a87d7c58 | |||
| 56976e68b4 | |||
| db1ff0268b | |||
| 47107a53c2 | |||
| 08f245bdf9 | |||
| 52cd43d17c | |||
| 8cc033b86f | |||
| 7ff70930ef | |||
| 45f83dff43 | |||
| 6b7542620b | |||
| fcbaed2f7f | |||
| 67d9865861 | |||
| 4acd5b3464 | |||
| cd809a6573 | |||
| e10df20e98 | |||
| ac446691c4 | |||
| d0f85ccc7e | |||
| 0feb307e8d | |||
| 781d61e96e | |||
| d12fe9d8de | |||
| 223d374edd | |||
| e795e03365 | |||
| 7f011f199e | |||
| 873b8bd823 | |||
| 59ece16059 | |||
| 91208a4968 | |||
| 2f3aff9584 | |||
| 1f74cb538f | |||
| 128c2c97f6 | |||
| 424dc33bba | |||
| 2cce917ab2 | |||
| 1cb48b4f0c | |||
| fd320be203 | |||
| 79b182819b | |||
| 6e5c0eebce | |||
| 621781d336 | |||
| cae8e7e8d2 | |||
| e70844ce61 | |||
| e00ffa2670 | |||
| ada97acb04 | |||
| 6c15b45a8c | |||
| 01547ef83e | |||
| f67cfa1bfb | |||
| 8d91caa4e6 | |||
| b3ed442b55 | |||
| 0d9f9a04cc | |||
| 9cca8f8719 | |||
| 90565fcf79 | |||
| ae6069882f | |||
| cf4d888839 | |||
| f0a5edaa49 | |||
| f510578cb7 | |||
| ab98b26cac | |||
| 2d7c51eadc | |||
| 216d4b9a1f | |||
| 5972c74b43 | |||
| 64b3c9e42e | |||
| 8eb81493ce | |||
| d69cb121fc | |||
| 044fc47586 | |||
| 10a9edd10b | |||
| bbe18c582c | |||
| c71ff3fbcc | |||
| b894125b53 | |||
| 08612d5bfa | |||
| 2ecce8f02d | |||
| 62937f14fb | |||
| d6ccc4a326 | |||
| 1893108136 | |||
| 7980915bdc | |||
| 550997268b | |||
| 2e236703b2 | |||
| a771d24a57 | |||
| 69f5d12de7 | |||
| 5b5999e786 | |||
| 9e7d40a080 | |||
| fc3fe39d95 | |||
| 2c38db456b | |||
| 48e23aafab | |||
| e52538d304 | |||
| e91cbd5cd8 | |||
| 70cec8b5c6 | |||
| d2365088ce | |||
| a679571a5a | |||
| b9c74e549f | |||
| 81c1724016 | |||
| 56d59eb589 | |||
| a00fcae3a3 | |||
| 286d10a50f | |||
| 68f36cb1e5 | |||
| 78454d3434 | |||
| 56fbc2ed8f | |||
| 197d7adaaf | |||
| e952518d71 | |||
| 1e259c805e | |||
| 8a82141c95 | |||
| 888a032266 | |||
| 309c76d268 | |||
| f78eabfc66 | |||
| 748331d649 | |||
| b70e709e53 | |||
| 2ba17648c4 | |||
| 36caab37e2 | |||
| 6e721e4120 | |||
| a17b403675 | |||
| 632fd72d79 | |||
| 15fc262675 | |||
| 6768d65e1e | |||
| 657b84d3cf | |||
| 2021bf39f8 | |||
| 410ab7bcc3 | |||
| 174f1ae432 | |||
| fdeb37c3d0 | |||
| f9123208e1 | |||
| eef2f7e269 | |||
| 6a0ec66d3d | |||
| 163b0b531f | |||
| accb8f2f9f | |||
| d1af49a33f | |||
| c643994546 | |||
| 03265691e6 | |||
| 6934a0adee | |||
| de9cc2f30d | |||
| 93e32a7177 | |||
| 26887959cd | |||
| c9f5bb4409 | |||
| 7e15e81218 | |||
| 10b68858d6 | |||
| 8b0e0367c7 | |||
| da72e8f9c5 | |||
| 334f7dbb62 | |||
| e9ddd21286 | |||
| 73e86686dc | |||
| 458526075e | |||
| a1b55f0d40 | |||
| f43d0d486b | |||
| 65bde3331b | |||
| 2f0a3bcd87 | |||
| b60942aa86 | |||
| 197b267e71 | |||
| 30d3a9f17b | |||
| 03739f2837 | |||
| 79f7134bd5 | |||
| 6d432bc186 | |||
| 415307eb9f | |||
| 48e20b2af5 | |||
| b7924b9ca8 | |||
| a10a2e0a9d | |||
| 4aa88189ed | |||
| fdb05443c2 | |||
| d9e8bd2bc8 | |||
| 81eef5a838 | |||
| f528f72903 | |||
| 918286c144 | |||
| 512234a804 | |||
| 65d737db6d | |||
| f04f7f9abf | |||
| 935e118d15 | |||
| e218367332 | |||
| 3a3f3cf367 | |||
| 0e762b4157 | |||
| f4712baa39 | |||
| 7d8b653d6e | |||
| cf7f3f94be | |||
| 49f63b92ac | |||
| 5670a15b20 | |||
| cacc59b8fd | |||
| c744633139 | |||
| 9e9ef20b7c | |||
| 0c034031d1 | |||
| 491a038b5a | |||
| 8a4fcb0023 | |||
| 0e4d304878 | |||
| 17a8964487 | |||
| e93dcc1a7e | |||
| 383cbf250f | |||
| 9fe1450ac9 | |||
| 88b9b12998 | |||
| 9e25566271 | |||
| 8e54e06978 | |||
| 8ac6989d2c | |||
| e5c082ae13 | |||
| 96ab98ac3e | |||
| 402ab0ffc4 | |||
| 7778ede90e | |||
| 1632c241ee | |||
| 06144206df | |||
| e9d05b0e75 | |||
| 6ade7e22f8 | |||
| 43d47c08cb | |||
| 3d3b5850ad | |||
| 816eeb9225 | |||
| 0b42efbbbf | |||
| e616c5d8fe | |||
| cddf23c787 | |||
| 70a37309dd | |||
| 48555b5219 | |||
| 06d58f202f | |||
| 628870af9b | |||
| b8e0d4391d | |||
| 72b34d082b | |||
| b46a6438e6 | |||
| 5488f99723 | |||
| 6bc1fe8d21 | |||
| 7cac16320c | |||
| 24dbd1db39 | |||
| 7112649a21 | |||
| fbc312c35d | |||
| b8162a4a6d | |||
| 28404d2fd3 | |||
| 46b026b989 | |||
| 94c45f5e0f | |||
| c92c9be49a | |||
| 58e85e0569 | |||
| 6fc34d8a39 | |||
| 790df934b5 | |||
| 8aee4011a2 | |||
| 8950e19d4e | |||
| 99eb88f71c | |||
| 6e0277c60a | |||
| e923654161 | |||
| 06b41ae479 | |||
| 824d7839d8 | |||
| c88f9d95d4 | |||
| beb0bc2f64 | |||
| f741db874c | |||
| d684c09392 | |||
| 364be22dd0 | |||
| 11af85d87a | |||
| 4caff75cce | |||
| 20efa3bf9a | |||
| c7977dda3d | |||
| 811861a957 | |||
| 24623ba4b0 | |||
| 7c68b46943 | |||
| c27c5a9a9b | |||
| 0ab280f812 | |||
| c3066dac17 | |||
| b650d512b3 | |||
| 7c34161369 | |||
| bc52aa918d | |||
| 925ec9447d | |||
| 411a73c1bf | |||
| 84077e6e24 | |||
| 184cf97304 | |||
| 9dfdd47b36 | |||
| 272a7540eb | |||
| ad796f079f | |||
| e7e7716d05 | |||
| 5f425b49b2 | |||
| 3ac8ee304a | |||
| c5d620d2b2 | |||
| cdea05ebb0 | |||
| ca9a433f3c | |||
| 2be6052cd4 | |||
| 68ed67ee49 | |||
| d5903ba52a | |||
| 3ee635eddf | |||
| 21bc68fb53 | |||
| 0faac01bb7 | |||
| 74d88a8fcc | |||
| f532e57862 | |||
| da08adbea1 | |||
| 46e3e62b59 | |||
| 3656264f8a | |||
| 3361d48cd4 | |||
| ed86ea3da1 | |||
| 3deb0e5487 | |||
| 9e4792941e | |||
| b5fd5fe782 | |||
| 33c0104a02 | |||
| 81ed5100af | |||
| 87f9dc0064 | |||
| b311fedc6b | |||
| e321a0405f | |||
| d8bc74794d | |||
| 732f05dc74 | |||
| 6ce92798c6 | |||
| f4454e9348 | |||
| d9134ae0c8 | |||
| 25180e41c8 | |||
| a99e3f2268 | |||
| d27c354bf1 | |||
| d818b1dd9d | |||
| bcdbec0091 | |||
| 098bb12b9e | |||
| 4a6c750b19 | |||
| d396e5304a | |||
| 9bed25be8c | |||
| 7109b42092 | |||
| 1667b14194 | |||
| e9514873d2 | |||
| 0ee090664e | |||
| 4a9eda4ab0 | |||
| 2416c8b251 | |||
| 5b056f9dd6 | |||
| a93998dc56 | |||
| 268c27a782 | |||
| 23ace2712a | |||
| 157feeb925 | |||
| 4e25d4162f | |||
| 47a68f31e1 |
3
.github/ISSUE_TEMPLATE/bug_report.md
vendored
3
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -10,6 +10,9 @@ assignees: getActivity
|
|||||||
##### 版本号:
|
##### 版本号:
|
||||||
|
|
||||||
|
|
||||||
|
##### 分支:
|
||||||
|
|
||||||
|
|
||||||
##### 问题描述:
|
##### 问题描述:
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
4
.github/ISSUE_TEMPLATE/feature_request.md
vendored
4
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@ -6,10 +6,12 @@ assignees: getActivity
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
||||||
##### 版本号:
|
##### 版本号:
|
||||||
|
|
||||||
|
|
||||||
|
##### 分支:
|
||||||
|
|
||||||
|
|
||||||
##### 问题描述:
|
##### 问题描述:
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
3
.gitignore
vendored
3
.gitignore
vendored
@ -13,3 +13,6 @@ os_del.cmd
|
|||||||
os_del_doc.cmd
|
os_del_doc.cmd
|
||||||
.svn
|
.svn
|
||||||
derby.log
|
derby.log
|
||||||
|
*.log
|
||||||
|
.cursor
|
||||||
|
.history
|
||||||
@ -7,7 +7,7 @@
|
|||||||
JEECG BOOT AI Low Code Platform
|
JEECG BOOT AI Low Code Platform
|
||||||
===============
|
===============
|
||||||
|
|
||||||
Current version: 3.8.2 (Release date: 2025-08-04)
|
Current version: 3.8.3 (Release date: 2025-10-09)
|
||||||
|
|
||||||
|
|
||||||
[](https://github.com/zhangdaiscott/jeecg-boot/blob/master/LICENSE)
|
[](https://github.com/zhangdaiscott/jeecg-boot/blob/master/LICENSE)
|
||||||
|
|||||||
54
README.md
54
README.md
@ -2,13 +2,13 @@
|
|||||||
JeecgBoot AI低代码平台
|
JeecgBoot AI低代码平台
|
||||||
===============
|
===============
|
||||||
|
|
||||||
当前最新版本: 3.8.2(发布日期:2025-08-04)
|
当前最新版本: 3.8.3(发布日期:2025-10-09)
|
||||||
|
|
||||||
|
|
||||||
[](https://github.com/jeecgboot/JeecgBoot/blob/master/LICENSE)
|
[](https://github.com/jeecgboot/JeecgBoot/blob/master/LICENSE)
|
||||||
[](https://jeecg.com)
|
[](https://jeecg.com)
|
||||||
[](https://jeecg.blog.csdn.net)
|
[](https://jeecg.blog.csdn.net)
|
||||||
[](https://github.com/jeecgboot/JeecgBoot)
|
[](https://github.com/jeecgboot/JeecgBoot)
|
||||||
[](https://github.com/jeecgboot/JeecgBoot)
|
[](https://github.com/jeecgboot/JeecgBoot)
|
||||||
[](https://github.com/jeecgboot/JeecgBoot)
|
[](https://github.com/jeecgboot/JeecgBoot)
|
||||||
|
|
||||||
@ -19,16 +19,18 @@ JeecgBoot AI低代码平台
|
|||||||
|
|
||||||
<h3 align="center">企业级AI低代码平台</h3>
|
<h3 align="center">企业级AI低代码平台</h3>
|
||||||
|
|
||||||
JeecgBoot是一款企业级低代码平台集成了AI应用平台功能,旨在帮助开发者快速实现低代码开发和构建、部署个性化的 AI 应用。
|
JeecgBoot 是一款基于BPM流程和代码生成的AI低代码平台,助力企业快速实现低代码开发和构建AI应用。
|
||||||
前后端分离架构Ant Design4、Vue3,SpringBoot2/3,SpringCloud Alibaba,Mybatis-plus,Shiro/SpringAuthorizationServer,强大的代码生成器让前后端代码一键生成,无需写任何代码;提供强大的报表和大屏工具,满足企业级数据产品需求!
|
采用前后端分离架构(Ant Design&Vue3,SpringBoot3,SpringCloud Alibaba,Mybatis-plus),强大代码生成器实现前后端一键生成,无需手写代码。
|
||||||
引领AI低代码开发模式: AI生成->OnlineCoding-> 代码生成-> 手工MERGE, 帮助Java项目解决80%的重复工作,让开发更多关注业务,提高效率、节省成本,同时又不失灵活性!低代码能力:Online表单、表单设计、流程设计、Online报表、大屏/仪表盘设计、报表设计; AI应用平台功能:AI知识库问答、AI模型管理、AI流程编排、AI聊天等,支持含ChatGPT、DeepSeek、Ollama等多种AI大模型
|
平台引领AI低代码开发模式:AI生成→在线编码→代码生成→手工合并,解决Java项目80%重复工作,提升效率,节省成本,兼顾灵活性。
|
||||||
|
具备强大且颗粒化的权限控制,支持按钮权限和数据权限设置,满足大型业务系统需求。功能涵盖在线表单、表单设计、流程设计、门户设计、报表与大屏设计、OA办公、AI应用、AI知识库、大模型管理、AI流程编排、AI聊天,支持ChatGPT、DeepSeek、Ollama等多种AI大模型。
|
||||||
|
|
||||||
|
`AI赋能报表:` 积木报表是一款自主研发的强大开源企业级Web报表与大屏工具。它通过零编码的拖拽式操作,赋能用户如同搭积木般轻松构建各类复杂报表和数据大屏,全面满足企业数据可视化与分析需求,助力企业级数据产品的高效打造与应用。
|
||||||
|
|
||||||
`AI赋能低代码:` 提供一套成熟AI应用平台功能:包含AI应用管理、AI模型管理、AI对话助手、AI知识库问答、AI流程编排、AI流程设计器,AI建表等功能; 支持各种AI大模型ChatGPT、DeepSeek、Ollama、智普、千问等.
|
`AI赋能低代码:` 提供完善成熟的AI应用平台,涵盖AI应用管理、AI模型管理、智能对话助手、知识库问答、流程编排与设计器、AI建表等多项功能。平台兼容多种主流大模型,包括ChatGPT、DeepSeek、Ollama、智普、千问等,助力企业高效构建智能化应用,推动低代码开发与AI深度融合。
|
||||||
|
|
||||||
`JEECG宗旨是:` 简单功能由OnlineCoding零代码搭建,做到`零代码开发`;复杂功能由代码生成器生成进行手工Merge 实现`低代码开发`,既保证了`智能`又兼顾`灵活`,解决了当前低代码产品普遍不灵活的弊端!
|
`JEECG宗旨是:` JEECG旨在通过OnlineCoding平台实现简单功能的零代码快速搭建,同时针对复杂功能采用代码生成器生成代码并手工合并,打造智能且灵活的低代码开发模式,有效解决了当前低代码产品普遍缺乏灵活性的问题,提升开发效率的同时兼顾系统的扩展性和定制化能力。
|
||||||
|
|
||||||
`JEECG业务流程:` 采用工作流来实现、扩展出任务接口,供开发编写业务逻辑,表单提供多种解决方案: 表单设计器、online配置表单、编码表单。同时实现了流程与表单的分离设计(松耦合)、并支持任务节点灵活配置,既保证了公司流程的保密性,又减少了开发人员的工作量。
|
`JEECG业务流程:` JEECG业务流程采用BPM工作流引擎实现业务审批,扩展任务接口供开发人员编写业务逻辑,表单提供表单设计器、在线配置表单和编码表单等多种解决方案。通过流程与表单的分离设计(松耦合)及任务节点的灵活配置,既保障了企业流程的安全性与保密性,又大幅降低了开发人员的工作量。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -36,8 +38,8 @@ JeecgBoot是一款企业级低代码平台集成了AI应用平台功能,旨在
|
|||||||
|
|
||||||
适用项目
|
适用项目
|
||||||
-----------------------------------
|
-----------------------------------
|
||||||
JeecgBoot低代码平台,可以应用在任何J2EE项目的开发中,支持信创国产化。尤其适合SAAS项目、企业信息管理系统(MIS)、内部办公系统(OA)、企业资源计划系统(ERP)、客户关系管理系统(CRM)、AI知识库等,其半智能手工Merge的开发方式,可以显著提高开发效率70%以上,极大降低开发成本。
|
JeecgBoot低代码平台兼容所有J2EE项目开发,支持信创国产化,特别适用于SAAS、企业信息管理系统(MIS)、内部办公系统(OA)、企业资源计划系统(ERP)、客户关系管理系统(CRM)及AI知识库等场景。其半智能手工Merge开发模式,可显著提升70%以上的开发效率,极大降低开发成本。同时,JeecgBoot还是一款全栈式AI开发平台,助力企业快速构建和部署个性化AI应用。。
|
||||||
又是一个全栈式 AI 开发平台,快速帮助企业构建和部署个性化的 AI 应用。
|
|
||||||
|
|
||||||
**信创兼容说明**
|
**信创兼容说明**
|
||||||
- 操作系统:国产麒麟、银河麒麟等国产系统几乎都是基于 Linux 内核,因此它们具有良好的兼容性。
|
- 操作系统:国产麒麟、银河麒麟等国产系统几乎都是基于 Linux 内核,因此它们具有良好的兼容性。
|
||||||
@ -48,13 +50,13 @@ JeecgBoot低代码平台,可以应用在任何J2EE项目的开发中,支持
|
|||||||
版本说明
|
版本说明
|
||||||
-----------------------------------
|
-----------------------------------
|
||||||
|
|
||||||
|下载 | JDK17 + SpringBoot3.3 + Shiro |JDK17 + SpringBoot3.3+ SpringAuthorizationServer | JDK17/JDK8 + SpringBoot2.7 |
|
|下载 | SpringBoot3.5 + Shiro |SpringBoot3.5+ SpringAuthorizationServer | SpringBoot3.5 + Sa-Token | SpringBoot2.7(JDK17/JDK8) |
|
||||||
|------|----------------------------------------------------|-----------------------------------------------------------------------------|--------------------------------------------|
|
|------|----------------|----------------------------|-------------------|--------------------------------------------|
|
||||||
| Github | [`springboot3`](https://github.com/jeecgboot/JeecgBoot/tree/springboot3) | [`springboot3_sas`](https://github.com/jeecgboot/JeecgBoot/tree/springboot3_sas) 分支 |[`master`](https://github.com/jeecgboot/JeecgBoot) 分支|
|
| Github | [`springboot3`](https://github.com/jeecgboot/JeecgBoot/tree/springboot3) | [`springboot3_sas`](https://github.com/jeecgboot/JeecgBoot/tree/springboot3_sas) 分支 | [`springboot3-satoken`](https://github.com/jeecgboot/JeecgBoot/tree/springboot3-satoken) 分支|[`master`](https://github.com/jeecgboot/JeecgBoot) 分支|
|
||||||
| Gitee | [`springboot3`](https://gitee.com/jeecg/JeecgBoot/tree/springboot3/) | [`springboot3_sas`](https://gitee.com/jeecg/JeecgBoot/tree/springboot3_sas) 分支 |[`master`](https://gitee.com/jeecg/JeecgBoot) 分支 |
|
| Gitee | [`springboot3`](https://gitee.com/jeecg/JeecgBoot/tree/springboot3/) | [`springboot3_sas`](https://gitee.com/jeecg/JeecgBoot/tree/springboot3_sas) 分支| [`springboot3-satoken`](https://gitee.com/jeecg/JeecgBoot/tree/springboot3-satoken) 分支|[`master`](https://gitee.com/jeecg/JeecgBoot) 分支 |
|
||||||
|
|
||||||
|
|
||||||
- `jeecg-boot` 是后端JAVA源码项目(支持单体和微服务切换).
|
- `jeecg-boot` 是后端JAVA源码项目Springboot3+SpringCloudAlibaba(支持单体和微服务切换).
|
||||||
- `jeecgboot-vue3` 是前端VUE3源码项目(vue3+vite6+ts最新技术栈).
|
- `jeecgboot-vue3` 是前端VUE3源码项目(vue3+vite6+ts最新技术栈).
|
||||||
- `JeecgUniapp` 是[配套APP框架](https://github.com/jeecgboot/JeecgUniapp) 适配多个终端,支持APP、小程序、H5、鸿蒙、鸿蒙Next.
|
- `JeecgUniapp` 是[配套APP框架](https://github.com/jeecgboot/JeecgUniapp) 适配多个终端,支持APP、小程序、H5、鸿蒙、鸿蒙Next.
|
||||||
- 参考 [文档](https://help.jeecg.com/ui/2dev/mini) 可以删除不需要的demo,制作一个精简版本
|
- 参考 [文档](https://help.jeecg.com/ui/2dev/mini) 可以删除不需要的demo,制作一个精简版本
|
||||||
@ -81,6 +83,7 @@ JeecgBoot低代码平台,可以应用在任何J2EE项目的开发中,支持
|
|||||||
- 官方网站: [http://www.jeecg.com](http://www.jeecg.com)
|
- 官方网站: [http://www.jeecg.com](http://www.jeecg.com)
|
||||||
- 在线演示: [平台演示](https://boot3.jeecg.com) | [APP演示](https://jeecg.com/appIndex)
|
- 在线演示: [平台演示](https://boot3.jeecg.com) | [APP演示](https://jeecg.com/appIndex)
|
||||||
- 入门指南: [快速入门](http://www.jeecg.com/doc/quickstart) | [代码生成使用](https://help.jeecg.com/java/codegen/online) | [开发文档](https://help.jeecg.com) | [AI应用手册](https://help.jeecg.com/aigc) | [视频教程](http://jeecg.com/doc/video)
|
- 入门指南: [快速入门](http://www.jeecg.com/doc/quickstart) | [代码生成使用](https://help.jeecg.com/java/codegen/online) | [开发文档](https://help.jeecg.com) | [AI应用手册](https://help.jeecg.com/aigc) | [视频教程](http://jeecg.com/doc/video)
|
||||||
|
- AI编程实战视频: [JEECG低代码与Cursor+GitHub Copilot实现AI高效编程实战](https://www.bilibili.com/video/BV11XyaBVEoH)
|
||||||
- 技术支持: [反馈问题](https://github.com/jeecgboot/JeecgBoot/issues/new?template=bug_report.md) | [低代码体验一分钟](https://jeecg.blog.csdn.net/article/details/106079007)
|
- 技术支持: [反馈问题](https://github.com/jeecgboot/JeecgBoot/issues/new?template=bug_report.md) | [低代码体验一分钟](https://jeecg.blog.csdn.net/article/details/106079007)
|
||||||
- QQ交流群 : 964611995、⑩716488839(满)、⑨808791225(满)、其他(满)
|
- QQ交流群 : 964611995、⑩716488839(满)、⑨808791225(满)、其他(满)
|
||||||
|
|
||||||
@ -102,7 +105,7 @@ JeecgBoot平台提供了一套完善的AI应用管理系统模块,是一套类
|
|||||||
|
|
||||||
为什么选择JeecgBoot?
|
为什么选择JeecgBoot?
|
||||||
-----------------------------------
|
-----------------------------------
|
||||||
- 1.采用最新主流前后分离框架(Spring Boot3 + MyBatis + Ant Design4 + Vue3),容易上手;代码生成器依赖性低,灵活的扩展能力,可快速实现二次开发。
|
- 1.采用最新主流前后分离框架(Spring Boot3 + MyBatis + Shiro/SpringAuthorizationServer + Ant Design4 + Vue3),容易上手;代码生成器依赖性低,灵活的扩展能力,可快速实现二次开发。
|
||||||
- 2.前端大版本换代,最新版采用 Vue3.0 + TypeScript + Vite6 + Ant Design Vue4 等新技术方案。
|
- 2.前端大版本换代,最新版采用 Vue3.0 + TypeScript + Vite6 + Ant Design Vue4 等新技术方案。
|
||||||
- 3.支持微服务Spring Cloud Alibaba(Nacos、Gateway、Sentinel、Skywalking),提供简易机制,支持单体和微服务自由切换(这样可以满足各类项目需求)。
|
- 3.支持微服务Spring Cloud Alibaba(Nacos、Gateway、Sentinel、Skywalking),提供简易机制,支持单体和微服务自由切换(这样可以满足各类项目需求)。
|
||||||
- 4.开发效率高,支持在线建表和AI建表,提供强大代码生成器,单表、树列表、一对多、一对一等数据模型,增删改查功能一键生成,菜单配置直接使用。
|
- 4.开发效率高,支持在线建表和AI建表,提供强大代码生成器,单表、树列表、一对多、一对一等数据模型,增删改查功能一键生成,菜单配置直接使用。
|
||||||
@ -155,6 +158,9 @@ JeecgBoot平台提供了一套完善的AI应用管理系统模块,是一套类
|
|||||||
#### 前端
|
#### 前端
|
||||||
|
|
||||||
- 前端环境要求:Node.js要求`Node 20+` 版本以上、pnpm 要求`9+` 版本以上
|
- 前端环境要求:Node.js要求`Node 20+` 版本以上、pnpm 要求`9+` 版本以上
|
||||||
|
|
||||||
|
` ( Vite 不再支持已结束生命周期(EOL)的 Node.js 18。现在需要使用 Node.js 20.19+ 或 22.12+)`
|
||||||
|
|
||||||
- 依赖管理:node、npm、pnpm
|
- 依赖管理:node、npm、pnpm
|
||||||
- 前端IDE建议:IDEA、WebStorm、Vscode
|
- 前端IDE建议:IDEA、WebStorm、Vscode
|
||||||
- 采用 Vue3.0+TypeScript+Vite6+Ant-Design-Vue4等新技术方案,包括二次封装组件、utils、hooks、动态菜单、权限校验、按钮级别权限控制等功能
|
- 采用 Vue3.0+TypeScript+Vite6+Ant-Design-Vue4等新技术方案,包括二次封装组件、utils、hooks、动态菜单、权限校验、按钮级别权限控制等功能
|
||||||
@ -164,16 +170,16 @@ JeecgBoot平台提供了一套完善的AI应用管理系统模块,是一套类
|
|||||||
#### 后端
|
#### 后端
|
||||||
|
|
||||||
- IDE建议: IDEA (必须安装lombok插件 )
|
- IDE建议: IDEA (必须安装lombok插件 )
|
||||||
- 语言:Java 默认jdk17(支持jdk8、jdk21)
|
- 语言:Java 默认jdk17(jdk21、jdk24)
|
||||||
- 依赖管理:Maven
|
- 依赖管理:Maven
|
||||||
- 基础框架:Spring Boot 3.5.5/2.7.18
|
- 基础框架:Spring Boot 3.5.5
|
||||||
- 微服务框架: Spring Cloud Alibaba 2021.0.6.2
|
- 微服务框架: Spring Cloud Alibaba 2023.0.3.3
|
||||||
- 持久层框架:MybatisPlus 3.5.3.2
|
- 持久层框架:MybatisPlus 3.5.12
|
||||||
- 报表工具: JimuReport 2.1.2
|
- 报表工具: JimuReport 2.1.3
|
||||||
- 安全框架:Apache Shiro 1.13.0,Jwt 4.5.0
|
- 安全框架:Apache Shiro 2.0.4,Jwt 4.5.0
|
||||||
- 微服务技术栈:Spring Cloud Alibaba、Nacos、Gateway、Sentinel、Skywalking
|
- 微服务技术栈:Spring Cloud Alibaba、Nacos、Gateway、Sentinel、Skywalking
|
||||||
- 数据库连接池:阿里巴巴Druid 1.1.24
|
- 数据库连接池:阿里巴巴Druid 1.2.24
|
||||||
- AI大模型:支持 `ChatGPT` `DeepSeek`切换
|
- AI大模型:支持 `ChatGPT` `DeepSeek` `千问`等各种常规模式
|
||||||
- 日志打印:logback
|
- 日志打印:logback
|
||||||
- 缓存:Redis
|
- 缓存:Redis
|
||||||
- 其他:autopoi, fastjson,poi,Swagger-ui,quartz, lombok(简化代码)等。
|
- 其他:autopoi, fastjson,poi,Swagger-ui,quartz, lombok(简化代码)等。
|
||||||
|
|||||||
@ -109,27 +109,32 @@ services:
|
|||||||
# environment:
|
# environment:
|
||||||
# RABBITMQ_DEFAULT_USER: guest
|
# RABBITMQ_DEFAULT_USER: guest
|
||||||
# RABBITMQ_DEFAULT_PASS: guest
|
# RABBITMQ_DEFAULT_PASS: guest
|
||||||
# jeecg-boot-sentinel:
|
|
||||||
# restart: on-failure
|
jeecg-boot-sentinel:
|
||||||
# build:
|
restart: on-failure
|
||||||
# context: ./jeecg-visual/jeecg-cloud-sentinel
|
build:
|
||||||
# ports:
|
context: ./jeecg-boot/jeecg-server-cloud/jeecg-visual/jeecg-cloud-sentinel
|
||||||
# - 9000:9000
|
ports:
|
||||||
# depends_on:
|
- 9000:9000
|
||||||
# - jeecg-boot-nacos
|
depends_on:
|
||||||
# - jeecg-boot-demo
|
- jeecg-boot-nacos
|
||||||
# - jeecg-boot-system
|
- jeecg-boot-demo
|
||||||
# - jeecg-boot-gateway
|
- jeecg-boot-system
|
||||||
# container_name: jeecg-boot-sentinel
|
- jeecg-boot-gateway
|
||||||
# hostname: jeecg-boot-sentinel
|
container_name: jeecg-boot-sentinel
|
||||||
#
|
hostname: jeecg-boot-sentinel
|
||||||
# jeecg-boot-xxljob:
|
networks:
|
||||||
# build:
|
- jeecg-boot
|
||||||
# context: ./jeecg-visual/jeecg-cloud-xxljob
|
|
||||||
# ports:
|
jeecg-boot-xxljob:
|
||||||
# - 9080:9080
|
build:
|
||||||
# container_name: jeecg-boot-xxljob
|
context: ./jeecg-boot/jeecg-server-cloud/jeecg-visual/jeecg-cloud-xxljob
|
||||||
# hostname: jeecg-boot-xxljob
|
ports:
|
||||||
|
- 9080:9080
|
||||||
|
container_name: jeecg-boot-xxljob
|
||||||
|
hostname: jeecg-boot-xxljob
|
||||||
|
networks:
|
||||||
|
- jeecg-boot
|
||||||
|
|
||||||
jeecg-vue:
|
jeecg-vue:
|
||||||
build:
|
build:
|
||||||
|
|||||||
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.2(发布日期:2025-08-04)
|
当前最新版本: 3.8.3(发布日期:2025-10-09)
|
||||||
|
|
||||||
|
|
||||||
[](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)
|
||||||
|
|
||||||
@ -16,43 +16,127 @@ JeecgBoot 低代码开发平台
|
|||||||
项目介绍
|
项目介绍
|
||||||
-----------------------------------
|
-----------------------------------
|
||||||
|
|
||||||
<h3 align="center">Java Low Code Platform for Enterprise web applications</h3>
|
<h3 align="center">企业级AI低代码平台</h3>
|
||||||
|
|
||||||
|
JeecgBoot 是一款基于BPM流程和代码生成的AI低代码平台,助力企业快速实现低代码开发和构建AI应用。
|
||||||
|
采用前后端分离架构(Ant Design&Vue3,SpringBoot3,SpringCloud Alibaba,Mybatis-plus),强大代码生成器实现前后端一键生成,无需手写代码。
|
||||||
|
平台引领AI低代码开发模式:AI生成→在线编码→代码生成→手工合并,解决Java项目80%重复工作,提升效率,节省成本,兼顾灵活性。
|
||||||
|
具备强大且颗粒化的权限控制,支持按钮权限和数据权限设置,满足大型业务系统需求。功能涵盖在线表单、表单设计、流程设计、门户设计、报表与大屏设计、OA办公、AI应用、AI知识库、大模型管理、AI流程编排、AI聊天,支持ChatGPT、DeepSeek、Ollama等多种AI大模型。
|
||||||
|
|
||||||
|
`AI赋能报表:` 积木报表是一款自主研发的强大开源企业级Web报表与大屏工具。它通过零编码的拖拽式操作,赋能用户如同搭积木般轻松构建各类复杂报表和数据大屏,全面满足企业数据可视化与分析需求,助力企业级数据产品的高效打造与应用。
|
||||||
|
|
||||||
|
`AI赋能低代码:` 提供完善成熟的AI应用平台,涵盖AI应用管理、AI模型管理、智能对话助手、知识库问答、流程编排与设计器、AI建表等多项功能。平台兼容多种主流大模型,包括ChatGPT、DeepSeek、Ollama、智普、千问等,助力企业高效构建智能化应用,推动低代码开发与AI深度融合。
|
||||||
|
|
||||||
|
`JEECG宗旨是:` JEECG旨在通过OnlineCoding平台实现简单功能的零代码快速搭建,同时针对复杂功能采用代码生成器生成代码并手工合并,打造智能且灵活的低代码开发模式,有效解决了当前低代码产品普遍缺乏灵活性的问题,提升开发效率的同时兼顾系统的扩展性和定制化能力。
|
||||||
|
|
||||||
|
`JEECG业务流程:` JEECG业务流程采用BPM工作流引擎实现业务审批,扩展任务接口供开发人员编写业务逻辑,表单提供表单设计器、在线配置表单和编码表单等多种解决方案。通过流程与表单的分离设计(松耦合)及任务节点的灵活配置,既保障了企业流程的安全性与保密性,又大幅降低了开发人员的工作量。
|
||||||
|
|
||||||
|
|
||||||
|
适用项目
|
||||||
|
-----------------------------------
|
||||||
|
JeecgBoot低代码平台兼容所有J2EE项目开发,支持信创国产化,特别适用于SAAS、企业信息管理系统(MIS)、内部办公系统(OA)、企业资源计划系统(ERP)、客户关系管理系统(CRM)及AI知识库等场景。其半智能手工Merge开发模式,可显著提升70%以上的开发效率,极大降低开发成本。同时,JeecgBoot还是一款全栈式AI开发平台,助力企业快速构建和部署个性化AI应用。。
|
||||||
|
|
||||||
|
|
||||||
|
**信创兼容说明**
|
||||||
|
- 操作系统:国产麒麟、银河麒麟等国产系统几乎都是基于 Linux 内核,因此它们具有良好的兼容性。
|
||||||
|
- 数据库:达梦、人大金仓、TiDB
|
||||||
|
- 中间件:东方通 TongWeb、TongRDS,宝兰德 AppServer、CacheDB, [信创配置文档](https://help.jeecg.com/java/tongweb-deploy/)
|
||||||
|
|
||||||
|
|
||||||
JeecgBoot 是一款基于代码生成器的`低代码开发平台`!前后端分离架构 SpringBoot2.x和3.x,SpringCloud,Ant Design Vue3,Mybatis-plus,Shiro,JWT,支持微服务。强大的代码生成器让前后端代码一键生成,实现低代码开发! JeecgBoot 引领新的低代码开发模式(OnlineCoding-> 代码生成器-> 手工MERGE), 帮助解决Java项目70%的重复工作,让开发更多关注业务。既能快速提高效率,节省研发成本,同时又不失灵活性!
|
|
||||||
|
|
||||||
|
|
||||||
#### 项目说明
|
#### 项目说明
|
||||||
|
|
||||||
| 项目名 | 说明 |
|
| 项目名 | 说明 |
|
||||||
|--------------------|------------------------|
|
|--------------------|------------------------------------|
|
||||||
| `jeecg-boot` | 后端源码JAVA(SpringBoot微服务架构) |
|
| `jeecg-boot` | 后端源码JAVA(SpringBoot3微服务架构) |
|
||||||
| `jeecgboot-vue3` | 前端源码VUE3(vue3+vite5+ts最新技术栈) |
|
| `jeecgboot-vue3` | 前端源码VUE3(vue3+vite6+antd4+ts最新技术栈) |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
技术文档
|
|
||||||
-----------------------------------
|
|
||||||
|
|
||||||
- 官方网站: [http://www.jeecg.com](http://www.jeecg.com)
|
|
||||||
- 新手指南: [快速入门](http://www.jeecg.com/doc/quickstart)
|
|
||||||
- QQ交流群 : 964611995、⑩716488839(满)、⑨808791225(满)、其他(满)
|
|
||||||
- 在线演示 : [在线演示](http://boot3.jeecg.com) | [APP演示](http://jeecg.com/appIndex)
|
|
||||||
> 演示系统的登录账号密码,请点击 [获取账号密码](http://jeecg.com/doc/demo) 获取
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
启动项目
|
启动项目
|
||||||
-----------------------------------
|
-----------------------------------
|
||||||
|
|
||||||
- [IDEA启动前后端项目](https://help.jeecg.com/java/setup/idea/startup)
|
> 默认账号密码: admin/123456
|
||||||
- [Docker一键启动前后端](https://help.jeecg.com/java/docker/quick)
|
|
||||||
|
- [开发环境搭建](https://help.jeecg.com/java/setup/tools)
|
||||||
|
- [IDEA启动前后端(单体模式)](https://help.jeecg.com/java/setup/idea/startup)
|
||||||
|
- [Docker一键启动(单体模式)](https://help.jeecg.com/java/docker/quick)
|
||||||
|
- [IDEA启动前后端(微服务方式)](https://help.jeecg.com/java/springcloud/switchcloud/monomer)
|
||||||
|
- [Docker一键启动(微服务方式)](https://help.jeecg.com/java/docker/quickcloud)
|
||||||
|
|
||||||
|
|
||||||
微服务启动
|
技术文档
|
||||||
-----------------------------------
|
-----------------------------------
|
||||||
- [单体快速切换微服务](https://help.jeecg.com/java/springcloud/switchcloud/monomer)
|
|
||||||
- [Docker启动微服务后台](https://help.jeecg.com/java/docker/springcloud)
|
|
||||||
|
|
||||||
|
- 官方网站: [http://www.jeecg.com](http://www.jeecg.com)
|
||||||
|
- 在线演示: [平台演示](https://boot3.jeecg.com) | [APP演示](https://jeecg.com/appIndex)
|
||||||
|
- 入门指南: [快速入门](http://www.jeecg.com/doc/quickstart) | [代码生成使用](https://help.jeecg.com/java/codegen/online) | [开发文档](https://help.jeecg.com) | [AI应用手册](https://help.jeecg.com/aigc) | [视频教程](http://jeecg.com/doc/video)
|
||||||
|
- 技术支持: [反馈问题](https://github.com/jeecgboot/JeecgBoot/issues/new?template=bug_report.md) | [低代码体验一分钟](https://jeecg.blog.csdn.net/article/details/106079007)
|
||||||
|
- QQ交流群 : 964611995、⑩716488839(满)、⑨808791225(满)、其他(满)
|
||||||
|
|
||||||
|
|
||||||
|
AI 应用平台介绍
|
||||||
|
-----------------------------------
|
||||||
|
|
||||||
|
一个全栈式 AI 开发平台,旨在帮助开发者快速构建和部署个性化的 AI 应用。
|
||||||
|
|
||||||
|
JeecgBoot平台提供了一套完善的AI应用管理系统模块,是一套类似`Dify`的`AIGC应用开发平台`+`知识库问答`,是一款基于LLM大语言模型AI应用平台和 RAG 的知识库问答系统。
|
||||||
|
其直观的界面结合了 AI 流程编排、RAG 管道、知识库管理、模型管理、对接向量库、实时运行可观察等,让您可以快速从原型到生产,拥有AI服务能力。
|
||||||
|
|
||||||
|
- [详细专题介绍,请点击查看](README-AI.md)
|
||||||
|
|
||||||
|
- AI视频介绍
|
||||||
|
|
||||||
|
[](https://www.bilibili.com/video/BV1zmd7YFE4w)
|
||||||
|
|
||||||
|
|
||||||
|
为什么选择JeecgBoot?
|
||||||
|
-----------------------------------
|
||||||
|
- 1.采用最新主流前后分离框架(Spring Boot3 + MyBatis + Shiro/SpringAuthorizationServer + Ant Design4 + Vue3),容易上手;代码生成器依赖性低,灵活的扩展能力,可快速实现二次开发。
|
||||||
|
- 2.前端大版本换代,最新版采用 Vue3.0 + TypeScript + Vite6 + Ant Design Vue4 等新技术方案。
|
||||||
|
- 3.支持微服务Spring Cloud Alibaba(Nacos、Gateway、Sentinel、Skywalking),提供简易机制,支持单体和微服务自由切换(这样可以满足各类项目需求)。
|
||||||
|
- 4.开发效率高,支持在线建表和AI建表,提供强大代码生成器,单表、树列表、一对多、一对一等数据模型,增删改查功能一键生成,菜单配置直接使用。
|
||||||
|
- 5.代码生成器提供强大模板机制,支持自定义模板,目前提供四套风格模板(单表两套、树模型一套、一对多三套)。
|
||||||
|
- 6.提供强大的报表和大屏可视化工具,支持丰富的数据源连接,能够通过拖拉拽方式快速制作报表、大屏和门户设计;支持多种图表类型:柱形图、折线图、散点图、饼图、环形图、面积图、漏斗图、进度图、仪表盘、雷达图、地图等。
|
||||||
|
- 7.低代码能力:在线表单(无需编码,通过在线配置表单,实现表单的增删改查,支持单表、树、一对多、一对一等模型,实现人人皆可编码),在线配置零代码开发、所见即所得支持23种类控件。
|
||||||
|
- 8.低代码能力:在线报表、在线图表(无需编码,通过在线配置方式,实现数据报表和图形报表,可以快速抽取数据,减轻开发压力,实现人人皆可编码)。
|
||||||
|
- 9.Online支持在线增强开发,提供在线代码编辑器,支持代码高亮、代码提示等功能,支持多种语言(Java、SQL、JavaScript等)。
|
||||||
|
- 10.封装完善的用户、角色、菜单、组织机构、数据字典、在线定时任务等基础功能,支持访问授权、按钮权限、数据权限等功能。
|
||||||
|
- 11.前端UI提供丰富的组件库,支持各种常用组件,如表格、树形控件、下拉框、日期选择器等,满足各种复杂的业务需求 [UI组件库文档](https://help.jeecg.com/category/ui%E7%BB%84%E4%BB%B6%E5%BA%93)。
|
||||||
|
- 12.提供APP配套框架,一份多代码多终端适配,一份代码多终端适配,小程序、H5、安卓、iOS、鸿蒙Next。
|
||||||
|
- 13.新版APP框架采用Uniapp、Vue3.0、Vite、Wot-design-uni、TypeScript等最新技术栈,包括二次封装组件、路由拦截、请求拦截等功能。实现了与JeecgBoot完美对接:目前已经实现登录、用户信息、通讯录、公告、移动首页、九宫格、聊天、Online表单、仪表盘等功能,提供了丰富的组件。
|
||||||
|
- 14.提供了一套成熟的AI应用平台功能,从AI模型、知识库到AI应用搭建,助力企业快速落地AI服务,加速智能化升级。
|
||||||
|
- 15.AI能力:目前JeecgBoot支持AI大模型chatgpt和deepseek,现在最新版默认使用deepseek,速度更快质量更高。目前提供了AI对话助手、AI知识库、AI应用、AI建表、AI报表等功能。
|
||||||
|
- 16.提供新行编辑表格JVXETable,轻松满足各种复杂ERP布局,拥有更高的性能、更灵活的扩展、更强大的功能。
|
||||||
|
- 17.平台首页风格,提供多种组合模式,支持自定义风格;支持门户设计,支持自定义首页。
|
||||||
|
- 18.常用共通封装,各种工具类(定时任务、短信接口、邮件发送、Excel导入导出等),基本满足80%项目需求。
|
||||||
|
- 19.简易Excel导入导出,支持单表导出和一对多表模式导出,生成的代码自带导入导出功能。
|
||||||
|
- 20.集成智能报表工具,报表打印、图像报表和数据导出非常方便,可极其方便地生成PDF、Excel、Word等报表。
|
||||||
|
- 21.采用前后分离技术,页面UI风格精美,针对常用组件做了封装:时间、行表格控件、截取显示控件、报表组件、编辑器等。
|
||||||
|
- 22.查询过滤器:查询功能自动生成,后台动态拼SQL追加查询条件;支持多种匹配方式(全匹配/模糊查询/包含查询/不匹配查询)。
|
||||||
|
- 23.数据权限(精细化数据权限控制,控制到行级、列表级、表单字段级,实现不同人看不同数据,不同人对同一个页面操作不同字段)。
|
||||||
|
- 24.接口安全机制,可细化控制接口授权,非常简便实现不同客户端只看自己数据等控制;也提供了基于AK和SK认证鉴权的OpenAPI功能。
|
||||||
|
- 25.活跃的社区支持;近年来,随着网络威胁的日益增加,团队在安全和漏洞管理方面积累了丰富的经验,能够为企业提供全面的安全解决方案。
|
||||||
|
- 26.权限控制采用RBAC(Role-Based Access Control,基于角色的访问控制)。
|
||||||
|
- 27.页面校验自动生成(必须输入、数字校验、金额校验、时间空间等)。
|
||||||
|
- 28.支持SaaS服务模式,提供SaaS多租户架构方案。
|
||||||
|
- 29.分布式文件服务,集成MinIO、阿里OSS等优秀的第三方,提供便捷的文件上传与管理,同时也支持本地存储。
|
||||||
|
- 30.主流数据库兼容,一套代码完全兼容MySQL、PostgreSQL、Oracle、SQL Server、MariaDB、达梦、人大金仓等主流数据库。
|
||||||
|
- 31.集成工作流Flowable,并实现了只需在页面配置流程转向,可极大简化BPM工作流的开发;用BPM的流程设计器画出了流程走向,一个工作流基本就完成了,只需写很少量的Java代码。
|
||||||
|
- 32.低代码能力:在线流程设计,采用开源Flowable流程引擎,实现在线画流程、自定义表单、表单挂靠、业务流转。
|
||||||
|
- 33.多数据源:极其简易的使用方式,在线配置数据源配置,便捷地从其他数据抓取数据。
|
||||||
|
- 34.提供单点登录CAS集成方案,项目中已经提供完善的对接代码。
|
||||||
|
- 35.低代码能力:表单设计器,支持用户自定义表单布局,支持单表、一对多表单,支持select、radio、checkbox、textarea、date、popup、列表、宏等控件。
|
||||||
|
- 36.专业接口对接机制,统一采用RESTful接口方式,集成Swagger-UI在线接口文档,JWT token安全验证,方便客户端对接。
|
||||||
|
- 37.高级组合查询功能,在线配置支持主子表关联查询,可保存查询历史。
|
||||||
|
- 38.提供各种系统监控,实时跟踪系统运行情况(监控Redis、Tomcat、JVM、服务器信息、请求追踪、SQL监控)。
|
||||||
|
- 39.消息中心(支持短信、邮件、微信推送等);集成WebSocket消息通知机制。
|
||||||
|
- 40.支持多语言,提供国际化方案。
|
||||||
|
- 41.数据变更记录日志,可记录数据每次变更内容,通过版本对比功能查看历史变化。
|
||||||
|
- 42.提供简单易用的打印插件,支持谷歌、火狐、IE11+等各种浏览器。
|
||||||
|
- 43.后端采用Maven分模块开发方式;前端支持菜单动态路由。
|
||||||
|
- 44.提供丰富的示例代码,涵盖了常用的业务场景,便于学习和参考。
|
||||||
|
|
||||||
|
|
||||||
技术架构:
|
技术架构:
|
||||||
@ -61,28 +145,33 @@ JeecgBoot 是一款基于代码生成器的`低代码开发平台`!前后端
|
|||||||
#### 后端
|
#### 后端
|
||||||
|
|
||||||
- IDE建议: IDEA (必须安装lombok插件 )
|
- IDE建议: IDEA (必须安装lombok插件 )
|
||||||
- 语言:Java 8+ (支持17)
|
- 语言:Java 默认jdk17(jdk21、jdk24)
|
||||||
- 依赖管理:Maven
|
- 依赖管理:Maven
|
||||||
- 基础框架:Spring Boot 2.7.18
|
- 基础框架:Spring Boot 3.5.5
|
||||||
- 微服务框架: Spring Cloud Alibaba 2021.0.1.0
|
- 微服务框架: Spring Cloud Alibaba 2023.0.3.3
|
||||||
- 持久层框架:MybatisPlus 3.5.3.2
|
- 持久层框架:MybatisPlus 3.5.12
|
||||||
- 报表工具: JimuReport 1.9.4
|
- 报表工具: JimuReport 2.1.3
|
||||||
- 安全框架:Apache Shiro 1.12.0,Jwt 3.11.0
|
- 安全框架:Apache Shiro 2.0.4,Jwt 4.5.0
|
||||||
- 微服务技术栈:Spring Cloud Alibaba、Nacos、Gateway、Sentinel、Skywalking
|
- 微服务技术栈:Spring Cloud Alibaba、Nacos、Gateway、Sentinel、Skywalking
|
||||||
- 数据库连接池:阿里巴巴Druid 1.1.24
|
- 数据库连接池:阿里巴巴Druid 1.2.24
|
||||||
|
- AI大模型:支持 `ChatGPT` `DeepSeek` `千问`等各种常规模式
|
||||||
- 日志打印:logback
|
- 日志打印:logback
|
||||||
- 缓存:Redis
|
- 缓存:Redis
|
||||||
- 其他:autopoi, fastjson,poi,Swagger-ui,quartz, lombok(简化代码)等。
|
- 其他:autopoi, fastjson,poi,Swagger-ui,quartz, lombok(简化代码)等。
|
||||||
- 默认数据库脚本:MySQL5.7+
|
- 默认提供MySQL5.7+数据库脚本
|
||||||
- [其他数据库,需要自己转](https://my.oschina.net/jeecg/blog/4905722)
|
- [其他数据库,需要自己转](https://my.oschina.net/jeecg/blog/4905722)
|
||||||
|
|
||||||
|
|
||||||
#### 前端
|
#### 前端
|
||||||
|
|
||||||
- 前端IDE建议:WebStorm、Vscode
|
- 前端环境要求:Node.js要求`Node 20+` 版本以上、pnpm 要求`9+` 版本以上
|
||||||
- 采用 Vue3.0+TypeScript+Vite+Ant-Design-Vue等新技术方案,包括二次封装组件、utils、hooks、动态菜单、权限校验、按钮级别权限控制等功能
|
` ( Vite 不再支持已结束生命周期(EOL)的 Node.js 18。现在需要使用 Node.js 20.19+ 或 22.12+)`
|
||||||
- 最新技术栈:Vue3.0 + TypeScript + Vite5 + ant-design-vue4 + pinia + echarts + unocss + vxe-table + qiankun + es6
|
|
||||||
- 依赖管理:node、npm、pnpm
|
- 依赖管理:node、npm、pnpm
|
||||||
|
- 前端IDE建议:IDEA、WebStorm、Vscode
|
||||||
|
- 采用 Vue3.0+TypeScript+Vite6+Ant-Design-Vue4等新技术方案,包括二次封装组件、utils、hooks、动态菜单、权限校验、按钮级别权限控制等功能
|
||||||
|
- 最新技术栈:Vue3.0 + TypeScript + Vite6 + ant-design-vue4 + pinia + echarts + unocss + vxe-table + qiankun + es6
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -1,5 +1,5 @@
|
|||||||
#
|
#
|
||||||
# XXL-JOB v2.2.0
|
# XXL-JOB v2.4.0
|
||||||
# Copyright (c) 2015-present, xuxueli.
|
# Copyright (c) 2015-present, xuxueli.
|
||||||
|
|
||||||
CREATE database if NOT EXISTS `xxl_job` default character set utf8mb4 collate utf8mb4_general_ci;
|
CREATE database if NOT EXISTS `xxl_job` default character set utf8mb4 collate utf8mb4_general_ci;
|
||||||
|
|||||||
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
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]}');
|
||||||
@ -3,6 +3,7 @@
|
|||||||
> JeecgBoot属于平台级产品,每次升级改动较大,目前做不到平滑升级。
|
> JeecgBoot属于平台级产品,每次升级改动较大,目前做不到平滑升级。
|
||||||
|
|
||||||
### 增量升级方案
|
### 增量升级方案
|
||||||
|
|
||||||
#### 1.代码合并
|
#### 1.代码合并
|
||||||
本地通过svn或git做好主干,在分支上做业务开发,jeecg每次版本发布,可以手工覆盖主干的代码,对比合并代码;
|
本地通过svn或git做好主干,在分支上做业务开发,jeecg每次版本发布,可以手工覆盖主干的代码,对比合并代码;
|
||||||
|
|
||||||
@ -11,5 +12,12 @@
|
|||||||
- 其他库请手工执行SQL, 目录: `jeecg-module-system\jeecg-system-start\src\main\resources\flyway\sql\mysql`
|
- 其他库请手工执行SQL, 目录: `jeecg-module-system\jeecg-system-start\src\main\resources\flyway\sql\mysql`
|
||||||
> 注意: 升级sql只提供mysql版本;如果有权限升级, 还需要手工角色授权,退出重新登录才好使。
|
> 注意: 升级sql只提供mysql版本;如果有权限升级, 还需要手工角色授权,退出重新登录才好使。
|
||||||
|
|
||||||
#### 3.兼容问题
|
#### 3.其他数据库脚本说明
|
||||||
|
原先官方默认提供oracle和SqlServer的脚本,但是维护成本太高,未提供脚本的数据库,可以参考下面的文档自己转
|
||||||
|
https://my.oschina.net/jeecg/blog/4905722
|
||||||
|
(注意:定时任务的表qrtz_*,需要删掉用原始的脚本重新执行一下)
|
||||||
|
quartz-2.2.3-distribution.tar.gz放到百度网盘中,大家自己下载,执行所需数据库脚本
|
||||||
|
https://pan.baidu.com/s/1WrmZdUuAPg3iBwJ-LoHWyg?pwd=8mdz
|
||||||
|
|
||||||
|
#### 4.兼容问题
|
||||||
每次发版,会针对不兼容地方重点说明。
|
每次发版,会针对不兼容地方重点说明。
|
||||||
@ -24,8 +24,8 @@ services:
|
|||||||
|
|
||||||
jeecg-boot-redis:
|
jeecg-boot-redis:
|
||||||
image: registry.cn-hangzhou.aliyuncs.com/jeecgdocker/redis:5.0
|
image: registry.cn-hangzhou.aliyuncs.com/jeecgdocker/redis:5.0
|
||||||
ports:
|
# ports:
|
||||||
- 6379:6379
|
# - 6379:6379
|
||||||
restart: always
|
restart: always
|
||||||
hostname: jeecg-boot-redis
|
hostname: jeecg-boot-redis
|
||||||
container_name: jeecg-boot-redis
|
container_name: jeecg-boot-redis
|
||||||
@ -39,12 +39,26 @@ services:
|
|||||||
POSTGRES_USER: postgres
|
POSTGRES_USER: postgres
|
||||||
POSTGRES_PASSWORD: postgres
|
POSTGRES_PASSWORD: postgres
|
||||||
POSTGRES_DB: vector_db
|
POSTGRES_DB: vector_db
|
||||||
ports:
|
# ports:
|
||||||
- 5432:5432
|
# - 5432:5432
|
||||||
restart: always
|
restart: always
|
||||||
networks:
|
networks:
|
||||||
- jeecg-boot
|
- jeecg-boot
|
||||||
|
|
||||||
|
# jeecg-boot-rabbitmq:
|
||||||
|
# image: rabbitmq:3.7.7-management
|
||||||
|
## ports:
|
||||||
|
## - 5672:5672
|
||||||
|
## - 15672:15672
|
||||||
|
# restart: always
|
||||||
|
# container_name: jeecg-boot-rabbitmq
|
||||||
|
# hostname: jeecg-boot-rabbitmq
|
||||||
|
# environment:
|
||||||
|
# RABBITMQ_DEFAULT_USER: guest
|
||||||
|
# RABBITMQ_DEFAULT_PASS: guest
|
||||||
|
# networks:
|
||||||
|
# - jeecg-boot
|
||||||
|
|
||||||
jeecg-boot-system:
|
jeecg-boot-system:
|
||||||
build:
|
build:
|
||||||
context: ./jeecg-module-system/jeecg-system-start
|
context: ./jeecg-module-system/jeecg-system-start
|
||||||
@ -59,6 +73,8 @@ services:
|
|||||||
- 8080:8080
|
- 8080:8080
|
||||||
networks:
|
networks:
|
||||||
- jeecg-boot
|
- jeecg-boot
|
||||||
|
volumes:
|
||||||
|
- ./config:/jeecg-boot/config
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
jeecg-boot:
|
jeecg-boot:
|
||||||
|
|||||||
@ -2,9 +2,9 @@
|
|||||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
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">
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
<parent>
|
<parent>
|
||||||
<groupId>org.jeecgframework.boot</groupId>
|
<groupId>org.jeecgframework.boot3</groupId>
|
||||||
<artifactId>jeecg-boot-parent</artifactId>
|
<artifactId>jeecg-boot-parent</artifactId>
|
||||||
<version>3.8.2</version>
|
<version>3.8.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>
|
||||||
@ -42,23 +42,13 @@
|
|||||||
<dependencies>
|
<dependencies>
|
||||||
<!--jeecg-tools-->
|
<!--jeecg-tools-->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.jeecgframework.boot</groupId>
|
<groupId>org.jeecgframework.boot3</groupId>
|
||||||
<artifactId>jeecg-boot-common</artifactId>
|
<artifactId>jeecg-boot-common</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
<!--集成springmvc框架并实现自动配置 -->
|
<!--集成springmvc框架并实现自动配置 -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-web</artifactId>
|
<artifactId>spring-boot-starter-web</artifactId>
|
||||||
<exclusions>
|
|
||||||
<exclusion>
|
|
||||||
<groupId>org.springframework.boot</groupId>
|
|
||||||
<artifactId>spring-boot-starter-tomcat</artifactId>
|
|
||||||
</exclusion>
|
|
||||||
</exclusions>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.springframework.boot</groupId>
|
|
||||||
<artifactId>spring-boot-starter-undertow</artifactId>
|
|
||||||
</dependency>
|
</dependency>
|
||||||
<!-- websocket -->
|
<!-- websocket -->
|
||||||
<dependency>
|
<dependency>
|
||||||
@ -108,7 +98,7 @@
|
|||||||
<!-- mybatis-plus -->
|
<!-- mybatis-plus -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.baomidou</groupId>
|
<groupId>com.baomidou</groupId>
|
||||||
<artifactId>mybatis-plus-boot-starter</artifactId>
|
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
|
||||||
<version>${mybatis-plus.version}</version>
|
<version>${mybatis-plus.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
@ -117,22 +107,22 @@
|
|||||||
<version>${mybatis-plus.version}</version>
|
<version>${mybatis-plus.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<!-- minidao -->
|
<!-- minidao -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.jeecgframework</groupId>
|
<groupId>org.jeecgframework.boot3</groupId>
|
||||||
<artifactId>minidao-spring-boot-starter-jsqlparser-4.9</artifactId>
|
<artifactId>minidao-spring-boot-starter-jsqlparser-4.9</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- druid -->
|
<!-- druid -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.alibaba</groupId>
|
<groupId>com.alibaba</groupId>
|
||||||
<artifactId>druid-spring-boot-starter</artifactId>
|
<artifactId>druid-spring-boot-3-starter</artifactId>
|
||||||
<version>${druid.version}</version>
|
<version>${druid.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- 动态数据源 -->
|
<!-- 动态数据源 -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.baomidou</groupId>
|
<groupId>com.baomidou</groupId>
|
||||||
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
|
<artifactId>dynamic-datasource-spring-boot3-starter</artifactId>
|
||||||
<version>${dynamic-datasource-spring-boot-starter.version}</version>
|
<version>${dynamic-datasource-spring-boot-starter.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
@ -147,7 +137,7 @@
|
|||||||
<!-- sqlserver-->
|
<!-- sqlserver-->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.microsoft.sqlserver</groupId>
|
<groupId>com.microsoft.sqlserver</groupId>
|
||||||
<artifactId>sqljdbc4</artifactId>
|
<artifactId>mssql-jdbc</artifactId>
|
||||||
<version>${sqljdbc4.version}</version>
|
<version>${sqljdbc4.version}</version>
|
||||||
<scope>runtime</scope>
|
<scope>runtime</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
@ -169,13 +159,13 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.jeecgframework</groupId>
|
<groupId>org.jeecgframework</groupId>
|
||||||
<artifactId>kingbase8</artifactId>
|
<artifactId>kingbase8</artifactId>
|
||||||
<version>9.0.0</version>
|
<version>${kingbase8.version}</version>
|
||||||
<scope>runtime</scope>
|
<scope>runtime</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
<!--达梦数据库驱动 版本号1-3-26-2023.07.26-197096-20046-ENT -->
|
<!--达梦数据库驱动 版本号1-3-26-2023.07.26-197096-20046-ENT -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.dameng</groupId>
|
<groupId>com.dameng</groupId>
|
||||||
<artifactId>Dm8JdbcDriver18</artifactId>
|
<artifactId>DmJdbcDriver18</artifactId>
|
||||||
<version>${dm8.version}</version>
|
<version>${dm8.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
@ -197,35 +187,38 @@
|
|||||||
<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>
|
||||||
<version>${shiro.version}</version>
|
|
||||||
</dependency>
|
</dependency>
|
||||||
<!-- shiro-redis -->
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.crazycake</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>shiro-redis</artifactId>
|
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
|
||||||
<version>${shiro-redis.version}</version>
|
</dependency>
|
||||||
<exclusions>
|
<!-- 添加spring security cas支持 -->
|
||||||
<exclusion>
|
<dependency>
|
||||||
<groupId>org.apache.shiro</groupId>
|
<groupId>org.springframework.security</groupId>
|
||||||
<artifactId>shiro-core</artifactId>
|
<artifactId>spring-security-cas</artifactId>
|
||||||
</exclusion>
|
|
||||||
<exclusion>
|
|
||||||
<artifactId>checkstyle</artifactId>
|
|
||||||
<groupId>com.puppycrawl.tools</groupId>
|
|
||||||
</exclusion>
|
|
||||||
</exclusions>
|
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- knife4j -->
|
|
||||||
|
<!-- <dependency>
|
||||||
|
<groupId>com.github.xiaoymin</groupId>
|
||||||
|
<artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
|
||||||
|
<version>${knife4j-spring-boot-starter.version}</version>
|
||||||
|
</dependency>-->
|
||||||
|
<!-- knife4j 升级springboot3.4.5报错 -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.github.xiaoymin</groupId>
|
<groupId>com.github.xiaoymin</groupId>
|
||||||
<artifactId>knife4j-openapi3-spring-boot-starter</artifactId>
|
<artifactId>knife4j-openapi3-ui</artifactId>
|
||||||
<version>${knife4j-spring-boot-starter.version}</version>
|
<version>${knife4j-spring-boot-starter.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springdoc</groupId>
|
||||||
|
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
|
||||||
|
<version>2.7.0</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<!-- 代码生成器 -->
|
<!-- 代码生成器 -->
|
||||||
<!-- 如下载失败,请参考此文档 https://help.jeecg.com/java/setup/maven.html -->
|
<!-- 如下载失败,请参考此文档 https://help.jeecg.com/java/setup/maven.html -->
|
||||||
@ -247,19 +240,8 @@
|
|||||||
|
|
||||||
<!-- AutoPoi Excel工具类-->
|
<!-- AutoPoi Excel工具类-->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.jeecgframework</groupId>
|
<groupId>org.jeecgframework.boot3</groupId>
|
||||||
<artifactId>autopoi-web</artifactId>
|
<artifactId>autopoi-web</artifactId>
|
||||||
<version>${autopoi-web.version}</version>
|
|
||||||
<exclusions>
|
|
||||||
<exclusion>
|
|
||||||
<groupId>commons-codec</groupId>
|
|
||||||
<artifactId>commons-codec</artifactId>
|
|
||||||
</exclusion>
|
|
||||||
<exclusion>
|
|
||||||
<artifactId>xercesImpl</artifactId>
|
|
||||||
<groupId>xerces</groupId>
|
|
||||||
</exclusion>
|
|
||||||
</exclusions>
|
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>xerces</groupId>
|
<groupId>xerces</groupId>
|
||||||
@ -296,6 +278,16 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.xkcoding.justauth</groupId>
|
<groupId>com.xkcoding.justauth</groupId>
|
||||||
<artifactId>justauth-spring-boot-starter</artifactId>
|
<artifactId>justauth-spring-boot-starter</artifactId>
|
||||||
|
<exclusions>
|
||||||
|
<exclusion>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-autoconfigure</artifactId>
|
||||||
|
</exclusion>
|
||||||
|
<exclusion>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-configuration-processor</artifactId>
|
||||||
|
</exclusion>
|
||||||
|
</exclusions>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.squareup.okhttp3</groupId>
|
<groupId>com.squareup.okhttp3</groupId>
|
||||||
@ -321,7 +313,7 @@
|
|||||||
</dependency>
|
</dependency>
|
||||||
<!-- chatgpt -->
|
<!-- chatgpt -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.jeecgframework.boot</groupId>
|
<groupId>org.jeecgframework.boot3</groupId>
|
||||||
<artifactId>jeecg-boot-starter-chatgpt</artifactId>
|
<artifactId>jeecg-boot-starter-chatgpt</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|||||||
@ -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,7 @@
|
|||||||
package org.jeecg.common.api;
|
package org.jeecg.common.api;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson.JSONObject;
|
||||||
|
import org.jeecg.common.api.dto.AiragFlowDTO;
|
||||||
import org.jeecg.common.system.vo.*;
|
import org.jeecg.common.system.vo.*;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@ -64,6 +66,13 @@ public interface CommonAPI {
|
|||||||
*/
|
*/
|
||||||
public String getUserIdByName(String username);
|
public String getUserIdByName(String username);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 5根据用户手机号查询用户信息
|
||||||
|
* @param username
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public LoginUser getUserByPhone(String phone);
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 6字典表的 翻译
|
* 6字典表的 翻译
|
||||||
@ -144,4 +153,42 @@ 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]解决分布式下表字典跨库无法查询问题------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 16 运行AIRag流程
|
||||||
|
* for [QQYUN-13634]在baseapi里面封装方法,方便其他模块调用
|
||||||
|
*
|
||||||
|
* @param airagFlowDTO
|
||||||
|
* @return 流程执行结果,可能是String或者Map
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2025/9/2 11:43
|
||||||
|
*/
|
||||||
|
Object runAiragFlow(AiragFlowDTO airagFlowDTO);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 登录加载系统字典
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
Map<String,List<DictModel>> queryAllDictItems();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询SysDepart集合
|
||||||
|
* @param userId
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
List<SysDepartModel> queryUserDeparts(String userId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据用户名设置部门ID
|
||||||
|
* @param username
|
||||||
|
* @param orgCode
|
||||||
|
*/
|
||||||
|
void updateUserDepart(String username,String orgCode,Integer loginTenantId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置登录租户
|
||||||
|
* @param username
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
JSONObject setLoginTenant(String username);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,36 @@
|
|||||||
|
package org.jeecg.common.api.dto;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 调用AI流程入参
|
||||||
|
* for [QQYUN-13634]在baseapi里面封装方法,方便其他模块调用
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2025/9/2 14:11
|
||||||
|
*/
|
||||||
|
@Builder
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
@Data
|
||||||
|
public class AiragFlowDTO implements Serializable {
|
||||||
|
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 7431775881170684867L;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 流程id
|
||||||
|
*/
|
||||||
|
private String flowId;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 输入参数
|
||||||
|
*/
|
||||||
|
private Map<String, Object> inputParams;
|
||||||
|
}
|
||||||
@ -2,7 +2,7 @@ package org.jeecg.common.api.dto;
|
|||||||
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -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,19 +16,21 @@ 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.LocalVariableTableParameterNameDiscoverer;
|
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;
|
||||||
import javax.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
import javax.servlet.ServletRequest;
|
import jakarta.servlet.ServletRequest;
|
||||||
import javax.servlet.ServletResponse;
|
import jakarta.servlet.ServletResponse;
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
|
||||||
@ -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());
|
||||||
@ -172,7 +175,7 @@ public class AutoLogAspect {
|
|||||||
// 请求的方法参数值
|
// 请求的方法参数值
|
||||||
Object[] args = joinPoint.getArgs();
|
Object[] args = joinPoint.getArgs();
|
||||||
// 请求的方法参数名称
|
// 请求的方法参数名称
|
||||||
LocalVariableTableParameterNameDiscoverer u = new LocalVariableTableParameterNameDiscoverer();
|
StandardReflectionParameterNameDiscoverer u=new StandardReflectionParameterNameDiscoverer();
|
||||||
String[] paramNames = u.getParameterNames(method);
|
String[] paramNames = u.getParameterNames(method);
|
||||||
if (args != null && paramNames != null) {
|
if (args != null && paramNames != null) {
|
||||||
for (int i = 0; i < args.length; i++) {
|
for (int i = 0; i < args.length; i++) {
|
||||||
|
|||||||
@ -21,7 +21,7 @@ import org.springframework.beans.factory.annotation.Autowired;
|
|||||||
import org.springframework.context.annotation.Lazy;
|
import org.springframework.context.annotation.Lazy;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
|||||||
@ -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;
|
||||||
|
|
||||||
@ -642,11 +642,12 @@ public interface CommonConstant {
|
|||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 自定义首页关联关系(ROLE:表示角色 USER:表示用户)
|
* 自定义首页关联关系(ROLE:表示角色 USER:表示用户 DEFAULT:默认首页)
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
String HOME_RELATION_ROLE = "ROLE";
|
String HOME_RELATION_ROLE = "ROLE";
|
||||||
String HOME_RELATION_USER = "USER";
|
String HOME_RELATION_USER = "USER";
|
||||||
|
String HOME_RELATION_DEFAULT = "DEFAULT";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 是否置顶(0否 1是)
|
* 是否置顶(0否 1是)
|
||||||
@ -659,4 +660,53 @@ public interface CommonConstant {
|
|||||||
String FLOW_FOCUS_NOTICE_PREFIX = "flow:runtimeData:focus:notice:";
|
String FLOW_FOCUS_NOTICE_PREFIX = "flow:runtimeData:focus:notice:";
|
||||||
//任务缓办时间缓存前缀
|
//任务缓办时间缓存前缀
|
||||||
String FLOW_TASK_DELAY_PREFIX = "flow:runtimeData:task:delay:";
|
String FLOW_TASK_DELAY_PREFIX = "flow:runtimeData:task:delay:";
|
||||||
|
/**
|
||||||
|
* 用户代理类型:离职:quit 代理:agent
|
||||||
|
*/
|
||||||
|
String USER_AGENT_TYPE_QUIT = "quit";
|
||||||
|
String USER_AGENT_TYPE_AGENT = "agent";
|
||||||
|
/**
|
||||||
|
* 督办流程首节点任务taskKey
|
||||||
|
*/
|
||||||
|
String SUPERVISE_FIRST_TASK_KEY = "Task_1bhxpt0";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* wps模板预览数据缓存前缀
|
||||||
|
*/
|
||||||
|
String EOA_WPS_TEMPLATE_VIEW_DATA ="eoa:wps:templateViewData:";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* wps模板预览版本号缓存前缀
|
||||||
|
*/
|
||||||
|
String EOA_WPS_TEMPLATE_VIEW_VERSION ="eoa:wps:templateViewVersion:";
|
||||||
|
/**
|
||||||
|
* 表单设计器oa新增字段
|
||||||
|
* x_oa_timeout_date:逾期时间
|
||||||
|
* x_oa_archive_status:归档状态
|
||||||
|
*/
|
||||||
|
String X_OA_TIMEOUT_DATE ="x_oa_timeout_date";
|
||||||
|
String X_OA_ARCHIVE_STATUS ="x_oa_archive_status";
|
||||||
|
/**
|
||||||
|
* 流程状态
|
||||||
|
* 待提交: 1
|
||||||
|
* 处理中: 2
|
||||||
|
* 已完成: 3
|
||||||
|
* 已作废: 4
|
||||||
|
* 已挂起: 5
|
||||||
|
*/
|
||||||
|
String BPM_STATUS_1 ="1";
|
||||||
|
String BPM_STATUS_2 ="2";
|
||||||
|
String BPM_STATUS_3 ="3";
|
||||||
|
String BPM_STATUS_4 ="4";
|
||||||
|
String BPM_STATUS_5 ="5";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 默认租户产品包
|
||||||
|
*/
|
||||||
|
String TENANT_PACK_DEFAULT = "default";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 部门名称redisKey(全路径)
|
||||||
|
*/
|
||||||
|
String DEPART_NAME_REDIS_KEY_PRE = "sys:cache:departPathName:";
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,15 @@
|
|||||||
|
package org.jeecg.common.constant;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Description: 密码常量类
|
||||||
|
*
|
||||||
|
* @author: wangshuai
|
||||||
|
* @date: 2025/8/27 20:10
|
||||||
|
*/
|
||||||
|
public interface PasswordConstant {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导入用户默认密码
|
||||||
|
*/
|
||||||
|
String DEFAULT_PASSWORD = "123456";
|
||||||
|
}
|
||||||
@ -121,7 +121,7 @@ public class ProvinceCityArea {
|
|||||||
|
|
||||||
public void getAreaByCode(String code,List<String> ls){
|
public void getAreaByCode(String code,List<String> ls){
|
||||||
for(Area area: areaList){
|
for(Area area: areaList){
|
||||||
if(area.getId().equals(code)){
|
if(null != area && area.getId().equals(code)){
|
||||||
String pid = area.getPid();
|
String pid = area.getPid();
|
||||||
ls.add(0,area.getText());
|
ls.add(0,area.getText());
|
||||||
getAreaByCode(pid,ls);
|
getAreaByCode(pid,ls);
|
||||||
|
|||||||
@ -0,0 +1,97 @@
|
|||||||
|
package org.jeecg.common.constant.enums;
|
||||||
|
|
||||||
|
import org.jeecg.common.util.oConvertUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Description: 部门类型枚举类
|
||||||
|
*
|
||||||
|
* @author: wangshuai
|
||||||
|
* @date: 2025/8/19 21:37
|
||||||
|
*/
|
||||||
|
public enum DepartCategoryEnum {
|
||||||
|
|
||||||
|
DEPART_CATEGORY_COMPANY("部门类型:公司","公司","1"),
|
||||||
|
DEPART_CATEGORY_DEPART("部门类型:部门","部门","2"),
|
||||||
|
DEPART_CATEGORY_POST("部门类型:岗位","岗位","3"),
|
||||||
|
DEPART_CATEGORY_SUB_COMPANY("部门类型:子公司","子公司","4");
|
||||||
|
|
||||||
|
DepartCategoryEnum(String described, String name, String value) {
|
||||||
|
this.value = value;
|
||||||
|
this.name = name;
|
||||||
|
this.described = described;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 描述
|
||||||
|
*/
|
||||||
|
private String described;
|
||||||
|
/**
|
||||||
|
* 值
|
||||||
|
*/
|
||||||
|
private String value;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 名称
|
||||||
|
*/
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
public String getDescribed() {
|
||||||
|
return described;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDescribed(String described) {
|
||||||
|
this.described = described;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getValue() {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setValue(String value) {
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setName(String name) {
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据值获取名称
|
||||||
|
*
|
||||||
|
* @param value
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public static String getNameByValue(String value){
|
||||||
|
if (oConvertUtils.isEmpty(value)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
for (DepartCategoryEnum val : values()) {
|
||||||
|
if (val.getValue().equals(value)) {
|
||||||
|
return val.getName();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据名称获取值
|
||||||
|
*
|
||||||
|
* @param name
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public static String getValueByName(String name){
|
||||||
|
if (oConvertUtils.isEmpty(name)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
for (DepartCategoryEnum val : values()) {
|
||||||
|
if (val.getName().equals(name)) {
|
||||||
|
return val.getValue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -8,21 +8,30 @@ import java.util.List;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 消息类型
|
* 消息类型
|
||||||
|
*
|
||||||
* @author: jeecg-boot
|
* @author: jeecg-boot
|
||||||
*/
|
*/
|
||||||
@EnumDict("messageType")
|
@EnumDict("messageType")
|
||||||
public enum MessageTypeEnum {
|
public enum MessageTypeEnum {
|
||||||
|
|
||||||
/** 系统消息 */
|
/**
|
||||||
XT("system", "系统消息"),
|
* 系统消息
|
||||||
/** 邮件消息 */
|
*/
|
||||||
YJ("email", "邮件消息"),
|
XT("system", "系统消息"),
|
||||||
/** 钉钉消息 */
|
/**
|
||||||
|
* 邮件消息
|
||||||
|
*/
|
||||||
|
YJ("email", "邮件消息"),
|
||||||
|
/**
|
||||||
|
* 钉钉消息
|
||||||
|
*/
|
||||||
DD("dingtalk", "钉钉消息"),
|
DD("dingtalk", "钉钉消息"),
|
||||||
/** 企业微信 */
|
/**
|
||||||
|
* 企业微信
|
||||||
|
*/
|
||||||
QYWX("wechat_enterprise", "企业微信");
|
QYWX("wechat_enterprise", "企业微信");
|
||||||
|
|
||||||
MessageTypeEnum(String type, String note){
|
MessageTypeEnum(String type, String note) {
|
||||||
this.type = type;
|
this.type = type;
|
||||||
this.note = note;
|
this.note = note;
|
||||||
}
|
}
|
||||||
@ -56,12 +65,13 @@ public enum MessageTypeEnum {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取字典数据
|
* 获取字典数据
|
||||||
|
*
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
public static List<DictModel> getDictList(){
|
public static List<DictModel> getDictList() {
|
||||||
List<DictModel> list = new ArrayList<>();
|
List<DictModel> list = new ArrayList<>();
|
||||||
DictModel dictModel = null;
|
DictModel dictModel = null;
|
||||||
for(MessageTypeEnum e: MessageTypeEnum.values()){
|
for (MessageTypeEnum e : MessageTypeEnum.values()) {
|
||||||
dictModel = new DictModel();
|
dictModel = new DictModel();
|
||||||
dictModel.setValue(e.getType());
|
dictModel.setValue(e.getType());
|
||||||
dictModel.setText(e.getNote());
|
dictModel.setText(e.getNote());
|
||||||
|
|||||||
@ -14,7 +14,16 @@ public enum NoticeTypeEnum {
|
|||||||
NOTICE_TYPE_PLAN("日程消息","plan"),
|
NOTICE_TYPE_PLAN("日程消息","plan"),
|
||||||
//暂时没用到
|
//暂时没用到
|
||||||
NOTICE_TYPE_MEETING("会议消息","meeting"),
|
NOTICE_TYPE_MEETING("会议消息","meeting"),
|
||||||
NOTICE_TYPE_SYSTEM("系统消息","system");
|
NOTICE_TYPE_SYSTEM("系统消息","system"),
|
||||||
|
/**
|
||||||
|
* 协同工作
|
||||||
|
* for [JHHB-136]【vue3】协同工作系统消息需要添加一个类型
|
||||||
|
*/
|
||||||
|
NOTICE_TYPE_COLLABORATION("协同工作", "collab"),
|
||||||
|
/**
|
||||||
|
* 督办
|
||||||
|
*/
|
||||||
|
NOTICE_TYPE_SUPERVISE("督办管理", "supe");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 文件类型名称
|
* 文件类型名称
|
||||||
|
|||||||
@ -0,0 +1,180 @@
|
|||||||
|
package org.jeecg.common.constant.enums;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 职级枚举类
|
||||||
|
*
|
||||||
|
* 注意:此枚举仅适用于天津临港控股OA项目,职级的名称和等级均为写死(需要与数据库配置一致)
|
||||||
|
* @date 2025-08-26
|
||||||
|
* @author scott
|
||||||
|
*/
|
||||||
|
public enum PositionLevelEnum {
|
||||||
|
|
||||||
|
// 领导层级(等级1-3)
|
||||||
|
CHAIRMAN("董事长", 1, PositionType.LEADER),
|
||||||
|
GENERAL_MANAGER("总经理", 2, PositionType.LEADER),
|
||||||
|
VICE_GENERAL_MANAGER("副总经理", 3, PositionType.LEADER),
|
||||||
|
|
||||||
|
// 职员层级(等级4-6)
|
||||||
|
MINISTER("部长", 4, PositionType.STAFF),
|
||||||
|
VICE_MINISTER("副部长", 5, PositionType.STAFF),
|
||||||
|
STAFF("职员", 6, PositionType.STAFF);
|
||||||
|
|
||||||
|
private final String name;
|
||||||
|
private final int level;
|
||||||
|
private final PositionType type;
|
||||||
|
|
||||||
|
PositionLevelEnum(String name, int level, PositionType type) {
|
||||||
|
this.name = name;
|
||||||
|
this.level = level;
|
||||||
|
this.type = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getLevel() {
|
||||||
|
return level;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PositionType getType() {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 职级类型枚举
|
||||||
|
*/
|
||||||
|
public enum PositionType {
|
||||||
|
STAFF("职员层级"),
|
||||||
|
LEADER("领导层级");
|
||||||
|
|
||||||
|
private final String desc;
|
||||||
|
|
||||||
|
PositionType(String desc) {
|
||||||
|
this.desc = desc;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDesc() {
|
||||||
|
return desc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据职级名称获取枚举
|
||||||
|
* @param name 职级名称
|
||||||
|
* @return 职级枚举
|
||||||
|
*/
|
||||||
|
public static PositionLevelEnum getByName(String name) {
|
||||||
|
for (PositionLevelEnum position : values()) {
|
||||||
|
if (position.getName().equals(name)) {
|
||||||
|
return position;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据职级等级获取枚举
|
||||||
|
* @param level 职级等级
|
||||||
|
* @return 职级枚举
|
||||||
|
*/
|
||||||
|
public static PositionLevelEnum getByLevel(int level) {
|
||||||
|
for (PositionLevelEnum position : values()) {
|
||||||
|
if (position.getLevel() == level) {
|
||||||
|
return position;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据职级名称判断是否为职员层级
|
||||||
|
* @param name 职级名称
|
||||||
|
* @return true-职员层级,false-非职员层级
|
||||||
|
*/
|
||||||
|
public static boolean isStaffLevel(String name) {
|
||||||
|
PositionLevelEnum position = getByName(name);
|
||||||
|
return position != null && position.getType() == PositionType.STAFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据职级名称判断是否为领导层级
|
||||||
|
* @param name 职级名称
|
||||||
|
* @return true-领导层级,false-非领导层级
|
||||||
|
*/
|
||||||
|
public static boolean isLeaderLevel(String name) {
|
||||||
|
PositionLevelEnum position = getByName(name);
|
||||||
|
return position != null && position.getType() == PositionType.LEADER;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 比较两个职级的等级高低
|
||||||
|
* @param name1 职级名称1
|
||||||
|
* @param name2 职级名称2
|
||||||
|
* @return 正数表示name1等级更高,负数表示name2等级更高,0表示等级相同
|
||||||
|
*/
|
||||||
|
public static int compareLevel(String name1, String name2) {
|
||||||
|
PositionLevelEnum pos1 = getByName(name1);
|
||||||
|
PositionLevelEnum pos2 = getByName(name2);
|
||||||
|
|
||||||
|
if (pos1 == null || pos2 == null) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 等级数字越小代表职级越高
|
||||||
|
return pos2.getLevel() - pos1.getLevel();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断是否为更高等级
|
||||||
|
* @param currentName 当前职级名称
|
||||||
|
* @param targetName 目标职级名称
|
||||||
|
* @return true-目标职级更高,false-目标职级不高于当前职级
|
||||||
|
*/
|
||||||
|
public static boolean isHigherLevel(String currentName, String targetName) {
|
||||||
|
return compareLevel(targetName, currentName) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有职员层级名称
|
||||||
|
* @return 职员层级名称列表
|
||||||
|
*/
|
||||||
|
public static List<String> getStaffLevelNames() {
|
||||||
|
return Arrays.asList(MINISTER.getName(), VICE_MINISTER.getName(), STAFF.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有领导层级名称
|
||||||
|
* @return 领导层级名称列表
|
||||||
|
*/
|
||||||
|
public static List<String> getLeaderLevelNames() {
|
||||||
|
return Arrays.asList(CHAIRMAN.getName(), GENERAL_MANAGER.getName(), VICE_GENERAL_MANAGER.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有职级名称(按等级排序)
|
||||||
|
* @return 所有职级名称列表
|
||||||
|
*/
|
||||||
|
public static List<String> getAllPositionNames() {
|
||||||
|
return Arrays.asList(
|
||||||
|
CHAIRMAN.getName(), GENERAL_MANAGER.getName(), VICE_GENERAL_MANAGER.getName(),
|
||||||
|
MINISTER.getName(), VICE_MINISTER.getName(), STAFF.getName()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定等级范围的职级
|
||||||
|
* @param minLevel 最小等级
|
||||||
|
* @param maxLevel 最大等级
|
||||||
|
* @return 职级名称列表
|
||||||
|
*/
|
||||||
|
public static List<String> getPositionsByLevelRange(int minLevel, int maxLevel) {
|
||||||
|
return Arrays.stream(values())
|
||||||
|
.filter(p -> p.getLevel() >= minLevel && p.getLevel() <= maxLevel)
|
||||||
|
.map(PositionLevelEnum::getName)
|
||||||
|
.collect(java.util.stream.Collectors.toList());
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -23,7 +23,25 @@ public enum SysAnnmentTypeEnum {
|
|||||||
/**
|
/**
|
||||||
* 邀请用户跳转到个人设置
|
* 邀请用户跳转到个人设置
|
||||||
*/
|
*/
|
||||||
TENANT_INVITE("tenant_invite", "url", "/system/usersetting");
|
TENANT_INVITE("tenant_invite", "url", "/system/usersetting"),
|
||||||
|
/**
|
||||||
|
* 协同工作-待办通知
|
||||||
|
* for [JHHB-136]【vue3】协同工作系统消息需要添加一个类型
|
||||||
|
*/
|
||||||
|
EOA_CO_NOTIFY("eoa_co_notify", "url", "/collaboration/pending"),
|
||||||
|
/**
|
||||||
|
* 协同工作-催办通知
|
||||||
|
* for [JHHB-136]【vue3】协同工作系统消息需要添加一个类型
|
||||||
|
*/
|
||||||
|
EOA_CO_REMIND("eoa_co_remind", "url", "/collaboration/pending"),
|
||||||
|
/**
|
||||||
|
* 督办管理-催办
|
||||||
|
*/
|
||||||
|
EOA_SUP_REMIND("eoa_sup_remind", "url", "/superivse/list"),
|
||||||
|
/**
|
||||||
|
* 督办管理-通知
|
||||||
|
*/
|
||||||
|
EOA_SUP_NOTIFY("eoa_sup_notify", "url", "/superivse/list");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 业务类型(email:邮件 bpm:流程)
|
* 业务类型(email:邮件 bpm:流程)
|
||||||
|
|||||||
@ -1,12 +1,11 @@
|
|||||||
package org.jeecg.common.exception;
|
package org.jeecg.common.exception;
|
||||||
|
|
||||||
import cn.hutool.core.util.ObjectUtil;
|
import cn.hutool.core.util.ObjectUtil;
|
||||||
import io.undertow.server.RequestTooBigException;
|
import jakarta.annotation.Resource;
|
||||||
|
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;
|
||||||
@ -23,6 +22,8 @@ import org.springframework.dao.DataIntegrityViolationException;
|
|||||||
import org.springframework.dao.DuplicateKeyException;
|
import org.springframework.dao.DuplicateKeyException;
|
||||||
import org.springframework.data.redis.connection.PoolException;
|
import org.springframework.data.redis.connection.PoolException;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.security.access.AccessDeniedException;
|
||||||
|
import org.springframework.security.core.AuthenticationException;
|
||||||
import org.springframework.util.CollectionUtils;
|
import org.springframework.util.CollectionUtils;
|
||||||
import org.springframework.validation.ObjectError;
|
import org.springframework.validation.ObjectError;
|
||||||
import org.springframework.web.HttpRequestMethodNotSupportedException;
|
import org.springframework.web.HttpRequestMethodNotSupportedException;
|
||||||
@ -34,8 +35,6 @@ import org.springframework.web.multipart.MaxUploadSizeExceededException;
|
|||||||
import org.springframework.web.multipart.MultipartException;
|
import org.springframework.web.multipart.MultipartException;
|
||||||
import org.springframework.web.servlet.NoHandlerFoundException;
|
import org.springframework.web.servlet.NoHandlerFoundException;
|
||||||
|
|
||||||
import javax.annotation.Resource;
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@ -49,9 +48,27 @@ import java.util.stream.Collectors;
|
|||||||
@Slf4j
|
@Slf4j
|
||||||
public class JeecgBootExceptionHandler {
|
public class JeecgBootExceptionHandler {
|
||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
BaseCommonService baseCommonService;
|
BaseCommonService baseCommonService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证码错误异常
|
||||||
|
*/
|
||||||
|
|
||||||
|
@ExceptionHandler(JeecgCaptchaException.class)
|
||||||
|
@ResponseStatus(HttpStatus.OK)
|
||||||
|
public Result<?> handleJeecgCaptchaException(JeecgCaptchaException e) {
|
||||||
|
log.error(e.getMessage(), e);
|
||||||
|
return Result.error(e.getCode(), e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(AuthenticationException.class)
|
||||||
|
@ResponseStatus(HttpStatus.OK)
|
||||||
|
public Result<?> handleJeecgCaptchaException(AuthenticationException e) {
|
||||||
|
log.error(e.getMessage(), e);
|
||||||
|
return Result.error(401, e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
@ExceptionHandler(MethodArgumentNotValidException.class)
|
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||||
public Result<?> handleValidationExceptions(MethodArgumentNotValidException e) {
|
public Result<?> handleValidationExceptions(MethodArgumentNotValidException e) {
|
||||||
log.error(e.getMessage(), e);
|
log.error(e.getMessage(), e);
|
||||||
@ -113,8 +130,8 @@ public class JeecgBootExceptionHandler {
|
|||||||
return Result.error("数据库中已存在该记录");
|
return Result.error("数据库中已存在该记录");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ExceptionHandler({UnauthorizedException.class, AuthorizationException.class})
|
@ExceptionHandler(AccessDeniedException.class)
|
||||||
public Result<?> handleAuthorizationException(AuthorizationException e){
|
public Result<?> handleAuthorizationException(AccessDeniedException e){
|
||||||
log.error(e.getMessage(), e);
|
log.error(e.getMessage(), e);
|
||||||
return Result.noauth("没有权限,请联系管理员分配权限!");
|
return Result.noauth("没有权限,请联系管理员分配权限!");
|
||||||
}
|
}
|
||||||
@ -180,7 +197,7 @@ public class JeecgBootExceptionHandler {
|
|||||||
@ExceptionHandler(MultipartException.class)
|
@ExceptionHandler(MultipartException.class)
|
||||||
public Result<?> handleMaxUploadSizeExceededException(MultipartException e) {
|
public Result<?> handleMaxUploadSizeExceededException(MultipartException e) {
|
||||||
Throwable cause = e.getCause();
|
Throwable cause = e.getCause();
|
||||||
if (cause instanceof IllegalStateException && cause.getCause() instanceof RequestTooBigException) {
|
if (cause instanceof IllegalStateException) {
|
||||||
log.error("文件大小超出限制: {}", cause.getMessage(), e);
|
log.error("文件大小超出限制: {}", cause.getMessage(), e);
|
||||||
addSysLog(e);
|
addSysLog(e);
|
||||||
return Result.error("文件大小超出限制, 请压缩或降低文件质量!");
|
return Result.error("文件大小超出限制, 请压缩或降低文件质量!");
|
||||||
@ -291,7 +308,7 @@ public class JeecgBootExceptionHandler {
|
|||||||
boolean isTooBigException = false;
|
boolean isTooBigException = false;
|
||||||
if(e instanceof MultipartException){
|
if(e instanceof MultipartException){
|
||||||
Throwable cause = e.getCause();
|
Throwable cause = e.getCause();
|
||||||
if (cause instanceof IllegalStateException && cause.getCause() instanceof RequestTooBigException){
|
if (cause instanceof IllegalStateException){
|
||||||
isTooBigException = true;
|
isTooBigException = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -4,6 +4,11 @@ import java.lang.annotation.*;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 将枚举类转化成字典数据
|
* 将枚举类转化成字典数据
|
||||||
|
*
|
||||||
|
* <<使用说明>>
|
||||||
|
* 1. 枚举类需以 `Enum` 结尾,并且在类上添加 `@EnumDict` 注解。
|
||||||
|
* 2. 需要手动将枚举类所在包路径** 添加到 `org.jeecg.common.system.util.ResourceUtil.BASE_SCAN_PACKAGES` 配置数组中。
|
||||||
|
*
|
||||||
* @Author taoYan
|
* @Author taoYan
|
||||||
* @Date 2022/7/8 10:34
|
* @Date 2022/7/8 10:34
|
||||||
**/
|
**/
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
package org.jeecg.common.system.base.controller;
|
package org.jeecg.common.system.base.controller;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson.JSON;
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
@ -12,20 +13,23 @@ 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;
|
||||||
import org.jeecgframework.poi.excel.entity.ImportParams;
|
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.jeecgframework.poi.handler.inter.IExcelExportServer;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
import org.springframework.web.multipart.MultipartHttpServletRequest;
|
import org.springframework.web.multipart.MultipartHttpServletRequest;
|
||||||
import org.springframework.web.servlet.ModelAndView;
|
import org.springframework.web.servlet.ModelAndView;
|
||||||
|
|
||||||
import javax.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import javax.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
@ -51,7 +55,7 @@ public class JeecgController<T, S extends IService<T>> {
|
|||||||
protected ModelAndView exportXls(HttpServletRequest request, T object, Class<T> clazz, String title) {
|
protected ModelAndView exportXls(HttpServletRequest request, T object, Class<T> clazz, String title) {
|
||||||
// Step.1 组装查询条件
|
// Step.1 组装查询条件
|
||||||
QueryWrapper<T> queryWrapper = QueryGenerator.initQueryWrapper(object, request.getParameterMap());
|
QueryWrapper<T> queryWrapper = QueryGenerator.initQueryWrapper(object, request.getParameterMap());
|
||||||
LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();
|
LoginUser sysUser = SecureUtil.currentUser();
|
||||||
|
|
||||||
// 过滤选中数据
|
// 过滤选中数据
|
||||||
String selections = request.getParameter("selections");
|
String selections = request.getParameter("selections");
|
||||||
@ -89,7 +93,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);
|
||||||
@ -127,6 +131,53 @@ public class JeecgController<T, S extends IService<T>> {
|
|||||||
return mv;
|
return mv;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 大数据导出
|
||||||
|
* @param request
|
||||||
|
* @param object
|
||||||
|
* @param clazz
|
||||||
|
* @param title
|
||||||
|
* @param pageSize 每次查询的数据量
|
||||||
|
* @return
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2025/8/11 16:11
|
||||||
|
*/
|
||||||
|
protected ModelAndView exportXlsForBigData(HttpServletRequest request, T object, Class<T> clazz, String title,Integer pageSize) {
|
||||||
|
// 组装查询条件
|
||||||
|
QueryWrapper<T> queryWrapper = QueryGenerator.initQueryWrapper(object, request.getParameterMap());
|
||||||
|
LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();
|
||||||
|
// 计算分页数
|
||||||
|
double total = service.count();
|
||||||
|
int count = (int) Math.ceil(total / pageSize);
|
||||||
|
// 过滤选中数据
|
||||||
|
String selections = request.getParameter("selections");
|
||||||
|
if (oConvertUtils.isNotEmpty(selections)) {
|
||||||
|
List<String> selectionList = Arrays.asList(selections.split(","));
|
||||||
|
queryWrapper.in("id", selectionList);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 定义IExcelExportServer
|
||||||
|
IExcelExportServer excelExportServer = (queryParams, pageNum) -> {
|
||||||
|
if (pageNum > count) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
Page<T> page = new Page<T>(pageNum, pageSize);
|
||||||
|
IPage<T> pageList = service.page(page, (QueryWrapper<T>) queryParams);
|
||||||
|
return new ArrayList<>(pageList.getRecords());
|
||||||
|
};
|
||||||
|
|
||||||
|
// AutoPoi 导出Excel
|
||||||
|
ModelAndView mv = new ModelAndView(new JeecgEntityExcelView());
|
||||||
|
//此处设置的filename无效 ,前端会重更新设置一下
|
||||||
|
mv.addObject(NormalExcelConstants.FILE_NAME, title);
|
||||||
|
mv.addObject(NormalExcelConstants.CLASS, clazz);
|
||||||
|
ExportParams exportParams = new ExportParams(title + "报表", "导出人:" + sysUser.getRealname(), title, jeecgBaseConfig.getPath().getUpload());
|
||||||
|
mv.addObject(NormalExcelConstants.PARAMS, exportParams);
|
||||||
|
mv.addObject(NormalExcelConstants.EXPORT_SERVER, excelExportServer);
|
||||||
|
mv.addObject(NormalExcelConstants.QUERY_PARAMS, queryWrapper);
|
||||||
|
return mv;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据权限导出excel,传入导出字段参数
|
* 根据权限导出excel,传入导出字段参数
|
||||||
|
|||||||
@ -414,9 +414,11 @@ public class QueryGenerator {
|
|||||||
}
|
}
|
||||||
// update-begin-author:sunjianlei date:20220119 for: 【JTC-573】 过滤空条件查询,防止 sql 拼接多余的 and
|
// update-begin-author:sunjianlei date:20220119 for: 【JTC-573】 过滤空条件查询,防止 sql 拼接多余的 and
|
||||||
List<QueryCondition> filterConditions = conditions.stream().filter(
|
List<QueryCondition> filterConditions = conditions.stream().filter(
|
||||||
rule -> oConvertUtils.isNotEmpty(rule.getField())
|
rule -> (oConvertUtils.isNotEmpty(rule.getField())
|
||||||
&& oConvertUtils.isNotEmpty(rule.getRule())
|
&& oConvertUtils.isNotEmpty(rule.getRule())
|
||||||
&& oConvertUtils.isNotEmpty(rule.getVal())
|
&& oConvertUtils.isNotEmpty(rule.getVal())
|
||||||
|
)
|
||||||
|
|| "empty".equals(rule.getRule())
|
||||||
).collect(Collectors.toList());
|
).collect(Collectors.toList());
|
||||||
if (filterConditions.size() == 0) {
|
if (filterConditions.size() == 0) {
|
||||||
return;
|
return;
|
||||||
@ -427,9 +429,12 @@ public class QueryGenerator {
|
|||||||
queryWrapper.and(andWrapper -> {
|
queryWrapper.and(andWrapper -> {
|
||||||
for (int i = 0; i < filterConditions.size(); i++) {
|
for (int i = 0; i < filterConditions.size(); i++) {
|
||||||
QueryCondition rule = filterConditions.get(i);
|
QueryCondition rule = filterConditions.get(i);
|
||||||
if (oConvertUtils.isNotEmpty(rule.getField())
|
if (
|
||||||
&& oConvertUtils.isNotEmpty(rule.getRule())
|
(
|
||||||
&& oConvertUtils.isNotEmpty(rule.getVal())) {
|
oConvertUtils.isNotEmpty(rule.getField()) && oConvertUtils.isNotEmpty(rule.getRule()) && oConvertUtils.isNotEmpty(rule.getVal())
|
||||||
|
)
|
||||||
|
|| "empty".equals(rule.getRule())
|
||||||
|
) {
|
||||||
|
|
||||||
log.debug("SuperQuery ==> " + rule.toString());
|
log.debug("SuperQuery ==> " + rule.toString());
|
||||||
|
|
||||||
@ -716,7 +721,11 @@ public class QueryGenerator {
|
|||||||
* @param value 查询条件值
|
* @param value 查询条件值
|
||||||
*/
|
*/
|
||||||
public static void addEasyQuery(QueryWrapper<?> queryWrapper, String name, QueryRuleEnum rule, Object value) {
|
public static void addEasyQuery(QueryWrapper<?> queryWrapper, String name, QueryRuleEnum rule, Object value) {
|
||||||
if (name==null || value == null || rule == null || oConvertUtils.isEmpty(value)) {
|
if (
|
||||||
|
(
|
||||||
|
name==null || value == null || rule == null || oConvertUtils.isEmpty(value)
|
||||||
|
)
|
||||||
|
&& !QueryRuleEnum.EMPTY.equals(rule)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
name = oConvertUtils.camelToUnderline(name);
|
name = oConvertUtils.camelToUnderline(name);
|
||||||
@ -728,6 +737,9 @@ public class QueryGenerator {
|
|||||||
case GE:
|
case GE:
|
||||||
queryWrapper.ge(name, value);
|
queryWrapper.ge(name, value);
|
||||||
break;
|
break;
|
||||||
|
case EMPTY:
|
||||||
|
queryWrapper.isNull(name);
|
||||||
|
break;
|
||||||
case LT:
|
case LT:
|
||||||
queryWrapper.lt(name, value);
|
queryWrapper.lt(name, value);
|
||||||
break;
|
break;
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import org.jeecg.common.system.vo.SysUserCacheInfo;
|
|||||||
import org.jeecg.common.util.SpringContextUtils;
|
import org.jeecg.common.util.SpringContextUtils;
|
||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
|||||||
@ -1,24 +1,28 @@
|
|||||||
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;
|
||||||
import com.auth0.jwt.exceptions.JWTDecodeException;
|
import com.auth0.jwt.exceptions.JWTDecodeException;
|
||||||
import com.auth0.jwt.interfaces.DecodedJWT;
|
import com.auth0.jwt.interfaces.DecodedJWT;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
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 javax.servlet.ServletResponse;
|
import java.util.stream.Stream;
|
||||||
import javax.servlet.http.HttpServletRequest;
|
|
||||||
import javax.servlet.http.HttpServletResponse;
|
import jakarta.servlet.ServletResponse;
|
||||||
import javax.servlet.http.HttpSession;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
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;
|
||||||
@ -30,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
|
||||||
@ -43,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
|
||||||
@ -53,6 +75,7 @@ public class JwtUtil {
|
|||||||
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
|
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
|
||||||
// issues/I4YH95浏览器显示乱码问题
|
// issues/I4YH95浏览器显示乱码问题
|
||||||
httpServletResponse.setHeader("Content-type", "text/html;charset=UTF-8");
|
httpServletResponse.setHeader("Content-type", "text/html;charset=UTF-8");
|
||||||
|
response.setContentType("application/json;charset=UTF-8");
|
||||||
Result jsonResult = new Result(code, errorMsg);
|
Result jsonResult = new Result(code, errorMsg);
|
||||||
jsonResult.setSuccess(false);
|
jsonResult.setSuccess(false);
|
||||||
OutputStream os = null;
|
OutputStream os = null;
|
||||||
@ -78,10 +101,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);
|
||||||
@ -99,23 +121,31 @@ public class JwtUtil {
|
|||||||
DecodedJWT jwt = JWT.decode(token);
|
DecodedJWT jwt = JWT.decode(token);
|
||||||
return jwt.getClaim("username").asString();
|
return jwt.getClaim("username").asString();
|
||||||
} catch (JWTDecodeException e) {
|
} catch (JWTDecodeException e) {
|
||||||
log.warn(e.getMessage(), e);
|
log.error(e.getMessage(), e);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 生成签名,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();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -180,7 +210,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());
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,31 +13,33 @@ import org.springframework.core.type.classreading.CachingMetadataReaderFactory;
|
|||||||
import org.springframework.core.type.classreading.MetadataReader;
|
import org.springframework.core.type.classreading.MetadataReader;
|
||||||
import org.springframework.core.type.classreading.MetadataReaderFactory;
|
import org.springframework.core.type.classreading.MetadataReaderFactory;
|
||||||
import org.springframework.util.ClassUtils;
|
import org.springframework.util.ClassUtils;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
import java.util.ArrayList;
|
import java.util.*;
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 资源加载工具类
|
* 枚举字典数据 资源加载工具类
|
||||||
|
*
|
||||||
* @Author taoYan
|
* @Author taoYan
|
||||||
* @Date 2022/7/8 10:40
|
* @Date 2022/7/8 10:40
|
||||||
**/
|
**/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class ResourceUtil {
|
public class ResourceUtil {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 多个包扫描根路径
|
||||||
|
*
|
||||||
|
* 之所以让用户手工配置扫描路径,是为了避免不必要的类加载开销,提升启动性能。
|
||||||
|
* 请务必将所有枚举类所在包路径添加到此配置中。
|
||||||
|
*/
|
||||||
|
private final static String[] BASE_SCAN_PACKAGES = {
|
||||||
|
"org.jeecg.common.constant.enums",
|
||||||
|
"org.jeecg.modules.message.enums"
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 枚举字典数据
|
* 枚举字典数据
|
||||||
*/
|
*/
|
||||||
private final static Map<String, List<DictModel>> enumDictData = new HashMap<>(5);
|
private final static Map<String, List<DictModel>> enumDictData = new HashMap<>(5);
|
||||||
|
|
||||||
/**
|
|
||||||
* 所有java类
|
|
||||||
*/
|
|
||||||
private final static String CLASS_PATTERN="/**/*.class";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 所有枚举java类
|
* 所有枚举java类
|
||||||
*/
|
*/
|
||||||
@ -45,9 +47,9 @@ public class ResourceUtil {
|
|||||||
private final static String CLASS_ENUM_PATTERN="/**/*Enum.class";
|
private final static String CLASS_ENUM_PATTERN="/**/*Enum.class";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 包路径 org.jeecg
|
* 初始化状态标识
|
||||||
*/
|
*/
|
||||||
private final static String BASE_PACKAGE = "org.jeecg";
|
private static volatile boolean initialized = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 枚举类中获取字典数据的方法名
|
* 枚举类中获取字典数据的方法名
|
||||||
@ -55,59 +57,135 @@ public class ResourceUtil {
|
|||||||
private final static String METHOD_NAME = "getDictList";
|
private final static String METHOD_NAME = "getDictList";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* 获取枚举字典数据
|
||||||
* 获取枚举类对应的字典数据 SysDictServiceImpl#queryAllDictItems()
|
* 获取枚举类对应的字典数据 SysDictServiceImpl#queryAllDictItems()
|
||||||
* @return
|
*
|
||||||
|
* @return 枚举字典数据
|
||||||
*/
|
*/
|
||||||
public static Map<String, List<DictModel>> getEnumDictData(){
|
public static Map<String, List<DictModel>> getEnumDictData() {
|
||||||
if(enumDictData.keySet().size()>0){
|
if (!initialized) {
|
||||||
return enumDictData;
|
synchronized (ResourceUtil.class) {
|
||||||
}
|
if (!initialized) {
|
||||||
ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();
|
long startTime = System.currentTimeMillis();
|
||||||
String pattern = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX + ClassUtils.convertClassNameToResourcePath(BASE_PACKAGE) + CLASS_ENUM_PATTERN;
|
log.info("【枚举字典加载】开始初始化枚举字典数据...");
|
||||||
try {
|
|
||||||
Resource[] resources = resourcePatternResolver.getResources(pattern);
|
initEnumDictData();
|
||||||
MetadataReaderFactory readerFactory = new CachingMetadataReaderFactory(resourcePatternResolver);
|
initialized = true;
|
||||||
for (Resource resource : resources) {
|
|
||||||
MetadataReader reader = readerFactory.getMetadataReader(resource);
|
long endTime = System.currentTimeMillis();
|
||||||
String classname = reader.getClassMetadata().getClassName();
|
log.info("【枚举字典加载】枚举字典数据初始化完成,共加载 {} 个字典,总耗时: {}ms", enumDictData.size(), endTime - startTime);
|
||||||
Class<?> clazz = Class.forName(classname);
|
|
||||||
EnumDict enumDict = clazz.getAnnotation(EnumDict.class);
|
|
||||||
if (enumDict != null) {
|
|
||||||
EnumDict annotation = clazz.getAnnotation(EnumDict.class);
|
|
||||||
String key = annotation.value();
|
|
||||||
if(oConvertUtils.isNotEmpty(key)){
|
|
||||||
List<DictModel> list = (List<DictModel>) clazz.getDeclaredMethod(METHOD_NAME).invoke(null);
|
|
||||||
enumDictData.put(key, list);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}catch (Exception e){
|
|
||||||
log.error("获取枚举类字典数据异常", e.getMessage());
|
|
||||||
// e.printStackTrace();
|
|
||||||
}
|
}
|
||||||
return enumDictData;
|
return enumDictData;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 用于后端字典翻译 SysDictServiceImpl#queryManyDictByKeys(java.util.List, java.util.List)
|
* 使用多包路径扫描方式初始化枚举字典数据
|
||||||
* @param dictCodeList
|
|
||||||
* @param keys
|
|
||||||
* @return
|
|
||||||
*/
|
*/
|
||||||
public static Map<String, List<DictModel>> queryManyDictByKeys(List<String> dictCodeList, List<String> keys){
|
private static void initEnumDictData() {
|
||||||
if(enumDictData.keySet().size()==0){
|
ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();
|
||||||
getEnumDictData();
|
|
||||||
|
long scanStartTime = System.currentTimeMillis();
|
||||||
|
List<Resource> allResources = new ArrayList<>();
|
||||||
|
|
||||||
|
// 扫描多个包路径
|
||||||
|
for (String basePackage : BASE_SCAN_PACKAGES) {
|
||||||
|
String pattern = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX + ClassUtils.convertClassNameToResourcePath(basePackage) + CLASS_ENUM_PATTERN;
|
||||||
|
|
||||||
|
try {
|
||||||
|
Resource[] resources = resourcePatternResolver.getResources(pattern);
|
||||||
|
allResources.addAll(Arrays.asList(resources));
|
||||||
|
log.debug("【枚举字典加载】扫描包 {} 找到 {} 个枚举类文件", basePackage, resources.length);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("【枚举字典加载】扫描包 {} 时出现异常: {}", basePackage, e.getMessage());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
long scanEndTime = System.currentTimeMillis();
|
||||||
|
log.info("【枚举字典加载】文件扫描完成,总共找到 {} 个枚举类文件,扫描耗时: {}ms", allResources.size(), scanEndTime - scanStartTime);
|
||||||
|
|
||||||
|
MetadataReaderFactory readerFactory = new CachingMetadataReaderFactory(resourcePatternResolver);
|
||||||
|
|
||||||
|
long processStartTime = System.currentTimeMillis();
|
||||||
|
int processedCount = 0;
|
||||||
|
|
||||||
|
for (Resource resource : allResources) {
|
||||||
|
try {
|
||||||
|
MetadataReader reader = readerFactory.getMetadataReader(resource);
|
||||||
|
String classname = reader.getClassMetadata().getClassName();
|
||||||
|
|
||||||
|
// 提前检查是否有@EnumDict注解,避免不必要的Class.forName
|
||||||
|
if (hasEnumDictAnnotation(reader)) {
|
||||||
|
processEnumClass(classname);
|
||||||
|
processedCount++;
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.debug("处理资源异常: {} - {}", resource.getFilename(), e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
long processEndTime = System.currentTimeMillis();
|
||||||
|
log.info("【枚举字典加载】处理完成,实际处理 {} 个带注解的枚举类,处理耗时: {}ms", processedCount, processEndTime - processStartTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查类是否有EnumDict注解(通过元数据,避免类加载)
|
||||||
|
*/
|
||||||
|
private static boolean hasEnumDictAnnotation(MetadataReader reader) {
|
||||||
|
try {
|
||||||
|
return reader.getAnnotationMetadata().hasAnnotation(EnumDict.class.getName());
|
||||||
|
} catch (Exception e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理单个枚举类
|
||||||
|
*/
|
||||||
|
private static void processEnumClass(String classname) {
|
||||||
|
try {
|
||||||
|
Class<?> clazz = Class.forName(classname);
|
||||||
|
EnumDict enumDict = clazz.getAnnotation(EnumDict.class);
|
||||||
|
|
||||||
|
if (enumDict != null) {
|
||||||
|
String key = enumDict.value();
|
||||||
|
if (oConvertUtils.isNotEmpty(key)) {
|
||||||
|
Method method = clazz.getDeclaredMethod(METHOD_NAME);
|
||||||
|
List<DictModel> list = (List<DictModel>) method.invoke(null);
|
||||||
|
enumDictData.put(key, list);
|
||||||
|
log.debug("成功加载枚举字典: {} -> {}", key, classname);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.debug("处理枚举类异常: {} - {}", classname, e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用于后端字典翻译 SysDictServiceImpl#queryManyDictByKeys(java.util.List, java.util.List)
|
||||||
|
*
|
||||||
|
* @param dictCodeList 字典编码列表
|
||||||
|
* @param keys 键值列表
|
||||||
|
* @return 字典数据映射
|
||||||
|
*/
|
||||||
|
public static Map<String, List<DictModel>> queryManyDictByKeys(List<String> dictCodeList, List<String> keys) {
|
||||||
|
Map<String, List<DictModel>> enumDict = getEnumDictData();
|
||||||
Map<String, List<DictModel>> map = new HashMap<>();
|
Map<String, List<DictModel>> map = new HashMap<>();
|
||||||
for (String code : enumDictData.keySet()) {
|
|
||||||
if(dictCodeList.indexOf(code)>=0){
|
// 使用更高效的查找方式
|
||||||
List<DictModel> dictItemList = enumDictData.get(code);
|
Set<String> dictCodeSet = new HashSet<>(dictCodeList);
|
||||||
for(DictModel dm: dictItemList){
|
Set<String> keySet = new HashSet<>(keys);
|
||||||
|
|
||||||
|
for (String code : enumDict.keySet()) {
|
||||||
|
if (dictCodeSet.contains(code)) {
|
||||||
|
List<DictModel> dictItemList = enumDict.get(code);
|
||||||
|
for (DictModel dm : dictItemList) {
|
||||||
String value = dm.getValue();
|
String value = dm.getValue();
|
||||||
if(keys.indexOf(value)>=0){
|
if (keySet.contains(value)) {
|
||||||
List<DictModel> list = new ArrayList<>();
|
List<DictModel> list = new ArrayList<>();
|
||||||
list.add(new DictModel(value, dm.getText()));
|
list.add(new DictModel(value, dm.getText()));
|
||||||
map.put(code,list);
|
map.put(code, list);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -116,21 +194,4 @@ public class ResourceUtil {
|
|||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取实现类
|
|
||||||
*
|
|
||||||
* @param classPath
|
|
||||||
*/
|
|
||||||
public static Object getImplementationClass(String classPath){
|
|
||||||
try {
|
|
||||||
Class<?> aClass = Class.forName(classPath);
|
|
||||||
return SpringContextUtils.getBean(aClass);
|
|
||||||
} catch (ClassNotFoundException e) {
|
|
||||||
log.error("类没有找到",e);
|
|
||||||
return null;
|
|
||||||
} catch (NoSuchBeanDefinitionException e){
|
|
||||||
log.error(classPath + "没有实现",e);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -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
|
||||||
*/
|
*/
|
||||||
@ -144,4 +151,34 @@ public class LoginUser {
|
|||||||
/**设备id uniapp推送用*/
|
/**设备id uniapp推送用*/
|
||||||
private String clientId;
|
private String clientId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 主岗位
|
||||||
|
*/
|
||||||
|
private String mainDepPostId;
|
||||||
|
|
||||||
|
@SensitiveField
|
||||||
|
private String salt;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
// 重新构建对象过滤一些敏感字段
|
||||||
|
LoginUser loginUser = new LoginUser();
|
||||||
|
loginUser.setId(id);
|
||||||
|
loginUser.setUsername(username);
|
||||||
|
loginUser.setRealname(realname);
|
||||||
|
loginUser.setOrgCode(orgCode);
|
||||||
|
loginUser.setSex(sex);
|
||||||
|
loginUser.setEmail(email);
|
||||||
|
loginUser.setPhone(phone);
|
||||||
|
loginUser.setDelFlag(delFlag);
|
||||||
|
loginUser.setStatus(status);
|
||||||
|
loginUser.setActivitiSync(activitiSync);
|
||||||
|
loginUser.setUserIdentity(userIdentity);
|
||||||
|
loginUser.setDepartIds(departIds);
|
||||||
|
loginUser.setPost(post);
|
||||||
|
loginUser.setTelephone(telephone);
|
||||||
|
loginUser.setRelTenantIds(relTenantIds);
|
||||||
|
loginUser.setClientId(clientId);
|
||||||
|
return JSON.toJSONString(loginUser);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import java.util.Map;
|
|||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
|
|||||||
@ -19,12 +19,13 @@ import org.springframework.jdbc.datasource.DriverManagerDataSource;
|
|||||||
import org.springframework.util.FileCopyUtils;
|
import org.springframework.util.FileCopyUtils;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import javax.sql.DataSource;
|
import javax.sql.DataSource;
|
||||||
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayInputStream;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.sql.Connection;
|
import java.sql.Connection;
|
||||||
import java.sql.DatabaseMetaData;
|
import java.sql.DatabaseMetaData;
|
||||||
import java.sql.SQLException;
|
import java.sql.SQLException;
|
||||||
@ -152,9 +153,9 @@ public class CommonUtils {
|
|||||||
*/
|
*/
|
||||||
public static String uploadLocal(MultipartFile mf,String bizPath,String uploadpath){
|
public static String uploadLocal(MultipartFile mf,String bizPath,String uploadpath){
|
||||||
try {
|
try {
|
||||||
//update-begin-author:liusq date:20210809 for: 过滤上传文件类型
|
// 文件安全校验,防止上传漏洞文件
|
||||||
SsrfFileTypeFilter.checkUploadFileType(mf);
|
SsrfFileTypeFilter.checkUploadFileType(mf, bizPath);
|
||||||
//update-end-author:liusq date:20210809 for: 过滤上传文件类型
|
|
||||||
String fileName = null;
|
String fileName = null;
|
||||||
File file = new File(uploadpath + File.separator + bizPath + File.separator );
|
File file = new File(uploadpath + File.separator + bizPath + File.separator );
|
||||||
if (!file.exists()) {
|
if (!file.exists()) {
|
||||||
@ -163,6 +164,10 @@ public class CommonUtils {
|
|||||||
}
|
}
|
||||||
// 获取文件名
|
// 获取文件名
|
||||||
String orgName = mf.getOriginalFilename();
|
String orgName = mf.getOriginalFilename();
|
||||||
|
// 无中文情况下进行转码
|
||||||
|
if (orgName != null && !CommonUtils.ifContainChinese(orgName)) {
|
||||||
|
orgName = new String(orgName.getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8);
|
||||||
|
}
|
||||||
orgName = CommonUtils.getFileName(orgName);
|
orgName = CommonUtils.getFileName(orgName);
|
||||||
if(orgName.indexOf(SymbolConstant.SPOT)!=-1){
|
if(orgName.indexOf(SymbolConstant.SPOT)!=-1){
|
||||||
fileName = orgName.substring(0, orgName.lastIndexOf(".")) + "_" + System.currentTimeMillis() + orgName.substring(orgName.lastIndexOf("."));
|
fileName = orgName.substring(0, orgName.lastIndexOf(".")) + "_" + System.currentTimeMillis() + orgName.substring(orgName.lastIndexOf("."));
|
||||||
@ -242,6 +247,10 @@ public class CommonUtils {
|
|||||||
try {
|
try {
|
||||||
DataSource dataSource = SpringContextUtils.getApplicationContext().getBean(DataSource.class);
|
DataSource dataSource = SpringContextUtils.getApplicationContext().getBean(DataSource.class);
|
||||||
dbTypeEnum = JdbcUtils.getDbType(dataSource.getConnection().getMetaData().getURL());
|
dbTypeEnum = JdbcUtils.getDbType(dataSource.getConnection().getMetaData().getURL());
|
||||||
|
//【采用SQL_SERVER2005引擎】QQYUN-13298 解决升级mybatisPlus后SqlServer分页使用OFFSET,无排序字段报错问题
|
||||||
|
if (dbTypeEnum == DbType.SQL_SERVER) {
|
||||||
|
dbTypeEnum = DbType.SQL_SERVER2005;
|
||||||
|
}
|
||||||
return dbTypeEnum;
|
return dbTypeEnum;
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
log.warn(e.getMessage(), e);
|
log.warn(e.getMessage(), e);
|
||||||
|
|||||||
@ -13,6 +13,8 @@ import java.time.LocalDate;
|
|||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.time.ZoneId;
|
import java.time.ZoneId;
|
||||||
import java.time.temporal.ChronoUnit;
|
import java.time.temporal.ChronoUnit;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Calendar;
|
import java.util.Calendar;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.GregorianCalendar;
|
import java.util.GregorianCalendar;
|
||||||
@ -814,4 +816,44 @@ public class DateUtils extends PropertyEditorSupport {
|
|||||||
return calendar1.get(Calendar.YEAR) == calendar2.get(Calendar.YEAR);
|
return calendar1.get(Calendar.YEAR) == calendar2.get(Calendar.YEAR);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取两个日期之间的所有日期列表,包含开始和结束日期
|
||||||
|
*
|
||||||
|
* @param begin
|
||||||
|
* @param end
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public static List<Date> getDateRangeList(Date begin, Date end) {
|
||||||
|
List<Date> dateList = new ArrayList<>();
|
||||||
|
if (begin == null || end == null) {
|
||||||
|
return dateList;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清除时间部分,只比较日期
|
||||||
|
Calendar beginCal = Calendar.getInstance();
|
||||||
|
beginCal.setTime(begin);
|
||||||
|
beginCal.set(Calendar.HOUR_OF_DAY, 0);
|
||||||
|
beginCal.set(Calendar.MINUTE, 0);
|
||||||
|
beginCal.set(Calendar.SECOND, 0);
|
||||||
|
beginCal.set(Calendar.MILLISECOND, 0);
|
||||||
|
|
||||||
|
Calendar endCal = Calendar.getInstance();
|
||||||
|
endCal.setTime(end);
|
||||||
|
endCal.set(Calendar.HOUR_OF_DAY, 0);
|
||||||
|
endCal.set(Calendar.MINUTE, 0);
|
||||||
|
endCal.set(Calendar.SECOND, 0);
|
||||||
|
endCal.set(Calendar.MILLISECOND, 0);
|
||||||
|
|
||||||
|
if (endCal.before(beginCal)) {
|
||||||
|
return dateList;
|
||||||
|
}
|
||||||
|
|
||||||
|
dateList.add(beginCal.getTime());
|
||||||
|
while (beginCal.before(endCal)) {
|
||||||
|
beginCal.add(Calendar.DAY_OF_YEAR, 1);
|
||||||
|
dateList.add(beginCal.getTime());
|
||||||
|
}
|
||||||
|
return dateList;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -1,14 +1,22 @@
|
|||||||
package org.jeecg.common.util;
|
package org.jeecg.common.util;
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import cn.hutool.core.io.IoUtil;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
|
||||||
|
import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;
|
||||||
import org.apache.commons.io.FilenameUtils;
|
import org.apache.commons.io.FilenameUtils;
|
||||||
|
import org.apache.commons.io.IOUtils;
|
||||||
|
import org.jeecg.common.constant.CommonConstant;
|
||||||
import org.jeecg.common.exception.JeecgBootException;
|
import org.jeecg.common.exception.JeecgBootException;
|
||||||
|
import org.jeecg.common.util.filter.SsrfFileTypeFilter;
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletResponse;
|
|
||||||
import java.io.*;
|
import java.io.*;
|
||||||
|
import java.net.HttpURLConnection;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.net.URLConnection;
|
import java.net.URLConnection;
|
||||||
import java.net.URLEncoder;
|
import java.net.URLEncoder;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.zip.ZipEntry;
|
import java.util.zip.ZipEntry;
|
||||||
@ -203,4 +211,150 @@ public class FileDownloadUtils {
|
|||||||
dir.mkdirs();
|
dir.mkdirs();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下载单个文件到ZIP流
|
||||||
|
* 核心功能:获取文件流,写入ZIP条目
|
||||||
|
* @param fileUrl 文件URL(可以是HTTP URL或本地路径)
|
||||||
|
* @param fileName ZIP内的文件名
|
||||||
|
* @param zous ZIP输出流
|
||||||
|
*/
|
||||||
|
public static void downLoadSingleFile(String fileUrl, String fileName, String uploadUrl,ZipArchiveOutputStream zous) {
|
||||||
|
InputStream inputStream = null;
|
||||||
|
try {
|
||||||
|
// 创建ZIP条目:每个文件在ZIP中都是一个独立条目
|
||||||
|
ZipArchiveEntry entry = new ZipArchiveEntry(fileName);
|
||||||
|
zous.putArchiveEntry(entry);
|
||||||
|
|
||||||
|
// 获取文件输入流:区分普通文件和快捷方式
|
||||||
|
if (fileUrl.endsWith(".url")) {
|
||||||
|
// 处理快捷方式:生成.url文件内容
|
||||||
|
inputStream = FileDownloadUtils.createInternetShortcut(fileName, fileUrl, "");
|
||||||
|
} else {
|
||||||
|
// 普通文件下载:从URL或本地路径获取流
|
||||||
|
inputStream = getDownInputStream(fileUrl,uploadUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inputStream != null) {
|
||||||
|
// 将文件流写入ZIP
|
||||||
|
IOUtils.copy(inputStream, zous);
|
||||||
|
}
|
||||||
|
// 关闭当前ZIP条目
|
||||||
|
zous.closeArchiveEntry();
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error("文件下载失败: {}", e);
|
||||||
|
} finally {
|
||||||
|
// 确保输入流关闭
|
||||||
|
IoUtil.close(inputStream);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取下载文件输入流
|
||||||
|
* 功能:根据URL类型(HTTP或本地)获取文件流
|
||||||
|
* @param fileUrl 文件URL(支持HTTP和本地路径)
|
||||||
|
* @return 文件输入流,失败返回null
|
||||||
|
*/
|
||||||
|
public static InputStream getDownInputStream(String fileUrl, String uploadUrl) {
|
||||||
|
try {
|
||||||
|
// 处理HTTP URL:通过网络下载
|
||||||
|
if (oConvertUtils.isNotEmpty(fileUrl) && fileUrl.startsWith(CommonConstant.STR_HTTP)) {
|
||||||
|
URL url = new URL(fileUrl);
|
||||||
|
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
|
||||||
|
connection.setConnectTimeout(5000); // 连接超时5秒
|
||||||
|
connection.setReadTimeout(30000); // 读取超时30秒
|
||||||
|
return connection.getInputStream();
|
||||||
|
} else {
|
||||||
|
// 处理本地文件:直接读取文件系统
|
||||||
|
String downloadFilePath = uploadUrl + File.separator + fileUrl;
|
||||||
|
// 安全检查:防止下载危险文件类型
|
||||||
|
SsrfFileTypeFilter.checkDownloadFileType(downloadFilePath);
|
||||||
|
return new BufferedInputStream(new FileInputStream(downloadFilePath));
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
// 异常时返回null,上层会处理空流情况
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取文件扩展名
|
||||||
|
* 功能:从文件名中提取扩展名
|
||||||
|
* @param fileName 文件名
|
||||||
|
* @return 文件扩展名(不含点),如"txt"、"png"
|
||||||
|
*/
|
||||||
|
public static String getFileExtension(String fileName) {
|
||||||
|
int dotIndex = fileName.lastIndexOf('.');
|
||||||
|
return (dotIndex == -1) ? "" : fileName.substring(dotIndex + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建快捷方式(.url文件内容)
|
||||||
|
* 功能:生成Internet快捷方式文件内容
|
||||||
|
* @param name 快捷方式名称
|
||||||
|
* @param url 目标URL地址
|
||||||
|
* @param icon 图标路径(可选)
|
||||||
|
* @return 包含.url文件内容的输入流
|
||||||
|
*/
|
||||||
|
public static InputStream createInternetShortcut(String name, String url, String icon) {
|
||||||
|
StringWriter sw = new StringWriter();
|
||||||
|
try {
|
||||||
|
// 按照Windows快捷方式格式写入内容
|
||||||
|
sw.write("[InternetShortcut]\n");
|
||||||
|
sw.write("URL=" + url + "\n");
|
||||||
|
if (oConvertUtils.isNotEmpty(icon)) {
|
||||||
|
sw.write("IconFile=" + icon + "\n");
|
||||||
|
}
|
||||||
|
// 将字符串内容转换为输入流
|
||||||
|
return new ByteArrayInputStream(sw.toString().getBytes(StandardCharsets.UTF_8));
|
||||||
|
} finally {
|
||||||
|
IoUtil.close(sw);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 从URL中提取文件名
|
||||||
|
* 功能:从HTTP URL或本地路径中提取纯文件名
|
||||||
|
* @param fileUrl 文件URL
|
||||||
|
* @return 文件名(不含路径)
|
||||||
|
*/
|
||||||
|
public static String getFileNameFromUrl(String fileUrl) {
|
||||||
|
try {
|
||||||
|
// 处理HTTP URL:从路径部分提取文件名
|
||||||
|
if (fileUrl.startsWith(CommonConstant.STR_HTTP)) {
|
||||||
|
URL url = new URL(fileUrl);
|
||||||
|
String path = url.getPath();
|
||||||
|
return path.substring(path.lastIndexOf('/') + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理本地文件路径:从文件路径提取文件名
|
||||||
|
return fileUrl.substring(fileUrl.lastIndexOf(File.separator) + 1);
|
||||||
|
} catch (Exception e) {
|
||||||
|
// 如果解析失败,使用时间戳作为文件名
|
||||||
|
return "file_" + System.currentTimeMillis();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 生成ZIP中的文件名
|
||||||
|
* 功能:避免文件名冲突,为多个文件添加序号
|
||||||
|
* @param fileUrl 文件URL(用于提取原始文件名)
|
||||||
|
* @param index 文件序号(从0开始)
|
||||||
|
* @param total 文件总数
|
||||||
|
* @return 处理后的文件名(带序号)
|
||||||
|
*/
|
||||||
|
public static String generateFileName(String fileUrl, int index, int total) {
|
||||||
|
// 从URL中提取原始文件名
|
||||||
|
String originalFileName = getFileNameFromUrl(fileUrl);
|
||||||
|
|
||||||
|
// 如果只有一个文件,直接使用原始文件名
|
||||||
|
if (total == 1) {
|
||||||
|
return originalFileName;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 多个文件时,使用序号+原始文件名
|
||||||
|
String extension = getFileExtension(originalFileName);
|
||||||
|
String nameWithoutExtension = originalFileName.replace("." + extension, "");
|
||||||
|
|
||||||
|
return String.format("%s_%d.%s", nameWithoutExtension, index + 1, extension);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,13 +4,13 @@ import com.alibaba.fastjson.JSON;
|
|||||||
import com.alibaba.fastjson.JSONObject;
|
import com.alibaba.fastjson.JSONObject;
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
|
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.constant.SymbolConstant;
|
import org.jeecg.common.constant.SymbolConstant;
|
||||||
import org.jeecg.common.handler.IFillRuleHandler;
|
import org.jeecg.common.handler.IFillRuleHandler;
|
||||||
import org.jeecg.common.system.query.QueryGenerator;
|
import org.jeecg.common.system.query.QueryGenerator;
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
package org.jeecg.common.util;
|
package org.jeecg.common.util;
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.jeecg.common.constant.CommonConstant;
|
import org.jeecg.common.constant.CommonConstant;
|
||||||
|
|||||||
@ -55,13 +55,11 @@ public class MinioUtil {
|
|||||||
*/
|
*/
|
||||||
public static String upload(MultipartFile file, String bizPath, String customBucket) throws Exception {
|
public static String upload(MultipartFile file, String bizPath, String customBucket) throws Exception {
|
||||||
String fileUrl = "";
|
String fileUrl = "";
|
||||||
//update-begin-author:wangshuai date:20201012 for: 过滤上传文件夹名特殊字符,防止攻击
|
// 业务路径过滤,防止攻击
|
||||||
bizPath = StrAttackFilter.filter(bizPath);
|
bizPath = StrAttackFilter.filter(bizPath);
|
||||||
//update-end-author:wangshuai date:20201012 for: 过滤上传文件夹名特殊字符,防止攻击
|
|
||||||
|
|
||||||
//update-begin-author:liusq date:20210809 for: 过滤上传文件类型
|
// 文件安全校验,防止上传漏洞文件
|
||||||
SsrfFileTypeFilter.checkUploadFileType(file);
|
SsrfFileTypeFilter.checkUploadFileType(file, bizPath);
|
||||||
//update-end-author:liusq date:20210809 for: 过滤上传文件类型
|
|
||||||
|
|
||||||
String newBucket = bucketName;
|
String newBucket = bucketName;
|
||||||
if(oConvertUtils.isNotEmpty(customBucket)){
|
if(oConvertUtils.isNotEmpty(customBucket)){
|
||||||
|
|||||||
@ -2,6 +2,7 @@ package org.jeecg.common.util;
|
|||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.context.annotation.Lazy;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import java.io.BufferedWriter;
|
import java.io.BufferedWriter;
|
||||||
@ -16,6 +17,7 @@ import java.util.List;
|
|||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Component
|
@Component
|
||||||
|
@Lazy(false)
|
||||||
public class PmsUtil {
|
public class PmsUtil {
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import com.alibaba.fastjson.JSONObject;
|
|||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.springframework.http.*;
|
import org.springframework.http.*;
|
||||||
import org.springframework.http.client.SimpleClientHttpRequestFactory;
|
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
|
||||||
import org.springframework.http.converter.StringHttpMessageConverter;
|
import org.springframework.http.converter.StringHttpMessageConverter;
|
||||||
import org.springframework.web.client.RestTemplate;
|
import org.springframework.web.client.RestTemplate;
|
||||||
|
|
||||||
@ -56,12 +56,22 @@ public class RestUtil {
|
|||||||
private final static RestTemplate RT;
|
private final static RestTemplate RT;
|
||||||
|
|
||||||
static {
|
static {
|
||||||
SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
|
//update-begin---author:chenrui ---date:20251011 for:[issues/8859]online表单java增强失效------------
|
||||||
|
// 使用 Apache HttpClient 避免 JDK HttpURLConnection 的 too many bytes written 问题
|
||||||
|
HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory();
|
||||||
|
//update-end---author:chenrui ---date:20251011 for:[issues/8859]online表单java增强失效------------
|
||||||
requestFactory.setConnectTimeout(30000);
|
requestFactory.setConnectTimeout(30000);
|
||||||
requestFactory.setReadTimeout(30000);
|
requestFactory.setReadTimeout(30000);
|
||||||
RT = new RestTemplate(requestFactory);
|
RT = new RestTemplate(requestFactory);
|
||||||
// 解决乱码问题
|
//update-begin---author:chenrui ---date:20251011 for:[issues/8859]online表单java增强失效------------
|
||||||
RT.getMessageConverters().set(1, new StringHttpMessageConverter(StandardCharsets.UTF_8));
|
// 解决乱码问题(替换 StringHttpMessageConverter 为 UTF-8)
|
||||||
|
for (int i = 0; i < RT.getMessageConverters().size(); i++) {
|
||||||
|
if (RT.getMessageConverters().get(i) instanceof StringHttpMessageConverter) {
|
||||||
|
RT.getMessageConverters().set(i, new StringHttpMessageConverter(StandardCharsets.UTF_8));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//update-end---author:chenrui ---date:20251011 for:[issues/8859]online表单java增强失效------------
|
||||||
}
|
}
|
||||||
|
|
||||||
public static RestTemplate getRestTemplate() {
|
public static RestTemplate getRestTemplate() {
|
||||||
@ -221,6 +231,72 @@ public class RestUtil {
|
|||||||
return RT.exchange(url, method, request, responseType);
|
return RT.exchange(url, method, request, responseType);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送请求(支持自定义超时时间)
|
||||||
|
*
|
||||||
|
* @param url 请求地址
|
||||||
|
* @param method 请求方式
|
||||||
|
* @param headers 请求头 可空
|
||||||
|
* @param variables 请求url参数 可空
|
||||||
|
* @param params 请求body参数 可空
|
||||||
|
* @param responseType 返回类型
|
||||||
|
* @param timeout 超时时间(毫秒),如果为0或负数则使用默认超时
|
||||||
|
* @return ResponseEntity<responseType>
|
||||||
|
*/
|
||||||
|
public static <T> ResponseEntity<T> request(String url, HttpMethod method, HttpHeaders headers,
|
||||||
|
JSONObject variables, Object params, Class<T> responseType, int timeout) {
|
||||||
|
log.info(" RestUtil --- request --- url = "+ url + ", timeout = " + timeout);
|
||||||
|
|
||||||
|
if (StringUtils.isEmpty(url)) {
|
||||||
|
throw new RuntimeException("url 不能为空");
|
||||||
|
}
|
||||||
|
if (method == null) {
|
||||||
|
throw new RuntimeException("method 不能为空");
|
||||||
|
}
|
||||||
|
if (headers == null) {
|
||||||
|
headers = new HttpHeaders();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建自定义RestTemplate(如果需要设置超时)
|
||||||
|
RestTemplate restTemplate = RT;
|
||||||
|
if (timeout > 0) {
|
||||||
|
//update-begin---author:chenrui ---date:20251011 for:[issues/8859]online表单java增强失效------------
|
||||||
|
HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory();
|
||||||
|
//update-end---author:chenrui ---date:20251011 for:[issues/8859]online表单java增强失效------------
|
||||||
|
requestFactory.setConnectTimeout(timeout);
|
||||||
|
requestFactory.setReadTimeout(timeout);
|
||||||
|
restTemplate = new RestTemplate(requestFactory);
|
||||||
|
//update-begin---author:chenrui ---date:20251011 for:[issues/8859]online表单java增强失效------------
|
||||||
|
// 解决乱码问题(替换 StringHttpMessageConverter 为 UTF-8)
|
||||||
|
for (int i = 0; i < restTemplate.getMessageConverters().size(); i++) {
|
||||||
|
if (restTemplate.getMessageConverters().get(i) instanceof StringHttpMessageConverter) {
|
||||||
|
restTemplate.getMessageConverters().set(i, new StringHttpMessageConverter(StandardCharsets.UTF_8));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//update-end---author:chenrui ---date:20251011 for:[issues/8859]online表单java增强失效------------
|
||||||
|
}
|
||||||
|
|
||||||
|
// 请求体
|
||||||
|
String body = "";
|
||||||
|
if (params != null) {
|
||||||
|
if (params instanceof JSONObject) {
|
||||||
|
body = ((JSONObject) params).toJSONString();
|
||||||
|
} else {
|
||||||
|
body = params.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 拼接 url 参数
|
||||||
|
if (variables != null && !variables.isEmpty()) {
|
||||||
|
url += ("?" + asUrlVariables(variables));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送请求
|
||||||
|
HttpEntity<String> request = new HttpEntity<>(body, headers);
|
||||||
|
return restTemplate.exchange(url, method, request, responseType);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取JSON请求头
|
* 获取JSON请求头
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -0,0 +1,32 @@
|
|||||||
|
package org.jeecg.common.util;
|
||||||
|
|
||||||
|
import org.springframework.security.core.context.SecurityContext;
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
|
import java.util.concurrent.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @date 2025-09-04
|
||||||
|
* @author scott
|
||||||
|
*
|
||||||
|
* @Description: 支持Spring Security的API,获取当前登录人方法的线程池
|
||||||
|
*/
|
||||||
|
public class ShiroThreadPoolExecutor extends ThreadPoolExecutor {
|
||||||
|
|
||||||
|
public ShiroThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
|
||||||
|
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void execute(Runnable command) {
|
||||||
|
SecurityContext context = SecurityContextHolder.getContext();
|
||||||
|
super.execute(() -> {
|
||||||
|
SecurityContext previousContext = SecurityContextHolder.getContext();
|
||||||
|
try {
|
||||||
|
SecurityContextHolder.setContext(context);
|
||||||
|
command.run();
|
||||||
|
} finally {
|
||||||
|
SecurityContextHolder.setContext(previousContext);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,7 +1,7 @@
|
|||||||
package org.jeecg.common.util;
|
package org.jeecg.common.util;
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import javax.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
import org.jeecg.common.constant.CommonConstant;
|
import org.jeecg.common.constant.CommonConstant;
|
||||||
import org.jeecg.common.constant.ServiceNameConstants;
|
import org.jeecg.common.constant.ServiceNameConstants;
|
||||||
|
|||||||
@ -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 javax.servlet.http.HttpServletRequest;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @Author scott
|
* @Author scott
|
||||||
* @Date 2019/9/23 14:12
|
* @Date 2019/9/23 14:12
|
||||||
@ -65,6 +64,10 @@ public class TokenUtils {
|
|||||||
if (tenantId == null) {
|
if (tenantId == null) {
|
||||||
tenantId = oConvertUtils.getString(request.getHeader(CommonConstant.TENANT_ID));
|
tenantId = oConvertUtils.getString(request.getHeader(CommonConstant.TENANT_ID));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (oConvertUtils.isNotEmpty(tenantId) && "undefined".equals(tenantId)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return tenantId;
|
return tenantId;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -106,8 +109,8 @@ public class TokenUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 查询用户信息
|
// 查询用户信息
|
||||||
LoginUser user = TokenUtils.getLoginUser(username, commonApi, redisUtil);
|
//LoginUser user = TokenUtils.getLoginUser(username, commonApi, redisUtil);
|
||||||
//LoginUser user = commonApi.getUserByName(username);
|
LoginUser user = commonApi.getUserByName(username);
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
throw new JeecgBoot401Exception("用户不存在!");
|
throw new JeecgBoot401Exception("用户不存在!");
|
||||||
}
|
}
|
||||||
@ -158,10 +161,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,10 +1,9 @@
|
|||||||
package org.jeecg.common.util.encryption;
|
package org.jeecg.common.util.encryption;
|
||||||
|
|
||||||
import org.apache.shiro.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 加密
|
||||||
@ -49,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();
|
||||||
@ -67,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");
|
||||||
|
|||||||
@ -2,6 +2,7 @@ package org.jeecg.common.util.filter;
|
|||||||
|
|
||||||
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.exception.JeecgBootException;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@ -149,29 +150,38 @@ public class SsrfFileTypeFilter {
|
|||||||
public static void checkDownloadFileType(String filePath) throws IOException {
|
public static void checkDownloadFileType(String filePath) throws IOException {
|
||||||
//文件后缀
|
//文件后缀
|
||||||
String suffix = getFileTypeBySuffix(filePath);
|
String suffix = getFileTypeBySuffix(filePath);
|
||||||
log.info("suffix:{}", suffix);
|
log.debug(" 【文件下载校验】文件后缀 suffix: {}", suffix);
|
||||||
boolean isAllowExtension = FILE_TYPE_WHITE_LIST.contains(suffix.toLowerCase());
|
boolean isAllowExtension = FILE_TYPE_WHITE_LIST.contains(suffix.toLowerCase());
|
||||||
//是否允许下载的文件
|
//是否允许下载的文件
|
||||||
if (!isAllowExtension) {
|
if (!isAllowExtension) {
|
||||||
throw new IOException("下载失败,存在非法文件类型:" + suffix);
|
throw new JeecgBootException("下载失败,存在非法文件类型:" + suffix);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 上传文件类型过滤
|
* 上传文件类型过滤
|
||||||
*
|
*
|
||||||
* @param file
|
* @param file
|
||||||
*/
|
*/
|
||||||
public static void checkUploadFileType(MultipartFile file) throws Exception {
|
public static void checkUploadFileType(MultipartFile file) throws Exception {
|
||||||
//获取文件真是后缀
|
checkUploadFileType(file, null);
|
||||||
String suffix = getFileType(file);
|
}
|
||||||
|
|
||||||
log.info("suffix:{}", suffix);
|
/**
|
||||||
|
* 上传文件类型过滤
|
||||||
|
*
|
||||||
|
* @param file
|
||||||
|
*/
|
||||||
|
public static void checkUploadFileType(MultipartFile file, String customPath) throws Exception {
|
||||||
|
//1. 路径安全校验
|
||||||
|
validatePathSecurity(customPath);
|
||||||
|
//2. 校验文件后缀和头
|
||||||
|
String suffix = getFileType(file, customPath);
|
||||||
|
log.info("【文件上传校验】文件后缀 suffix: {},customPath:{}", suffix, customPath);
|
||||||
boolean isAllowExtension = FILE_TYPE_WHITE_LIST.contains(suffix.toLowerCase());
|
boolean isAllowExtension = FILE_TYPE_WHITE_LIST.contains(suffix.toLowerCase());
|
||||||
//是否允许下载的文件
|
//是否允许下载的文件
|
||||||
if (!isAllowExtension) {
|
if (!isAllowExtension) {
|
||||||
throw new Exception("上传失败,存在非法文件类型:" + suffix);
|
throw new JeecgBootException("上传失败,存在非法文件类型:" + suffix);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -183,7 +193,7 @@ public class SsrfFileTypeFilter {
|
|||||||
* @throws Exception
|
* @throws Exception
|
||||||
*/
|
*/
|
||||||
|
|
||||||
private static String getFileType(MultipartFile file) throws Exception {
|
private static String getFileType(MultipartFile file, String customPath) throws Exception {
|
||||||
//update-begin-author:liusq date:20230404 for: [issue/4672]方法造成的文件被占用,注释掉此方法tomcat就能自动清理掉临时文件
|
//update-begin-author:liusq date:20230404 for: [issue/4672]方法造成的文件被占用,注释掉此方法tomcat就能自动清理掉临时文件
|
||||||
String fileExtendName = null;
|
String fileExtendName = null;
|
||||||
InputStream is = null;
|
InputStream is = null;
|
||||||
@ -203,7 +213,7 @@ public class SsrfFileTypeFilter {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
log.info("-----获取到的指定文件类型------"+fileExtendName);
|
log.debug("-----获取到的指定文件类型------"+fileExtendName);
|
||||||
// 如果不是上述类型,则判断扩展名
|
// 如果不是上述类型,则判断扩展名
|
||||||
if (StringUtils.isBlank(fileExtendName)) {
|
if (StringUtils.isBlank(fileExtendName)) {
|
||||||
String fileName = file.getOriginalFilename();
|
String fileName = file.getOriginalFilename();
|
||||||
@ -214,7 +224,6 @@ public class SsrfFileTypeFilter {
|
|||||||
// 如果有扩展名,则返回扩展名
|
// 如果有扩展名,则返回扩展名
|
||||||
return getFileTypeBySuffix(fileName);
|
return getFileTypeBySuffix(fileName);
|
||||||
}
|
}
|
||||||
log.info("-----最終的文件类型------"+fileExtendName);
|
|
||||||
is.close();
|
is.close();
|
||||||
return fileExtendName;
|
return fileExtendName;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
@ -249,4 +258,34 @@ public class SsrfFileTypeFilter {
|
|||||||
}
|
}
|
||||||
return stringBuilder.toString();
|
return stringBuilder.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 路径安全校验
|
||||||
|
*/
|
||||||
|
private static void validatePathSecurity(String customPath) throws JeecgBootException {
|
||||||
|
if (customPath == null || customPath.trim().isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 统一分隔符为 /
|
||||||
|
String normalized = customPath.replace("\\", "/");
|
||||||
|
|
||||||
|
// 1. 防止路径遍历攻击
|
||||||
|
if (normalized.contains("..") || normalized.contains("~")) {
|
||||||
|
throw new JeecgBootException("上传业务路径包含非法字符!");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 限制路径深度
|
||||||
|
int depth = normalized.split("/").length;
|
||||||
|
if (depth > 5) {
|
||||||
|
throw new JeecgBootException("上传业务路径深度超出限制!");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 限制字符集(只允许字母、数字、下划线、横线、斜杠)
|
||||||
|
if (!normalized.matches("^[a-zA-Z0-9/_-]+$")) {
|
||||||
|
throw new JeecgBootException("上传业务路径包含非法字符!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,7 +10,7 @@ import org.jeecg.common.constant.SymbolConstant;
|
|||||||
import org.jeecg.config.mybatis.MybatisPlusSaasConfig;
|
import org.jeecg.config.mybatis.MybatisPlusSaasConfig;
|
||||||
import org.springframework.beans.BeanUtils;
|
import org.springframework.beans.BeanUtils;
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
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;
|
||||||
@ -474,6 +474,23 @@ public class oConvertUtils {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断字符串是否为JSON格式
|
||||||
|
* @param str
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public static boolean isJson(String str) {
|
||||||
|
if (str == null || str.trim().isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
com.alibaba.fastjson.JSON.parse(str);
|
||||||
|
return true;
|
||||||
|
} catch (Exception e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取Map对象
|
* 获取Map对象
|
||||||
*/
|
*/
|
||||||
@ -1132,7 +1149,15 @@ public class oConvertUtils {
|
|||||||
* @date 2020/9/12 15:50
|
* @date 2020/9/12 15:50
|
||||||
*/
|
*/
|
||||||
public static <T> boolean isIn(T obj, T... objs) {
|
public static <T> boolean isIn(T obj, T... objs) {
|
||||||
return isIn(obj, objs);
|
if (isEmpty(objs)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (T obj1 : objs) {
|
||||||
|
if (isEqual(obj, obj1)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -97,9 +97,8 @@ public class OssBootUtil {
|
|||||||
* @return oss 中的相对文件路径
|
* @return oss 中的相对文件路径
|
||||||
*/
|
*/
|
||||||
public static String upload(MultipartFile file, String fileDir,String customBucket) throws Exception {
|
public static String upload(MultipartFile file, String fileDir,String customBucket) throws Exception {
|
||||||
//update-begin-author:liusq date:20210809 for: 过滤上传文件类型
|
// 文件安全校验,防止上传漏洞文件
|
||||||
SsrfFileTypeFilter.checkUploadFileType(file);
|
SsrfFileTypeFilter.checkUploadFileType(file);
|
||||||
//update-end-author:liusq date:20210809 for: 过滤上传文件类型
|
|
||||||
|
|
||||||
String filePath = null;
|
String filePath = null;
|
||||||
initOss(endPoint, accessKeyId, accessKeySecret);
|
initOss(endPoint, accessKeyId, accessKeySecret);
|
||||||
|
|||||||
@ -3,13 +3,14 @@ package org.jeecg.config;
|
|||||||
import org.jeecgframework.core.util.ApplicationContextUtil;
|
import org.jeecgframework.core.util.ApplicationContextUtil;
|
||||||
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.Lazy;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @Author: Scott
|
* @Author: Scott
|
||||||
* @Date: 2018/2/7
|
* @Date: 2018/2/7
|
||||||
* @description: autopoi 配置类
|
* @description: autopoi 配置类
|
||||||
*/
|
*/
|
||||||
|
@Lazy(false)
|
||||||
@Configuration
|
@Configuration
|
||||||
public class AutoPoiConfig {
|
public class AutoPoiConfig {
|
||||||
|
|
||||||
|
|||||||
@ -3,7 +3,7 @@ package org.jeecg.config;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import javax.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
|
|
||||||
import org.jeecg.common.api.CommonAPI;
|
import org.jeecg.common.api.CommonAPI;
|
||||||
import org.jeecg.common.system.vo.DictModel;
|
import org.jeecg.common.system.vo.DictModel;
|
||||||
@ -25,6 +25,7 @@ import lombok.extern.slf4j.Slf4j;
|
|||||||
* @Version:1.0
|
* @Version:1.0
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
|
@Lazy(false)
|
||||||
@Service
|
@Service
|
||||||
public class AutoPoiDictConfig implements AutoPoiDictServiceI {
|
public class AutoPoiDictConfig implements AutoPoiDictServiceI {
|
||||||
final static String EXCEL_SPLIT_TAG = "_";
|
final static String EXCEL_SPLIT_TAG = "_";
|
||||||
|
|||||||
@ -2,7 +2,9 @@ package org.jeecg.config;
|
|||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
import javax.servlet.*;
|
import com.alibaba.druid.spring.boot3.autoconfigure.DruidDataSourceAutoConfigure;
|
||||||
|
import com.alibaba.druid.spring.boot3.autoconfigure.properties.DruidStatProperties;
|
||||||
|
import jakarta.servlet.*;
|
||||||
|
|
||||||
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
|
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
@ -11,8 +13,6 @@ import org.springframework.boot.web.servlet.FilterRegistrationBean;
|
|||||||
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 com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure;
|
|
||||||
import com.alibaba.druid.spring.boot.autoconfigure.properties.DruidStatProperties;
|
|
||||||
import com.alibaba.druid.util.Utils;
|
import com.alibaba.druid.util.Utils;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import java.util.HashMap;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @author eightmonth@qq.com
|
||||||
* 启动程序修改DruidWallConfig配置
|
* 启动程序修改DruidWallConfig配置
|
||||||
* 允许SELECT语句的WHERE子句是一个永真条件
|
* 允许SELECT语句的WHERE子句是一个永真条件
|
||||||
* @author eightmonth
|
* @author eightmonth
|
||||||
|
|||||||
@ -1,7 +1,10 @@
|
|||||||
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;
|
||||||
|
|
||||||
|
|
||||||
@ -11,6 +14,7 @@ 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 {
|
||||||
/**
|
/**
|
||||||
* 签名密钥串(字典等敏感接口)
|
* 签名密钥串(字典等敏感接口)
|
||||||
@ -36,10 +40,6 @@ public class JeecgBaseConfig {
|
|||||||
*/
|
*/
|
||||||
private Firewall firewall;
|
private Firewall firewall;
|
||||||
|
|
||||||
/**
|
|
||||||
* shiro拦截排除
|
|
||||||
*/
|
|
||||||
private Shiro shiro;
|
|
||||||
/**
|
/**
|
||||||
* 上传文件配置
|
* 上传文件配置
|
||||||
*/
|
*/
|
||||||
@ -72,11 +72,6 @@ public class JeecgBaseConfig {
|
|||||||
*/
|
*/
|
||||||
private BaiduApi baiduApi;
|
private BaiduApi baiduApi;
|
||||||
|
|
||||||
/**
|
|
||||||
* 高德开放API配置
|
|
||||||
*/
|
|
||||||
private GaoDeApi gaoDeApi;
|
|
||||||
|
|
||||||
public String getCustomResourcePrefixPath() {
|
public String getCustomResourcePrefixPath() {
|
||||||
return customResourcePrefixPath;
|
return customResourcePrefixPath;
|
||||||
}
|
}
|
||||||
@ -109,14 +104,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;
|
||||||
}
|
}
|
||||||
@ -173,11 +160,4 @@ public class JeecgBaseConfig {
|
|||||||
this.baiduApi = baiduApi;
|
this.baiduApi = baiduApi;
|
||||||
}
|
}
|
||||||
|
|
||||||
public GaoDeApi getGaoDeApi() {
|
|
||||||
return gaoDeApi;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setGaoDeApi(GaoDeApi gaoDeApi) {
|
|
||||||
this.gaoDeApi = gaoDeApi;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,29 @@
|
|||||||
|
package org.jeecg.config;
|
||||||
|
|
||||||
|
import org.jeecg.config.vo.GaoDeApi;
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.context.annotation.Lazy;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 高德账号配置
|
||||||
|
*/
|
||||||
|
@Lazy(false)
|
||||||
|
@Configuration("jeecgGaodeBaseConfig")
|
||||||
|
@ConfigurationProperties(prefix = "jeecg.jmreport")
|
||||||
|
public class JeecgGaodeBaseConfig {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 高德开放API配置
|
||||||
|
*/
|
||||||
|
private GaoDeApi gaoDeApi;
|
||||||
|
|
||||||
|
public GaoDeApi getGaoDeApi() {
|
||||||
|
return gaoDeApi;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setGaoDeApi(GaoDeApi gaoDeApi) {
|
||||||
|
this.gaoDeApi = gaoDeApi;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -2,12 +2,14 @@ package org.jeecg.config;
|
|||||||
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.context.annotation.Lazy;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 设置静态参数初始化
|
* 设置静态参数初始化
|
||||||
* @author: jeecg-boot
|
* @author: jeecg-boot
|
||||||
*/
|
*/
|
||||||
|
@Lazy(false)
|
||||||
@Component
|
@Component
|
||||||
@Data
|
@Data
|
||||||
public class StaticConfig {
|
public class StaticConfig {
|
||||||
|
|||||||
@ -10,11 +10,13 @@ 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 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.customizers.OperationCustomizer;
|
||||||
import org.springdoc.core.filters.GlobalOpenApiMethodFilter;
|
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.context.annotation.PropertySource;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.method.HandlerMethod;
|
||||||
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
|
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
|
||||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||||
|
|
||||||
@ -61,40 +63,71 @@ public class Swagger3Config implements WebMvcConfigurer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public GlobalOpenApiCustomizer globalOpenApiCustomizer() {
|
public OperationCustomizer operationCustomizer() {
|
||||||
return openApi -> {
|
return (operation, handlerMethod) -> {
|
||||||
// 全局添加鉴权参数
|
String path = getFullPath(handlerMethod);
|
||||||
if (openApi.getPaths() != null) {
|
if (!isExcludedPath(path)) {
|
||||||
openApi.getPaths().forEach((path, pathItem) -> {
|
operation.addSecurityItem(new SecurityRequirement().addList(CommonConstant.X_ACCESS_TOKEN));
|
||||||
//log.debug("path: {}", path);
|
}else{
|
||||||
// 检查当前路径是否在排除列表中
|
log.info("忽略加入 X_ACCESS_TOKEN 的 PATH:" + path);
|
||||||
boolean isExcluded = excludedPaths.stream().anyMatch(
|
|
||||||
excludedPath -> excludedPath.equals(path) || (excludedPath.endsWith("**") && path.startsWith(excludedPath.substring(0, excludedPath.length() - 2)))
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!isExcluded) {
|
|
||||||
// 接口添加鉴权参数
|
|
||||||
pathItem.readOperations().forEach(operation ->
|
|
||||||
operation.addSecurityItem(new SecurityRequirement().addList(CommonConstant.X_ACCESS_TOKEN))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
return operation;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String getFullPath(HandlerMethod handlerMethod) {
|
||||||
|
StringBuilder fullPath = new StringBuilder();
|
||||||
|
|
||||||
|
// 获取类级别的路径
|
||||||
|
RequestMapping classMapping = handlerMethod.getBeanType().getAnnotation(RequestMapping.class);
|
||||||
|
if (classMapping != null && classMapping.value().length > 0) {
|
||||||
|
fullPath.append(classMapping.value()[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取方法级别的路径
|
||||||
|
RequestMapping methodMapping = handlerMethod.getMethodAnnotation(RequestMapping.class);
|
||||||
|
if (methodMapping != null && methodMapping.value().length > 0) {
|
||||||
|
String methodPath = methodMapping.value()[0];
|
||||||
|
// 确保路径正确拼接,处理斜杠
|
||||||
|
if (!fullPath.toString().endsWith("/") && !methodPath.startsWith("/")) {
|
||||||
|
fullPath.append("/");
|
||||||
|
}
|
||||||
|
fullPath.append(methodPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
return fullPath.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private boolean isExcludedPath(String path) {
|
||||||
|
return excludedPaths.stream()
|
||||||
|
.anyMatch(pattern -> {
|
||||||
|
if (pattern.endsWith("/**")) {
|
||||||
|
// 处理通配符匹配
|
||||||
|
String basePath = pattern.substring(0, pattern.length() - 3);
|
||||||
|
return path.startsWith(basePath);
|
||||||
|
}
|
||||||
|
// 精确匹配
|
||||||
|
return pattern.equals(path);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public OpenAPI customOpenAPI() {
|
public OpenAPI customOpenAPI() {
|
||||||
return new OpenAPI()
|
return new OpenAPI()
|
||||||
.info(new Info()
|
.info(new Info()
|
||||||
.title("JeecgBoot 后台服务API接口文档")
|
.title("JeecgBoot 后台服务API接口文档")
|
||||||
.version("3.8.2")
|
.version("3.8.3")
|
||||||
.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))
|
.addSecurityItem(new SecurityRequirement().addList(CommonConstant.X_ACCESS_TOKEN))
|
||||||
.components(new Components().addSecuritySchemes(CommonConstant.X_ACCESS_TOKEN,
|
.components(new Components().addSecuritySchemes(CommonConstant.X_ACCESS_TOKEN,
|
||||||
new SecurityScheme().name(CommonConstant.X_ACCESS_TOKEN).type(SecurityScheme.Type.HTTP)));
|
new SecurityScheme()
|
||||||
|
.name(CommonConstant.X_ACCESS_TOKEN)
|
||||||
|
.type(SecurityScheme.Type.APIKEY)
|
||||||
|
.in(SecurityScheme.In.HEADER) // 关键:指定为 header
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
//package org.jeecg.config;
|
||||||
|
//
|
||||||
|
//import io.undertow.server.DefaultByteBufferPool;
|
||||||
|
//import io.undertow.websockets.jsr.WebSocketDeploymentInfo;
|
||||||
|
//import org.springframework.boot.web.embedded.undertow.UndertowServletWebServerFactory;
|
||||||
|
//import org.springframework.boot.web.server.WebServerFactoryCustomizer;
|
||||||
|
//import org.springframework.stereotype.Component;
|
||||||
|
//
|
||||||
|
//@Component
|
||||||
|
//public class UndertowCustomizer implements WebServerFactoryCustomizer<UndertowServletWebServerFactory> {
|
||||||
|
// @Override
|
||||||
|
// public void customize(UndertowServletWebServerFactory factory) {
|
||||||
|
// factory.addDeploymentInfoCustomizers(deploymentInfo -> {
|
||||||
|
// WebSocketDeploymentInfo webSocketDeploymentInfo = new WebSocketDeploymentInfo();
|
||||||
|
// webSocketDeploymentInfo.setBuffers(new DefaultByteBufferPool(false, 1024));
|
||||||
|
// deploymentInfo.addServletContextAttribute("io.undertow.websockets.jsr.WebSocketDeploymentInfo", webSocketDeploymentInfo);
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
//}
|
||||||
@ -10,17 +10,18 @@ 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.prometheus.PrometheusMeterRegistry;
|
import io.micrometer.prometheusmetrics.PrometheusMeterRegistry;
|
||||||
|
import jakarta.annotation.PostConstruct;
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.beans.factory.annotation.Qualifier;
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
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.context.event.ApplicationReadyEvent;
|
|
||||||
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.context.event.EventListener;
|
|
||||||
import org.springframework.http.CacheControl;
|
import org.springframework.http.CacheControl;
|
||||||
import org.springframework.http.converter.HttpMessageConverter;
|
import org.springframework.http.converter.HttpMessageConverter;
|
||||||
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
|
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
|
||||||
@ -32,7 +33,6 @@ import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry
|
|||||||
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
|
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
|
||||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||||
|
|
||||||
import javax.annotation.Resource;
|
|
||||||
import java.text.SimpleDateFormat;
|
import java.text.SimpleDateFormat;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
@ -47,6 +47,7 @@ import java.util.concurrent.TimeUnit;
|
|||||||
* @Author qinfeng
|
* @Author qinfeng
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
@Slf4j
|
||||||
@Configuration
|
@Configuration
|
||||||
public class WebMvcConfiguration implements WebMvcConfigurer {
|
public class WebMvcConfiguration implements WebMvcConfigurer {
|
||||||
|
|
||||||
@ -154,16 +155,17 @@ public class WebMvcConfiguration implements WebMvcConfigurer {
|
|||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 监听应用启动完成事件,确保 PrometheusMeterRegistry 已经初始化
|
* 在Bean初始化完成后立即配置PrometheusMeterRegistry,避免在Meter注册后才配置MeterFilter
|
||||||
* for [QQYUN-12558]【监控】系统监控的头两个tab不好使,接口404
|
* for [QQYUN-12558]【监控】系统监控的头两个tab不好使,接口404
|
||||||
* @param event
|
|
||||||
* @author chenrui
|
* @author chenrui
|
||||||
* @date 2025/5/26 16:46
|
* @date 2025/5/26 16:46
|
||||||
*/
|
*/
|
||||||
@EventListener
|
@PostConstruct
|
||||||
public void onApplicationReady(ApplicationReadyEvent event) {
|
public void initPrometheusMeterRegistry() {
|
||||||
if(null != meterRegistryPostProcessor){
|
// 确保在应用启动早期就配置MeterFilter,避免警告
|
||||||
meterRegistryPostProcessor.postProcessAfterInitialization(prometheusMeterRegistry, "");
|
if (null != meterRegistryPostProcessor && null != prometheusMeterRegistry) {
|
||||||
|
meterRegistryPostProcessor.postProcessAfterInitialization(prometheusMeterRegistry, "prometheusMeterRegistry");
|
||||||
|
log.info("PrometheusMeterRegistry配置完成");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -3,8 +3,8 @@ package org.jeecg.config.filter;
|
|||||||
import org.jeecg.common.constant.CommonConstant;
|
import org.jeecg.common.constant.CommonConstant;
|
||||||
import org.jeecg.config.sign.util.BodyReaderHttpServletRequestWrapper;
|
import org.jeecg.config.sign.util.BodyReaderHttpServletRequestWrapper;
|
||||||
|
|
||||||
import javax.servlet.*;
|
import jakarta.servlet.*;
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -7,9 +7,9 @@ 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 javax.servlet.*;
|
import jakarta.servlet.*;
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import javax.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -1,6 +1,9 @@
|
|||||||
package org.jeecg.config.firewall.interceptor;
|
package org.jeecg.config.firewall.interceptor;
|
||||||
|
|
||||||
import com.alibaba.fastjson.JSON;
|
import com.alibaba.fastjson.JSON;
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.shiro.SecurityUtils;
|
import org.apache.shiro.SecurityUtils;
|
||||||
import org.jeecg.common.api.CommonAPI;
|
import org.jeecg.common.api.CommonAPI;
|
||||||
@ -14,9 +17,6 @@ import org.jeecg.config.JeecgBaseConfig;
|
|||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.web.servlet.HandlerInterceptor;
|
import org.springframework.web.servlet.HandlerInterceptor;
|
||||||
|
|
||||||
import javax.annotation.Resource;
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
|
||||||
import javax.servlet.http.HttpServletResponse;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.PrintWriter;
|
import java.io.PrintWriter;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|||||||
@ -6,13 +6,13 @@ import org.apache.ibatis.executor.Executor;
|
|||||||
import org.apache.ibatis.mapping.MappedStatement;
|
import org.apache.ibatis.mapping.MappedStatement;
|
||||||
import org.apache.ibatis.mapping.SqlCommandType;
|
import org.apache.ibatis.mapping.SqlCommandType;
|
||||||
import org.apache.ibatis.plugin.*;
|
import org.apache.ibatis.plugin.*;
|
||||||
import org.apache.shiro.SecurityUtils;
|
|
||||||
import org.jeecg.common.config.TenantContext;
|
import org.jeecg.common.config.TenantContext;
|
||||||
import org.jeecg.common.constant.TenantConstant;
|
import org.jeecg.common.constant.TenantConstant;
|
||||||
import org.jeecg.common.system.vo.LoginUser;
|
import org.jeecg.common.system.vo.LoginUser;
|
||||||
import org.jeecg.common.util.SpringContextUtils;
|
import org.jeecg.common.util.SpringContextUtils;
|
||||||
import org.jeecg.common.util.TokenUtils;
|
import org.jeecg.common.util.TokenUtils;
|
||||||
import org.jeecg.common.util.oConvertUtils;
|
import org.jeecg.common.util.oConvertUtils;
|
||||||
|
import org.jeecg.config.security.utils.SecureUtil;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import java.lang.reflect.Field;
|
import java.lang.reflect.Field;
|
||||||
@ -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;
|
||||||
|
|||||||
@ -1,16 +1,18 @@
|
|||||||
package org.jeecg.config.mybatis;
|
package org.jeecg.config.mybatis;
|
||||||
|
|
||||||
import java.sql.SQLException;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import cn.hutool.core.util.ObjectUtil;
|
import cn.hutool.core.util.ObjectUtil;
|
||||||
import com.baomidou.mybatisplus.annotation.DbType;
|
import com.baomidou.mybatisplus.annotation.DbType;
|
||||||
|
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
|
||||||
|
import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler;
|
||||||
import com.baomidou.mybatisplus.extension.plugins.inner.DynamicTableNameInnerInterceptor;
|
import com.baomidou.mybatisplus.extension.plugins.inner.DynamicTableNameInnerInterceptor;
|
||||||
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
|
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
|
||||||
|
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
|
||||||
|
import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
|
||||||
import com.baomidou.mybatisplus.extension.toolkit.JdbcUtils;
|
import com.baomidou.mybatisplus.extension.toolkit.JdbcUtils;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import me.zhyd.oauth.log.Log;
|
import me.zhyd.oauth.log.Log;
|
||||||
|
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;
|
||||||
@ -22,14 +24,10 @@ import org.springframework.beans.factory.annotation.Autowired;
|
|||||||
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 com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
|
|
||||||
import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler;
|
|
||||||
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
|
|
||||||
import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
|
|
||||||
|
|
||||||
import net.sf.jsqlparser.expression.Expression;
|
|
||||||
import net.sf.jsqlparser.expression.LongValue;
|
|
||||||
import javax.sql.DataSource;
|
import javax.sql.DataSource;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 单数据源配置(jeecg.datasource.open = false时生效)
|
* 单数据源配置(jeecg.datasource.open = false时生效)
|
||||||
|
|||||||
@ -11,7 +11,7 @@ import org.jeecg.common.util.SpringContextUtils;
|
|||||||
import org.jeecg.config.mybatis.ThreadLocalDataHelper;
|
import org.jeecg.config.mybatis.ThreadLocalDataHelper;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -6,8 +6,8 @@ import org.apache.commons.lang3.StringUtils;
|
|||||||
import org.springframework.web.servlet.HandlerInterceptor;
|
import org.springframework.web.servlet.HandlerInterceptor;
|
||||||
import org.springframework.web.servlet.ModelAndView;
|
import org.springframework.web.servlet.ModelAndView;
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import javax.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 动态数据源切换拦截器
|
* 动态数据源切换拦截器
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
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;
|
||||||
@ -8,11 +9,13 @@ import org.springframework.beans.factory.annotation.Value;
|
|||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
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.Lazy;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Minio文件上传配置文件
|
* Minio文件上传配置文件
|
||||||
* @author: jeecg-boot
|
* @author: jeecg-boot
|
||||||
*/
|
*/
|
||||||
|
@Lazy(false)
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Configuration
|
@Configuration
|
||||||
@ConditionalOnProperty(prefix = "jeecg.minio", name = "minio_url")
|
@ConditionalOnProperty(prefix = "jeecg.minio", name = "minio_url")
|
||||||
@ -26,7 +29,7 @@ public class MinioConfig {
|
|||||||
@Value(value = "${jeecg.minio.bucketName}")
|
@Value(value = "${jeecg.minio.bucketName}")
|
||||||
private String bucketName;
|
private String bucketName;
|
||||||
|
|
||||||
@Bean
|
@PostConstruct
|
||||||
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,15 +1,18 @@
|
|||||||
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;
|
||||||
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.Lazy;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 云存储 配置
|
* 云存储 配置
|
||||||
* @author: jeecg-boot
|
* @author: jeecg-boot
|
||||||
*/
|
*/
|
||||||
|
@Lazy(false)
|
||||||
@Configuration
|
@Configuration
|
||||||
@ConditionalOnProperty(prefix = "jeecg.oss", name = "endpoint")
|
@ConditionalOnProperty(prefix = "jeecg.oss", name = "endpoint")
|
||||||
public class OssConfiguration {
|
public class OssConfiguration {
|
||||||
@ -26,7 +29,7 @@ public class OssConfiguration {
|
|||||||
private String staticDomain;
|
private String staticDomain;
|
||||||
|
|
||||||
|
|
||||||
@Bean
|
@PostConstruct
|
||||||
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,80 @@
|
|||||||
|
package org.jeecg.config.security;
|
||||||
|
|
||||||
|
import jakarta.servlet.FilterChain;
|
||||||
|
import jakarta.servlet.ServletException;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletRequestWrapper;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.core.annotation.Order;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
import org.springframework.web.filter.OncePerRequestFilter;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 仪盘表请求query体携带的token
|
||||||
|
* @author eightmonth
|
||||||
|
* @date 2024/7/3 14:04
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
@Order(value = Integer.MIN_VALUE)
|
||||||
|
public class CopyTokenFilter extends OncePerRequestFilter {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
|
||||||
|
throws ServletException, IOException {
|
||||||
|
// 以下为undertow定制代码,如切换其它servlet容器,需要同步更换
|
||||||
|
String token = request.getHeader("Authorization");
|
||||||
|
String bearerToken = request.getParameter("token");
|
||||||
|
String headerBearerToken = request.getHeader("X-Access-Token");
|
||||||
|
String finalToken;
|
||||||
|
|
||||||
|
log.debug("【仪盘表请求query体携带的token】CopyTokenFilter token: {}, bearerToken: {}, headerBearerToken: {}", token, bearerToken, headerBearerToken);
|
||||||
|
|
||||||
|
if (StringUtils.hasText(token)) {
|
||||||
|
finalToken = "bearer " + token;
|
||||||
|
} else if (StringUtils.hasText(bearerToken)) {
|
||||||
|
finalToken = "bearer " + bearerToken;
|
||||||
|
} else if (StringUtils.hasText(headerBearerToken)) {
|
||||||
|
finalToken = "bearer " + headerBearerToken;
|
||||||
|
} else {
|
||||||
|
finalToken = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (finalToken != null) {
|
||||||
|
HttpServletRequestWrapper wrapper = new HttpServletRequestWrapper(request) {
|
||||||
|
@Override
|
||||||
|
public String getHeader(String name) {
|
||||||
|
if ("Authorization".equalsIgnoreCase(name)) {
|
||||||
|
return finalToken;
|
||||||
|
}
|
||||||
|
return super.getHeader(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Enumeration<String> getHeaders(String name) {
|
||||||
|
if ("Authorization".equalsIgnoreCase(name)) {
|
||||||
|
return Collections.enumeration(Collections.singleton(finalToken));
|
||||||
|
}
|
||||||
|
return super.getHeaders(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Enumeration<String> getHeaderNames() {
|
||||||
|
List<String> names = Collections.list(super.getHeaderNames());
|
||||||
|
if (!names.contains("Authorization")) {
|
||||||
|
names.add("Authorization");
|
||||||
|
}
|
||||||
|
return Collections.enumeration(names);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
filterChain.doFilter(wrapper, response);
|
||||||
|
} else {
|
||||||
|
filterChain.doFilter(request, response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,34 @@
|
|||||||
|
package org.jeecg.config.security;
|
||||||
|
|
||||||
|
import org.jeecg.common.api.CommonAPI;
|
||||||
|
import org.jeecg.common.system.vo.LoginUser;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.context.annotation.Lazy;
|
||||||
|
import org.springframework.core.convert.converter.Converter;
|
||||||
|
import org.springframework.security.authentication.AbstractAuthenticationToken;
|
||||||
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||||
|
import org.springframework.security.oauth2.jwt.Jwt;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* token只存储用户名与过期时间
|
||||||
|
* 这里通过取用户名转全量用户信息存储到Security中
|
||||||
|
* @author eightmonth@qq.com
|
||||||
|
* @date 2024/7/15 11:05
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class JeecgAuthenticationConvert implements Converter<Jwt, AbstractAuthenticationToken> {
|
||||||
|
|
||||||
|
@Lazy
|
||||||
|
@Autowired
|
||||||
|
private CommonAPI commonAPI;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AbstractAuthenticationToken convert(Jwt source) {
|
||||||
|
String username = source.getClaims().get("username").toString();
|
||||||
|
LoginUser loginUser = commonAPI.getUserByName(username);
|
||||||
|
return new UsernamePasswordAuthenticationToken(loginUser, null, new ArrayList<>());
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,135 @@
|
|||||||
|
package org.jeecg.config.security;
|
||||||
|
|
||||||
|
import org.jeecg.common.system.util.JwtUtil;
|
||||||
|
import org.springframework.lang.Nullable;
|
||||||
|
import org.springframework.security.oauth2.core.ClaimAccessor;
|
||||||
|
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||||
|
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||||
|
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
|
||||||
|
import org.springframework.security.oauth2.jwt.JwsHeader;
|
||||||
|
import org.springframework.security.oauth2.jwt.JwtClaimsSet;
|
||||||
|
import org.springframework.security.oauth2.jwt.JwtEncoder;
|
||||||
|
import org.springframework.security.oauth2.jwt.JwtEncoderParameters;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationGrantAuthenticationToken;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.token.*;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
import org.springframework.util.CollectionUtils;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.temporal.Temporal;
|
||||||
|
import java.time.temporal.TemporalUnit;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author eightmonth@qq.com
|
||||||
|
* @date 2024/7/11 17:10
|
||||||
|
*/
|
||||||
|
public class JeecgOAuth2AccessTokenGenerator implements OAuth2TokenGenerator<OAuth2AccessToken> {
|
||||||
|
private final JwtEncoder jwtEncoder;
|
||||||
|
|
||||||
|
private OAuth2TokenCustomizer<OAuth2TokenClaimsContext> accessTokenCustomizer;
|
||||||
|
|
||||||
|
public JeecgOAuth2AccessTokenGenerator(JwtEncoder jwtEncoder) {
|
||||||
|
this.jwtEncoder = jwtEncoder;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
public OAuth2AccessToken generate(OAuth2TokenContext context) {
|
||||||
|
if (!OAuth2TokenType.ACCESS_TOKEN.equals(context.getTokenType())) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String issuer = null;
|
||||||
|
if (context.getAuthorizationServerContext() != null) {
|
||||||
|
issuer = context.getAuthorizationServerContext().getIssuer();
|
||||||
|
}
|
||||||
|
RegisteredClient registeredClient = context.getRegisteredClient();
|
||||||
|
|
||||||
|
Instant issuedAt = Instant.now();
|
||||||
|
Instant expiresAt = issuedAt.plusMillis(JwtUtil.EXPIRE_TIME);
|
||||||
|
|
||||||
|
OAuth2TokenClaimsSet.Builder claimsBuilder = OAuth2TokenClaimsSet.builder();
|
||||||
|
if (StringUtils.hasText(issuer)) {
|
||||||
|
claimsBuilder.issuer(issuer);
|
||||||
|
}
|
||||||
|
claimsBuilder
|
||||||
|
.subject(context.getPrincipal().getName())
|
||||||
|
.audience(Collections.singletonList(registeredClient.getClientId()))
|
||||||
|
.issuedAt(issuedAt)
|
||||||
|
.expiresAt(expiresAt)
|
||||||
|
.notBefore(issuedAt)
|
||||||
|
.id(UUID.randomUUID().toString());
|
||||||
|
if (!CollectionUtils.isEmpty(context.getAuthorizedScopes())) {
|
||||||
|
claimsBuilder.claim(OAuth2ParameterNames.SCOPE, context.getAuthorizedScopes());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.accessTokenCustomizer != null) {
|
||||||
|
OAuth2TokenClaimsContext.Builder accessTokenContextBuilder = OAuth2TokenClaimsContext.with(claimsBuilder)
|
||||||
|
.registeredClient(context.getRegisteredClient())
|
||||||
|
.principal(context.getPrincipal())
|
||||||
|
.authorizationServerContext(context.getAuthorizationServerContext())
|
||||||
|
.authorizedScopes(context.getAuthorizedScopes())
|
||||||
|
.tokenType(context.getTokenType())
|
||||||
|
.authorizationGrantType(context.getAuthorizationGrantType());
|
||||||
|
if (context.getAuthorization() != null) {
|
||||||
|
accessTokenContextBuilder.authorization(context.getAuthorization());
|
||||||
|
}
|
||||||
|
if (context.getAuthorizationGrant() != null) {
|
||||||
|
accessTokenContextBuilder.authorizationGrant(context.getAuthorizationGrant());
|
||||||
|
}
|
||||||
|
|
||||||
|
OAuth2TokenClaimsContext accessTokenContext = accessTokenContextBuilder.build();
|
||||||
|
this.accessTokenCustomizer.customize(accessTokenContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
OAuth2TokenClaimsSet accessTokenClaimsSet = claimsBuilder.build();
|
||||||
|
OAuth2AuthorizationGrantAuthenticationToken oAuth2ResourceOwnerBaseAuthenticationToken = context.getAuthorizationGrant();
|
||||||
|
String username = (String) oAuth2ResourceOwnerBaseAuthenticationToken.getAdditionalParameters().get("username");
|
||||||
|
String tokenValue = jwtEncoder.encode(JwtEncoderParameters.from(JwsHeader.with(SignatureAlgorithm.ES256).keyId("jeecg").build(),
|
||||||
|
JwtClaimsSet.builder().claim("username", username).expiresAt(expiresAt).build())).getTokenValue();
|
||||||
|
|
||||||
|
//此处可以做改造将tokenValue随机数换成用户信息,方便后续多系统token互通认证(通过解密token得到username)
|
||||||
|
return new OAuth2AccessTokenClaims(OAuth2AccessToken.TokenType.BEARER, tokenValue,
|
||||||
|
accessTokenClaimsSet.getIssuedAt(), accessTokenClaimsSet.getExpiresAt(), context.getAuthorizedScopes(),
|
||||||
|
accessTokenClaimsSet.getClaims());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the {@link OAuth2TokenCustomizer} that customizes the
|
||||||
|
* {@link OAuth2TokenClaimsContext#getClaims() claims} for the
|
||||||
|
* {@link OAuth2AccessToken}.
|
||||||
|
* @param accessTokenCustomizer the {@link OAuth2TokenCustomizer} that customizes the
|
||||||
|
* claims for the {@code OAuth2AccessToken}
|
||||||
|
*/
|
||||||
|
public void setAccessTokenCustomizer(OAuth2TokenCustomizer<OAuth2TokenClaimsContext> accessTokenCustomizer) {
|
||||||
|
Assert.notNull(accessTokenCustomizer, "accessTokenCustomizer cannot be null");
|
||||||
|
this.accessTokenCustomizer = accessTokenCustomizer;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class OAuth2AccessTokenClaims extends OAuth2AccessToken implements ClaimAccessor {
|
||||||
|
|
||||||
|
private final Map<String, Object> claims;
|
||||||
|
|
||||||
|
private OAuth2AccessTokenClaims(TokenType tokenType, String tokenValue, Instant issuedAt, Instant expiresAt,
|
||||||
|
Set<String> scopes, Map<String, Object> claims) {
|
||||||
|
super(tokenType, tokenValue, issuedAt, expiresAt, scopes);
|
||||||
|
this.claims = claims;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String, Object> getClaims() {
|
||||||
|
return this.claims;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,104 @@
|
|||||||
|
package org.jeecg.config.security;
|
||||||
|
|
||||||
|
import cn.hutool.core.util.ArrayUtil;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.jeecg.common.api.CommonAPI;
|
||||||
|
import org.jeecg.common.system.vo.LoginUser;
|
||||||
|
import org.jeecg.common.util.RedisUtil;
|
||||||
|
import org.jeecg.config.security.utils.SecureUtil;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.context.annotation.Lazy;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.util.PatternMatchUtils;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* spring authorization server自定义权限处理,根据@PreAuthorize注解,判断当前用户是否具备权限
|
||||||
|
* @author EightMonth
|
||||||
|
* @date 2024/1/10 17:00
|
||||||
|
*/
|
||||||
|
@Service("jps")
|
||||||
|
@Slf4j
|
||||||
|
public class JeecgPermissionService {
|
||||||
|
private final String SPLIT = "::";
|
||||||
|
private final String PERM_PREFIX = "jps" + SPLIT;
|
||||||
|
|
||||||
|
@Lazy
|
||||||
|
@Autowired
|
||||||
|
private CommonAPI commonAPI;
|
||||||
|
@Autowired
|
||||||
|
private RedisUtil redisUtil;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断接口是否有任意xxx,xxx权限
|
||||||
|
* @param permissions 权限
|
||||||
|
* @return {boolean}
|
||||||
|
*/
|
||||||
|
public boolean requiresPermissions(String... permissions) {
|
||||||
|
if (ArrayUtil.isEmpty(permissions)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
LoginUser loginUser = SecureUtil.currentUser();
|
||||||
|
|
||||||
|
Object cache = redisUtil.get(buildKey("permission", loginUser.getId()));
|
||||||
|
Set<String> permissionList;
|
||||||
|
if (Objects.nonNull(cache)) {
|
||||||
|
permissionList = (Set<String>) cache;
|
||||||
|
} else {
|
||||||
|
permissionList = commonAPI.queryUserAuths(loginUser.getId());
|
||||||
|
redisUtil.set(buildKey("permission", loginUser.getId()), permissionList);
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean pass = permissionList.stream().filter(StringUtils::hasText)
|
||||||
|
.anyMatch(x -> PatternMatchUtils.simpleMatch(permissions, x));
|
||||||
|
if (!pass) {
|
||||||
|
log.error("权限不足,缺少权限:"+ Arrays.toString(permissions));
|
||||||
|
}
|
||||||
|
return pass;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断接口是否有任意xxx,xxx角色
|
||||||
|
* @param roles 角色
|
||||||
|
* @return {boolean}
|
||||||
|
*/
|
||||||
|
public boolean requiresRoles(String... roles) {
|
||||||
|
if (ArrayUtil.isEmpty(roles)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
LoginUser loginUser = SecureUtil.currentUser();
|
||||||
|
|
||||||
|
Object cache = redisUtil.get(buildKey("role", loginUser.getUsername()));
|
||||||
|
Set<String> roleList;
|
||||||
|
if (Objects.nonNull(cache)) {
|
||||||
|
roleList = (Set<String>) cache;
|
||||||
|
} else {
|
||||||
|
roleList = commonAPI.queryUserRoles(loginUser.getUsername());
|
||||||
|
redisUtil.set(buildKey("role", loginUser.getUsername()), roleList);
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean pass = roleList.stream().filter(StringUtils::hasText)
|
||||||
|
.anyMatch(x -> PatternMatchUtils.simpleMatch(roles, x));
|
||||||
|
if (!pass) {
|
||||||
|
log.error("权限不足,缺少角色:" + Arrays.toString(roles));
|
||||||
|
}
|
||||||
|
return pass;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 由于缓存key是以人的维度,角色列表、权限列表在值中,jeecg是以权限列表绑定在角色上,形成的权限集合
|
||||||
|
* 权限发生变更时,需要清理全部人的权限缓存
|
||||||
|
*/
|
||||||
|
public void clearCache() {
|
||||||
|
redisUtil.removeAll(PERM_PREFIX);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String buildKey(String type, String username) {
|
||||||
|
return PERM_PREFIX + type + SPLIT + username;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,54 @@
|
|||||||
|
package org.jeecg.config.security;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.data.redis.core.RedisTemplate;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsent;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* spring authorization server 自定义redis保存授权范围信息
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class JeecgRedisOAuth2AuthorizationConsentService implements OAuth2AuthorizationConsentService {
|
||||||
|
|
||||||
|
private final RedisTemplate<String, Object> redisTemplate;
|
||||||
|
|
||||||
|
private final static Long TIMEOUT = 10L;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void save(OAuth2AuthorizationConsent authorizationConsent) {
|
||||||
|
Assert.notNull(authorizationConsent, "authorizationConsent cannot be null");
|
||||||
|
|
||||||
|
redisTemplate.opsForValue().set(buildKey(authorizationConsent), authorizationConsent, TIMEOUT,
|
||||||
|
TimeUnit.MINUTES);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void remove(OAuth2AuthorizationConsent authorizationConsent) {
|
||||||
|
Assert.notNull(authorizationConsent, "authorizationConsent cannot be null");
|
||||||
|
redisTemplate.delete(buildKey(authorizationConsent));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public OAuth2AuthorizationConsent findById(String registeredClientId, String principalName) {
|
||||||
|
Assert.hasText(registeredClientId, "registeredClientId cannot be empty");
|
||||||
|
Assert.hasText(principalName, "principalName cannot be empty");
|
||||||
|
return (OAuth2AuthorizationConsent) redisTemplate.opsForValue()
|
||||||
|
.get(buildKey(registeredClientId, principalName));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String buildKey(String registeredClientId, String principalName) {
|
||||||
|
return "token:consent:" + registeredClientId + ":" + principalName;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String buildKey(OAuth2AuthorizationConsent authorizationConsent) {
|
||||||
|
return buildKey(authorizationConsent.getRegisteredClientId(), authorizationConsent.getPrincipalName());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,192 @@
|
|||||||
|
package org.jeecg.config.security;
|
||||||
|
|
||||||
|
import cn.hutool.core.collection.CollUtil;
|
||||||
|
import jakarta.annotation.PostConstruct;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||||
|
import org.springframework.data.redis.core.RedisTemplate;
|
||||||
|
import org.springframework.data.redis.serializer.RedisSerializer;
|
||||||
|
import org.springframework.lang.Nullable;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||||
|
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
|
||||||
|
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationCode;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
|
||||||
|
import java.time.temporal.ChronoUnit;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* spring authorization server自定义redis保存认证信息
|
||||||
|
* @author EightMonth
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class JeecgRedisOAuth2AuthorizationService implements OAuth2AuthorizationService{
|
||||||
|
|
||||||
|
private final static Long TIMEOUT = 10L;
|
||||||
|
|
||||||
|
private static final String AUTHORIZATION = "token";
|
||||||
|
|
||||||
|
private final RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private RedisConnectionFactory redisConnectionFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 因为保存sas的认证信息至redis,无法使用jeecg对redisTemplate的某些设置。
|
||||||
|
* 如果在使用时修改redisTemplate属性,会发生线程安全问题,最终容易引起系统无法正常运行。
|
||||||
|
* 所以重新建了一个redis client给到sas操作redis,并且该redis实例不注入spring 容器中
|
||||||
|
*/
|
||||||
|
@PostConstruct
|
||||||
|
public void initSasRedis() {
|
||||||
|
redisTemplate.setValueSerializer(RedisSerializer.java());
|
||||||
|
redisTemplate.setConnectionFactory(redisConnectionFactory);
|
||||||
|
redisTemplate.afterPropertiesSet();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void save(OAuth2Authorization authorization) {
|
||||||
|
Assert.notNull(authorization, "authorization cannot be null");
|
||||||
|
|
||||||
|
if (isState(authorization)) {
|
||||||
|
String token = authorization.getAttribute("state");
|
||||||
|
redisTemplate.opsForValue().set(buildKey(OAuth2ParameterNames.STATE, token), authorization, TIMEOUT,
|
||||||
|
TimeUnit.MINUTES);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isCode(authorization)) {
|
||||||
|
OAuth2Authorization.Token<OAuth2AuthorizationCode> authorizationCode = authorization
|
||||||
|
.getToken(OAuth2AuthorizationCode.class);
|
||||||
|
OAuth2AuthorizationCode authorizationCodeToken = authorizationCode.getToken();
|
||||||
|
long between = ChronoUnit.MINUTES.between(authorizationCodeToken.getIssuedAt(),
|
||||||
|
authorizationCodeToken.getExpiresAt());
|
||||||
|
redisTemplate.opsForValue().set(buildKey(OAuth2ParameterNames.CODE, authorizationCodeToken.getTokenValue()),
|
||||||
|
authorization, between, TimeUnit.MINUTES);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isRefreshToken(authorization)) {
|
||||||
|
OAuth2RefreshToken refreshToken = authorization.getRefreshToken().getToken();
|
||||||
|
long between = ChronoUnit.SECONDS.between(refreshToken.getIssuedAt(), refreshToken.getExpiresAt());
|
||||||
|
redisTemplate.opsForValue().set(buildKey(OAuth2ParameterNames.REFRESH_TOKEN, refreshToken.getTokenValue()),
|
||||||
|
authorization, between, TimeUnit.SECONDS);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isAccessToken(authorization)) {
|
||||||
|
OAuth2AccessToken accessToken = authorization.getAccessToken().getToken();
|
||||||
|
long between = ChronoUnit.SECONDS.between(accessToken.getIssuedAt(), accessToken.getExpiresAt());
|
||||||
|
redisTemplate.opsForValue().set(buildKey(OAuth2ParameterNames.ACCESS_TOKEN, accessToken.getTokenValue()),
|
||||||
|
authorization, between, TimeUnit.SECONDS);
|
||||||
|
|
||||||
|
// 扩展记录 access-token 、username 的关系 1::token::username::admin::xxx
|
||||||
|
String tokenUsername = String.format("%s::%s::%s", AUTHORIZATION, authorization.getPrincipalName(), accessToken.getTokenValue());
|
||||||
|
redisTemplate.opsForValue().set(tokenUsername, accessToken.getTokenValue(), between, TimeUnit.SECONDS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void remove(OAuth2Authorization authorization) {
|
||||||
|
Assert.notNull(authorization, "authorization cannot be null");
|
||||||
|
|
||||||
|
List<String> keys = new ArrayList<>();
|
||||||
|
if (isState(authorization)) {
|
||||||
|
String token = authorization.getAttribute("state");
|
||||||
|
keys.add(buildKey(OAuth2ParameterNames.STATE, token));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isCode(authorization)) {
|
||||||
|
OAuth2Authorization.Token<OAuth2AuthorizationCode> authorizationCode = authorization
|
||||||
|
.getToken(OAuth2AuthorizationCode.class);
|
||||||
|
OAuth2AuthorizationCode authorizationCodeToken = authorizationCode.getToken();
|
||||||
|
keys.add(buildKey(OAuth2ParameterNames.CODE, authorizationCodeToken.getTokenValue()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isRefreshToken(authorization)) {
|
||||||
|
OAuth2RefreshToken refreshToken = authorization.getRefreshToken().getToken();
|
||||||
|
keys.add(buildKey(OAuth2ParameterNames.REFRESH_TOKEN, refreshToken.getTokenValue()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isAccessToken(authorization)) {
|
||||||
|
OAuth2AccessToken accessToken = authorization.getAccessToken().getToken();
|
||||||
|
keys.add(buildKey(OAuth2ParameterNames.ACCESS_TOKEN, accessToken.getTokenValue()));
|
||||||
|
|
||||||
|
// 扩展记录 access-token 、username 的关系 1::token::username::admin::xxx
|
||||||
|
String key = String.format("%s::%s::%s", AUTHORIZATION, authorization.getPrincipalName(), accessToken.getTokenValue());
|
||||||
|
keys.add(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
redisTemplate.delete(keys);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Nullable
|
||||||
|
public OAuth2Authorization findById(String id) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Nullable
|
||||||
|
public OAuth2Authorization findByToken(String token, @Nullable OAuth2TokenType tokenType) {
|
||||||
|
Assert.hasText(token, "token cannot be empty");
|
||||||
|
Assert.notNull(tokenType, "tokenType cannot be empty");
|
||||||
|
return (OAuth2Authorization) redisTemplate.opsForValue().get(buildKey(tokenType.getValue(), token));
|
||||||
|
}
|
||||||
|
|
||||||
|
private String buildKey(String type, String id) {
|
||||||
|
return String.format("%s::%s::%s", AUTHORIZATION, type, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isState(OAuth2Authorization authorization) {
|
||||||
|
return Objects.nonNull(authorization.getAttribute("state"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isCode(OAuth2Authorization authorization) {
|
||||||
|
OAuth2Authorization.Token<OAuth2AuthorizationCode> authorizationCode = authorization
|
||||||
|
.getToken(OAuth2AuthorizationCode.class);
|
||||||
|
return Objects.nonNull(authorizationCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isRefreshToken(OAuth2Authorization authorization) {
|
||||||
|
return Objects.nonNull(authorization.getRefreshToken());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isAccessToken(OAuth2Authorization authorization) {
|
||||||
|
return Objects.nonNull(authorization.getAccessToken());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 扩展方法根据 username 查询是否存在存储的
|
||||||
|
* @param authentication
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public void removeByUsername(Authentication authentication) {
|
||||||
|
// 根据 username查询对应access-token
|
||||||
|
String authenticationName = authentication.getName();
|
||||||
|
|
||||||
|
// 扩展记录 access-token 、username 的关系 1::token::username::admin::xxx
|
||||||
|
String tokenUsernameKey = String.format("%s::%s::*", AUTHORIZATION, authenticationName);
|
||||||
|
Set<String> keys = redisTemplate.keys(tokenUsernameKey);
|
||||||
|
if (CollUtil.isEmpty(keys)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Object> tokenList = redisTemplate.opsForValue().multiGet(keys);
|
||||||
|
|
||||||
|
for (Object token : tokenList) {
|
||||||
|
// 根据token 查询存储的 OAuth2Authorization
|
||||||
|
OAuth2Authorization authorization = this.findByToken((String) token, OAuth2TokenType.ACCESS_TOKEN);
|
||||||
|
// 根据 OAuth2Authorization 删除相关令牌
|
||||||
|
this.remove(authorization);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,38 @@
|
|||||||
|
package org.jeecg.config.security;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 登录模式
|
||||||
|
* @author EightMonth
|
||||||
|
* @date 2024/1/10 17:43
|
||||||
|
*/
|
||||||
|
public class LoginType {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 密码模式
|
||||||
|
*/
|
||||||
|
public static final String PASSWORD = "password";
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 手机号+验证码模式
|
||||||
|
*/
|
||||||
|
public static final String PHONE = "phone";
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* app登录
|
||||||
|
*/
|
||||||
|
public static final String APP = "app";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 扫码登录
|
||||||
|
*/
|
||||||
|
public static final String SCAN = "scan";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 所有联合登录,比如github\钉钉\企业微信\微信
|
||||||
|
*/
|
||||||
|
public static final String SOCIAL = "social";
|
||||||
|
|
||||||
|
public static final String SELF = "self";
|
||||||
|
}
|
||||||
@ -0,0 +1,49 @@
|
|||||||
|
package org.jeecg.config.security;
|
||||||
|
|
||||||
|
import jakarta.servlet.FilterChain;
|
||||||
|
import jakarta.servlet.ServletException;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import org.jeecg.common.system.util.JwtUtil;
|
||||||
|
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||||
|
import org.springframework.security.oauth2.jwt.JwtDecoder;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
|
||||||
|
import org.springframework.security.oauth2.server.resource.BearerTokenErrors;
|
||||||
|
import org.springframework.security.oauth2.server.resource.web.DefaultBearerTokenResolver;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.web.filter.OncePerRequestFilter;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当用户被强退时,使客户端token失效
|
||||||
|
* @author eightmonth@qq.com
|
||||||
|
* @date 2024/3/7 17:30
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class RedisTokenValidationFilter extends OncePerRequestFilter {
|
||||||
|
private OAuth2AuthorizationService authorizationService;
|
||||||
|
private JwtDecoder jwtDecoder;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
|
||||||
|
// 从请求中获取token
|
||||||
|
DefaultBearerTokenResolver defaultBearerTokenResolver = new DefaultBearerTokenResolver();
|
||||||
|
String token = defaultBearerTokenResolver.resolve(request);
|
||||||
|
|
||||||
|
|
||||||
|
if (Objects.nonNull(token)) {
|
||||||
|
// 检查认证信息是否已被清除,如果已被清除,则令该token失效
|
||||||
|
OAuth2Authorization oAuth2Authorization = authorizationService.findByToken(token, OAuth2TokenType.ACCESS_TOKEN);
|
||||||
|
if (Objects.isNull(oAuth2Authorization)) {
|
||||||
|
throw new OAuth2AuthenticationException(BearerTokenErrors.invalidToken("认证信息已失效,请重新登录"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
filterChain.doFilter(request, response);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,310 @@
|
|||||||
|
package org.jeecg.config.security;
|
||||||
|
|
||||||
|
import com.nimbusds.jose.jwk.Curve;
|
||||||
|
import com.nimbusds.jose.jwk.ECKey;
|
||||||
|
import com.nimbusds.jose.jwk.JWKSet;
|
||||||
|
import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
|
||||||
|
import com.nimbusds.jose.jwk.source.JWKSource;
|
||||||
|
import com.nimbusds.jose.proc.SecurityContext;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.SneakyThrows;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.jeecg.common.system.util.JwtUtil;
|
||||||
|
import org.jeecg.config.security.app.AppGrantAuthenticationConvert;
|
||||||
|
import org.jeecg.config.security.app.AppGrantAuthenticationProvider;
|
||||||
|
import org.jeecg.config.security.password.PasswordGrantAuthenticationConvert;
|
||||||
|
import org.jeecg.config.security.password.PasswordGrantAuthenticationProvider;
|
||||||
|
import org.jeecg.config.security.phone.PhoneGrantAuthenticationConvert;
|
||||||
|
import org.jeecg.config.security.phone.PhoneGrantAuthenticationProvider;
|
||||||
|
import org.jeecg.config.security.social.SocialGrantAuthenticationConvert;
|
||||||
|
import org.jeecg.config.security.social.SocialGrantAuthenticationProvider;
|
||||||
|
import org.jeecg.config.shiro.ignore.InMemoryIgnoreAuth;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.core.annotation.Order;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.jdbc.core.JdbcTemplate;
|
||||||
|
import org.springframework.security.config.Customizer;
|
||||||
|
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
|
||||||
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||||
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||||
|
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||||
|
import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer;
|
||||||
|
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
|
import org.springframework.security.oauth2.jwt.JwtDecoder;
|
||||||
|
import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.token.*;
|
||||||
|
import org.springframework.security.web.SecurityFilterChain;
|
||||||
|
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
|
||||||
|
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||||
|
import org.springframework.security.web.util.matcher.MediaTypeRequestMatcher;
|
||||||
|
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||||
|
import org.springframework.util.CollectionUtils;
|
||||||
|
import org.springframework.web.cors.CorsConfiguration;
|
||||||
|
|
||||||
|
import java.security.KeyPair;
|
||||||
|
import java.security.KeyPairGenerator;
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
import java.security.interfaces.ECPrivateKey;
|
||||||
|
import java.security.interfaces.ECPublicKey;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* spring authorization server核心配置
|
||||||
|
* @author eightmonth@qq.com
|
||||||
|
* @date 2024/1/2 9:29
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
@EnableWebSecurity
|
||||||
|
@EnableMethodSecurity
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Slf4j
|
||||||
|
public class SecurityConfig {
|
||||||
|
|
||||||
|
private JdbcTemplate jdbcTemplate;
|
||||||
|
private OAuth2AuthorizationService authorizationService;
|
||||||
|
private JeecgAuthenticationConvert jeecgAuthenticationConvert;
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@Order(1)
|
||||||
|
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http)
|
||||||
|
throws Exception {
|
||||||
|
// 使用新的配置方式替代弃用的applyDefaultSecurity
|
||||||
|
http.securityMatcher(new AntPathRequestMatcher("/oauth2/**"))
|
||||||
|
.authorizeHttpRequests(authorize ->
|
||||||
|
authorize.anyRequest().authenticated()
|
||||||
|
)
|
||||||
|
.csrf(csrf -> csrf.disable())
|
||||||
|
.with(new OAuth2AuthorizationServerConfigurer(), oauth2 -> {
|
||||||
|
oauth2
|
||||||
|
.tokenEndpoint(tokenEndpoint -> tokenEndpoint
|
||||||
|
.accessTokenRequestConverter(new PasswordGrantAuthenticationConvert())
|
||||||
|
.authenticationProvider(new PasswordGrantAuthenticationProvider(authorizationService, tokenGenerator()))
|
||||||
|
)
|
||||||
|
.tokenEndpoint(tokenEndpoint -> tokenEndpoint
|
||||||
|
.accessTokenRequestConverter(new PhoneGrantAuthenticationConvert())
|
||||||
|
.authenticationProvider(new PhoneGrantAuthenticationProvider(authorizationService, tokenGenerator()))
|
||||||
|
)
|
||||||
|
.tokenEndpoint(tokenEndpoint -> tokenEndpoint
|
||||||
|
.accessTokenRequestConverter(new AppGrantAuthenticationConvert())
|
||||||
|
.authenticationProvider(new AppGrantAuthenticationProvider(authorizationService, tokenGenerator()))
|
||||||
|
)
|
||||||
|
.tokenEndpoint(tokenEndpoint -> tokenEndpoint
|
||||||
|
.accessTokenRequestConverter(new SocialGrantAuthenticationConvert())
|
||||||
|
.authenticationProvider(new SocialGrantAuthenticationProvider(authorizationService, tokenGenerator()))
|
||||||
|
)
|
||||||
|
//开启OpenID Connect 1.0(其中oidc为OpenID Connect的缩写)。 访问 /.well-known/openid-configuration即可获取认证信息
|
||||||
|
.oidc(Customizer.withDefaults());
|
||||||
|
});
|
||||||
|
|
||||||
|
//请求接口异常处理:无Token和Token无效的情况
|
||||||
|
http.exceptionHandling(exceptions -> exceptions
|
||||||
|
.authenticationEntryPoint((request, response, authException) -> {
|
||||||
|
// 记录详细的异常信息 - 未认证
|
||||||
|
log.error("接口访问失败(未认证),请求路径:{},错误信息:{}", request.getRequestURI(), authException.getMessage(), authException);
|
||||||
|
JwtUtil.responseError(response, 401, "Token格式错误或已过期");
|
||||||
|
})
|
||||||
|
.accessDeniedHandler((request, response, accessDeniedException) -> {
|
||||||
|
// 记录详细的异常信息 - token无效或权限不足
|
||||||
|
log.error("接口访问失败(token无效或权限不足),请求路径:{},错误信息:{}", request.getRequestURI(), accessDeniedException.getMessage(), accessDeniedException);
|
||||||
|
JwtUtil.responseError(response, 403, "权限不足");
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return http.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@Order(2)
|
||||||
|
public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http)
|
||||||
|
throws Exception {
|
||||||
|
http
|
||||||
|
//设置所有请求都需要认证,未认证的请求都被重定向到login页面进行登录
|
||||||
|
.authorizeHttpRequests((authorize) -> authorize
|
||||||
|
.requestMatchers(InMemoryIgnoreAuth.get().stream().map(AntPathRequestMatcher::antMatcher).toList().toArray(new AntPathRequestMatcher[0])).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/sys/cas/client/validateLogin")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/sys/randomImage/**")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/sys/checkCaptcha")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/sys/login")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/sys/mLogin")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/sys/logout")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/sys/thirdLogin/**")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/sys/getEncryptedString")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/sys/sms")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/sys/phoneLogin")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/sys/user/checkOnlyUser")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/sys/user/register")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/sys/user/phoneVerification")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/sys/user/passwordChange")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/auth/2step-code")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/sys/common/static/**")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/sys/common/pdf/**")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/generic/**")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/sys/getLoginQrcode/**")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/sys/getQrcodeToken/**")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/sys/checkAuth")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/doc.html")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/**/*.js")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/**/*.css")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/**/*.html")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/**/*.svg")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/**/*.pdf")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/**/*.jpg")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/**/*.png")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/**/*.gif")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/**/*.ico")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/**/*.ttf")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/**/*.woff")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/**/*.woff2")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/druid/**")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/swagger-ui.html")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/swagger**/**")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/webjars/**")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/v3/**")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/WW_verify*")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/sys/annountCement/show/**")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/api/getUserInfo")).permitAll()
|
||||||
|
|
||||||
|
//积木报表排除
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/jmreport/**")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/**/*.js.map")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/**/*.css.map")).permitAll()
|
||||||
|
//积木BI大屏和仪表盘排除
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/drag/view")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/drag/onlDragDatasetHead/getLoginUser")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/drag/page/queryById")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/drag/page/addVisitsNumber")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/drag/page/queryTemplateList")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/drag/share/view/**")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/drag/onlDragDatasetHead/getAllChartData")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/drag/onlDragDatasetHead/getTotalData")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/drag/mock/json/**")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/jimubi/view")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/jimubi/share/view/**")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/drag/onlDragDatasetHead/getMapDataByCode")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/drag/onlDragDatasetHead/getTotalDataByCompId")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/drag/onlDragDatasetHead/queryAllById")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/drag/onlDragDatasetHead/getDictByCodes")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/dragChannelSocket/**")).permitAll()
|
||||||
|
//大屏模板例子
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/test/bigScreen/**")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/bigscreen/template1/**")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/bigscreen/template1/**")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/websocket/**")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/newsWebsocket/**")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/vxeSocket/**")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/test/seata/**")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/error")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/openapi/call/**")).permitAll()
|
||||||
|
// APP版本信息
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/sys/version/app3version")).permitAll()
|
||||||
|
.anyRequest().authenticated()
|
||||||
|
)
|
||||||
|
.headers(headers -> headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable))
|
||||||
|
.cors(cors -> cors
|
||||||
|
.configurationSource(req -> {
|
||||||
|
CorsConfiguration config = new CorsConfiguration();
|
||||||
|
config.applyPermitDefaultValues();
|
||||||
|
config.setAllowedMethods(Arrays.asList("HEAD", "GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"));
|
||||||
|
return config;
|
||||||
|
}))
|
||||||
|
.csrf(AbstractHttpConfigurer::disable)
|
||||||
|
// 配置OAuth2资源服务器,并添加JWT异常处理
|
||||||
|
.oauth2ResourceServer(oauth2 -> oauth2
|
||||||
|
.jwt(jwt -> jwt.jwtAuthenticationConverter(jeecgAuthenticationConvert))
|
||||||
|
.authenticationEntryPoint((request, response, authException) -> {
|
||||||
|
// 处理JWT解析失败的情况
|
||||||
|
log.error("JWT验证失败,请求路径:{},错误信息:{}", request.getRequestURI(), authException.getMessage(), authException);
|
||||||
|
JwtUtil.responseError(response, 401, "Token格式错误或已过期");
|
||||||
|
})
|
||||||
|
.accessDeniedHandler((request, response, accessDeniedException) -> {
|
||||||
|
// 处理权限不足的情况
|
||||||
|
log.error("权限验证失败,请求路径:{},错误信息:{}", request.getRequestURI(), accessDeniedException.getMessage(), accessDeniedException);
|
||||||
|
JwtUtil.responseError(response, 403, "权限不足");
|
||||||
|
})
|
||||||
|
)
|
||||||
|
// 全局异常处理
|
||||||
|
.exceptionHandling(exceptions -> exceptions
|
||||||
|
.authenticationEntryPoint((request, response, authException) -> {
|
||||||
|
// 记录详细的异常信息 - 未认证
|
||||||
|
log.error("接口访问失败(未认证),请求路径:{},错误信息:{}", request.getRequestURI(), authException.getMessage(), authException);
|
||||||
|
JwtUtil.responseError(response, 401, "Token格式错误或已过期");
|
||||||
|
})
|
||||||
|
.accessDeniedHandler((request, response, accessDeniedException) -> {
|
||||||
|
// 记录详细的异常信息 - token无效或权限不足
|
||||||
|
log.error("接口访问失败(token无效或权限不足),请求路径:{},错误信息:{}", request.getRequestURI(), accessDeniedException.getMessage(), accessDeniedException);
|
||||||
|
JwtUtil.responseError(response, 403, "权限不足");
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return http.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数据库保存注册客户端信息
|
||||||
|
*/
|
||||||
|
@Bean
|
||||||
|
public RegisteredClientRepository registeredClientRepository() {
|
||||||
|
return new JdbcRegisteredClientRepository(jdbcTemplate);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*配置 JWK,为JWT(id_token)提供加密密钥,用于加密/解密或签名/验签
|
||||||
|
* JWK详细见:https://datatracker.ietf.org/doc/html/draft-ietf-jose-json-web-key-41
|
||||||
|
*/
|
||||||
|
@Bean
|
||||||
|
@SneakyThrows
|
||||||
|
public JWKSource<SecurityContext> jwkSource() {
|
||||||
|
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("EC");
|
||||||
|
// 如果不设置secureRandom,会存在一个问题,当应用重启后,原有的token将会全部失效,因为重启的keyPair与之前已经不同
|
||||||
|
SecureRandom secureRandom = SecureRandom.getInstance("SHA1PRNG");
|
||||||
|
// 重要!生产环境需要修改!
|
||||||
|
secureRandom.setSeed("jeecg".getBytes());
|
||||||
|
keyPairGenerator.initialize(256, secureRandom);
|
||||||
|
KeyPair keyPair = keyPairGenerator.generateKeyPair();
|
||||||
|
ECPublicKey publicKey = (ECPublicKey) keyPair.getPublic();
|
||||||
|
ECPrivateKey privateKey = (ECPrivateKey) keyPair.getPrivate();
|
||||||
|
|
||||||
|
ECKey jwk = new ECKey.Builder(Curve.P_256, publicKey)
|
||||||
|
.privateKey(privateKey)
|
||||||
|
.keyID("jeecg")
|
||||||
|
.build();
|
||||||
|
JWKSet jwkSet = new JWKSet(jwk);
|
||||||
|
return new ImmutableJWKSet<>(jwkSet);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public PasswordEncoder passwordEncoder() {
|
||||||
|
return NoOpPasswordEncoder.getInstance();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 配置jwt解析器
|
||||||
|
*/
|
||||||
|
@Bean
|
||||||
|
public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
|
||||||
|
return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*配置token生成器
|
||||||
|
*/
|
||||||
|
@Bean
|
||||||
|
OAuth2TokenGenerator<?> tokenGenerator() {
|
||||||
|
JwtGenerator jwtGenerator = new JwtGenerator(new NimbusJwtEncoder(jwkSource()));
|
||||||
|
OAuth2AccessTokenGenerator accessTokenGenerator = new OAuth2AccessTokenGenerator();
|
||||||
|
OAuth2RefreshTokenGenerator refreshTokenGenerator = new OAuth2RefreshTokenGenerator();
|
||||||
|
return new DelegatingOAuth2TokenGenerator(
|
||||||
|
new JeecgOAuth2AccessTokenGenerator(new NimbusJwtEncoder(jwkSource())),
|
||||||
|
new OAuth2RefreshTokenGenerator()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,81 @@
|
|||||||
|
package org.jeecg.config.security.app;
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import org.jeecg.config.security.LoginType;
|
||||||
|
import org.jeecg.config.security.password.PasswordGrantAuthenticationToken;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
|
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||||
|
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||||
|
import org.springframework.security.web.authentication.AuthenticationConverter;
|
||||||
|
import org.springframework.util.LinkedMultiValueMap;
|
||||||
|
import org.springframework.util.MultiValueMap;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* APP模式认证转换器
|
||||||
|
* @author EightMonth
|
||||||
|
* @date 2024/1/1
|
||||||
|
*/
|
||||||
|
public class AppGrantAuthenticationConvert implements AuthenticationConverter {
|
||||||
|
@Override
|
||||||
|
public Authentication convert(HttpServletRequest request) {
|
||||||
|
|
||||||
|
String grantType = request.getParameter(OAuth2ParameterNames.GRANT_TYPE);
|
||||||
|
if (!LoginType.APP.equals(grantType)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Authentication clientPrincipal = SecurityContextHolder.getContext().getAuthentication();
|
||||||
|
|
||||||
|
//从request中提取请求参数,然后存入MultiValueMap<String, String>
|
||||||
|
MultiValueMap<String, String> parameters = getParameters(request);
|
||||||
|
|
||||||
|
// username (REQUIRED)
|
||||||
|
String username = parameters.getFirst(OAuth2ParameterNames.USERNAME);
|
||||||
|
if (!StringUtils.hasText(username) ||
|
||||||
|
parameters.get(OAuth2ParameterNames.USERNAME).size() != 1) {
|
||||||
|
throw new OAuth2AuthenticationException("无效请求,用户名不能为空!");
|
||||||
|
}
|
||||||
|
String password = parameters.getFirst(OAuth2ParameterNames.PASSWORD);
|
||||||
|
if (!StringUtils.hasText(password) ||
|
||||||
|
parameters.get(OAuth2ParameterNames.PASSWORD).size() != 1) {
|
||||||
|
throw new OAuth2AuthenticationException("无效请求,密码不能为空!");
|
||||||
|
}
|
||||||
|
|
||||||
|
//收集要传入PasswordGrantAuthenticationToken构造方法的参数,
|
||||||
|
//该参数接下来在PasswordGrantAuthenticationProvider中使用
|
||||||
|
Map<String, Object> additionalParameters = new HashMap<>();
|
||||||
|
//遍历从request中提取的参数,排除掉grant_type、client_id、code等字段参数,其他参数收集到additionalParameters中
|
||||||
|
parameters.forEach((key, value) -> {
|
||||||
|
if (!key.equals(OAuth2ParameterNames.GRANT_TYPE) &&
|
||||||
|
!key.equals(OAuth2ParameterNames.CLIENT_ID) &&
|
||||||
|
!key.equals(OAuth2ParameterNames.CODE)) {
|
||||||
|
additionalParameters.put(key, value.get(0));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
//返回自定义的PasswordGrantAuthenticationToken对象
|
||||||
|
return new PasswordGrantAuthenticationToken(clientPrincipal, additionalParameters);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*从request中提取请求参数,然后存入MultiValueMap<String, String>
|
||||||
|
*/
|
||||||
|
private static MultiValueMap<String, String> getParameters(HttpServletRequest request) {
|
||||||
|
Map<String, String[]> parameterMap = request.getParameterMap();
|
||||||
|
MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>(parameterMap.size());
|
||||||
|
parameterMap.forEach((key, values) -> {
|
||||||
|
if (values.length > 0) {
|
||||||
|
for (String value : values) {
|
||||||
|
parameters.add(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return parameters;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,320 @@
|
|||||||
|
package org.jeecg.config.security.app;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson.JSONObject;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.jeecg.common.api.CommonAPI;
|
||||||
|
import org.jeecg.common.constant.CacheConstant;
|
||||||
|
import org.jeecg.common.constant.CommonConstant;
|
||||||
|
import org.jeecg.common.exception.JeecgBootException;
|
||||||
|
import org.jeecg.common.exception.JeecgCaptchaException;
|
||||||
|
import org.jeecg.common.system.vo.LoginUser;
|
||||||
|
import org.jeecg.common.system.vo.SysDepartModel;
|
||||||
|
import org.jeecg.common.util.Md5Util;
|
||||||
|
import org.jeecg.common.util.PasswordUtil;
|
||||||
|
import org.jeecg.common.util.RedisUtil;
|
||||||
|
import org.jeecg.common.util.oConvertUtils;
|
||||||
|
import org.jeecg.config.JeecgBaseConfig;
|
||||||
|
import org.jeecg.config.security.password.PasswordGrantAuthenticationToken;
|
||||||
|
import org.jeecg.modules.base.service.BaseCommonService;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.context.annotation.Lazy;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.security.authentication.AuthenticationProvider;
|
||||||
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.core.AuthenticationException;
|
||||||
|
import org.springframework.security.oauth2.core.*;
|
||||||
|
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AccessTokenAuthenticationToken;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContextHolder;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.token.DefaultOAuth2TokenContext;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenContext;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
|
import java.security.Principal;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* APP模式认证处理器,负责处理该认证模式下的核心逻辑
|
||||||
|
* @author EightMonth
|
||||||
|
* @date 2024/1/1
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
public class AppGrantAuthenticationProvider implements AuthenticationProvider {
|
||||||
|
|
||||||
|
private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-5.2";
|
||||||
|
|
||||||
|
private final OAuth2AuthorizationService authorizationService;
|
||||||
|
private final OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator;
|
||||||
|
@Lazy
|
||||||
|
@Autowired
|
||||||
|
private CommonAPI commonAPI;
|
||||||
|
@Autowired
|
||||||
|
private RedisUtil redisUtil;
|
||||||
|
@Autowired
|
||||||
|
private JeecgBaseConfig jeecgBaseConfig;
|
||||||
|
@Autowired
|
||||||
|
private BaseCommonService baseCommonService;
|
||||||
|
|
||||||
|
public AppGrantAuthenticationProvider(OAuth2AuthorizationService authorizationService, OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator) {
|
||||||
|
Assert.notNull(authorizationService, "authorizationService cannot be null");
|
||||||
|
Assert.notNull(tokenGenerator, "tokenGenerator cannot be null");
|
||||||
|
this.authorizationService = authorizationService;
|
||||||
|
this.tokenGenerator = tokenGenerator;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
|
||||||
|
AppGrantAuthenticationToken appGrantAuthenticationToken = (AppGrantAuthenticationToken) authentication;
|
||||||
|
Map<String, Object> additionalParameter = appGrantAuthenticationToken.getAdditionalParameters();
|
||||||
|
|
||||||
|
// 授权类型
|
||||||
|
AuthorizationGrantType authorizationGrantType = appGrantAuthenticationToken.getGrantType();
|
||||||
|
// 用户名
|
||||||
|
String username = (String) additionalParameter.get(OAuth2ParameterNames.USERNAME);
|
||||||
|
// 密码
|
||||||
|
String password = (String) additionalParameter.get(OAuth2ParameterNames.PASSWORD);
|
||||||
|
//请求参数权限范围
|
||||||
|
String requestScopesStr = (String)additionalParameter.getOrDefault(OAuth2ParameterNames.SCOPE, "*");
|
||||||
|
//请求参数权限范围专场集合
|
||||||
|
Set<String> requestScopeSet = Stream.of(requestScopesStr.split(" ")).collect(Collectors.toSet());
|
||||||
|
// 验证码
|
||||||
|
String captcha = (String) additionalParameter.get("captcha");
|
||||||
|
String checkKey = (String) additionalParameter.get("checkKey");
|
||||||
|
|
||||||
|
OAuth2ClientAuthenticationToken clientPrincipal = getAuthenticatedClientElseThrowInvalidClient(appGrantAuthenticationToken);
|
||||||
|
RegisteredClient registeredClient = clientPrincipal.getRegisteredClient();
|
||||||
|
|
||||||
|
// 检查登录失败次数
|
||||||
|
if(isLoginFailOvertimes(username)){
|
||||||
|
Map<String, Object> map = new HashMap<>();
|
||||||
|
map.put("message", "该用户登录失败次数过多,请于10分钟后再次登录!");
|
||||||
|
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,"fdsafas", Instant.now(), Instant.now().plusNanos(1)), null, map);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(captcha==null){
|
||||||
|
Map<String, Object> map = new HashMap<>();
|
||||||
|
map.put("message", "验证码无效");
|
||||||
|
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,"fdsafas", Instant.now(), Instant.now().plusNanos(1)), null, map);
|
||||||
|
}
|
||||||
|
String lowerCaseCaptcha = captcha.toLowerCase();
|
||||||
|
// 加入密钥作为混淆,避免简单的拼接,被外部利用,用户自定义该密钥即可
|
||||||
|
String origin = lowerCaseCaptcha+checkKey+jeecgBaseConfig.getSignatureSecret();
|
||||||
|
String realKey = Md5Util.md5Encode(origin, "utf-8");
|
||||||
|
Object checkCode = redisUtil.get(realKey);
|
||||||
|
//当进入登录页时,有一定几率出现验证码错误 #1714
|
||||||
|
if(checkCode==null || !checkCode.toString().equals(lowerCaseCaptcha)) {
|
||||||
|
Map<String, Object> map = new HashMap<>();
|
||||||
|
map.put("message", "验证码错误");
|
||||||
|
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,"fdsafas", Instant.now(), Instant.now().plusNanos(1)), null, map);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!registeredClient.getAuthorizationGrantTypes().contains(authorizationGrantType)) {
|
||||||
|
Map<String, Object> map = new HashMap<>();
|
||||||
|
map.put("message", "非法登录");
|
||||||
|
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,"fdsafas", Instant.now(), Instant.now().plusNanos(1)), null, map);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 通过用户名获取用户信息
|
||||||
|
LoginUser loginUser = commonAPI.getUserByName(username);
|
||||||
|
//update-begin---author:eightmonth ---date:2024-04-30 for:【6168】master分支切sas分支登录发生错误-----------
|
||||||
|
if (Objects.isNull(loginUser) || !StringUtils.hasText(loginUser.getSalt())) {
|
||||||
|
redisUtil.del(CacheConstant.SYS_USERS_CACHE+"::"+username);
|
||||||
|
loginUser = commonAPI.getUserByName(username);
|
||||||
|
}
|
||||||
|
//update-end---author:eightmonth ---date::2024-04-30 for:【6168】master分支切sas分支登录发生错误--------------
|
||||||
|
// 检查用户可行性
|
||||||
|
checkUserIsEffective(loginUser);
|
||||||
|
|
||||||
|
// 不使用spring security passwordEncoder针对密码进行匹配,使用自有加密匹配,针对 spring security使用noop传输
|
||||||
|
password = PasswordUtil.encrypt(username, password, loginUser.getSalt());
|
||||||
|
if (!password.equals(loginUser.getPassword())) {
|
||||||
|
addLoginFailOvertimes(username);
|
||||||
|
Map<String, Object> map = new HashMap<>();
|
||||||
|
map.put("message", "用户名或密码不正确");
|
||||||
|
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,"fdsafas", Instant.now(), Instant.now().plusNanos(1)), null, map);
|
||||||
|
}
|
||||||
|
|
||||||
|
//由于在上面已验证过用户名、密码,现在构建一个已认证的对象UsernamePasswordAuthenticationToken
|
||||||
|
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = UsernamePasswordAuthenticationToken.authenticated(loginUser,clientPrincipal,new ArrayList<>());
|
||||||
|
|
||||||
|
DefaultOAuth2TokenContext.Builder tokenContextBuilder = DefaultOAuth2TokenContext.builder()
|
||||||
|
.registeredClient(registeredClient)
|
||||||
|
.principal(usernamePasswordAuthenticationToken)
|
||||||
|
.authorizationServerContext(AuthorizationServerContextHolder.getContext())
|
||||||
|
.authorizationGrantType(authorizationGrantType)
|
||||||
|
.authorizedScopes(requestScopeSet)
|
||||||
|
.authorizationGrant(appGrantAuthenticationToken);
|
||||||
|
|
||||||
|
OAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization.withRegisteredClient(registeredClient)
|
||||||
|
.principalName(clientPrincipal.getName())
|
||||||
|
.authorizedScopes(requestScopeSet)
|
||||||
|
.attribute(Principal.class.getName(), username)
|
||||||
|
.authorizationGrantType(authorizationGrantType);
|
||||||
|
|
||||||
|
|
||||||
|
// ----- Access token -----
|
||||||
|
OAuth2TokenContext tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.ACCESS_TOKEN).build();
|
||||||
|
OAuth2Token generatedAccessToken = this.tokenGenerator.generate(tokenContext);
|
||||||
|
if (generatedAccessToken == null) {
|
||||||
|
Map<String, Object> map = new HashMap<>();
|
||||||
|
map.put("message", "无法生成访问token,请联系管理系。");
|
||||||
|
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,"fdsafas", Instant.now(), Instant.now().plusNanos(1)), null, map);
|
||||||
|
}
|
||||||
|
OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
|
||||||
|
generatedAccessToken.getTokenValue(), generatedAccessToken.getIssuedAt(),
|
||||||
|
generatedAccessToken.getExpiresAt(), tokenContext.getAuthorizedScopes());
|
||||||
|
if (generatedAccessToken instanceof ClaimAccessor) {
|
||||||
|
authorizationBuilder.token(accessToken, (metadata) -> {
|
||||||
|
metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, ((ClaimAccessor) generatedAccessToken).getClaims());
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
authorizationBuilder.accessToken(accessToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- Refresh token -----
|
||||||
|
OAuth2RefreshToken refreshToken = null;
|
||||||
|
if (registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.REFRESH_TOKEN) &&
|
||||||
|
// 不向公共客户端颁发刷新令牌
|
||||||
|
!clientPrincipal.getClientAuthenticationMethod().equals(ClientAuthenticationMethod.NONE)) {
|
||||||
|
|
||||||
|
tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.REFRESH_TOKEN).build();
|
||||||
|
OAuth2Token generatedRefreshToken = this.tokenGenerator.generate(tokenContext);
|
||||||
|
if (!(generatedRefreshToken instanceof OAuth2RefreshToken)) {
|
||||||
|
Map<String, Object> map = new HashMap<>();
|
||||||
|
map.put("message", "无法生成刷新token,请联系管理员。");
|
||||||
|
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,"fdsafas", Instant.now(), Instant.now().plusNanos(1)), null, map);
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshToken = (OAuth2RefreshToken) generatedRefreshToken;
|
||||||
|
authorizationBuilder.refreshToken(refreshToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
OAuth2Authorization authorization = authorizationBuilder.build();
|
||||||
|
|
||||||
|
// 保存认证信息至redis
|
||||||
|
authorizationService.save(authorization);
|
||||||
|
|
||||||
|
// 登录成功,删除redis中的验证码
|
||||||
|
redisUtil.del(realKey);
|
||||||
|
redisUtil.del(CommonConstant.LOGIN_FAIL + username);
|
||||||
|
baseCommonService.addLog("用户名: " + username + ",登录成功!", CommonConstant.LOG_TYPE_1, null,loginUser);
|
||||||
|
|
||||||
|
JSONObject addition = new JSONObject(new LinkedHashMap<>());
|
||||||
|
addition.put("token", accessToken.getTokenValue());
|
||||||
|
// 设置租户
|
||||||
|
JSONObject jsonObject = commonAPI.setLoginTenant(username);
|
||||||
|
addition.putAll(jsonObject.getInnerMap());
|
||||||
|
|
||||||
|
// 设置登录用户信息
|
||||||
|
addition.put("userInfo", loginUser);
|
||||||
|
addition.put("sysAllDictItems", commonAPI.queryAllDictItems());
|
||||||
|
|
||||||
|
List<SysDepartModel> departs = commonAPI.queryUserDeparts(loginUser.getId());
|
||||||
|
addition.put("departs", departs);
|
||||||
|
if (departs == null || departs.size() == 0) {
|
||||||
|
addition.put("multi_depart", 0);
|
||||||
|
} else if (departs.size() == 1) {
|
||||||
|
commonAPI.updateUserDepart(username, departs.get(0).getOrgCode(),null);
|
||||||
|
addition.put("multi_depart", 1);
|
||||||
|
} else {
|
||||||
|
//查询当前是否有登录部门
|
||||||
|
if(oConvertUtils.isEmpty(loginUser.getOrgCode())){
|
||||||
|
commonAPI.updateUserDepart(username, departs.get(0).getOrgCode(),null);
|
||||||
|
}
|
||||||
|
addition.put("multi_depart", 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 兼容原有shiro登录结果处理
|
||||||
|
Map<String, Object> map = new HashMap<>();
|
||||||
|
map.put("result", addition);
|
||||||
|
map.put("code", 200);
|
||||||
|
map.put("success", true);
|
||||||
|
map.put("timestamp", System.currentTimeMillis());
|
||||||
|
|
||||||
|
// 返回access_token、refresh_token以及其它信息给到前端
|
||||||
|
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, accessToken, refreshToken, map);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean supports(Class<?> authentication) {
|
||||||
|
return AppGrantAuthenticationToken.class.isAssignableFrom(authentication);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static OAuth2ClientAuthenticationToken getAuthenticatedClientElseThrowInvalidClient(Authentication authentication) {
|
||||||
|
OAuth2ClientAuthenticationToken clientPrincipal = null;
|
||||||
|
if (OAuth2ClientAuthenticationToken.class.isAssignableFrom(authentication.getPrincipal().getClass())) {
|
||||||
|
clientPrincipal = (OAuth2ClientAuthenticationToken) authentication.getPrincipal();
|
||||||
|
}
|
||||||
|
if (clientPrincipal != null && clientPrincipal.isAuthenticated()) {
|
||||||
|
return clientPrincipal;
|
||||||
|
}
|
||||||
|
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_CLIENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 登录失败超出次数5 返回true
|
||||||
|
* @param username
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
private boolean isLoginFailOvertimes(String username){
|
||||||
|
String key = CommonConstant.LOGIN_FAIL + username;
|
||||||
|
Object failTime = redisUtil.get(key);
|
||||||
|
if(failTime!=null){
|
||||||
|
Integer val = Integer.parseInt(failTime.toString());
|
||||||
|
if(val>5){
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录登录失败次数
|
||||||
|
* @param username
|
||||||
|
*/
|
||||||
|
private void addLoginFailOvertimes(String username){
|
||||||
|
String key = CommonConstant.LOGIN_FAIL + username;
|
||||||
|
Object failTime = redisUtil.get(key);
|
||||||
|
Integer val = 0;
|
||||||
|
if(failTime!=null){
|
||||||
|
val = Integer.parseInt(failTime.toString());
|
||||||
|
}
|
||||||
|
// 10分钟
|
||||||
|
redisUtil.set(key, ++val, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验用户是否有效
|
||||||
|
*/
|
||||||
|
private void checkUserIsEffective(LoginUser loginUser) {
|
||||||
|
//情况1:根据用户信息查询,该用户不存在
|
||||||
|
if (Objects.isNull(loginUser)) {
|
||||||
|
baseCommonService.addLog("用户登录失败,用户不存在!", CommonConstant.LOG_TYPE_1, null);
|
||||||
|
throw new JeecgBootException("该用户不存在,请注册");
|
||||||
|
}
|
||||||
|
//情况2:根据用户信息查询,该用户已注销
|
||||||
|
//update-begin---author:王帅 Date:20200601 for:if条件永远为falsebug------------
|
||||||
|
if (CommonConstant.DEL_FLAG_1.equals(loginUser.getDelFlag())) {
|
||||||
|
//update-end---author:王帅 Date:20200601 for:if条件永远为falsebug------------
|
||||||
|
baseCommonService.addLog("用户登录失败,用户名:" + loginUser.getUsername() + "已注销!", CommonConstant.LOG_TYPE_1, null);
|
||||||
|
throw new JeecgBootException("该用户已注销");
|
||||||
|
}
|
||||||
|
//情况3:根据用户信息查询,该用户已冻结
|
||||||
|
if (CommonConstant.USER_FREEZE.equals(loginUser.getStatus())) {
|
||||||
|
baseCommonService.addLog("用户登录失败,用户名:" + loginUser.getUsername() + "已冻结!", CommonConstant.LOG_TYPE_1, null);
|
||||||
|
throw new JeecgBootException("该用户已冻结");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,21 @@
|
|||||||
|
package org.jeecg.config.security.app;
|
||||||
|
|
||||||
|
import org.jeecg.config.security.LoginType;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationGrantAuthenticationToken;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* APP模式认证专用token类型,方法spring authorization server进行认证流转,配合convert使用
|
||||||
|
* @author EightMonth
|
||||||
|
* @date 2024/1/1
|
||||||
|
*/
|
||||||
|
public class AppGrantAuthenticationToken extends OAuth2AuthorizationGrantAuthenticationToken {
|
||||||
|
|
||||||
|
public AppGrantAuthenticationToken(Authentication clientPrincipal, Map<String, Object> additionalParameters) {
|
||||||
|
super(new AuthorizationGrantType(LoginType.APP), clientPrincipal, additionalParameters);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,82 @@
|
|||||||
|
package org.jeecg.config.security.password;
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import org.jeecg.common.constant.CommonConstant;
|
||||||
|
import org.jeecg.config.security.LoginType;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
|
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||||
|
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||||
|
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||||
|
import org.springframework.security.web.authentication.AuthenticationConverter;
|
||||||
|
import org.springframework.util.LinkedMultiValueMap;
|
||||||
|
import org.springframework.util.MultiValueMap;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 密码模式认证转换器
|
||||||
|
* @author EightMonth
|
||||||
|
* @date 2024/1/1
|
||||||
|
*/
|
||||||
|
public class PasswordGrantAuthenticationConvert implements AuthenticationConverter {
|
||||||
|
@Override
|
||||||
|
public Authentication convert(HttpServletRequest request) {
|
||||||
|
|
||||||
|
String grantType = request.getParameter(OAuth2ParameterNames.GRANT_TYPE);
|
||||||
|
if (!LoginType.PASSWORD.equals(grantType)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Authentication clientPrincipal = SecurityContextHolder.getContext().getAuthentication();
|
||||||
|
|
||||||
|
//从request中提取请求参数,然后存入MultiValueMap<String, String>
|
||||||
|
MultiValueMap<String, String> parameters = getParameters(request);
|
||||||
|
|
||||||
|
// username (REQUIRED)
|
||||||
|
String username = parameters.getFirst(OAuth2ParameterNames.USERNAME);
|
||||||
|
if (!StringUtils.hasText(username) ||
|
||||||
|
parameters.get(OAuth2ParameterNames.USERNAME).size() != 1) {
|
||||||
|
throw new OAuth2AuthenticationException("无效请求,用户名不能为空!");
|
||||||
|
}
|
||||||
|
String password = parameters.getFirst(OAuth2ParameterNames.PASSWORD);
|
||||||
|
if (!StringUtils.hasText(password) ||
|
||||||
|
parameters.get(OAuth2ParameterNames.PASSWORD).size() != 1) {
|
||||||
|
throw new OAuth2AuthenticationException("无效请求,密码不能为空!");
|
||||||
|
}
|
||||||
|
|
||||||
|
//收集要传入PasswordGrantAuthenticationToken构造方法的参数,
|
||||||
|
//该参数接下来在PasswordGrantAuthenticationProvider中使用
|
||||||
|
Map<String, Object> additionalParameters = new HashMap<>();
|
||||||
|
//遍历从request中提取的参数,排除掉grant_type、client_id、code等字段参数,其他参数收集到additionalParameters中
|
||||||
|
parameters.forEach((key, value) -> {
|
||||||
|
if (!key.equals(OAuth2ParameterNames.GRANT_TYPE) &&
|
||||||
|
!key.equals(OAuth2ParameterNames.CLIENT_ID) &&
|
||||||
|
!key.equals(OAuth2ParameterNames.CODE)) {
|
||||||
|
additionalParameters.put(key, value.get(0));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
//返回自定义的PasswordGrantAuthenticationToken对象
|
||||||
|
return new PasswordGrantAuthenticationToken(clientPrincipal, additionalParameters);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*从request中提取请求参数,然后存入MultiValueMap<String, String>
|
||||||
|
*/
|
||||||
|
private static MultiValueMap<String, String> getParameters(HttpServletRequest request) {
|
||||||
|
Map<String, String[]> parameterMap = request.getParameterMap();
|
||||||
|
MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>(parameterMap.size());
|
||||||
|
parameterMap.forEach((key, values) -> {
|
||||||
|
if (values.length > 0) {
|
||||||
|
for (String value : values) {
|
||||||
|
parameters.add(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return parameters;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,319 @@
|
|||||||
|
package org.jeecg.config.security.password;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson.JSONObject;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.jeecg.common.api.CommonAPI;
|
||||||
|
import org.jeecg.common.constant.CacheConstant;
|
||||||
|
import org.jeecg.common.constant.CommonConstant;
|
||||||
|
import org.jeecg.common.exception.JeecgBootException;
|
||||||
|
import org.jeecg.common.exception.JeecgCaptchaException;
|
||||||
|
import org.jeecg.common.system.vo.LoginUser;
|
||||||
|
import org.jeecg.common.system.vo.SysDepartModel;
|
||||||
|
import org.jeecg.common.util.Md5Util;
|
||||||
|
import org.jeecg.common.util.PasswordUtil;
|
||||||
|
import org.jeecg.common.util.RedisUtil;
|
||||||
|
import org.jeecg.common.util.oConvertUtils;
|
||||||
|
import org.jeecg.config.JeecgBaseConfig;
|
||||||
|
import org.jeecg.modules.base.service.BaseCommonService;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.context.annotation.Lazy;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.security.authentication.AuthenticationProvider;
|
||||||
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.core.AuthenticationException;
|
||||||
|
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||||
|
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||||
|
import org.springframework.security.oauth2.core.*;
|
||||||
|
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AccessTokenAuthenticationToken;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContextHolder;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.token.DefaultOAuth2TokenContext;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenContext;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
|
import java.security.Principal;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 密码模式认证处理器,负责处理该认证模式下的核心逻辑
|
||||||
|
* @author EightMonth
|
||||||
|
* @date 2024/1/1
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
public class PasswordGrantAuthenticationProvider implements AuthenticationProvider {
|
||||||
|
|
||||||
|
private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-5.2";
|
||||||
|
|
||||||
|
private final OAuth2AuthorizationService authorizationService;
|
||||||
|
private final OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator;
|
||||||
|
@Lazy
|
||||||
|
@Autowired
|
||||||
|
private CommonAPI commonAPI;
|
||||||
|
@Autowired
|
||||||
|
private RedisUtil redisUtil;
|
||||||
|
@Autowired
|
||||||
|
private JeecgBaseConfig jeecgBaseConfig;
|
||||||
|
@Autowired
|
||||||
|
private BaseCommonService baseCommonService;
|
||||||
|
|
||||||
|
public PasswordGrantAuthenticationProvider(OAuth2AuthorizationService authorizationService, OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator) {
|
||||||
|
Assert.notNull(authorizationService, "authorizationService cannot be null");
|
||||||
|
Assert.notNull(tokenGenerator, "tokenGenerator cannot be null");
|
||||||
|
this.authorizationService = authorizationService;
|
||||||
|
this.tokenGenerator = tokenGenerator;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
|
||||||
|
PasswordGrantAuthenticationToken passwordGrantAuthenticationToken = (PasswordGrantAuthenticationToken) authentication;
|
||||||
|
Map<String, Object> additionalParameter = passwordGrantAuthenticationToken.getAdditionalParameters();
|
||||||
|
|
||||||
|
// 授权类型
|
||||||
|
AuthorizationGrantType authorizationGrantType = passwordGrantAuthenticationToken.getGrantType();
|
||||||
|
// 用户名
|
||||||
|
String username = (String) additionalParameter.get(OAuth2ParameterNames.USERNAME);
|
||||||
|
// 密码
|
||||||
|
String password = (String) additionalParameter.get(OAuth2ParameterNames.PASSWORD);
|
||||||
|
//请求参数权限范围
|
||||||
|
String requestScopesStr = (String)additionalParameter.getOrDefault(OAuth2ParameterNames.SCOPE, "*");
|
||||||
|
//请求参数权限范围专场集合
|
||||||
|
Set<String> requestScopeSet = Stream.of(requestScopesStr.split(" ")).collect(Collectors.toSet());
|
||||||
|
// 验证码
|
||||||
|
String captcha = (String) additionalParameter.get("captcha");
|
||||||
|
String checkKey = (String) additionalParameter.get("checkKey");
|
||||||
|
|
||||||
|
OAuth2ClientAuthenticationToken clientPrincipal = getAuthenticatedClientElseThrowInvalidClient(passwordGrantAuthenticationToken);
|
||||||
|
RegisteredClient registeredClient = clientPrincipal.getRegisteredClient();
|
||||||
|
|
||||||
|
// 检查登录失败次数
|
||||||
|
if(isLoginFailOvertimes(username)){
|
||||||
|
throw new JeecgBootException("该用户登录失败次数过多,请于10分钟后再次登录!");
|
||||||
|
}
|
||||||
|
|
||||||
|
if(captcha==null){
|
||||||
|
throw new JeecgBootException("验证码无效");
|
||||||
|
}
|
||||||
|
String lowerCaseCaptcha = captcha.toLowerCase();
|
||||||
|
// 加入密钥作为混淆,避免简单的拼接,被外部利用,用户自定义该密钥即可
|
||||||
|
String origin = lowerCaseCaptcha+checkKey+jeecgBaseConfig.getSignatureSecret();
|
||||||
|
String realKey = Md5Util.md5Encode(origin, "utf-8");
|
||||||
|
Object checkCode = redisUtil.get(realKey);
|
||||||
|
//当进入登录页时,有一定几率出现验证码错误 #1714
|
||||||
|
if(checkCode==null || !checkCode.toString().equals(lowerCaseCaptcha)) {
|
||||||
|
Map<String, Object> map = new HashMap<>();
|
||||||
|
map.put("message", "验证码错误");
|
||||||
|
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,"fdsafas", Instant.now(), Instant.now().plusNanos(1)), null, map);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!registeredClient.getAuthorizationGrantTypes().contains(authorizationGrantType)) {
|
||||||
|
Map<String, Object> map = new HashMap<>();
|
||||||
|
map.put("message", "非法登录");
|
||||||
|
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,"fdsafas", Instant.now(), Instant.now().plusNanos(1)), null, map);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 通过用户名获取用户信息
|
||||||
|
LoginUser loginUser = commonAPI.getUserByName(username);
|
||||||
|
//update-begin---author:eightmonth ---date:2024-04-30 for:【6168】master分支切sas分支登录发生错误-----------
|
||||||
|
if (Objects.isNull(loginUser) || !StringUtils.hasText(loginUser.getSalt())) {
|
||||||
|
redisUtil.del(CacheConstant.SYS_USERS_CACHE+"::"+username);
|
||||||
|
loginUser = commonAPI.getUserByName(username);
|
||||||
|
}
|
||||||
|
//update-end---author:eightmonth ---date::2024-04-30 for:【6168】master分支切sas分支登录发生错误--------------
|
||||||
|
// 检查用户可行性
|
||||||
|
checkUserIsEffective(loginUser);
|
||||||
|
|
||||||
|
// 不使用spring security passwordEncoder针对密码进行匹配,使用自有加密匹配,针对 spring security使用noop传输
|
||||||
|
password = PasswordUtil.encrypt(username, password, loginUser.getSalt());
|
||||||
|
if (!password.equals(loginUser.getPassword())) {
|
||||||
|
addLoginFailOvertimes(username);
|
||||||
|
|
||||||
|
Map<String, Object> map = new HashMap<>();
|
||||||
|
map.put("message", "用户名或密码不正确");
|
||||||
|
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,"fdsafas", Instant.now(), Instant.now().plusNanos(1)), null, map);
|
||||||
|
}
|
||||||
|
|
||||||
|
//由于在上面已验证过用户名、密码,现在构建一个已认证的对象UsernamePasswordAuthenticationToken
|
||||||
|
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = UsernamePasswordAuthenticationToken.authenticated(loginUser,clientPrincipal,new ArrayList<>());
|
||||||
|
|
||||||
|
DefaultOAuth2TokenContext.Builder tokenContextBuilder = DefaultOAuth2TokenContext.builder()
|
||||||
|
.registeredClient(registeredClient)
|
||||||
|
.principal(usernamePasswordAuthenticationToken)
|
||||||
|
.authorizationServerContext(AuthorizationServerContextHolder.getContext())
|
||||||
|
.authorizationGrantType(authorizationGrantType)
|
||||||
|
.authorizedScopes(requestScopeSet)
|
||||||
|
.authorizationGrant(passwordGrantAuthenticationToken);
|
||||||
|
|
||||||
|
OAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization.withRegisteredClient(registeredClient)
|
||||||
|
.principalName(clientPrincipal.getName())
|
||||||
|
.authorizedScopes(requestScopeSet)
|
||||||
|
.attribute(Principal.class.getName(), username)
|
||||||
|
.authorizationGrantType(authorizationGrantType);
|
||||||
|
|
||||||
|
|
||||||
|
// ----- Access token -----
|
||||||
|
OAuth2TokenContext tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.ACCESS_TOKEN).build();
|
||||||
|
OAuth2Token generatedAccessToken = this.tokenGenerator.generate(tokenContext);
|
||||||
|
if (generatedAccessToken == null) {
|
||||||
|
Map<String, Object> map = new HashMap<>();
|
||||||
|
map.put("message", "无法生成访问token,请联系管理系。");
|
||||||
|
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,"fdsafas", Instant.now(), Instant.now().plusNanos(1)), null, map);
|
||||||
|
}
|
||||||
|
OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
|
||||||
|
generatedAccessToken.getTokenValue(), generatedAccessToken.getIssuedAt(),
|
||||||
|
generatedAccessToken.getExpiresAt(), tokenContext.getAuthorizedScopes());
|
||||||
|
if (generatedAccessToken instanceof ClaimAccessor) {
|
||||||
|
authorizationBuilder.token(accessToken, (metadata) -> {
|
||||||
|
metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, ((ClaimAccessor) generatedAccessToken).getClaims());
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
authorizationBuilder.accessToken(accessToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- Refresh token -----
|
||||||
|
OAuth2RefreshToken refreshToken = null;
|
||||||
|
if (registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.REFRESH_TOKEN) &&
|
||||||
|
// 不向公共客户端颁发刷新令牌
|
||||||
|
!clientPrincipal.getClientAuthenticationMethod().equals(ClientAuthenticationMethod.NONE)) {
|
||||||
|
|
||||||
|
tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.REFRESH_TOKEN).build();
|
||||||
|
OAuth2Token generatedRefreshToken = this.tokenGenerator.generate(tokenContext);
|
||||||
|
if (!(generatedRefreshToken instanceof OAuth2RefreshToken)) {
|
||||||
|
Map<String, Object> map = new HashMap<>();
|
||||||
|
map.put("message", "无法生成访问token,请联系管理系。");
|
||||||
|
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,"fdsafas", Instant.now(), Instant.now().plusNanos(1)), null, map);
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshToken = (OAuth2RefreshToken) generatedRefreshToken;
|
||||||
|
authorizationBuilder.refreshToken(refreshToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
OAuth2Authorization authorization = authorizationBuilder.build();
|
||||||
|
|
||||||
|
// 保存认证信息至redis
|
||||||
|
authorizationService.save(authorization);
|
||||||
|
|
||||||
|
// 登录成功,删除redis中的验证码
|
||||||
|
redisUtil.del(realKey);
|
||||||
|
redisUtil.del(CommonConstant.LOGIN_FAIL + username);
|
||||||
|
baseCommonService.addLog("用户名: " + username + ",登录成功!", CommonConstant.LOG_TYPE_1, null,loginUser);
|
||||||
|
|
||||||
|
JSONObject addition = new JSONObject(new LinkedHashMap<>());
|
||||||
|
addition.put("token", accessToken.getTokenValue());
|
||||||
|
|
||||||
|
// 设置租户
|
||||||
|
JSONObject jsonObject = commonAPI.setLoginTenant(username);
|
||||||
|
addition.putAll(jsonObject.getInnerMap());
|
||||||
|
|
||||||
|
// 设置登录用户信息
|
||||||
|
addition.put("userInfo", loginUser);
|
||||||
|
addition.put("sysAllDictItems", commonAPI.queryAllDictItems());
|
||||||
|
|
||||||
|
List<SysDepartModel> departs = commonAPI.queryUserDeparts(loginUser.getId());
|
||||||
|
addition.put("departs", departs);
|
||||||
|
if (departs == null || departs.size() == 0) {
|
||||||
|
addition.put("multi_depart", 0);
|
||||||
|
} else if (departs.size() == 1) {
|
||||||
|
commonAPI.updateUserDepart(username, departs.get(0).getOrgCode(),null);
|
||||||
|
addition.put("multi_depart", 1);
|
||||||
|
} else {
|
||||||
|
//查询当前是否有登录部门
|
||||||
|
if(oConvertUtils.isEmpty(loginUser.getOrgCode())){
|
||||||
|
commonAPI.updateUserDepart(username, departs.get(0).getOrgCode(),null);
|
||||||
|
}
|
||||||
|
addition.put("multi_depart", 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 兼容原有shiro登录结果处理
|
||||||
|
Map<String, Object> map = new HashMap<>();
|
||||||
|
map.put("result", addition);
|
||||||
|
map.put("code", 200);
|
||||||
|
map.put("success", true);
|
||||||
|
map.put("timestamp", System.currentTimeMillis());
|
||||||
|
|
||||||
|
// 返回access_token、refresh_token以及其它信息给到前端
|
||||||
|
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, accessToken, refreshToken, map);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean supports(Class<?> authentication) {
|
||||||
|
return PasswordGrantAuthenticationToken.class.isAssignableFrom(authentication);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static OAuth2ClientAuthenticationToken getAuthenticatedClientElseThrowInvalidClient(Authentication authentication) {
|
||||||
|
OAuth2ClientAuthenticationToken clientPrincipal = null;
|
||||||
|
if (OAuth2ClientAuthenticationToken.class.isAssignableFrom(authentication.getPrincipal().getClass())) {
|
||||||
|
clientPrincipal = (OAuth2ClientAuthenticationToken) authentication.getPrincipal();
|
||||||
|
}
|
||||||
|
if (clientPrincipal != null && clientPrincipal.isAuthenticated()) {
|
||||||
|
return clientPrincipal;
|
||||||
|
}
|
||||||
|
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_CLIENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 登录失败超出次数5 返回true
|
||||||
|
* @param username
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
private boolean isLoginFailOvertimes(String username){
|
||||||
|
String key = CommonConstant.LOGIN_FAIL + username;
|
||||||
|
Object failTime = redisUtil.get(key);
|
||||||
|
if(failTime!=null){
|
||||||
|
Integer val = Integer.parseInt(failTime.toString());
|
||||||
|
if(val>5){
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录登录失败次数
|
||||||
|
* @param username
|
||||||
|
*/
|
||||||
|
private void addLoginFailOvertimes(String username){
|
||||||
|
String key = CommonConstant.LOGIN_FAIL + username;
|
||||||
|
Object failTime = redisUtil.get(key);
|
||||||
|
Integer val = 0;
|
||||||
|
if(failTime!=null){
|
||||||
|
val = Integer.parseInt(failTime.toString());
|
||||||
|
}
|
||||||
|
// 10分钟
|
||||||
|
redisUtil.set(key, ++val, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验用户是否有效
|
||||||
|
*/
|
||||||
|
private void checkUserIsEffective(LoginUser loginUser) {
|
||||||
|
//情况1:根据用户信息查询,该用户不存在
|
||||||
|
if (Objects.isNull(loginUser)) {
|
||||||
|
baseCommonService.addLog("用户登录失败,用户不存在!", CommonConstant.LOG_TYPE_1, null);
|
||||||
|
throw new JeecgBootException("该用户不存在,请注册");
|
||||||
|
}
|
||||||
|
//情况2:根据用户信息查询,该用户已注销
|
||||||
|
//update-begin---author:王帅 Date:20200601 for:if条件永远为falsebug------------
|
||||||
|
if (CommonConstant.DEL_FLAG_1.equals(loginUser.getDelFlag())) {
|
||||||
|
//update-end---author:王帅 Date:20200601 for:if条件永远为falsebug------------
|
||||||
|
baseCommonService.addLog("用户登录失败,用户名:" + loginUser.getUsername() + "已注销!", CommonConstant.LOG_TYPE_1, null);
|
||||||
|
throw new JeecgBootException("该用户已注销");
|
||||||
|
}
|
||||||
|
//情况3:根据用户信息查询,该用户已冻结
|
||||||
|
if (CommonConstant.USER_FREEZE.equals(loginUser.getStatus())) {
|
||||||
|
baseCommonService.addLog("用户登录失败,用户名:" + loginUser.getUsername() + "已冻结!", CommonConstant.LOG_TYPE_1, null);
|
||||||
|
throw new JeecgBootException("该用户已冻结");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,21 @@
|
|||||||
|
package org.jeecg.config.security.password;
|
||||||
|
|
||||||
|
import org.jeecg.config.security.LoginType;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationGrantAuthenticationToken;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 密码模式认证专用token类型,方法spring authorization server进行认证流转,配合convert使用
|
||||||
|
* @author EightMonth
|
||||||
|
* @date 2024/1/1
|
||||||
|
*/
|
||||||
|
public class PasswordGrantAuthenticationToken extends OAuth2AuthorizationGrantAuthenticationToken {
|
||||||
|
|
||||||
|
public PasswordGrantAuthenticationToken(Authentication clientPrincipal, Map<String, Object> additionalParameters) {
|
||||||
|
super(new AuthorizationGrantType(LoginType.PASSWORD), clientPrincipal, additionalParameters);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,77 @@
|
|||||||
|
package org.jeecg.config.security.phone;
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import org.jeecg.config.security.LoginType;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
|
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||||
|
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||||
|
import org.springframework.security.web.authentication.AuthenticationConverter;
|
||||||
|
import org.springframework.util.LinkedMultiValueMap;
|
||||||
|
import org.springframework.util.MultiValueMap;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 手机号模式认证转换器
|
||||||
|
* @author EightMonth
|
||||||
|
* @date 2024/1/1
|
||||||
|
*/
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class PhoneGrantAuthenticationConvert implements AuthenticationConverter {
|
||||||
|
@Override
|
||||||
|
public Authentication convert(HttpServletRequest request) {
|
||||||
|
|
||||||
|
String grantType = request.getParameter(OAuth2ParameterNames.GRANT_TYPE);
|
||||||
|
if (!LoginType.PHONE.equals(grantType)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Authentication clientPrincipal = SecurityContextHolder.getContext().getAuthentication();
|
||||||
|
|
||||||
|
//从request中提取请求参数,然后存入MultiValueMap<String, String>
|
||||||
|
MultiValueMap<String, String> parameters = getParameters(request);
|
||||||
|
|
||||||
|
// 验证码
|
||||||
|
String captcha = parameters.getFirst("captcha");
|
||||||
|
if (!StringUtils.hasText(captcha)) {
|
||||||
|
throw new OAuth2AuthenticationException("无效请求,验证码不能为空!");
|
||||||
|
}
|
||||||
|
|
||||||
|
//收集要传入PhoneGrantAuthenticationToken构造方法的参数,
|
||||||
|
//该参数接下来在PhoneGrantAuthenticationProvider中使用
|
||||||
|
Map<String, Object> additionalParameters = new HashMap<>();
|
||||||
|
//遍历从request中提取的参数,排除掉grant_type、client_id、code等字段参数,其他参数收集到additionalParameters中
|
||||||
|
parameters.forEach((key, value) -> {
|
||||||
|
if (!key.equals(OAuth2ParameterNames.GRANT_TYPE) &&
|
||||||
|
!key.equals(OAuth2ParameterNames.CLIENT_ID) &&
|
||||||
|
!key.equals(OAuth2ParameterNames.CODE)) {
|
||||||
|
additionalParameters.put(key, value.get(0));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
//返回自定义的PhoneGrantAuthenticationToken对象
|
||||||
|
return new PhoneGrantAuthenticationToken(clientPrincipal, additionalParameters);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*从request中提取请求参数,然后存入MultiValueMap<String, String>
|
||||||
|
*/
|
||||||
|
private static MultiValueMap<String, String> getParameters(HttpServletRequest request) {
|
||||||
|
Map<String, String[]> parameterMap = request.getParameterMap();
|
||||||
|
MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>(parameterMap.size());
|
||||||
|
parameterMap.forEach((key, values) -> {
|
||||||
|
if (values.length > 0) {
|
||||||
|
for (String value : values) {
|
||||||
|
parameters.add(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return parameters;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,292 @@
|
|||||||
|
package org.jeecg.config.security.phone;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson.JSONObject;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.jeecg.common.api.CommonAPI;
|
||||||
|
import org.jeecg.common.constant.CommonConstant;
|
||||||
|
import org.jeecg.common.exception.JeecgBootException;
|
||||||
|
import org.jeecg.common.exception.JeecgCaptchaException;
|
||||||
|
import org.jeecg.common.system.vo.LoginUser;
|
||||||
|
import org.jeecg.common.system.vo.SysDepartModel;
|
||||||
|
import org.jeecg.common.util.Md5Util;
|
||||||
|
import org.jeecg.common.util.PasswordUtil;
|
||||||
|
import org.jeecg.common.util.RedisUtil;
|
||||||
|
import org.jeecg.common.util.oConvertUtils;
|
||||||
|
import org.jeecg.config.JeecgBaseConfig;
|
||||||
|
import org.jeecg.config.security.password.PasswordGrantAuthenticationToken;
|
||||||
|
import org.jeecg.modules.base.service.BaseCommonService;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.context.annotation.Lazy;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.security.authentication.AuthenticationProvider;
|
||||||
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.core.AuthenticationException;
|
||||||
|
import org.springframework.security.oauth2.core.*;
|
||||||
|
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AccessTokenAuthenticationToken;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContextHolder;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.token.DefaultOAuth2TokenContext;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenContext;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
|
||||||
|
import java.security.Principal;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 手机号模式认证处理器,负责处理该认证模式下的核心逻辑
|
||||||
|
* @author EightMonth
|
||||||
|
* @date 2024/1/1
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
public class PhoneGrantAuthenticationProvider implements AuthenticationProvider {
|
||||||
|
|
||||||
|
private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-5.2";
|
||||||
|
|
||||||
|
private final OAuth2AuthorizationService authorizationService;
|
||||||
|
private final OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator;
|
||||||
|
@Lazy
|
||||||
|
@Autowired
|
||||||
|
private CommonAPI commonAPI;
|
||||||
|
@Autowired
|
||||||
|
private RedisUtil redisUtil;
|
||||||
|
@Autowired
|
||||||
|
private JeecgBaseConfig jeecgBaseConfig;
|
||||||
|
@Autowired
|
||||||
|
private BaseCommonService baseCommonService;
|
||||||
|
|
||||||
|
public PhoneGrantAuthenticationProvider(OAuth2AuthorizationService authorizationService, OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator) {
|
||||||
|
Assert.notNull(authorizationService, "authorizationService cannot be null");
|
||||||
|
Assert.notNull(tokenGenerator, "tokenGenerator cannot be null");
|
||||||
|
this.authorizationService = authorizationService;
|
||||||
|
this.tokenGenerator = tokenGenerator;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
|
||||||
|
PhoneGrantAuthenticationToken phoneGrantAuthenticationToken = (PhoneGrantAuthenticationToken) authentication;
|
||||||
|
Map<String, Object> additionalParameter = phoneGrantAuthenticationToken.getAdditionalParameters();
|
||||||
|
|
||||||
|
// 授权类型
|
||||||
|
AuthorizationGrantType authorizationGrantType = phoneGrantAuthenticationToken.getGrantType();
|
||||||
|
// 手机号
|
||||||
|
String phone = (String) additionalParameter.get("mobile");
|
||||||
|
|
||||||
|
if(isLoginFailOvertimes(phone)){
|
||||||
|
throw new JeecgBootException("该用户登录失败次数过多,请于10分钟后再次登录!");
|
||||||
|
}
|
||||||
|
|
||||||
|
//请求参数权限范围
|
||||||
|
String requestScopesStr = (String)additionalParameter.getOrDefault(OAuth2ParameterNames.SCOPE, "*");
|
||||||
|
//请求参数权限范围专场集合
|
||||||
|
Set<String> requestScopeSet = Stream.of(requestScopesStr.split(" ")).collect(Collectors.toSet());
|
||||||
|
// 验证码
|
||||||
|
String captcha = (String) additionalParameter.get("captcha");
|
||||||
|
|
||||||
|
OAuth2ClientAuthenticationToken clientPrincipal = getAuthenticatedClientElseThrowInvalidClient(phoneGrantAuthenticationToken);
|
||||||
|
RegisteredClient registeredClient = clientPrincipal.getRegisteredClient();
|
||||||
|
|
||||||
|
// 通过手机号获取用户信息
|
||||||
|
LoginUser loginUser = commonAPI.getUserByPhone(phone);
|
||||||
|
// 检查用户可行性
|
||||||
|
checkUserIsEffective(loginUser);
|
||||||
|
|
||||||
|
|
||||||
|
String redisKey = CommonConstant.PHONE_REDIS_KEY_PRE+phone;
|
||||||
|
Object code = redisUtil.get(redisKey);
|
||||||
|
|
||||||
|
if (!captcha.equals(code)) {
|
||||||
|
//update-begin-author:taoyan date:2022-11-7 for: issues/4109 平台用户登录失败锁定用户
|
||||||
|
addLoginFailOvertimes(phone);
|
||||||
|
|
||||||
|
Map<String, Object> map = new HashMap<>();
|
||||||
|
map.put("message", "手机验证码错误");
|
||||||
|
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,"fdsafas", Instant.now(), Instant.now().plusNanos(1)), null, map);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!registeredClient.getAuthorizationGrantTypes().contains(authorizationGrantType)) {
|
||||||
|
Map<String, Object> map = new HashMap<>();
|
||||||
|
map.put("message", "非法登录");
|
||||||
|
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,"fdsafas", Instant.now(), Instant.now().plusNanos(1)), null, map);
|
||||||
|
}
|
||||||
|
|
||||||
|
//由于在上面已验证过用户名、密码,现在构建一个已认证的对象UsernamePasswordAuthenticationToken
|
||||||
|
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = UsernamePasswordAuthenticationToken.authenticated(loginUser,clientPrincipal,new ArrayList<>());
|
||||||
|
|
||||||
|
DefaultOAuth2TokenContext.Builder tokenContextBuilder = DefaultOAuth2TokenContext.builder()
|
||||||
|
.registeredClient(registeredClient)
|
||||||
|
.principal(usernamePasswordAuthenticationToken)
|
||||||
|
.authorizationServerContext(AuthorizationServerContextHolder.getContext())
|
||||||
|
.authorizationGrantType(authorizationGrantType)
|
||||||
|
.authorizedScopes(requestScopeSet)
|
||||||
|
.authorizationGrant(phoneGrantAuthenticationToken);
|
||||||
|
|
||||||
|
OAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization.withRegisteredClient(registeredClient)
|
||||||
|
.principalName(clientPrincipal.getName())
|
||||||
|
.authorizedScopes(requestScopeSet)
|
||||||
|
.attribute(Principal.class.getName(), loginUser.getUsername())
|
||||||
|
.authorizationGrantType(authorizationGrantType);
|
||||||
|
|
||||||
|
|
||||||
|
// ----- Access token -----
|
||||||
|
OAuth2TokenContext tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.ACCESS_TOKEN).build();
|
||||||
|
OAuth2Token generatedAccessToken = this.tokenGenerator.generate(tokenContext);
|
||||||
|
if (generatedAccessToken == null) {
|
||||||
|
Map<String, Object> map = new HashMap<>();
|
||||||
|
map.put("message", "无法生成刷新token,请联系管理员。");
|
||||||
|
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,"fdsafas", Instant.now(), Instant.now().plusNanos(1)), null, map);
|
||||||
|
}
|
||||||
|
OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
|
||||||
|
generatedAccessToken.getTokenValue(), generatedAccessToken.getIssuedAt(),
|
||||||
|
generatedAccessToken.getExpiresAt(), tokenContext.getAuthorizedScopes());
|
||||||
|
if (generatedAccessToken instanceof ClaimAccessor) {
|
||||||
|
authorizationBuilder.token(accessToken, (metadata) -> {
|
||||||
|
metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, ((ClaimAccessor) generatedAccessToken).getClaims());
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
authorizationBuilder.accessToken(accessToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- Refresh token -----
|
||||||
|
OAuth2RefreshToken refreshToken = null;
|
||||||
|
if (registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.REFRESH_TOKEN) &&
|
||||||
|
// 不向公共客户端颁发刷新令牌
|
||||||
|
!clientPrincipal.getClientAuthenticationMethod().equals(ClientAuthenticationMethod.NONE)) {
|
||||||
|
|
||||||
|
tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.REFRESH_TOKEN).build();
|
||||||
|
OAuth2Token generatedRefreshToken = this.tokenGenerator.generate(tokenContext);
|
||||||
|
if (!(generatedRefreshToken instanceof OAuth2RefreshToken)) {
|
||||||
|
Map<String, Object> map = new HashMap<>();
|
||||||
|
map.put("message", "无法生成刷新token,请联系管理员。");
|
||||||
|
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,"fdsafas", Instant.now(), Instant.now().plusNanos(1)), null, map);
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshToken = (OAuth2RefreshToken) generatedRefreshToken;
|
||||||
|
authorizationBuilder.refreshToken(refreshToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
OAuth2Authorization authorization = authorizationBuilder.build();
|
||||||
|
|
||||||
|
// 保存认证信息至redis
|
||||||
|
authorizationService.save(authorization);
|
||||||
|
|
||||||
|
baseCommonService.addLog("用户名: " + loginUser.getUsername() + ",登录成功!", CommonConstant.LOG_TYPE_1, null,loginUser);
|
||||||
|
|
||||||
|
JSONObject addition = new JSONObject(new LinkedHashMap<>());
|
||||||
|
addition.put("token", accessToken.getTokenValue());
|
||||||
|
// 设置租户
|
||||||
|
JSONObject jsonObject = commonAPI.setLoginTenant(loginUser.getUsername());
|
||||||
|
addition.putAll(jsonObject.getInnerMap());
|
||||||
|
|
||||||
|
// 设置登录用户信息
|
||||||
|
addition.put("userInfo", loginUser);
|
||||||
|
addition.put("sysAllDictItems", commonAPI.queryAllDictItems());
|
||||||
|
|
||||||
|
List<SysDepartModel> departs = commonAPI.queryUserDeparts(loginUser.getId());
|
||||||
|
addition.put("departs", departs);
|
||||||
|
if (departs == null || departs.size() == 0) {
|
||||||
|
addition.put("multi_depart", 0);
|
||||||
|
} else if (departs.size() == 1) {
|
||||||
|
commonAPI.updateUserDepart(loginUser.getUsername(), departs.get(0).getOrgCode(),null);
|
||||||
|
addition.put("multi_depart", 1);
|
||||||
|
} else {
|
||||||
|
//查询当前是否有登录部门
|
||||||
|
if(oConvertUtils.isEmpty(loginUser.getOrgCode())){
|
||||||
|
commonAPI.updateUserDepart(loginUser.getUsername(), departs.get(0).getOrgCode(),null);
|
||||||
|
}
|
||||||
|
addition.put("multi_depart", 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 兼容原有shiro登录结果处理
|
||||||
|
Map<String, Object> map = new HashMap<>();
|
||||||
|
map.put("result", addition);
|
||||||
|
map.put("code", 200);
|
||||||
|
map.put("success", true);
|
||||||
|
map.put("timestamp", System.currentTimeMillis());
|
||||||
|
|
||||||
|
// 返回access_token、refresh_token以及其它信息给到前端
|
||||||
|
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, accessToken, refreshToken, map);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean supports(Class<?> authentication) {
|
||||||
|
return PhoneGrantAuthenticationToken.class.isAssignableFrom(authentication);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static OAuth2ClientAuthenticationToken getAuthenticatedClientElseThrowInvalidClient(Authentication authentication) {
|
||||||
|
OAuth2ClientAuthenticationToken clientPrincipal = null;
|
||||||
|
if (OAuth2ClientAuthenticationToken.class.isAssignableFrom(authentication.getPrincipal().getClass())) {
|
||||||
|
clientPrincipal = (OAuth2ClientAuthenticationToken) authentication.getPrincipal();
|
||||||
|
}
|
||||||
|
if (clientPrincipal != null && clientPrincipal.isAuthenticated()) {
|
||||||
|
return clientPrincipal;
|
||||||
|
}
|
||||||
|
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_CLIENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 登录失败超出次数5 返回true
|
||||||
|
* @param username
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
private boolean isLoginFailOvertimes(String username){
|
||||||
|
String key = CommonConstant.LOGIN_FAIL + username;
|
||||||
|
Object failTime = redisUtil.get(key);
|
||||||
|
if(failTime!=null){
|
||||||
|
Integer val = Integer.parseInt(failTime.toString());
|
||||||
|
if(val>5){
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录登录失败次数
|
||||||
|
* @param username
|
||||||
|
*/
|
||||||
|
private void addLoginFailOvertimes(String username){
|
||||||
|
String key = CommonConstant.LOGIN_FAIL + username;
|
||||||
|
Object failTime = redisUtil.get(key);
|
||||||
|
Integer val = 0;
|
||||||
|
if(failTime!=null){
|
||||||
|
val = Integer.parseInt(failTime.toString());
|
||||||
|
}
|
||||||
|
// 10分钟
|
||||||
|
redisUtil.set(key, ++val, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验用户是否有效
|
||||||
|
*/
|
||||||
|
private void checkUserIsEffective(LoginUser loginUser) {
|
||||||
|
//情况1:根据用户信息查询,该用户不存在
|
||||||
|
if (Objects.isNull(loginUser)) {
|
||||||
|
baseCommonService.addLog("用户登录失败,用户不存在!", CommonConstant.LOG_TYPE_1, null);
|
||||||
|
throw new JeecgBootException("该用户不存在,请注册");
|
||||||
|
}
|
||||||
|
//情况2:根据用户信息查询,该用户已注销
|
||||||
|
//update-begin---author:王帅 Date:20200601 for:if条件永远为falsebug------------
|
||||||
|
if (CommonConstant.DEL_FLAG_1.equals(loginUser.getDelFlag())) {
|
||||||
|
//update-end---author:王帅 Date:20200601 for:if条件永远为falsebug------------
|
||||||
|
baseCommonService.addLog("用户登录失败,用户名:" + loginUser.getUsername() + "已注销!", CommonConstant.LOG_TYPE_1, null);
|
||||||
|
throw new JeecgBootException("该用户已注销");
|
||||||
|
}
|
||||||
|
//情况3:根据用户信息查询,该用户已冻结
|
||||||
|
if (CommonConstant.USER_FREEZE.equals(loginUser.getStatus())) {
|
||||||
|
baseCommonService.addLog("用户登录失败,用户名:" + loginUser.getUsername() + "已冻结!", CommonConstant.LOG_TYPE_1, null);
|
||||||
|
throw new JeecgBootException("该用户已冻结");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,21 @@
|
|||||||
|
package org.jeecg.config.security.phone;
|
||||||
|
|
||||||
|
import org.jeecg.config.security.LoginType;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationGrantAuthenticationToken;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 手机号模式认证专用token类型,方法spring authorization server进行认证流转,配合convert使用
|
||||||
|
* @author EightMonth
|
||||||
|
* @date 2024/1/1
|
||||||
|
*/
|
||||||
|
public class PhoneGrantAuthenticationToken extends OAuth2AuthorizationGrantAuthenticationToken {
|
||||||
|
|
||||||
|
public PhoneGrantAuthenticationToken(Authentication clientPrincipal, Map<String, Object> additionalParameters) {
|
||||||
|
super(new AuthorizationGrantType(LoginType.PHONE), clientPrincipal, additionalParameters);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,187 @@
|
|||||||
|
package org.jeecg.config.security.self;
|
||||||
|
|
||||||
|
import org.jeecg.common.api.CommonAPI;
|
||||||
|
import org.jeecg.common.constant.CommonConstant;
|
||||||
|
import org.jeecg.common.exception.JeecgBoot401Exception;
|
||||||
|
import org.jeecg.common.exception.JeecgBootException;
|
||||||
|
import org.jeecg.common.system.vo.LoginUser;
|
||||||
|
import org.jeecg.common.util.RedisUtil;
|
||||||
|
import org.jeecg.config.JeecgBaseConfig;
|
||||||
|
import org.jeecg.modules.base.service.BaseCommonService;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.context.annotation.Lazy;
|
||||||
|
import org.springframework.security.authentication.AuthenticationProvider;
|
||||||
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.core.AuthenticationException;
|
||||||
|
import org.springframework.security.oauth2.core.*;
|
||||||
|
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AccessTokenAuthenticationToken;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.token.DefaultOAuth2TokenContext;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenContext;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
|
||||||
|
import java.security.Principal;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自用生成token处理器,不对外开放,外部请求无法通过该方式生成token
|
||||||
|
* @author eightmonth@qq.com
|
||||||
|
* @date 2024/3/19 11:40
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class SelfAuthenticationProvider implements AuthenticationProvider {
|
||||||
|
|
||||||
|
private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-5.2";
|
||||||
|
|
||||||
|
private final OAuth2AuthorizationService authorizationService;
|
||||||
|
private final OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator;
|
||||||
|
@Lazy
|
||||||
|
@Autowired
|
||||||
|
private CommonAPI commonAPI;
|
||||||
|
@Autowired
|
||||||
|
private RedisUtil redisUtil;
|
||||||
|
@Autowired
|
||||||
|
private JeecgBaseConfig jeecgBaseConfig;
|
||||||
|
@Autowired
|
||||||
|
private BaseCommonService baseCommonService;
|
||||||
|
|
||||||
|
public SelfAuthenticationProvider(OAuth2AuthorizationService authorizationService, OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator) {
|
||||||
|
Assert.notNull(authorizationService, "authorizationService cannot be null");
|
||||||
|
Assert.notNull(tokenGenerator, "tokenGenerator cannot be null");
|
||||||
|
this.authorizationService = authorizationService;
|
||||||
|
this.tokenGenerator = tokenGenerator;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
|
||||||
|
SelfAuthenticationToken passwordGrantAuthenticationToken = (SelfAuthenticationToken) authentication;
|
||||||
|
Map<String, Object> additionalParameter = passwordGrantAuthenticationToken.getAdditionalParameters();
|
||||||
|
|
||||||
|
// 授权类型
|
||||||
|
AuthorizationGrantType authorizationGrantType = passwordGrantAuthenticationToken.getGrantType();
|
||||||
|
// 用户名
|
||||||
|
String username = (String) additionalParameter.get(OAuth2ParameterNames.USERNAME);
|
||||||
|
//请求参数权限范围
|
||||||
|
String requestScopesStr = "*";
|
||||||
|
//请求参数权限范围专场集合
|
||||||
|
Set<String> requestScopeSet = Stream.of(requestScopesStr.split(" ")).collect(Collectors.toSet());
|
||||||
|
|
||||||
|
OAuth2ClientAuthenticationToken clientPrincipal = getAuthenticatedClientElseThrowInvalidClient(passwordGrantAuthenticationToken);
|
||||||
|
RegisteredClient registeredClient = clientPrincipal.getRegisteredClient();
|
||||||
|
|
||||||
|
// 通过用户名获取用户信息
|
||||||
|
// LoginUser loginUser = commonAPI.getUserByName(username);
|
||||||
|
// 检查用户可行性
|
||||||
|
// checkUserIsEffective(loginUser);
|
||||||
|
|
||||||
|
//由于在上面已验证过用户名、密码,现在构建一个已认证的对象UsernamePasswordAuthenticationToken
|
||||||
|
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = UsernamePasswordAuthenticationToken.authenticated(username,clientPrincipal,new ArrayList<>());
|
||||||
|
|
||||||
|
DefaultOAuth2TokenContext.Builder tokenContextBuilder = DefaultOAuth2TokenContext.builder()
|
||||||
|
.registeredClient(registeredClient)
|
||||||
|
.principal(usernamePasswordAuthenticationToken)
|
||||||
|
.authorizationGrantType(authorizationGrantType)
|
||||||
|
.authorizedScopes(requestScopeSet)
|
||||||
|
.authorizationGrant(passwordGrantAuthenticationToken);
|
||||||
|
|
||||||
|
OAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization.withRegisteredClient(registeredClient)
|
||||||
|
.principalName(clientPrincipal.getName())
|
||||||
|
.authorizedScopes(requestScopeSet)
|
||||||
|
.attribute(Principal.class.getName(), username)
|
||||||
|
.authorizationGrantType(authorizationGrantType);
|
||||||
|
|
||||||
|
|
||||||
|
// ----- Access token -----
|
||||||
|
OAuth2TokenContext tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.ACCESS_TOKEN).build();
|
||||||
|
OAuth2Token generatedAccessToken = this.tokenGenerator.generate(tokenContext);
|
||||||
|
if (generatedAccessToken == null) {
|
||||||
|
throw new JeecgBoot401Exception("无法生成刷新token,请联系管理员。");
|
||||||
|
}
|
||||||
|
OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
|
||||||
|
generatedAccessToken.getTokenValue(), generatedAccessToken.getIssuedAt(),
|
||||||
|
generatedAccessToken.getExpiresAt(), tokenContext.getAuthorizedScopes());
|
||||||
|
if (generatedAccessToken instanceof ClaimAccessor) {
|
||||||
|
authorizationBuilder.token(accessToken, (metadata) -> {
|
||||||
|
metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, ((ClaimAccessor) generatedAccessToken).getClaims());
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
authorizationBuilder.accessToken(accessToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- Refresh token -----
|
||||||
|
OAuth2RefreshToken refreshToken = null;
|
||||||
|
if (registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.REFRESH_TOKEN) &&
|
||||||
|
// 不向公共客户端颁发刷新令牌
|
||||||
|
!clientPrincipal.getClientAuthenticationMethod().equals(ClientAuthenticationMethod.NONE)) {
|
||||||
|
|
||||||
|
tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.REFRESH_TOKEN).build();
|
||||||
|
OAuth2Token generatedRefreshToken = this.tokenGenerator.generate(tokenContext);
|
||||||
|
if (!(generatedRefreshToken instanceof OAuth2RefreshToken)) {
|
||||||
|
Map<String, Object> map = new HashMap<>();
|
||||||
|
map.put("message", "无法生成刷新token,请联系管理员。");
|
||||||
|
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,"fdsafas", Instant.now(), Instant.now().plusNanos(1)), null, map);
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshToken = (OAuth2RefreshToken) generatedRefreshToken;
|
||||||
|
authorizationBuilder.refreshToken(refreshToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
OAuth2Authorization authorization = authorizationBuilder.build();
|
||||||
|
|
||||||
|
// 保存认证信息至redis
|
||||||
|
authorizationService.save(authorization);
|
||||||
|
|
||||||
|
// 返回access_token、refresh_token以及其它信息给到前端
|
||||||
|
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, accessToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean supports(Class<?> authentication) {
|
||||||
|
return SelfAuthenticationToken.class.isAssignableFrom(authentication);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static OAuth2ClientAuthenticationToken getAuthenticatedClientElseThrowInvalidClient(Authentication authentication) {
|
||||||
|
OAuth2ClientAuthenticationToken clientPrincipal = null;
|
||||||
|
if (OAuth2ClientAuthenticationToken.class.isAssignableFrom(authentication.getPrincipal().getClass())) {
|
||||||
|
clientPrincipal = (OAuth2ClientAuthenticationToken) authentication.getPrincipal();
|
||||||
|
}
|
||||||
|
if (clientPrincipal != null && clientPrincipal.isAuthenticated()) {
|
||||||
|
return clientPrincipal;
|
||||||
|
}
|
||||||
|
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_CLIENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验用户是否有效
|
||||||
|
*/
|
||||||
|
private void checkUserIsEffective(LoginUser loginUser) {
|
||||||
|
//情况1:根据用户信息查询,该用户不存在
|
||||||
|
if (Objects.isNull(loginUser)) {
|
||||||
|
baseCommonService.addLog("用户登录失败,用户不存在!", CommonConstant.LOG_TYPE_1, null);
|
||||||
|
throw new JeecgBootException("该用户不存在,请注册");
|
||||||
|
}
|
||||||
|
//情况2:根据用户信息查询,该用户已注销
|
||||||
|
//update-begin---author:王帅 Date:20200601 for:if条件永远为falsebug------------
|
||||||
|
if (CommonConstant.DEL_FLAG_1.equals(loginUser.getDelFlag())) {
|
||||||
|
//update-end---author:王帅 Date:20200601 for:if条件永远为falsebug------------
|
||||||
|
baseCommonService.addLog("用户登录失败,用户名:" + loginUser.getUsername() + "已注销!", CommonConstant.LOG_TYPE_1, null);
|
||||||
|
throw new JeecgBootException("该用户已注销");
|
||||||
|
}
|
||||||
|
//情况3:根据用户信息查询,该用户已冻结
|
||||||
|
if (CommonConstant.USER_FREEZE.equals(loginUser.getStatus())) {
|
||||||
|
baseCommonService.addLog("用户登录失败,用户名:" + loginUser.getUsername() + "已冻结!", CommonConstant.LOG_TYPE_1, null);
|
||||||
|
throw new JeecgBootException("该用户已冻结");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
package org.jeecg.config.security.self;
|
||||||
|
|
||||||
|
import org.jeecg.config.security.LoginType;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationGrantAuthenticationToken;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自用生成token,不支持对外请求,仅为程序内部生成token
|
||||||
|
* @author eightmonth
|
||||||
|
* @date 2024/3/19 11:37
|
||||||
|
*/
|
||||||
|
public class SelfAuthenticationToken extends OAuth2AuthorizationGrantAuthenticationToken {
|
||||||
|
public SelfAuthenticationToken(Authentication clientPrincipal, Map<String, Object> additionalParameters) {
|
||||||
|
super(new AuthorizationGrantType(LoginType.SELF), clientPrincipal, additionalParameters);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,81 @@
|
|||||||
|
package org.jeecg.config.security.social;
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import org.jeecg.config.security.LoginType;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
|
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||||
|
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||||
|
import org.springframework.security.web.authentication.AuthenticationConverter;
|
||||||
|
import org.springframework.util.LinkedMultiValueMap;
|
||||||
|
import org.springframework.util.MultiValueMap;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 社交模式认证转换器,配合github、企业微信、钉钉、微信登录使用
|
||||||
|
* @author EightMonth
|
||||||
|
* @date 2024/1/1
|
||||||
|
*/
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class SocialGrantAuthenticationConvert implements AuthenticationConverter {
|
||||||
|
@Override
|
||||||
|
public Authentication convert(HttpServletRequest request) {
|
||||||
|
|
||||||
|
String grantType = request.getParameter(OAuth2ParameterNames.GRANT_TYPE);
|
||||||
|
if (!LoginType.SOCIAL.equals(grantType)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Authentication clientPrincipal = SecurityContextHolder.getContext().getAuthentication();
|
||||||
|
|
||||||
|
//从request中提取请求参数,然后存入MultiValueMap<String, String>
|
||||||
|
MultiValueMap<String, String> parameters = getParameters(request);
|
||||||
|
|
||||||
|
String token = parameters.getFirst("token");
|
||||||
|
if (!StringUtils.hasText(token)) {
|
||||||
|
throw new OAuth2AuthenticationException("无效请求,三方token不能为空!");
|
||||||
|
}
|
||||||
|
|
||||||
|
String source = parameters.getFirst("thirdType");
|
||||||
|
if (!StringUtils.hasText(source)) {
|
||||||
|
throw new OAuth2AuthenticationException("无效请求,三方来源不能为空!");
|
||||||
|
}
|
||||||
|
|
||||||
|
//收集要传入PhoneGrantAuthenticationToken构造方法的参数,
|
||||||
|
//该参数接下来在PhoneGrantAuthenticationProvider中使用
|
||||||
|
Map<String, Object> additionalParameters = new HashMap<>();
|
||||||
|
//遍历从request中提取的参数,排除掉grant_type、client_id、code等字段参数,其他参数收集到additionalParameters中
|
||||||
|
parameters.forEach((key, value) -> {
|
||||||
|
if (!key.equals(OAuth2ParameterNames.GRANT_TYPE) &&
|
||||||
|
!key.equals(OAuth2ParameterNames.CLIENT_ID) &&
|
||||||
|
!key.equals(OAuth2ParameterNames.CODE)) {
|
||||||
|
additionalParameters.put(key, value.get(0));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
//返回自定义的PhoneGrantAuthenticationToken对象
|
||||||
|
return new SocialGrantAuthenticationToken(clientPrincipal, additionalParameters);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*从request中提取请求参数,然后存入MultiValueMap<String, String>
|
||||||
|
*/
|
||||||
|
private static MultiValueMap<String, String> getParameters(HttpServletRequest request) {
|
||||||
|
Map<String, String[]> parameterMap = request.getParameterMap();
|
||||||
|
MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>(parameterMap.size());
|
||||||
|
parameterMap.forEach((key, values) -> {
|
||||||
|
if (values.length > 0) {
|
||||||
|
for (String value : values) {
|
||||||
|
parameters.add(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return parameters;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user