mirror of
https://github.com/jeecgboot/JeecgBoot.git
synced 2025-12-08 17:12:28 +08:00
Compare commits
177 Commits
springboot
...
springboot
| Author | SHA1 | Date | |
|---|---|---|---|
| a1486dae34 | |||
| 7d3ba49a66 | |||
| 7ba2527c67 | |||
| 526734c5a5 | |||
| 44b48ad916 | |||
| 1a3ae4f61c | |||
| 859c509f08 | |||
| 0704f187af | |||
| 199d2b439e | |||
| 5f898ed034 | |||
| 5a9cb05c86 | |||
| 98936680d5 | |||
| fc043fd5f3 | |||
| 54531002a7 | |||
| 728a95c00d | |||
| 13c9951c1f | |||
| 8dfaa3c3e1 | |||
| 050c478dce | |||
| 2688e8b6e2 | |||
| a9f30f0ca5 | |||
| 1bf4a0595a | |||
| 74cd57fd99 | |||
| c9ac4c9945 | |||
| 3b3371ee1a | |||
| 1d3bde9fe7 | |||
| 90b50a51a7 | |||
| 668ac59a5c | |||
| 5f01bdd29b | |||
| 82bfcc7b14 | |||
| ef210a2242 | |||
| 435445cb4e | |||
| fdf7cd1f6b | |||
| 92a38f41b0 | |||
| 7ed3bcc912 | |||
| 2706c0a519 | |||
| f4b80365a9 | |||
| 1108aa5288 | |||
| 9919ae2bc5 | |||
| 1f73837b7d | |||
| 9571e0b169 | |||
| 1a923596db | |||
| 62549e0a1c | |||
| 2740a2f419 | |||
| 899264250c | |||
| 0be7d00eb2 | |||
| d988a637b2 | |||
| 7152ae9e49 | |||
| 58b41db786 | |||
| 33cbc6e25b | |||
| 5bcb649384 | |||
| d02753ce50 | |||
| 0bfe5689cc | |||
| aa55c3a86c | |||
| 6a6d29254b | |||
| c4a87d7c58 | |||
| 47107a53c2 | |||
| 52cd43d17c | |||
| 7ff70930ef | |||
| 45f83dff43 | |||
| fcbaed2f7f | |||
| 4acd5b3464 | |||
| e10df20e98 | |||
| d0f85ccc7e | |||
| d12fe9d8de | |||
| 223d374edd | |||
| 7f011f199e | |||
| 873b8bd823 | |||
| 2f3aff9584 | |||
| 1f74cb538f | |||
| 2cce917ab2 | |||
| fd320be203 | |||
| 6e5c0eebce | |||
| cae8e7e8d2 | |||
| ada97acb04 | |||
| 01547ef83e | |||
| b3ed442b55 | |||
| 9cca8f8719 | |||
| ae6069882f | |||
| f0a5edaa49 | |||
| ab98b26cac | |||
| 64b3c9e42e | |||
| 8eb81493ce | |||
| 044fc47586 | |||
| bbe18c582c | |||
| b894125b53 | |||
| 2e236703b2 | |||
| a771d24a57 | |||
| 69f5d12de7 | |||
| 5b5999e786 | |||
| fc3fe39d95 | |||
| 48e23aafab | |||
| 657b84d3cf | |||
| 2021bf39f8 | |||
| fdeb37c3d0 | |||
| f9123208e1 | |||
| accb8f2f9f | |||
| c643994546 | |||
| 6934a0adee | |||
| 93e32a7177 | |||
| c9f5bb4409 | |||
| 10b68858d6 | |||
| da72e8f9c5 | |||
| 73e86686dc | |||
| f43d0d486b | |||
| 65bde3331b | |||
| b60942aa86 | |||
| 197b267e71 | |||
| 79f7134bd5 | |||
| 6d432bc186 | |||
| 415307eb9f | |||
| 48e20b2af5 | |||
| b7924b9ca8 | |||
| a10a2e0a9d | |||
| 4aa88189ed | |||
| fdb05443c2 | |||
| 65d737db6d | |||
| f04f7f9abf | |||
| 935e118d15 | |||
| e218367332 | |||
| 3a3f3cf367 | |||
| 0e762b4157 | |||
| f4712baa39 | |||
| 7d8b653d6e | |||
| cf7f3f94be | |||
| 49f63b92ac | |||
| 5670a15b20 | |||
| 9e9ef20b7c | |||
| 0c034031d1 | |||
| 491a038b5a | |||
| 8a4fcb0023 | |||
| e93dcc1a7e | |||
| 383cbf250f | |||
| 9fe1450ac9 | |||
| 88b9b12998 | |||
| 9e25566271 | |||
| 8e54e06978 | |||
| e5c082ae13 | |||
| 96ab98ac3e | |||
| 1632c241ee | |||
| e9d05b0e75 | |||
| 6ade7e22f8 | |||
| 43d47c08cb | |||
| e616c5d8fe | |||
| cddf23c787 | |||
| 70a37309dd | |||
| 48555b5219 | |||
| 06d58f202f | |||
| 628870af9b | |||
| b46a6438e6 | |||
| 5488f99723 | |||
| 6bc1fe8d21 | |||
| 7cac16320c | |||
| 24dbd1db39 | |||
| 46b026b989 | |||
| 94c45f5e0f | |||
| 8950e19d4e | |||
| 99eb88f71c | |||
| 824d7839d8 | |||
| c88f9d95d4 | |||
| beb0bc2f64 | |||
| f741db874c | |||
| d684c09392 | |||
| 364be22dd0 | |||
| 20efa3bf9a | |||
| c7977dda3d | |||
| c27c5a9a9b | |||
| 0ab280f812 | |||
| c3066dac17 | |||
| b650d512b3 | |||
| 925ec9447d | |||
| 411a73c1bf | |||
| 84077e6e24 | |||
| 184cf97304 | |||
| 5f425b49b2 | |||
| 3ac8ee304a | |||
| 0faac01bb7 | |||
| 74d88a8fcc |
1
.gitignore
vendored
1
.gitignore
vendored
@ -13,5 +13,6 @@ os_del.cmd
|
|||||||
os_del_doc.cmd
|
os_del_doc.cmd
|
||||||
.svn
|
.svn
|
||||||
derby.log
|
derby.log
|
||||||
|
*.log
|
||||||
.cursor
|
.cursor
|
||||||
.history
|
.history
|
||||||
@ -7,12 +7,12 @@
|
|||||||
JEECG BOOT AI Low Code Platform
|
JEECG BOOT AI Low Code Platform
|
||||||
===============
|
===============
|
||||||
|
|
||||||
Current version: 3.8.3 (Release date: 2025-10-09)
|
Current version: 3.9.0 (Release date: 2025-12-01)
|
||||||
|
|
||||||
|
|
||||||
[](https://github.com/zhangdaiscott/jeecg-boot/blob/master/LICENSE)
|
[](https://github.com/zhangdaiscott/jeecg-boot/blob/master/LICENSE)
|
||||||
[](http://www.jeecg.com)
|
[](http://www.jeecg.com)
|
||||||
[](https://github.com/zhangdaiscott/jeecg-boot)
|
[](https://github.com/zhangdaiscott/jeecg-boot)
|
||||||
[](https://github.com/zhangdaiscott/jeecg-boot)
|
[](https://github.com/zhangdaiscott/jeecg-boot)
|
||||||
[](https://github.com/zhangdaiscott/jeecg-boot)
|
[](https://github.com/zhangdaiscott/jeecg-boot)
|
||||||
|
|
||||||
|
|||||||
@ -10,7 +10,9 @@ JeecgBoot低代码平台(商业版介绍)
|
|||||||
<h3 align="center">企业级AI低代码平台</h3>
|
<h3 align="center">企业级AI低代码平台</h3>
|
||||||
|
|
||||||
|
|
||||||
JeecgBoot是一款集成AI应用的,基于BPM流程的低代码平台,旨在帮助企业快速实现低代码开发和构建个性化AI应用!前后端分离架构Ant Design&Vue3,SpringBoot,SpringCloud Alibaba,Mybatis-plus,Shiro。强大的代码生成器让前后端代码一键生成,无需写任何代码! 引领AI低代码开发模式: AI生成->OnlineCoding-> 代码生成-> 手工MERGE, 帮助Java项目解决80%的重复工作,让开发更多关注业务,提高效率、节省成本,同时又不失灵活性!低代码能力:Online表单、表单设计、流程设计、Online报表、大屏/仪表盘设计、报表设计; AI应用平台功能:AI知识库问答、AI模型管理、AI流程编排、AI聊天等,支持含ChatGPT、DeepSeek、Ollama等多种AI大模型
|
JeecgBoot是一款集成AI应用的,基于BPM流程的低代码平台,旨在帮助企业快速实现低代码开发和构建个性化AI应用,支持MCP和插件,实现聊天式业务操作(如 “一句话创建用户”)!
|
||||||
|
|
||||||
|
前后端分离架构Ant Design&Vue3,SpringBoot,SpringCloud Alibaba,Mybatis-plus,Shiro。强大的代码生成器让前后端代码一键生成,无需写任何代码! 引领AI低代码开发模式: AI生成->OnlineCoding-> 代码生成-> 手工MERGE, 帮助Java项目解决80%的重复工作,让开发更多关注业务,提高效率、节省成本,同时又不失灵活性!低代码能力:Online表单、表单设计、流程设计、Online报表、大屏/仪表盘设计、报表设计; AI应用平台功能:AI知识库问答、AI模型管理、AI流程编排、AI聊天等,支持含ChatGPT、DeepSeek、Ollama等多种AI大模型
|
||||||
|
|
||||||
JeecgBoot 提供了一系列 `低代码能力`,实现`真正的零代码`在线开发:Online表单开发、Online报表、复杂报表设计、打印设计、在线图表设计、仪表盘设计、大屏设计、移动图表能力、表单设计器、在线设计流程、流程自动化配置、插件能力(可插拔)
|
JeecgBoot 提供了一系列 `低代码能力`,实现`真正的零代码`在线开发:Online表单开发、Online报表、复杂报表设计、打印设计、在线图表设计、仪表盘设计、大屏设计、移动图表能力、表单设计器、在线设计流程、流程自动化配置、插件能力(可插拔)
|
||||||
|
|
||||||
|
|||||||
22
README.md
22
README.md
@ -2,13 +2,13 @@
|
|||||||
JeecgBoot AI低代码平台
|
JeecgBoot AI低代码平台
|
||||||
===============
|
===============
|
||||||
|
|
||||||
当前最新版本: 3.8.3(发布日期:2025-10-09)
|
当前最新版本: 3.9.0(发布日期:2025-12-01)
|
||||||
|
|
||||||
|
|
||||||
[](https://github.com/jeecgboot/JeecgBoot/blob/master/LICENSE)
|
[](https://github.com/jeecgboot/JeecgBoot/blob/master/LICENSE)
|
||||||
[](https://jeecg.com)
|
[](https://jeecg.com)
|
||||||
[](https://jeecg.blog.csdn.net)
|
[](https://jeecg.blog.csdn.net)
|
||||||
[](https://github.com/jeecgboot/JeecgBoot)
|
[](https://github.com/jeecgboot/JeecgBoot)
|
||||||
[](https://github.com/jeecgboot/JeecgBoot)
|
[](https://github.com/jeecgboot/JeecgBoot)
|
||||||
[](https://github.com/jeecgboot/JeecgBoot)
|
[](https://github.com/jeecgboot/JeecgBoot)
|
||||||
|
|
||||||
@ -19,14 +19,17 @@ JeecgBoot AI低代码平台
|
|||||||
|
|
||||||
<h3 align="center">企业级AI低代码平台</h3>
|
<h3 align="center">企业级AI低代码平台</h3>
|
||||||
|
|
||||||
JeecgBoot 是一款基于BPM流程和代码生成的AI低代码平台,助力企业快速实现低代码开发和构建AI应用。
|
JeecgBoot 是一款融合代码生成与AI应用的低代码开发平台,助力企业快速实现低代码开发和构建AI应用。平台支持MCP和插件扩展,提供聊天式业务操作(如“一句话创建用户”),大幅提升开发效率与用户便捷性。
|
||||||
|
|
||||||
采用前后端分离架构(Ant Design&Vue3,SpringBoot3,SpringCloud Alibaba,Mybatis-plus),强大代码生成器实现前后端一键生成,无需手写代码。
|
采用前后端分离架构(Ant Design&Vue3,SpringBoot3,SpringCloud Alibaba,Mybatis-plus),强大代码生成器实现前后端一键生成,无需手写代码。
|
||||||
平台引领AI低代码开发模式:AI生成→在线编码→代码生成→手工合并,解决Java项目80%重复工作,提升效率,节省成本,兼顾灵活性。
|
平台引领AI低代码开发模式:AI生成→在线编码→代码生成→手工合并,解决Java项目80%重复工作,提升效率,节省成本,兼顾灵活性。
|
||||||
具备强大且颗粒化的权限控制,支持按钮权限和数据权限设置,满足大型业务系统需求。功能涵盖在线表单、表单设计、流程设计、门户设计、报表与大屏设计、OA办公、AI应用、AI知识库、大模型管理、AI流程编排、AI聊天,支持ChatGPT、DeepSeek、Ollama等多种AI大模型。
|
具备强大且颗粒化的权限控制,支持按钮权限和数据权限设置,满足大型业务系统需求。功能涵盖在线表单、表单设计、流程设计、门户设计、报表与大屏设计、OA办公、AI应用、AI知识库、大模型管理、AI流程编排、AI聊天,支持ChatGPT、DeepSeek、Ollama等多种AI大模型。
|
||||||
|
|
||||||
`AI赋能报表:` 积木报表是一款自主研发的强大开源企业级Web报表与大屏工具。它通过零编码的拖拽式操作,赋能用户如同搭积木般轻松构建各类复杂报表和数据大屏,全面满足企业数据可视化与分析需求,助力企业级数据产品的高效打造与应用。
|
`傻瓜式报表:` JimuReport是一款自主研发的强大开源企业级Web报表工具。它通过零编码的拖拽式操作,赋能用户如同搭积木般轻松构建各类复杂报表,全面满足企业数据可视化与分析需求,助力企业级数据产品的高效打造与应用。
|
||||||
|
|
||||||
`AI赋能低代码:` 提供完善成熟的AI应用平台,涵盖AI应用管理、AI模型管理、智能对话助手、知识库问答、流程编排与设计器、AI建表等多项功能。平台兼容多种主流大模型,包括ChatGPT、DeepSeek、Ollama、智普、千问等,助力企业高效构建智能化应用,推动低代码开发与AI深度融合。
|
`傻瓜式大屏:` JimuBI一款自主研发的强大的大屏和仪表盘设计工具。专注数字孪生与数据可视化,支持交互式大屏、仪表盘、门户和移动端,实现“一次开发,多端适配”。 大屏设计类Word风格,支持多屏切换,自由拖拽,轻松打造炫酷动态界面。
|
||||||
|
|
||||||
|
`成熟AI应用功能:` 提供一套完善AI应用平台: 涵盖AI应用管理、AI模型管理、智能对话助手、知识库问答、流程编排与设计器、AI建表、MCP插件配置等功能。平台兼容主流大模型,包括ChatGPT、DeepSeek、Ollama、智普、千问等,助力企业高效构建智能化应用,推动低代码开发与AI深度融合。
|
||||||
|
|
||||||
`JEECG宗旨是:` JEECG旨在通过OnlineCoding平台实现简单功能的零代码快速搭建,同时针对复杂功能采用代码生成器生成代码并手工合并,打造智能且灵活的低代码开发模式,有效解决了当前低代码产品普遍缺乏灵活性的问题,提升开发效率的同时兼顾系统的扩展性和定制化能力。
|
`JEECG宗旨是:` JEECG旨在通过OnlineCoding平台实现简单功能的零代码快速搭建,同时针对复杂功能采用代码生成器生成代码并手工合并,打造智能且灵活的低代码开发模式,有效解决了当前低代码产品普遍缺乏灵活性的问题,提升开发效率的同时兼顾系统的扩展性和定制化能力。
|
||||||
|
|
||||||
@ -51,14 +54,15 @@ JeecgBoot低代码平台兼容所有J2EE项目开发,支持信创国产化,
|
|||||||
-----------------------------------
|
-----------------------------------
|
||||||
|
|
||||||
|下载 | SpringBoot3.5 + Shiro |SpringBoot3.5+ SpringAuthorizationServer | SpringBoot3.5 + Sa-Token | SpringBoot2.7(JDK17/JDK8) |
|
|下载 | 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) 分支 | [`springboot3-satoken`](https://github.com/jeecgboot/JeecgBoot/tree/springboot3-satoken) 分支|[`master`](https://github.com/jeecgboot/JeecgBoot) 分支|
|
| Github | [`main`](https://github.com/jeecgboot/JeecgBoot) | [`springboot3_sas`](https://github.com/jeecgboot/JeecgBoot/tree/springboot3_sas) 分支 | [`springboot3-satoken`](https://github.com/jeecgboot/JeecgBoot/tree/springboot3-satoken) 分支|[`springboot2`](https://github.com/jeecgboot/JeecgBoot/tree/springboot2) 分支|
|
||||||
| 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) 分支 |
|
| Gitee | [`main`](https://github.com/jeecgboot/JeecgBoot) | [`springboot3_sas`](https://gitee.com/jeecg/JeecgBoot/tree/springboot3_sas) 分支| [`springboot3-satoken`](https://gitee.com/jeecg/JeecgBoot/tree/springboot3-satoken) 分支|[`springboot2`](https://github.com/jeecgboot/JeecgBoot/tree/springboot2) 分支 |
|
||||||
|
|
||||||
|
|
||||||
- `jeecg-boot` 是后端JAVA源码项目Springboot3+SpringCloudAlibaba(支持单体和微服务切换).
|
- `jeecg-boot` 是后端JAVA源码项目Springboot3+Shiro+Mybatis+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.
|
||||||
|
- `jeecg-boot-starter` 是[jeecg-boot对应的底层封装starter](https://github.com/jeecgboot/jeecg-boot-starter) :微服务启动、xxljob、分布式锁starter、rabbitmq、分布式事务、分库分表shardingsphere等.
|
||||||
- 参考 [文档](https://help.jeecg.com/ui/2dev/mini) 可以删除不需要的demo,制作一个精简版本
|
- 参考 [文档](https://help.jeecg.com/ui/2dev/mini) 可以删除不需要的demo,制作一个精简版本
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
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.3(发布日期:2025-10-09)
|
当前最新版本: 3.9.0(发布日期:2025-12-01)
|
||||||
|
|
||||||
|
|
||||||
[](https://github.com/zhangdaiscott/jeecg-boot/blob/master/LICENSE)
|
[](https://github.com/zhangdaiscott/jeecg-boot/blob/master/LICENSE)
|
||||||
[](http://jeecg.com/aboutusIndex)
|
[](http://jeecg.com/aboutusIndex)
|
||||||
[](https://github.com/zhangdaiscott/jeecg-boot)
|
[](https://github.com/zhangdaiscott/jeecg-boot)
|
||||||
[](https://github.com/zhangdaiscott/jeecg-boot)
|
[](https://github.com/zhangdaiscott/jeecg-boot)
|
||||||
[](https://github.com/zhangdaiscott/jeecg-boot)
|
[](https://github.com/zhangdaiscott/jeecg-boot)
|
||||||
|
|
||||||
|
|||||||
@ -1,368 +0,0 @@
|
|||||||
# `Shiro 到 Sa-Token 迁移指南`
|
|
||||||
|
|
||||||
本项目已从 **Apache Shiro 2.0.4** 迁移到 **Sa-Token 1.44.0**,采用 JWT-Simple 模式,完全兼容原 JWT token 格式。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📦 1. 依赖配置
|
|
||||||
|
|
||||||
### 1.1 Maven 依赖
|
|
||||||
|
|
||||||
移除 Shiro 相关依赖,新增:
|
|
||||||
|
|
||||||
```xml
|
|
||||||
<dependency>
|
|
||||||
<groupId>cn.dev33</groupId>
|
|
||||||
<artifactId>sa-token-spring-boot3-starter</artifactId>
|
|
||||||
<version>1.44.0</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>cn.dev33</groupId>
|
|
||||||
<artifactId>sa-token-redis-jackson</artifactId>
|
|
||||||
<version>1.44.0</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>cn.dev33</groupId>
|
|
||||||
<artifactId>sa-token-jwt</artifactId>
|
|
||||||
<version>1.44.0</version>
|
|
||||||
</dependency>
|
|
||||||
```
|
|
||||||
|
|
||||||
### 1.2 配置文件(application.yml)
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
sa-token:
|
|
||||||
token-name: X-Access-Token
|
|
||||||
timeout: 2592000 # token有效期30天
|
|
||||||
is-concurrent: true # 允许同账号并发登录
|
|
||||||
token-style: jwt-simple # JWT模式(兼容原格式)
|
|
||||||
jwt-secret-key: "your-secret-key-here"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 💡 2. 核心代码实现
|
|
||||||
|
|
||||||
### 2.1 登录逻辑(⚠️ 使用 username 作为 loginId)
|
|
||||||
|
|
||||||
```java
|
|
||||||
// 从数据库查询用户信息
|
|
||||||
SysUser sysUser = userService.getUserByUsername(username);
|
|
||||||
|
|
||||||
// 执行登录(自动完成:Sa-Token登录 + 存储Session + 返回token)
|
|
||||||
String token = LoginUserUtils.doLogin(sysUser);
|
|
||||||
|
|
||||||
// 返回token给前端
|
|
||||||
return Result.ok(token);
|
|
||||||
```
|
|
||||||
|
|
||||||
**💡 设计说明:**
|
|
||||||
- `doLogin()` 方法自动完成:
|
|
||||||
1. 调用 `StpUtil.login(username)` (使用 username 而非 userId)
|
|
||||||
2. 调用 `setSessionUser()` 存储用户信息(自动清除 password 等15个字段)
|
|
||||||
3. 返回生成的 token
|
|
||||||
- 减少 Redis 存储约 50%,密码不再存储到 Session
|
|
||||||
|
|
||||||
### 2.2 权限认证接口(⚠️ 必须手动实现缓存)
|
|
||||||
|
|
||||||
```java
|
|
||||||
@Component
|
|
||||||
public class StpInterfaceImpl implements StpInterface {
|
|
||||||
|
|
||||||
@Lazy @Resource
|
|
||||||
private CommonAPI commonApi;
|
|
||||||
|
|
||||||
private static final long CACHE_TIMEOUT = 60 * 60 * 24 * 30; // 30天
|
|
||||||
private static final String PERMISSION_CACHE_PREFIX = "satoken:user-permission:";
|
|
||||||
private static final String ROLE_CACHE_PREFIX = "satoken:user-role:";
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
public List<String> getPermissionList(Object loginId, String loginType) {
|
|
||||||
String username = loginId.toString();
|
|
||||||
String cacheKey = PERMISSION_CACHE_PREFIX + username;
|
|
||||||
SaTokenDao dao = SaManager.getSaTokenDao();
|
|
||||||
|
|
||||||
// 1. 先从缓存获取
|
|
||||||
List<String> permissionList = (List<String>) dao.getObject(cacheKey);
|
|
||||||
|
|
||||||
if (permissionList == null) {
|
|
||||||
// 2. 缓存未命中,查询数据库
|
|
||||||
log.warn("权限缓存未命中,查询数据库 [ username={} ]", username);
|
|
||||||
|
|
||||||
String userId = commonApi.getUserIdByName(username);
|
|
||||||
Set<String> permissionSet = commonApi.queryUserAuths(userId);
|
|
||||||
permissionList = new ArrayList<>(permissionSet);
|
|
||||||
|
|
||||||
// 3. 将结果缓存起来
|
|
||||||
dao.setObject(cacheKey, permissionList, CACHE_TIMEOUT);
|
|
||||||
}
|
|
||||||
|
|
||||||
return permissionList;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<String> getRoleList(Object loginId, String loginType) {
|
|
||||||
// 实现类似 getPermissionList(),使用 ROLE_CACHE_PREFIX
|
|
||||||
// 详见:StpInterfaceImpl.java
|
|
||||||
}
|
|
||||||
|
|
||||||
// 清除缓存的静态方法
|
|
||||||
public static void clearUserCache(List<String> usernameList) {
|
|
||||||
SaTokenDao dao = SaManager.getSaTokenDao();
|
|
||||||
for (String username : usernameList) {
|
|
||||||
dao.deleteObject(PERMISSION_CACHE_PREFIX + username);
|
|
||||||
dao.deleteObject(ROLE_CACHE_PREFIX + username);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**⚠️ 关键:** Sa-Token 的 `StpInterface` **不提供自动缓存**,必须手动实现,否则每次请求都会查询数据库!
|
|
||||||
|
|
||||||
### 2.3 Filter 配置(支持 URL 参数传递 token)
|
|
||||||
|
|
||||||
```java
|
|
||||||
@Bean
|
|
||||||
@Primary
|
|
||||||
public StpLogic getStpLogicJwt() {
|
|
||||||
return new StpLogicJwtForSimple() {
|
|
||||||
@Override
|
|
||||||
public String getTokenValue() {
|
|
||||||
SaRequest request = SaHolder.getRequest();
|
|
||||||
|
|
||||||
// 优先级:Header > URL参数"token" > URL参数"X-Access-Token"
|
|
||||||
String tokenValue = request.getHeader(getConfigOrGlobal().getTokenName());
|
|
||||||
if (isEmpty(tokenValue)) {
|
|
||||||
tokenValue = request.getParam("token"); // 兼容 WebSocket、积木报表
|
|
||||||
}
|
|
||||||
if (isEmpty(tokenValue)) {
|
|
||||||
tokenValue = request.getParam(getConfigOrGlobal().getTokenName());
|
|
||||||
}
|
|
||||||
|
|
||||||
return isEmpty(tokenValue) ? super.getTokenValue() : tokenValue;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
public SaServletFilter getSaServletFilter() {
|
|
||||||
return new SaServletFilter()
|
|
||||||
.addInclude("/**")
|
|
||||||
.setExcludeList(getExcludeUrls()) // 排除登录、静态资源等
|
|
||||||
.setAuth(obj -> {
|
|
||||||
// 检查是否是免认证路径
|
|
||||||
String servletPath = SaHolder.getRequest().getRequestPath();
|
|
||||||
if (InMemoryIgnoreAuth.contains(servletPath)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ⚠️ 关键:如果请求带 token,先切换到对应的登录会话
|
|
||||||
try {
|
|
||||||
String token = StpUtil.getTokenValue();
|
|
||||||
if (isNotEmpty(token)) {
|
|
||||||
Object loginId = StpUtil.getLoginIdByToken(token);
|
|
||||||
if (loginId != null) {
|
|
||||||
StpUtil.switchTo(loginId); // 切换登录会话
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.debug("切换登录会话失败: {}", e.getMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
// 最终校验登录状态
|
|
||||||
StpUtil.checkLogin();
|
|
||||||
})
|
|
||||||
.setError(e -> {
|
|
||||||
// 返回401 JSON响应
|
|
||||||
SaHolder.getResponse()
|
|
||||||
.setStatus(401)
|
|
||||||
.setHeader("Content-Type", "application/json;charset=UTF-8");
|
|
||||||
return JwtUtil.responseErrorJson(401, "Token失效,请重新登录!");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2.4 全局异常处理
|
|
||||||
|
|
||||||
```java
|
|
||||||
@ExceptionHandler(NotLoginException.class)
|
|
||||||
public Result<?> handleNotLoginException(NotLoginException e) {
|
|
||||||
log.warn("用户未登录或Token失效: {}", e.getMessage());
|
|
||||||
return Result.error(401, "Token失效,请重新登录!");
|
|
||||||
}
|
|
||||||
|
|
||||||
@ExceptionHandler(NotPermissionException.class)
|
|
||||||
public Result<?> handleNotPermissionException(NotPermissionException e) {
|
|
||||||
log.warn("权限不足: {}", e.getMessage());
|
|
||||||
return Result.error(403, "用户权限不足,无法访问!");
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔄 3. API 迁移对照表
|
|
||||||
|
|
||||||
### 3.1 注解替换
|
|
||||||
|
|
||||||
| Shiro | Sa-Token | 说明 |
|
|
||||||
|-------|----------|------|
|
|
||||||
| `@RequiresPermissions("user:add")` | `@SaCheckPermission("user:add")` | 权限校验 |
|
|
||||||
| `@RequiresRoles("admin")` | `@SaCheckRole("admin")` | 角色校验 |
|
|
||||||
|
|
||||||
### 3.2 API 替换
|
|
||||||
|
|
||||||
| Shiro | Sa-Token | 说明 |
|
|
||||||
|-------|----------|------|
|
|
||||||
| `SecurityUtils.getSubject().getPrincipal()` | `LoginUserUtils.getSessionUser()` | 获取登录用户 |
|
|
||||||
| `Subject.login(token)` | `LoginUserUtils.doLogin(sysUser)` | 登录(推荐) |
|
|
||||||
| `Subject.login(token)` | `StpUtil.login(username)` | 登录(底层API) |
|
|
||||||
| `Subject.logout()` | `StpUtil.logout()` | 退出登录 |
|
|
||||||
| `Subject.isAuthenticated()` | `StpUtil.isLogin()` | 判断是否登录 |
|
|
||||||
| `Subject.hasRole("admin")` | `StpUtil.hasRole("admin")` | 判断角色 |
|
|
||||||
| `Subject.isPermitted("user:add")` | `StpUtil.hasPermission("user:add")` | 判断权限 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ⚠️ 4. 重要特性说明
|
|
||||||
|
|
||||||
### 4.1 JWT-Simple 模式特性
|
|
||||||
|
|
||||||
- ✅ **生成标准 JWT token**:与原 Shiro JWT 格式完全兼容
|
|
||||||
- ✅ **仍然检查 Redis Session**:支持强制退出(与纯 JWT 无状态模式不同)
|
|
||||||
- ✅ **支持 URL 参数传递**:兼容 WebSocket、积木报表等场景
|
|
||||||
- ⚠️ **非完全无状态**:依赖 Redis 存储会话和权限缓存
|
|
||||||
|
|
||||||
### 4.2 Session 数据优化
|
|
||||||
|
|
||||||
`LoginUserUtils.setSessionUser()` 会自动清除以下字段:
|
|
||||||
|
|
||||||
```
|
|
||||||
password, workNo, birthday, sex, email, phone, status,
|
|
||||||
delFlag, activitiSync, createTime, userIdentity, post,
|
|
||||||
telephone, clientId, mainDepPostId
|
|
||||||
```
|
|
||||||
|
|
||||||
**优势:**
|
|
||||||
- 减少 Redis 存储约 **50%**
|
|
||||||
- 密码不再存储在 Session 中,**安全性提升**
|
|
||||||
|
|
||||||
### 4.3 权限缓存动态更新
|
|
||||||
|
|
||||||
修改角色权限后,系统会自动清除受影响用户的权限缓存:
|
|
||||||
|
|
||||||
```java
|
|
||||||
// SysPermissionController.saveRolePermission() 中
|
|
||||||
@RequestMapping(value = "/saveRolePermission", method = RequestMethod.POST)
|
|
||||||
public Result<String> saveRolePermission(@RequestBody JSONObject json) {
|
|
||||||
String roleId = json.getString("roleId");
|
|
||||||
String permissionIds = json.getString("permissionIds");
|
|
||||||
String lastPermissionIds = json.getString("lastpermissionIds");
|
|
||||||
|
|
||||||
// 保存角色权限关系
|
|
||||||
sysRolePermissionService.saveRolePermission(roleId, permissionIds, lastPermissionIds);
|
|
||||||
|
|
||||||
// ⚠️ 关键:清除拥有该角色的所有用户的权限缓存
|
|
||||||
clearRolePermissionCache(roleId);
|
|
||||||
|
|
||||||
return Result.ok("保存成功!");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 实现:查询该角色下的所有用户,批量清除缓存
|
|
||||||
private void clearRolePermissionCache(String roleId) {
|
|
||||||
List<String> usernameList = new ArrayList<>();
|
|
||||||
|
|
||||||
// 分页查询拥有该角色的用户
|
|
||||||
int pageNo = 1, pageSize = 100;
|
|
||||||
while (true) {
|
|
||||||
Page<SysUser> page = new Page<>(pageNo, pageSize);
|
|
||||||
IPage<SysUser> userPage = sysUserService.getUserByRoleId(page, roleId, null, null);
|
|
||||||
|
|
||||||
if (userPage.getRecords().isEmpty()) break;
|
|
||||||
|
|
||||||
for (SysUser user : userPage.getRecords()) {
|
|
||||||
usernameList.add(user.getUsername());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pageNo >= userPage.getPages()) break;
|
|
||||||
pageNo++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 批量清除用户权限和角色缓存
|
|
||||||
if (!usernameList.isEmpty()) {
|
|
||||||
StpInterfaceImpl.clearUserCache(usernameList);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**结果:** 权限变更立即生效,用户无需重新登录。
|
|
||||||
|
|
||||||
|
|
||||||
## ✅ 6. 测试清单
|
|
||||||
|
|
||||||
### 6.1 登录功能测试
|
|
||||||
|
|
||||||
| 测试项 | 测试状态 | 说明 |
|
|
||||||
|--------|---------|------|
|
|
||||||
| 账号密码登录 | ✅ 通过 | 验证 `/sys/login` 接口 |
|
|
||||||
| 手机号登录 | ✅ 通过 | 验证 `/sys/phoneLogin` 接口 |
|
|
||||||
| APP 登录 | ✅ 通过 | 验证 APP 端登录流程 |
|
|
||||||
| 扫码登录 | ✅ 通过 | 验证二维码扫码登录 |
|
|
||||||
| 第三方登录 | ⏳ 待测试 | 微信、QQ 等第三方登录 |
|
|
||||||
| 钉钉 OAuth2.0 登录 | ⏳ 待测试 | 钉钉授权登录流程 |
|
|
||||||
| 企业微信 OAuth2.0 登录 | ⏳ 待测试 | 企业微信授权登录流程 |
|
|
||||||
| CAS 单点登录 | ⏳ 待测试 | CAS 单点登录集成 |
|
|
||||||
|
|
||||||
### 6.2 核心功能测试
|
|
||||||
|
|
||||||
| 测试项 | 测试状态 | 说明 |
|
|
||||||
|--------|---------|------|
|
|
||||||
| Token 权限拦截 | ✅ 通过 | 无 token 或失效 token 返回 401 |
|
|
||||||
| 权限注解 `@SaCheckPermission` | ✅ 通过 | 无权限返回 403 |
|
|
||||||
| 角色注解 `@SaCheckRole` | ✅ 通过 | 无角色返回 403 |
|
|
||||||
| `@IgnoreAuth` 免认证 | ✅ 通过 | 无 token 也能正常访问 |
|
|
||||||
| 自动续期(操作不掉线) | ✅ 通过 | 活跃用户 token 自动续期 |
|
|
||||||
| 用户权限变更即刻生效 | ✅ 通过 | 修改角色权限后无需重新登录 |
|
|
||||||
| 积木报表 token 参数模式 | ✅ 通过 | `/jmreport/**?token=xxx` 正常访问 |
|
|
||||||
|
|
||||||
### 6.3 异步和网关测试
|
|
||||||
|
|
||||||
| 测试项 | 测试状态 | 说明 |
|
|
||||||
|--------|---------|------|
|
|
||||||
| 异步接口(`@Async`) | ❌ 有问题 | **需排查:异步线程中获取登录用户失败** |
|
|
||||||
| Gateway 模式权限验证 | ⏳ 待测试 | 网关模式下的权限拦截 |
|
|
||||||
|
|
||||||
### 6.4 多租户测试
|
|
||||||
|
|
||||||
| 测试项 | 测试状态 | 说明 |
|
|
||||||
|--------|---------|------|
|
|
||||||
| 租户 ID 校验 | ⚠️ 缺失 | **需补充:校验用户 tenant_id 和前端传参一致性** |
|
|
||||||
|
|
||||||
### 6.5 测试说明
|
|
||||||
|
|
||||||
**✅ 通过** - 功能正常,符合预期
|
|
||||||
**❌ 有问题** - 功能异常,需要修复
|
|
||||||
**⏳ 待测试** - 尚未测试
|
|
||||||
**⚠️ 缺失** - 功能缺失,需要补充
|
|
||||||
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📊 7. 迁移总结
|
|
||||||
|
|
||||||
| 优化项 | 说明 | 收益 |
|
|
||||||
|--------|------|------|
|
|
||||||
| **loginId 设计** | 使用 `username` 而非 `userId` | 语义清晰,与业务逻辑一致 |
|
|
||||||
| **Session 优化** | 清除 15 个不必要字段 | Redis 存储减少 50%,安全性提升 |
|
|
||||||
| **权限缓存** | 手动实现 30 天缓存 | 性能提升 99%,降低 DB 压力 |
|
|
||||||
| **权限实时更新** | 角色权限修改后自动清除缓存 | 无需重新登录即生效 |
|
|
||||||
| **URL Token 支持** | Filter 中实现 `switchTo` | 兼容 WebSocket、积木报表等场景 |
|
|
||||||
| **JWT 兼容** | JWT-Simple 模式 | 完全兼容原 JWT token 格式 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📚 参考资料
|
|
||||||
|
|
||||||
- [Sa-Token 官方文档](https://sa-token.cc/)
|
|
||||||
- [Sa-Token JWT-Simple 模式](https://sa-token.cc/doc.html#/plugin/jwt-extend)
|
|
||||||
- [Sa-Token 权限缓存最佳实践](https://sa-token.cc/doc.html#/fun/jur-cache)
|
|
||||||
File diff suppressed because one or more lines are too long
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]}');
|
||||||
@ -63,6 +63,7 @@ services:
|
|||||||
build:
|
build:
|
||||||
context: ./jeecg-module-system/jeecg-system-start
|
context: ./jeecg-module-system/jeecg-system-start
|
||||||
restart: on-failure
|
restart: on-failure
|
||||||
|
mac_address: 02:42:ac:11:00:02
|
||||||
depends_on:
|
depends_on:
|
||||||
- jeecg-boot-mysql
|
- jeecg-boot-mysql
|
||||||
- jeecg-boot-redis
|
- jeecg-boot-redis
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>org.jeecgframework.boot3</groupId>
|
<groupId>org.jeecgframework.boot3</groupId>
|
||||||
<artifactId>jeecg-boot-parent</artifactId>
|
<artifactId>jeecg-boot-parent</artifactId>
|
||||||
<version>3.8.3</version>
|
<version>3.9.0</version>
|
||||||
</parent>
|
</parent>
|
||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
<artifactId>jeecg-boot-base-core</artifactId>
|
<artifactId>jeecg-boot-base-core</artifactId>
|
||||||
@ -44,6 +44,12 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.jeecgframework.boot3</groupId>
|
<groupId>org.jeecgframework.boot3</groupId>
|
||||||
<artifactId>jeecg-boot-common</artifactId>
|
<artifactId>jeecg-boot-common</artifactId>
|
||||||
|
<exclusions>
|
||||||
|
<exclusion>
|
||||||
|
<groupId>commons-logging</groupId>
|
||||||
|
<artifactId>commons-logging</artifactId>
|
||||||
|
</exclusion>
|
||||||
|
</exclusions>
|
||||||
</dependency>
|
</dependency>
|
||||||
<!--集成springmvc框架并实现自动配置 -->
|
<!--集成springmvc框架并实现自动配置 -->
|
||||||
<dependency>
|
<dependency>
|
||||||
@ -173,39 +179,34 @@
|
|||||||
<artifactId>DmDialect-for-hibernate5.0</artifactId>
|
<artifactId>DmDialect-for-hibernate5.0</artifactId>
|
||||||
<version>${dm8.version}</version>
|
<version>${dm8.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- Quartz定时任务 -->
|
<!-- Quartz定时任务 -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-quartz</artifactId>
|
<artifactId>spring-boot-starter-quartz</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- Sa-Token 权限认证,在线文档:https://sa-token.cc -->
|
<!--JWT-->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>cn.dev33</groupId>
|
<groupId>com.auth0</groupId>
|
||||||
<artifactId>sa-token-spring-boot3-starter</artifactId>
|
<artifactId>java-jwt</artifactId>
|
||||||
<version>${sa-token.version}</version>
|
<version>${java-jwt.version}</version>
|
||||||
</dependency>
|
|
||||||
<!-- Sa-Token 整合 Redis (使用 jackson 序列化方式) -->
|
|
||||||
<dependency>
|
|
||||||
<groupId>cn.dev33</groupId>
|
|
||||||
<artifactId>sa-token-redis-jackson</artifactId>
|
|
||||||
<version>${sa-token.version}</version>
|
|
||||||
</dependency>
|
|
||||||
<!-- Sa-Token 整合 jwt (Simple模式),保持与原JWT token格式兼容 -->
|
|
||||||
<dependency>
|
|
||||||
<groupId>cn.dev33</groupId>
|
|
||||||
<artifactId>sa-token-jwt</artifactId>
|
|
||||||
<version>${sa-token.version}</version>
|
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
|
||||||
<!-- <dependency>
|
<dependency>
|
||||||
<groupId>com.github.xiaoymin</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
|
<artifactId>spring-boot-starter-oauth2-authorization-server</artifactId>
|
||||||
<version>${knife4j-spring-boot-starter.version}</version>
|
</dependency>
|
||||||
</dependency>-->
|
<dependency>
|
||||||
<!-- knife4j 升级springboot3.4.5报错 -->
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<!-- 添加spring security cas支持 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.security</groupId>
|
||||||
|
<artifactId>spring-security-cas</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.github.xiaoymin</groupId>
|
<groupId>com.github.xiaoymin</groupId>
|
||||||
<artifactId>knife4j-openapi3-ui</artifactId>
|
<artifactId>knife4j-openapi3-ui</artifactId>
|
||||||
@ -237,8 +238,8 @@
|
|||||||
|
|
||||||
<!-- AutoPoi Excel工具类-->
|
<!-- AutoPoi Excel工具类-->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.jeecgframework.boot3</groupId>
|
<groupId>org.jeecgframework</groupId>
|
||||||
<artifactId>autopoi-web</artifactId>
|
<artifactId>autopoi-spring-boot-3-starter</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>xerces</groupId>
|
<groupId>xerces</groupId>
|
||||||
@ -256,6 +257,14 @@
|
|||||||
<artifactId>checker-qual</artifactId>
|
<artifactId>checker-qual</artifactId>
|
||||||
<groupId>org.checkerframework</groupId>
|
<groupId>org.checkerframework</groupId>
|
||||||
</exclusion>
|
</exclusion>
|
||||||
|
<exclusion>
|
||||||
|
<groupId>com.google.errorprone</groupId>
|
||||||
|
<artifactId>error_prone_annotations</artifactId>
|
||||||
|
</exclusion>
|
||||||
|
<exclusion>
|
||||||
|
<groupId>org.apache.commons</groupId>
|
||||||
|
<artifactId>commons-compress</artifactId>
|
||||||
|
</exclusion>
|
||||||
</exclusions>
|
</exclusions>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
@ -313,5 +322,21 @@
|
|||||||
<groupId>org.jeecgframework.boot3</groupId>
|
<groupId>org.jeecgframework.boot3</groupId>
|
||||||
<artifactId>jeecg-boot-starter-chatgpt</artifactId>
|
<artifactId>jeecg-boot-starter-chatgpt</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<!-- 腾讯云 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.tencentcloudapi</groupId>
|
||||||
|
<artifactId>tencentcloud-sdk-java-sms</artifactId>
|
||||||
|
<version>${tencentcloud-sdk-java-sms.version}</version>
|
||||||
|
<exclusions>
|
||||||
|
<exclusion>
|
||||||
|
<groupId>javax.xml.bind</groupId>
|
||||||
|
<artifactId>jaxb-api</artifactId>
|
||||||
|
</exclusion>
|
||||||
|
<exclusion>
|
||||||
|
<groupId>com.squareup.okio</groupId>
|
||||||
|
<artifactId>okio</artifactId>
|
||||||
|
</exclusion>
|
||||||
|
</exclusions>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</project>
|
</project>
|
||||||
@ -1,7 +1,6 @@
|
|||||||
package org.apache.shiro.subject;
|
package org.apache.shiro.subject;
|
||||||
|
|
||||||
|
import org.jeecg.config.security.utils.SecureUtil;
|
||||||
import org.jeecg.common.util.LoginUserUtils;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 兼容处理Online功能使用处理,请勿修改
|
* 兼容处理Online功能使用处理,请勿修改
|
||||||
@ -10,6 +9,6 @@ import org.jeecg.common.util.LoginUserUtils;
|
|||||||
*/
|
*/
|
||||||
public interface Subject {
|
public interface Subject {
|
||||||
default Object getPrincipal() {
|
default Object getPrincipal() {
|
||||||
return LoginUserUtils.getSessionUser();
|
return SecureUtil.currentUser();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
package org.jeecg.common.api;
|
package org.jeecg.common.api;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson.JSONObject;
|
||||||
import org.jeecg.common.api.dto.AiragFlowDTO;
|
import org.jeecg.common.api.dto.AiragFlowDTO;
|
||||||
import org.jeecg.common.system.vo.*;
|
import org.jeecg.common.system.vo.*;
|
||||||
|
|
||||||
@ -65,6 +66,13 @@ public interface CommonAPI {
|
|||||||
*/
|
*/
|
||||||
public String getUserIdByName(String username);
|
public String getUserIdByName(String username);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 5根据用户手机号查询用户信息
|
||||||
|
* @param username
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public LoginUser getUserByPhone(String phone);
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 6字典表的 翻译
|
* 6字典表的 翻译
|
||||||
@ -132,7 +140,6 @@ public interface CommonAPI {
|
|||||||
*/
|
*/
|
||||||
Map<String, List<DictModel>> translateManyDict(String dictCodes, String keys);
|
Map<String, List<DictModel>> translateManyDict(String dictCodes, String keys);
|
||||||
|
|
||||||
//update-begin---author:chenrui ---date:20231221 for:[issues/#5643]解决分布式下表字典跨库无法查询问题------------
|
|
||||||
/**
|
/**
|
||||||
* 15 字典表的 翻译,可批量
|
* 15 字典表的 翻译,可批量
|
||||||
* @param table
|
* @param table
|
||||||
@ -143,7 +150,6 @@ public interface CommonAPI {
|
|||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
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]解决分布式下表字典跨库无法查询问题------------
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 16 运行AIRag流程
|
* 16 运行AIRag流程
|
||||||
@ -156,4 +162,31 @@ public interface CommonAPI {
|
|||||||
*/
|
*/
|
||||||
Object runAiragFlow(AiragFlowDTO airagFlowDTO);
|
Object runAiragFlow(AiragFlowDTO airagFlowDTO);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 登录加载系统字典
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
Map<String,List<DictModel>> queryAllDictItems();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询SysDepart集合
|
||||||
|
* @param userId
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
List<SysDepartModel> queryUserDeparts(String userId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据用户名设置部门ID
|
||||||
|
* @param username
|
||||||
|
* @param orgCode
|
||||||
|
*/
|
||||||
|
void updateUserDepart(String username,String orgCode,Integer loginTenantId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置登录租户
|
||||||
|
* @param username
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
JSONObject setLoginTenant(String username);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,48 @@
|
|||||||
|
package org.jeecg.common.api.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 流程审批意见DTO
|
||||||
|
* @author scott
|
||||||
|
* @date 2025-01-29
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class ApprovalCommentDTO implements Serializable {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 任务ID
|
||||||
|
*/
|
||||||
|
private String taskId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 任务名称
|
||||||
|
*/
|
||||||
|
private String taskName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 审批人ID
|
||||||
|
*/
|
||||||
|
private String approverId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 审批人姓名
|
||||||
|
*/
|
||||||
|
private String approverName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 审批意见
|
||||||
|
*/
|
||||||
|
private String approvalComment;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 审批时间
|
||||||
|
*/
|
||||||
|
private Date approvalTime;
|
||||||
|
}
|
||||||
|
|
||||||
@ -30,12 +30,10 @@ public class OnlineAuthDTO implements Serializable {
|
|||||||
*/
|
*/
|
||||||
private String onlineFormUrl;
|
private String onlineFormUrl;
|
||||||
|
|
||||||
//update-begin---author:chenrui ---date:20240123 for:[QQYUN-7992]【online】工单申请下的online表单,未配置online表单开发菜单,操作报错无权限------------
|
|
||||||
/**
|
/**
|
||||||
* online工单的地址
|
* online工单的地址
|
||||||
*/
|
*/
|
||||||
private String onlineWorkOrderUrl;
|
private String onlineWorkOrderUrl;
|
||||||
//update-end---author:chenrui ---date:20240123 for:[QQYUN-7992]【online】工单申请下的online表单,未配置online表单开发菜单,操作报错无权限------------
|
|
||||||
|
|
||||||
public OnlineAuthDTO(){
|
public OnlineAuthDTO(){
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,55 @@
|
|||||||
|
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.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移动端消息推送
|
||||||
|
* @author liusq
|
||||||
|
* @date 2025/11/12 14:11
|
||||||
|
*/
|
||||||
|
@Builder
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
@Data
|
||||||
|
public class PushMessageDTO implements Serializable {
|
||||||
|
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 7431775881170684867L;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 消息标题
|
||||||
|
*/
|
||||||
|
private String title;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 消息内容
|
||||||
|
*/
|
||||||
|
private String content;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 推送形式:all:全推送 single:单用户推送
|
||||||
|
*/
|
||||||
|
private String pushType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户名usernameList
|
||||||
|
*/
|
||||||
|
List<String> usernames;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户名idList
|
||||||
|
*/
|
||||||
|
List<String> userIds;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 消息附加参数
|
||||||
|
*/
|
||||||
|
Map<String,Object> payload;
|
||||||
|
}
|
||||||
@ -1,8 +1,9 @@
|
|||||||
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.jeecg.common.util.LoginUserUtils;
|
import org.apache.shiro.SecurityUtils;
|
||||||
import org.aspectj.lang.JoinPoint;
|
import org.aspectj.lang.JoinPoint;
|
||||||
import org.aspectj.lang.ProceedingJoinPoint;
|
import org.aspectj.lang.ProceedingJoinPoint;
|
||||||
import org.aspectj.lang.annotation.Around;
|
import org.aspectj.lang.annotation.Around;
|
||||||
@ -15,12 +16,14 @@ import org.jeecg.common.aspect.annotation.AutoLog;
|
|||||||
import org.jeecg.common.constant.CommonConstant;
|
import org.jeecg.common.constant.CommonConstant;
|
||||||
import org.jeecg.common.constant.enums.ModuleType;
|
import org.jeecg.common.constant.enums.ModuleType;
|
||||||
import org.jeecg.common.constant.enums.OperateTypeEnum;
|
import org.jeecg.common.constant.enums.OperateTypeEnum;
|
||||||
|
import org.jeecg.config.security.utils.SecureUtil;
|
||||||
import org.jeecg.modules.base.service.BaseCommonService;
|
import org.jeecg.modules.base.service.BaseCommonService;
|
||||||
import org.jeecg.common.system.vo.LoginUser;
|
import org.jeecg.common.system.vo.LoginUser;
|
||||||
import org.jeecg.common.util.IpUtils;
|
import org.jeecg.common.util.IpUtils;
|
||||||
import org.jeecg.common.util.SpringContextUtils;
|
import org.jeecg.common.util.SpringContextUtils;
|
||||||
import org.jeecg.common.util.oConvertUtils;
|
import org.jeecg.common.util.oConvertUtils;
|
||||||
import org.springframework.core.StandardReflectionParameterNameDiscoverer;
|
import org.springframework.core.StandardReflectionParameterNameDiscoverer;
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.validation.BindingResult;
|
import org.springframework.validation.BindingResult;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
@ -100,7 +103,7 @@ public class AutoLogAspect {
|
|||||||
//设置IP地址
|
//设置IP地址
|
||||||
dto.setIp(IpUtils.getIpAddr(request));
|
dto.setIp(IpUtils.getIpAddr(request));
|
||||||
//获取登录用户信息
|
//获取登录用户信息
|
||||||
LoginUser sysUser = LoginUserUtils.getSessionUser();
|
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());
|
||||||
@ -121,9 +124,8 @@ public class AutoLogAspect {
|
|||||||
if (operateType > 0) {
|
if (operateType > 0) {
|
||||||
return operateType;
|
return operateType;
|
||||||
}
|
}
|
||||||
//update-begin---author:wangshuai ---date:20220331 for:阿里云代码扫描规范(不允许任何魔法值出现在代码中)------------
|
// 代码逻辑说明: 阿里云代码扫描规范(不允许任何魔法值出现在代码中)------------
|
||||||
return OperateTypeEnum.getTypeByMethodName(methodName);
|
return OperateTypeEnum.getTypeByMethodName(methodName);
|
||||||
//update-end---author:wangshuai ---date:20220331 for:阿里云代码扫描规范(不允许任何魔法值出现在代码中)------------
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -143,14 +145,15 @@ public class AutoLogAspect {
|
|||||||
// https://my.oschina.net/mengzhang6/blog/2395893
|
// https://my.oschina.net/mengzhang6/blog/2395893
|
||||||
Object[] arguments = new Object[paramsArray.length];
|
Object[] arguments = new Object[paramsArray.length];
|
||||||
for (int i = 0; i < paramsArray.length; i++) {
|
for (int i = 0; i < paramsArray.length; i++) {
|
||||||
if (paramsArray[i] instanceof BindingResult || paramsArray[i] instanceof ServletRequest || paramsArray[i] instanceof ServletResponse || paramsArray[i] instanceof MultipartFile) {
|
if (paramsArray[i] instanceof BindingResult || paramsArray[i] instanceof ServletRequest || paramsArray[i] instanceof ServletResponse || paramsArray[i] instanceof MultipartFile || paramsArray[i] instanceof MultipartFile[]) {
|
||||||
//ServletRequest不能序列化,从入参里排除,否则报异常:java.lang.IllegalStateException: It is illegal to call this method if the current request is not in asynchronous mode (i.e. isAsyncStarted() returns false)
|
//ServletRequest不能序列化,从入参里排除,否则报异常:java.lang.IllegalStateException: It is illegal to call this method if the current request is not in asynchronous mode (i.e. isAsyncStarted() returns false)
|
||||||
//ServletResponse不能序列化 从入参里排除,否则报异常:java.lang.IllegalStateException: getOutputStream() has already been called for this response
|
//ServletResponse不能序列化 从入参里排除,否则报异常:java.lang.IllegalStateException: getOutputStream() has already been called for this response
|
||||||
|
//MultipartFile和MultipartFile[]不能序列化,从入参里排除
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
arguments[i] = paramsArray[i];
|
arguments[i] = paramsArray[i];
|
||||||
}
|
}
|
||||||
//update-begin-author:taoyan date:20200724 for:日志数据太长的直接过滤掉
|
// 代码逻辑说明: 日志数据太长的直接过滤掉
|
||||||
PropertyFilter profilter = new PropertyFilter() {
|
PropertyFilter profilter = new PropertyFilter() {
|
||||||
@Override
|
@Override
|
||||||
public boolean apply(Object o, String name, Object value) {
|
public boolean apply(Object o, String name, Object value) {
|
||||||
@ -165,14 +168,13 @@ public class AutoLogAspect {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
params = JSONObject.toJSONString(arguments, profilter);
|
params = JSONObject.toJSONString(arguments, profilter);
|
||||||
//update-end-author:taoyan date:20200724 for:日志数据太长的直接过滤掉
|
|
||||||
} else {
|
} else {
|
||||||
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
|
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
|
||||||
Method method = signature.getMethod();
|
Method method = signature.getMethod();
|
||||||
// 请求的方法参数值
|
// 请求的方法参数值
|
||||||
Object[] args = joinPoint.getArgs();
|
Object[] args = joinPoint.getArgs();
|
||||||
// 请求的方法参数名称
|
// 请求的方法参数名称
|
||||||
StandardReflectionParameterNameDiscoverer u=new StandardReflectionParameterNameDiscoverer();
|
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++) {
|
||||||
@ -244,7 +246,7 @@ public class AutoLogAspect {
|
|||||||
sysLog.setIp(IPUtils.getIpAddr(request));
|
sysLog.setIp(IPUtils.getIpAddr(request));
|
||||||
|
|
||||||
//获取登录用户信息
|
//获取登录用户信息
|
||||||
LoginUser sysUser = LoginUserUtils.getLoginUser();
|
LoginUser sysUser = (LoginUser)SecurityUtils.getSubject().getPrincipal();
|
||||||
if(sysUser!=null){
|
if(sysUser!=null){
|
||||||
sysLog.setUserid(sysUser.getUsername());
|
sysLog.setUserid(sysUser.getUsername());
|
||||||
sysLog.setUsername(sysUser.getRealname());
|
sysLog.setUsername(sysUser.getRealname());
|
||||||
|
|||||||
@ -105,29 +105,24 @@ public class DictAspect {
|
|||||||
Map<String, List<String>> dataListMap = new HashMap<>(5);
|
Map<String, List<String>> dataListMap = new HashMap<>(5);
|
||||||
//取出结果集
|
//取出结果集
|
||||||
List<Object> records=((IPage) ((Result) result).getResult()).getRecords();
|
List<Object> records=((IPage) ((Result) result).getResult()).getRecords();
|
||||||
//update-begin--Author:zyf -- Date:20220606 ----for:【VUEN-1230】 判断是否含有字典注解,没有注解返回-----
|
// 代码逻辑说明: 【VUEN-1230】 判断是否含有字典注解,没有注解返回-----
|
||||||
Boolean hasDict= checkHasDict(records);
|
Boolean hasDict= checkHasDict(records);
|
||||||
if(!hasDict){
|
if(!hasDict){
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
log.debug(" __ 进入字典翻译切面 DictAspect —— " );
|
log.debug(" __ 进入字典翻译切面 DictAspect —— " );
|
||||||
//update-end--Author:zyf -- Date:20220606 ----for:【VUEN-1230】 判断是否含有字典注解,没有注解返回-----
|
|
||||||
for (Object record : records) {
|
for (Object record : records) {
|
||||||
String json="{}";
|
String json="{}";
|
||||||
try {
|
try {
|
||||||
//update-begin--Author:zyf -- Date:20220531 ----for:【issues/#3629】 DictAspect Jackson序列化报错-----
|
|
||||||
//解决@JsonFormat注解解析不了的问题详见SysAnnouncement类的@JsonFormat
|
//解决@JsonFormat注解解析不了的问题详见SysAnnouncement类的@JsonFormat
|
||||||
json = objectMapper.writeValueAsString(record);
|
json = objectMapper.writeValueAsString(record);
|
||||||
//update-end--Author:zyf -- Date:20220531 ----for:【issues/#3629】 DictAspect Jackson序列化报错-----
|
|
||||||
} catch (JsonProcessingException e) {
|
} catch (JsonProcessingException e) {
|
||||||
log.error("json解析失败"+e.getMessage(),e);
|
log.error("json解析失败"+e.getMessage(),e);
|
||||||
}
|
}
|
||||||
//update-begin--Author:scott -- Date:20211223 ----for:【issues/3303】restcontroller返回json数据后key顺序错乱 -----
|
// 代码逻辑说明: 【issues/3303】restcontroller返回json数据后key顺序错乱 -----
|
||||||
JSONObject item = JSONObject.parseObject(json, Feature.OrderedField);
|
JSONObject item = JSONObject.parseObject(json, Feature.OrderedField);
|
||||||
//update-end--Author:scott -- Date:20211223 ----for:【issues/3303】restcontroller返回json数据后key顺序错乱 -----
|
|
||||||
|
|
||||||
//update-begin--Author:scott -- Date:20190603 ----for:解决继承实体字段无法翻译问题------
|
|
||||||
//for (Field field : record.getClass().getDeclaredFields()) {
|
//for (Field field : record.getClass().getDeclaredFields()) {
|
||||||
// 遍历所有字段,把字典Code取出来,放到 map 里
|
// 遍历所有字段,把字典Code取出来,放到 map 里
|
||||||
for (Field field : oConvertUtils.getAllFields(record)) {
|
for (Field field : oConvertUtils.getAllFields(record)) {
|
||||||
@ -135,7 +130,6 @@ public class DictAspect {
|
|||||||
if (oConvertUtils.isEmpty(value)) {
|
if (oConvertUtils.isEmpty(value)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
//update-end--Author:scott -- Date:20190603 ----for:解决继承实体字段无法翻译问题------
|
|
||||||
if (field.getAnnotation(Dict.class) != null) {
|
if (field.getAnnotation(Dict.class) != null) {
|
||||||
if (!dictFieldList.contains(field)) {
|
if (!dictFieldList.contains(field)) {
|
||||||
dictFieldList.add(field);
|
dictFieldList.add(field);
|
||||||
@ -143,26 +137,22 @@ public class DictAspect {
|
|||||||
String code = field.getAnnotation(Dict.class).dicCode();
|
String code = field.getAnnotation(Dict.class).dicCode();
|
||||||
String text = field.getAnnotation(Dict.class).dicText();
|
String text = field.getAnnotation(Dict.class).dicText();
|
||||||
String table = field.getAnnotation(Dict.class).dictTable();
|
String table = field.getAnnotation(Dict.class).dictTable();
|
||||||
//update-begin---author:chenrui ---date:20231221 for:[issues/#5643]解决分布式下表字典跨库无法查询问题------------
|
// 代码逻辑说明: [issues/#5643]解决分布式下表字典跨库无法查询问题------------
|
||||||
String dataSource = field.getAnnotation(Dict.class).ds();
|
String dataSource = field.getAnnotation(Dict.class).ds();
|
||||||
//update-end---author:chenrui ---date:20231221 for:[issues/#5643]解决分布式下表字典跨库无法查询问题------------
|
|
||||||
List<String> dataList;
|
List<String> dataList;
|
||||||
String dictCode = code;
|
String dictCode = code;
|
||||||
if (!StringUtils.isEmpty(table)) {
|
if (!StringUtils.isEmpty(table)) {
|
||||||
//update-begin---author:chenrui ---date:20231221 for:[issues/#5643]解决分布式下表字典跨库无法查询问题------------
|
// 代码逻辑说明: [issues/#5643]解决分布式下表字典跨库无法查询问题------------
|
||||||
dictCode = String.format("%s,%s,%s,%s", table, text, code, dataSource);
|
dictCode = String.format("%s,%s,%s,%s", table, text, code, dataSource);
|
||||||
//update-end---author:chenrui ---date:20231221 for:[issues/#5643]解决分布式下表字典跨库无法查询问题------------
|
|
||||||
}
|
}
|
||||||
dataList = dataListMap.computeIfAbsent(dictCode, k -> new ArrayList<>());
|
dataList = dataListMap.computeIfAbsent(dictCode, k -> new ArrayList<>());
|
||||||
this.listAddAllDeduplicate(dataList, Arrays.asList(value.split(",")));
|
this.listAddAllDeduplicate(dataList, Arrays.asList(value.split(",")));
|
||||||
}
|
}
|
||||||
//date类型默认转换string格式化日期
|
//date类型默认转换string格式化日期
|
||||||
//update-begin--Author:zyf -- Date:20220531 ----for:【issues/#3629】 DictAspect Jackson序列化报错-----
|
|
||||||
//if (JAVA_UTIL_DATE.equals(field.getType().getName())&&field.getAnnotation(JsonFormat.class)==null&&item.get(field.getName())!=null){
|
//if (JAVA_UTIL_DATE.equals(field.getType().getName())&&field.getAnnotation(JsonFormat.class)==null&&item.get(field.getName())!=null){
|
||||||
//SimpleDateFormat aDate=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
|
//SimpleDateFormat aDate=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
|
||||||
// item.put(field.getName(), aDate.format(new Date((Long) item.get(field.getName()))));
|
// item.put(field.getName(), aDate.format(new Date((Long) item.get(field.getName()))));
|
||||||
//}
|
//}
|
||||||
//update-end--Author:zyf -- Date:20220531 ----for:【issues/#3629】 DictAspect Jackson序列化报错-----
|
|
||||||
}
|
}
|
||||||
items.add(item);
|
items.add(item);
|
||||||
}
|
}
|
||||||
@ -176,15 +166,12 @@ public class DictAspect {
|
|||||||
String code = field.getAnnotation(Dict.class).dicCode();
|
String code = field.getAnnotation(Dict.class).dicCode();
|
||||||
String text = field.getAnnotation(Dict.class).dicText();
|
String text = field.getAnnotation(Dict.class).dicText();
|
||||||
String table = field.getAnnotation(Dict.class).dictTable();
|
String table = field.getAnnotation(Dict.class).dictTable();
|
||||||
//update-begin---author:chenrui ---date:20231221 for:[issues/#5643]解决分布式下表字典跨库无法查询问题------------
|
|
||||||
// 自定义的字典表数据源
|
// 自定义的字典表数据源
|
||||||
String dataSource = field.getAnnotation(Dict.class).ds();
|
String dataSource = field.getAnnotation(Dict.class).ds();
|
||||||
//update-end---author:chenrui ---date:20231221 for:[issues/#5643]解决分布式下表字典跨库无法查询问题------------
|
|
||||||
String fieldDictCode = code;
|
String fieldDictCode = code;
|
||||||
if (!StringUtils.isEmpty(table)) {
|
if (!StringUtils.isEmpty(table)) {
|
||||||
//update-begin---author:chenrui ---date:20231221 for:[issues/#5643]解决分布式下表字典跨库无法查询问题------------
|
// 代码逻辑说明: [issues/#5643]解决分布式下表字典跨库无法查询问题------------
|
||||||
fieldDictCode = String.format("%s,%s,%s,%s", table, text, code, dataSource);
|
fieldDictCode = String.format("%s,%s,%s,%s", table, text, code, dataSource);
|
||||||
//update-end---author:chenrui ---date:20231221 for:[issues/#5643]解决分布式下表字典跨库无法查询问题------------
|
|
||||||
}
|
}
|
||||||
|
|
||||||
String value = record.getString(field.getName());
|
String value = record.getString(field.getName());
|
||||||
@ -286,25 +273,20 @@ public class DictAspect {
|
|||||||
String[] arr = dictCode.split(",");
|
String[] arr = dictCode.split(",");
|
||||||
String table = arr[0], text = arr[1], code = arr[2];
|
String table = arr[0], text = arr[1], code = arr[2];
|
||||||
String values = String.join(",", needTranslDataTable);
|
String values = String.join(",", needTranslDataTable);
|
||||||
//update-begin---author:chenrui ---date:20231221 for:[issues/#5643]解决分布式下表字典跨库无法查询问题------------
|
|
||||||
// 自定义的数据源
|
// 自定义的数据源
|
||||||
String dataSource = null;
|
String dataSource = null;
|
||||||
if (arr.length > 3) {
|
if (arr.length > 3) {
|
||||||
dataSource = arr[3];
|
dataSource = arr[3];
|
||||||
}
|
}
|
||||||
//update-end---author:chenrui ---date:20231221 for:[issues/#5643]解决分布式下表字典跨库无法查询问题------------
|
|
||||||
log.debug("translateDictFromTableByKeys.dictCode:" + dictCode);
|
log.debug("translateDictFromTableByKeys.dictCode:" + dictCode);
|
||||||
log.debug("translateDictFromTableByKeys.values:" + values);
|
log.debug("translateDictFromTableByKeys.values:" + values);
|
||||||
//update-begin---author:chenrui ---date:20231221 for:[issues/#5643]解决分布式下表字典跨库无法查询问题------------
|
|
||||||
|
|
||||||
//update-begin---author:wangshuai---date:2024-01-09---for:微服务下为空报错没有参数需要传递空字符串---
|
// 代码逻辑说明: 微服务下为空报错没有参数需要传递空字符串---
|
||||||
if(null == dataSource){
|
if(null == dataSource){
|
||||||
dataSource = "";
|
dataSource = "";
|
||||||
}
|
}
|
||||||
//update-end---author:wangshuai---date:2024-01-09---for:微服务下为空报错没有参数需要传递空字符串---
|
|
||||||
|
|
||||||
List<DictModel> texts = commonApi.translateDictFromTableByKeys(table, text, code, values, dataSource);
|
List<DictModel> texts = commonApi.translateDictFromTableByKeys(table, text, code, values, dataSource);
|
||||||
//update-end---author:chenrui ---date:20231221 for:[issues/#5643]解决分布式下表字典跨库无法查询问题------------
|
|
||||||
log.debug("translateDictFromTableByKeys.result:" + texts);
|
log.debug("translateDictFromTableByKeys.result:" + texts);
|
||||||
List<DictModel> list = translText.computeIfAbsent(dictCode, k -> new ArrayList<>());
|
List<DictModel> list = translText.computeIfAbsent(dictCode, k -> new ArrayList<>());
|
||||||
list.addAll(texts);
|
list.addAll(texts);
|
||||||
@ -313,10 +295,8 @@ public class DictAspect {
|
|||||||
for (DictModel dict : texts) {
|
for (DictModel dict : texts) {
|
||||||
String redisKey = String.format("sys:cache:dictTable::SimpleKey [%s,%s]", dictCode, dict.getValue());
|
String redisKey = String.format("sys:cache:dictTable::SimpleKey [%s,%s]", dictCode, dict.getValue());
|
||||||
try {
|
try {
|
||||||
// update-begin-author:taoyan date:20211012 for: 字典表翻译注解缓存未更新 issues/3061
|
|
||||||
// 保留5分钟
|
// 保留5分钟
|
||||||
redisTemplate.opsForValue().set(redisKey, dict.getText(), 300, TimeUnit.SECONDS);
|
redisTemplate.opsForValue().set(redisKey, dict.getText(), 300, TimeUnit.SECONDS);
|
||||||
// update-end-author:taoyan date:20211012 for: 字典表翻译注解缓存未更新 issues/3061
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.warn(e.getMessage(), e);
|
log.warn(e.getMessage(), e);
|
||||||
}
|
}
|
||||||
@ -400,7 +380,7 @@ public class DictAspect {
|
|||||||
if (k.trim().length() == 0) {
|
if (k.trim().length() == 0) {
|
||||||
continue; //跳过循环
|
continue; //跳过循环
|
||||||
}
|
}
|
||||||
//update-begin--Author:scott -- Date:20210531 ----for: !56 优化微服务应用下存在表字段需要字典翻译时加载缓慢问题-----
|
// 代码逻辑说明: !56 优化微服务应用下存在表字段需要字典翻译时加载缓慢问题-----
|
||||||
if (!StringUtils.isEmpty(table)){
|
if (!StringUtils.isEmpty(table)){
|
||||||
log.debug("--DictAspect------dicTable="+ table+" ,dicText= "+text+" ,dicCode="+code);
|
log.debug("--DictAspect------dicTable="+ table+" ,dicText= "+text+" ,dicCode="+code);
|
||||||
String keyString = String.format("sys:cache:dictTable::SimpleKey [%s,%s,%s,%s]",table,text,code,k.trim());
|
String keyString = String.format("sys:cache:dictTable::SimpleKey [%s,%s,%s,%s]",table,text,code,k.trim());
|
||||||
@ -425,7 +405,6 @@ public class DictAspect {
|
|||||||
tmpValue = commonApi.translateDict(code, k.trim());
|
tmpValue = commonApi.translateDict(code, k.trim());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//update-end--Author:scott -- Date:20210531 ----for: !56 优化微服务应用下存在表字段需要字典翻译时加载缓慢问题-----
|
|
||||||
|
|
||||||
if (tmpValue != null) {
|
if (tmpValue != null) {
|
||||||
if (!"".equals(textValue.toString())) {
|
if (!"".equals(textValue.toString())) {
|
||||||
|
|||||||
@ -57,7 +57,6 @@ public class PermissionDataAspect {
|
|||||||
String requestMethod = request.getMethod();
|
String requestMethod = request.getMethod();
|
||||||
String requestPath = request.getRequestURI().substring(request.getContextPath().length());
|
String requestPath = request.getRequestURI().substring(request.getContextPath().length());
|
||||||
requestPath = filterUrl(requestPath);
|
requestPath = filterUrl(requestPath);
|
||||||
//update-begin-author:taoyan date:20211027 for:JTC-132【online报表权限】online报表带参数的菜单配置数据权限无效
|
|
||||||
//先判断是否online报表请求
|
//先判断是否online报表请求
|
||||||
if(requestPath.indexOf(UrlMatchEnum.CGREPORT_DATA.getMatchUrl())>=0 || requestPath.indexOf(UrlMatchEnum.CGREPORT_ONLY_DATA.getMatchUrl())>=0){
|
if(requestPath.indexOf(UrlMatchEnum.CGREPORT_DATA.getMatchUrl())>=0 || requestPath.indexOf(UrlMatchEnum.CGREPORT_ONLY_DATA.getMatchUrl())>=0){
|
||||||
// 获取地址栏参数
|
// 获取地址栏参数
|
||||||
@ -66,7 +65,6 @@ public class PermissionDataAspect {
|
|||||||
requestPath+="?"+urlParamString;
|
requestPath+="?"+urlParamString;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//update-end-author:taoyan date:20211027 for:JTC-132【online报表权限】online报表带参数的菜单配置数据权限无效
|
|
||||||
log.debug("拦截请求 >> {} ; 请求类型 >> {} . ", requestPath, requestMethod);
|
log.debug("拦截请求 >> {} ; 请求类型 >> {} . ", requestPath, requestMethod);
|
||||||
String username = JwtUtil.getUserNameByToken(request);
|
String username = JwtUtil.getUserNameByToken(request);
|
||||||
//查询数据权限信息
|
//查询数据权限信息
|
||||||
|
|||||||
@ -41,7 +41,6 @@ public @interface Dict {
|
|||||||
String dictTable() default "";
|
String dictTable() default "";
|
||||||
|
|
||||||
|
|
||||||
//update-begin---author:chenrui ---date:20231221 for:[issues/#5643]解决分布式下表字典跨库无法查询问题------------
|
|
||||||
/**
|
/**
|
||||||
* 方法描述: 数据字典表所在数据源名称
|
* 方法描述: 数据字典表所在数据源名称
|
||||||
* 作 者: chenrui
|
* 作 者: chenrui
|
||||||
@ -50,5 +49,4 @@ public @interface Dict {
|
|||||||
* @return 返回类型: String
|
* @return 返回类型: String
|
||||||
*/
|
*/
|
||||||
String ds() default "";
|
String ds() default "";
|
||||||
//update-end---author:chenrui ---date:20231221 for:[issues/#5643]解决分布式下表字典跨库无法查询问题------------
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -87,6 +87,30 @@ public interface CommonConstant {
|
|||||||
/**访问权限认证未通过 510*/
|
/**访问权限认证未通过 510*/
|
||||||
Integer SC_JEECG_NO_AUTHZ=510;
|
Integer SC_JEECG_NO_AUTHZ=510;
|
||||||
|
|
||||||
|
/** 登录用户Shiro权限缓存KEY前缀 */
|
||||||
|
public static String PREFIX_USER_SHIRO_CACHE = "shiro:cache:org.jeecg.config.shiro.ShiroRealm.authorizationCache:";
|
||||||
|
/** 登录用户Token令牌缓存KEY前缀 */
|
||||||
|
String PREFIX_USER_TOKEN = "token::jeecg-client::";
|
||||||
|
/** 登录用户Token令牌作废提示信息,比如 “不允许同一账号多地同时登录,会往这个变量存提示信息” */
|
||||||
|
String PREFIX_USER_TOKEN_ERROR_MSG = "token::jeecg-client::error:msg_";
|
||||||
|
|
||||||
|
/**============================== 【是否允许同一账号多地同时登录】登录客户端类型常量 ==============================*/
|
||||||
|
/** 客户端类型:PC端 */
|
||||||
|
String CLIENT_TYPE_PC = "PC";
|
||||||
|
/** 客户端类型:APP端 */
|
||||||
|
String CLIENT_TYPE_APP = "APP";
|
||||||
|
/** 客户端类型:手机号登录 */
|
||||||
|
String CLIENT_TYPE_PHONE = "PHONE";
|
||||||
|
String PREFIX_USER_TOKEN_PC = "token::jeecg-client::single_login:pc:";
|
||||||
|
/** 单点登录:用户在APP端的Token缓存KEY前缀 (username -> token) */
|
||||||
|
String PREFIX_USER_TOKEN_APP = "token::jeecg-client::single_login:app:";
|
||||||
|
/** 单点登录:用户在手机号登录的Token缓存KEY前缀 (username -> token) */
|
||||||
|
String PREFIX_USER_TOKEN_PHONE = "token::jeecg-client::single_login:phone:";
|
||||||
|
/**============================== 【是否允许同一账号多地同时登录】登录客户端类型常量 ==============================*/
|
||||||
|
|
||||||
|
// /** Token缓存时间:3600秒即一小时 */
|
||||||
|
// int TOKEN_EXPIRE_TIME = 3600;
|
||||||
|
|
||||||
/** 登录二维码 */
|
/** 登录二维码 */
|
||||||
String LOGIN_QRCODE_PRE = "QRCODELOGIN:";
|
String LOGIN_QRCODE_PRE = "QRCODELOGIN:";
|
||||||
String LOGIN_QRCODE = "LQ:";
|
String LOGIN_QRCODE = "LQ:";
|
||||||
@ -301,6 +325,10 @@ public interface CommonConstant {
|
|||||||
*/
|
*/
|
||||||
String SYS_ROLE_ADMIN = "admin";
|
String SYS_ROLE_ADMIN = "admin";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 考勤补卡业务状态 (0:处理中)
|
||||||
|
*/
|
||||||
|
String SIGN_PATCH_BIZ_STATUS_0 = "0";
|
||||||
/**
|
/**
|
||||||
* 考勤补卡业务状态 (1:同意 2:不同意)
|
* 考勤补卡业务状态 (1:同意 2:不同意)
|
||||||
*/
|
*/
|
||||||
@ -584,7 +612,6 @@ public interface CommonConstant {
|
|||||||
String ORDER_TYPE_DESC = "DESC";
|
String ORDER_TYPE_DESC = "DESC";
|
||||||
|
|
||||||
|
|
||||||
//update-begin---author:scott ---date:2023-09-10 for:积木报表常量----
|
|
||||||
/**
|
/**
|
||||||
* 报表允许设计开发的角色
|
* 报表允许设计开发的角色
|
||||||
*/
|
*/
|
||||||
@ -599,9 +626,7 @@ public interface CommonConstant {
|
|||||||
* 数据隔离模式: 按照租户隔离
|
* 数据隔离模式: 按照租户隔离
|
||||||
*/
|
*/
|
||||||
public static final String SAAS_MODE_TENANT = "tenant";
|
public static final String SAAS_MODE_TENANT = "tenant";
|
||||||
//update-end---author:scott ---date::2023-09-10 for:积木报表常量----
|
|
||||||
|
|
||||||
//update-begin---author:wangshuai---date:2024-04-07---for:修改手机号常量---
|
|
||||||
/**
|
/**
|
||||||
* 修改手机号短信验证码redis-key的前缀
|
* 修改手机号短信验证码redis-key的前缀
|
||||||
*/
|
*/
|
||||||
@ -626,7 +651,6 @@ public interface CommonConstant {
|
|||||||
* 修改手机号
|
* 修改手机号
|
||||||
*/
|
*/
|
||||||
String UPDATE_PHONE = "updatePhone";
|
String UPDATE_PHONE = "updatePhone";
|
||||||
//update-end---author:wangshuai---date:2024-04-07---for:修改手机号常量---
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 修改手机号验证码请求次数超出
|
* 修改手机号验证码请求次数超出
|
||||||
@ -702,4 +726,19 @@ public interface CommonConstant {
|
|||||||
* 部门名称redisKey(全路径)
|
* 部门名称redisKey(全路径)
|
||||||
*/
|
*/
|
||||||
String DEPART_NAME_REDIS_KEY_PRE = "sys:cache:departPathName:";
|
String DEPART_NAME_REDIS_KEY_PRE = "sys:cache:departPathName:";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 默认用户排序值
|
||||||
|
*/
|
||||||
|
Integer DEFAULT_USER_SORT = 1000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送短信方式:腾讯
|
||||||
|
*/
|
||||||
|
String SMS_SEND_TYPE_TENCENT = "tencent";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送短信方式:阿里云
|
||||||
|
*/
|
||||||
|
String SMS_SEND_TYPE_ALI_YUN = "aliyun";
|
||||||
}
|
}
|
||||||
|
|||||||
@ -39,20 +39,18 @@ public class ProvinceCityArea {
|
|||||||
this.initAreaList();
|
this.initAreaList();
|
||||||
if(areaList!=null && areaList.size()>0){
|
if(areaList!=null && areaList.size()>0){
|
||||||
for(int i=areaList.size()-1;i>=0;i--){
|
for(int i=areaList.size()-1;i>=0;i--){
|
||||||
//update-begin-author:taoyan date:2022-5-24 for:VUEN-1088 online 导入 省市区导入后 导入数据错乱 北京市/市辖区/西城区-->山西省/晋城市/城区
|
// 代码逻辑说明: VUEN-1088 online 导入 省市区导入后 导入数据错乱 北京市/市辖区/西城区-->山西省/晋城市/城区
|
||||||
String areaText = areaList.get(i).getText();
|
String areaText = areaList.get(i).getText();
|
||||||
String cityText = areaList.get(i).getAheadText();
|
String cityText = areaList.get(i).getAheadText();
|
||||||
if(text.indexOf(areaText)>=0 && (cityText!=null && text.indexOf(cityText)>=0)){
|
if(text.indexOf(areaText)>=0 && (cityText!=null && text.indexOf(cityText)>=0)){
|
||||||
return areaList.get(i).getId();
|
return areaList.get(i).getId();
|
||||||
}
|
}
|
||||||
//update-end-author:taoyan date:2022-5-24 for:VUEN-1088 online 导入 省市区导入后 导入数据错乱 北京市/市辖区/西城区-->山西省/晋城市/城区
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// update-begin-author:sunjianlei date:20220121 for:【JTC-704】数据导入错误 省市区组件,文件中为北京市,导入后,导为了山西省
|
|
||||||
/**
|
/**
|
||||||
* 获取省市区code,精准匹配
|
* 获取省市区code,精准匹配
|
||||||
* @param texts 文本数组,省,市,区
|
* @param texts 文本数组,省,市,区
|
||||||
@ -117,7 +115,6 @@ public class ProvinceCityArea {
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
// update-end-author:sunjianlei date:20220121 for:【JTC-704】数据导入错误 省市区组件,文件中为北京市,导入后,导为了山西省
|
|
||||||
|
|
||||||
public void getAreaByCode(String code,List<String> ls){
|
public void getAreaByCode(String code,List<String> ls){
|
||||||
for(Area area: areaList){
|
for(Area area: areaList){
|
||||||
@ -154,9 +151,8 @@ public class ProvinceCityArea {
|
|||||||
for(String areaKey:areaJson.keySet()){
|
for(String areaKey:areaJson.keySet()){
|
||||||
//System.out.println("········"+areaKey);
|
//System.out.println("········"+areaKey);
|
||||||
Area area = new Area(areaKey,areaJson.getString(areaKey),cityKey);
|
Area area = new Area(areaKey,areaJson.getString(areaKey),cityKey);
|
||||||
//update-begin-author:taoyan date:2022-5-24 for:VUEN-1088 online 导入 省市区导入后 导入数据错乱 北京市/市辖区/西城区-->山西省/晋城市/城区
|
// 代码逻辑说明: VUEN-1088 online 导入 省市区导入后 导入数据错乱 北京市/市辖区/西城区-->山西省/晋城市/城区
|
||||||
area.setAheadText(cityJson.getString(cityKey));
|
area.setAheadText(cityJson.getString(cityKey));
|
||||||
//update-end-author:taoyan date:2022-5-24 for:VUEN-1088 online 导入 省市区导入后 导入数据错乱 北京市/市辖区/西城区-->山西省/晋城市/城区
|
|
||||||
this.areaList.add(area);
|
this.areaList.add(area);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,7 +23,11 @@ public enum NoticeTypeEnum {
|
|||||||
/**
|
/**
|
||||||
* 督办
|
* 督办
|
||||||
*/
|
*/
|
||||||
NOTICE_TYPE_SUPERVISE("督办管理", "supe");
|
NOTICE_TYPE_SUPERVISE("督办管理", "supe"),
|
||||||
|
/**
|
||||||
|
* 考勤
|
||||||
|
*/
|
||||||
|
NOTICE_TYPE_ATTENDANCE("考勤消息", "attendance");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 文件类型名称
|
* 文件类型名称
|
||||||
|
|||||||
@ -0,0 +1,82 @@
|
|||||||
|
package org.jeecg.common.constant.enums;
|
||||||
|
|
||||||
|
import org.jeecg.common.util.oConvertUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UniPush 消息推送枚举
|
||||||
|
* @author: jeecg-boot
|
||||||
|
*/
|
||||||
|
public enum UniPushTypeEnum {
|
||||||
|
/**
|
||||||
|
* 聊天
|
||||||
|
*/
|
||||||
|
CHAT("chat", "聊天消息", "收到%s发来的聊天消息"),
|
||||||
|
/**
|
||||||
|
* 流程跳转到我的任务
|
||||||
|
*/
|
||||||
|
BPM("bpm_task", "待办任务", "收到%s待办任务"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 流程抄送任务
|
||||||
|
*/
|
||||||
|
BPM_VIEW("bpm_cc", "知会任务", "收到%s知会任务"),
|
||||||
|
/**
|
||||||
|
* 系统消息
|
||||||
|
*/
|
||||||
|
SYS_MSG("system", "系统消息", "收到一条系统通告");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 业务类型(chat:聊天 bpm_task:流程 bpm_cc:流程抄送)
|
||||||
|
*/
|
||||||
|
private String type;
|
||||||
|
/**
|
||||||
|
* 消息标题
|
||||||
|
*/
|
||||||
|
private String title;
|
||||||
|
/**
|
||||||
|
* 消息内容
|
||||||
|
*/
|
||||||
|
private String content;
|
||||||
|
|
||||||
|
UniPushTypeEnum(String type, String title, String content) {
|
||||||
|
this.type = type;
|
||||||
|
this.title = title;
|
||||||
|
this.content = content;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getType() {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setType(String type) {
|
||||||
|
this.type = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTitle() {
|
||||||
|
return title ;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTitle(String openType) {
|
||||||
|
this.title = openType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getContent() {
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setContent(String content) {
|
||||||
|
this.content = content;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static UniPushTypeEnum getByType(String type) {
|
||||||
|
if (oConvertUtils.isEmpty(type)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
for (UniPushTypeEnum val : values()) {
|
||||||
|
if (val.getType().equals(type)) {
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -73,7 +73,7 @@ public class SensitiveDataAspect {
|
|||||||
SensitiveInfoUtil.handleNestedObject(result, entity, isEncode);
|
SensitiveInfoUtil.handleNestedObject(result, entity, isEncode);
|
||||||
}
|
}
|
||||||
long endTime=System.currentTimeMillis();
|
long endTime=System.currentTimeMillis();
|
||||||
log.info((isEncode ? "加密操作," : "解密操作,") + "Aspect程序耗时:" + (endTime - startTime) + "ms");
|
log.debug((isEncode ? "加密操作," : "解密操作,") + "Aspect程序耗时:" + (endTime - startTime) + "ms");
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -387,7 +387,7 @@ public class JeecgElasticsearchTemplate {
|
|||||||
data.remove("id");
|
data.remove("id");
|
||||||
bodySb.append(data.toJSONString()).append("\n");
|
bodySb.append(data.toJSONString()).append("\n");
|
||||||
}
|
}
|
||||||
System.out.println("+-+-+-: bodySb.toString(): " + bodySb.toString());
|
//System.out.println("+-+-+-: bodySb.toString(): " + bodySb.toString());
|
||||||
HttpHeaders headers = RestUtil.getHeaderApplicationJson();
|
HttpHeaders headers = RestUtil.getHeaderApplicationJson();
|
||||||
RestUtil.request(url, HttpMethod.PUT, headers, null, bodySb, JSONObject.class);
|
RestUtil.request(url, HttpMethod.PUT, headers, null, bodySb, JSONObject.class);
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@ -5,10 +5,7 @@ import jakarta.annotation.Resource;
|
|||||||
import jakarta.servlet.http.HttpServletRequest;
|
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.jeecg.common.util.LoginUserUtils;
|
import org.apache.shiro.SecurityUtils;
|
||||||
import cn.dev33.satoken.exception.NotLoginException;
|
|
||||||
import cn.dev33.satoken.exception.NotPermissionException;
|
|
||||||
import cn.dev33.satoken.exception.NotRoleException;
|
|
||||||
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;
|
||||||
@ -25,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;
|
||||||
@ -52,6 +51,24 @@ 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,44 +130,21 @@ public class JeecgBootExceptionHandler {
|
|||||||
return Result.error("数据库中已存在该记录");
|
return Result.error("数据库中已存在该记录");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
@ExceptionHandler(AccessDeniedException.class)
|
||||||
* 处理Sa-Token未登录异常
|
public Result<?> handleAuthorizationException(AccessDeniedException e){
|
||||||
*/
|
|
||||||
@ExceptionHandler(NotLoginException.class)
|
|
||||||
@ResponseStatus(HttpStatus.UNAUTHORIZED)
|
|
||||||
public Result<?> handleNotLoginException(NotLoginException e){
|
|
||||||
log.error("Sa-Token未登录异常: {}", e.getMessage());
|
|
||||||
return new Result(401, CommonConstant.TOKEN_IS_INVALID_MSG);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理Sa-Token无权限异常
|
|
||||||
*/
|
|
||||||
@ExceptionHandler(NotPermissionException.class)
|
|
||||||
public Result<?> handleNotPermissionException(NotPermissionException e){
|
|
||||||
log.error(e.getMessage(), e);
|
log.error(e.getMessage(), e);
|
||||||
return Result.noauth("没有权限,请联系管理员分配权限!");
|
return Result.noauth("没有权限,请联系管理员分配权限!");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理Sa-Token无角色异常
|
|
||||||
*/
|
|
||||||
@ExceptionHandler(NotRoleException.class)
|
|
||||||
public Result<?> handleNotRoleException(NotRoleException e){
|
|
||||||
log.error(e.getMessage(), e);
|
|
||||||
return Result.noauth("没有角色权限,请联系管理员分配角色!");
|
|
||||||
}
|
|
||||||
|
|
||||||
@ExceptionHandler(Exception.class)
|
@ExceptionHandler(Exception.class)
|
||||||
public Result<?> handleException(Exception e){
|
public Result<?> handleException(Exception e){
|
||||||
log.error(e.getMessage(), e);
|
log.error(e.getMessage(), e);
|
||||||
//update-begin---author:zyf ---date:20220411 for:处理Sentinel限流自定义异常
|
// 代码逻辑说明: 处理Sentinel限流自定义异常
|
||||||
Throwable throwable = e.getCause();
|
Throwable throwable = e.getCause();
|
||||||
SentinelErrorInfoEnum errorInfoEnum = SentinelErrorInfoEnum.getErrorByException(throwable);
|
SentinelErrorInfoEnum errorInfoEnum = SentinelErrorInfoEnum.getErrorByException(throwable);
|
||||||
if (ObjectUtil.isNotEmpty(errorInfoEnum)) {
|
if (ObjectUtil.isNotEmpty(errorInfoEnum)) {
|
||||||
return Result.error(errorInfoEnum.getError());
|
return Result.error(errorInfoEnum.getError());
|
||||||
}
|
}
|
||||||
//update-end---author:zyf ---date:20220411 for:处理Sentinel限流自定义异常
|
|
||||||
addSysLog(e);
|
addSysLog(e);
|
||||||
return Result.error("操作失败,"+e.getMessage());
|
return Result.error("操作失败,"+e.getMessage());
|
||||||
}
|
}
|
||||||
@ -247,7 +241,6 @@ public class JeecgBootExceptionHandler {
|
|||||||
return Result.error("校验失败,存在SQL注入风险!" + msg);
|
return Result.error("校验失败,存在SQL注入风险!" + msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
//update-begin---author:chenrui ---date:20240423 for:[QQYUN-8732]把错误的日志都抓取了 方便后续处理,单独弄个日志类型------------
|
|
||||||
/**
|
/**
|
||||||
* 添加异常新系统日志
|
* 添加异常新系统日志
|
||||||
* @param e 异常
|
* @param e 异常
|
||||||
@ -266,7 +259,6 @@ public class JeecgBootExceptionHandler {
|
|||||||
} catch (NullPointerException | BeansException ignored) {
|
} catch (NullPointerException | BeansException ignored) {
|
||||||
}
|
}
|
||||||
if (null != request) {
|
if (null != request) {
|
||||||
//update-begin---author:chenrui ---date:20250408 for:[QQYUN-11716]上传大图片失败没有精确提示------------
|
|
||||||
//请求的参数
|
//请求的参数
|
||||||
if (!isTooBigException(e)) {
|
if (!isTooBigException(e)) {
|
||||||
// 文件上传过大异常时不能获取参数,否则会报错
|
// 文件上传过大异常时不能获取参数,否则会报错
|
||||||
@ -275,7 +267,6 @@ public class JeecgBootExceptionHandler {
|
|||||||
log.setMethod(oConvertUtils.mapToString(request.getParameterMap()));
|
log.setMethod(oConvertUtils.mapToString(request.getParameterMap()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//update-end---author:chenrui ---date:20250408 for:[QQYUN-11716]上传大图片失败没有精确提示------------
|
|
||||||
// 请求地址
|
// 请求地址
|
||||||
log.setRequestUrl(request.getRequestURI());
|
log.setRequestUrl(request.getRequestURI());
|
||||||
//设置IP地址
|
//设置IP地址
|
||||||
@ -290,7 +281,7 @@ public class JeecgBootExceptionHandler {
|
|||||||
|
|
||||||
|
|
||||||
//获取登录用户信息
|
//获取登录用户信息
|
||||||
LoginUser sysUser = LoginUserUtils.getSessionUser();
|
LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();
|
||||||
if(sysUser!=null){
|
if(sysUser!=null){
|
||||||
log.setUserid(sysUser.getUsername());
|
log.setUserid(sysUser.getUsername());
|
||||||
log.setUsername(sysUser.getRealname());
|
log.setUsername(sysUser.getRealname());
|
||||||
@ -299,7 +290,6 @@ public class JeecgBootExceptionHandler {
|
|||||||
|
|
||||||
baseCommonService.addLog(log);
|
baseCommonService.addLog(log);
|
||||||
}
|
}
|
||||||
//update-end---author:chenrui ---date:20240423 for:[QQYUN-8732]把错误的日志都抓取了 方便后续处理,单独弄个日志类型------------
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 是否文件过大异常
|
* 是否文件过大异常
|
||||||
|
|||||||
@ -0,0 +1,28 @@
|
|||||||
|
package org.jeecg.common.exception;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author kezhijie@wuhandsj.com
|
||||||
|
* @date 2024/1/2 11:38
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class JeecgCaptchaException extends RuntimeException{
|
||||||
|
|
||||||
|
private Integer code;
|
||||||
|
|
||||||
|
private static final long serialVersionUID = -9093410345065209053L;
|
||||||
|
|
||||||
|
public JeecgCaptchaException(Integer code, String message) {
|
||||||
|
super(message);
|
||||||
|
this.code = code;
|
||||||
|
}
|
||||||
|
|
||||||
|
public JeecgCaptchaException(String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
public JeecgCaptchaException(Throwable cause) {
|
||||||
|
super(cause);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,17 +1,19 @@
|
|||||||
package org.jeecg.common.system.base.controller;
|
package org.jeecg.common.system.base.controller;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson.JSON;
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
import com.baomidou.mybatisplus.extension.service.IService;
|
import com.baomidou.mybatisplus.extension.service.IService;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.commons.beanutils.PropertyUtils;
|
import org.apache.commons.beanutils.PropertyUtils;
|
||||||
|
import org.apache.shiro.SecurityUtils;
|
||||||
import org.jeecg.common.api.vo.Result;
|
import org.jeecg.common.api.vo.Result;
|
||||||
import org.jeecg.common.system.query.QueryGenerator;
|
import org.jeecg.common.system.query.QueryGenerator;
|
||||||
import org.jeecg.common.system.vo.LoginUser;
|
import org.jeecg.common.system.vo.LoginUser;
|
||||||
import org.jeecg.common.util.LoginUserUtils;
|
|
||||||
import org.jeecg.common.util.oConvertUtils;
|
import org.jeecg.common.util.oConvertUtils;
|
||||||
import org.jeecg.config.JeecgBaseConfig;
|
import org.jeecg.config.JeecgBaseConfig;
|
||||||
|
import org.jeecg.config.security.utils.SecureUtil;
|
||||||
import org.jeecgframework.poi.excel.ExcelImportUtil;
|
import org.jeecgframework.poi.excel.ExcelImportUtil;
|
||||||
import org.jeecgframework.poi.excel.def.NormalExcelConstants;
|
import org.jeecgframework.poi.excel.def.NormalExcelConstants;
|
||||||
import org.jeecgframework.poi.excel.entity.ExportParams;
|
import org.jeecgframework.poi.excel.entity.ExportParams;
|
||||||
@ -20,6 +22,7 @@ import org.jeecgframework.poi.excel.entity.enmus.ExcelType;
|
|||||||
import org.jeecgframework.poi.excel.view.JeecgEntityExcelView;
|
import org.jeecgframework.poi.excel.view.JeecgEntityExcelView;
|
||||||
import org.jeecgframework.poi.handler.inter.IExcelExportServer;
|
import org.jeecgframework.poi.handler.inter.IExcelExportServer;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
import org.springframework.web.multipart.MultipartHttpServletRequest;
|
import org.springframework.web.multipart.MultipartHttpServletRequest;
|
||||||
import org.springframework.web.servlet.ModelAndView;
|
import org.springframework.web.servlet.ModelAndView;
|
||||||
@ -52,7 +55,7 @@ public class JeecgController<T, S extends IService<T>> {
|
|||||||
protected ModelAndView exportXls(HttpServletRequest request, T object, Class<T> clazz, String title) {
|
protected ModelAndView exportXls(HttpServletRequest request, T object, Class<T> clazz, String title) {
|
||||||
// Step.1 组装查询条件
|
// Step.1 组装查询条件
|
||||||
QueryWrapper<T> queryWrapper = QueryGenerator.initQueryWrapper(object, request.getParameterMap());
|
QueryWrapper<T> queryWrapper = QueryGenerator.initQueryWrapper(object, request.getParameterMap());
|
||||||
LoginUser sysUser = LoginUserUtils.getSessionUser();
|
LoginUser sysUser = SecureUtil.currentUser();
|
||||||
|
|
||||||
// 过滤选中数据
|
// 过滤选中数据
|
||||||
String selections = request.getParameter("selections");
|
String selections = request.getParameter("selections");
|
||||||
@ -68,12 +71,16 @@ public class JeecgController<T, S extends IService<T>> {
|
|||||||
//此处设置的filename无效 ,前端会重更新设置一下
|
//此处设置的filename无效 ,前端会重更新设置一下
|
||||||
mv.addObject(NormalExcelConstants.FILE_NAME, title);
|
mv.addObject(NormalExcelConstants.FILE_NAME, title);
|
||||||
mv.addObject(NormalExcelConstants.CLASS, clazz);
|
mv.addObject(NormalExcelConstants.CLASS, clazz);
|
||||||
//update-begin--Author:liusq Date:20210126 for:图片导出报错,ImageBasePath未设置--------------------
|
// 代码逻辑说明: 【QQYUN-13930】统一改成导出xlsx格式---
|
||||||
ExportParams exportParams=new ExportParams(title + "报表", "导出人:" + sysUser.getRealname(), title);
|
ExportParams exportParams=new ExportParams(title + "报表", "导出人:" + sysUser.getRealname(), title, ExcelType.XSSF);
|
||||||
exportParams.setImageBasePath(jeecgBaseConfig.getPath().getUpload());
|
exportParams.setImageBasePath(jeecgBaseConfig.getPath().getUpload());
|
||||||
//update-end--Author:liusq Date:20210126 for:图片导出报错,ImageBasePath未设置----------------------
|
|
||||||
mv.addObject(NormalExcelConstants.PARAMS,exportParams);
|
mv.addObject(NormalExcelConstants.PARAMS,exportParams);
|
||||||
mv.addObject(NormalExcelConstants.DATA_LIST, exportList);
|
mv.addObject(NormalExcelConstants.DATA_LIST, exportList);
|
||||||
|
// 代码逻辑说明: 【issues/9052】BasicTable列表页导出excel可以指定列---
|
||||||
|
String exportFields = request.getParameter(NormalExcelConstants.EXPORT_FIELDS);
|
||||||
|
if(oConvertUtils.isNotEmpty(exportFields)){
|
||||||
|
mv.addObject(NormalExcelConstants.EXPORT_FIELDS, exportFields);
|
||||||
|
}
|
||||||
return mv;
|
return mv;
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
@ -90,18 +97,16 @@ 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 = LoginUserUtils.getSessionUser();
|
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);
|
||||||
//update-begin-author:liusq---date:20220629--for: 多sheet导出根据选择导出写法调整 ---
|
|
||||||
// Step.3 过滤选中数据
|
// Step.3 过滤选中数据
|
||||||
String selections = request.getParameter("selections");
|
String selections = request.getParameter("selections");
|
||||||
if (oConvertUtils.isNotEmpty(selections)) {
|
if (oConvertUtils.isNotEmpty(selections)) {
|
||||||
List<String> selectionList = Arrays.asList(selections.split(","));
|
List<String> selectionList = Arrays.asList(selections.split(","));
|
||||||
queryWrapper.in("id",selectionList);
|
queryWrapper.in("id",selectionList);
|
||||||
}
|
}
|
||||||
//update-end-author:liusq---date:20220629--for: 多sheet导出根据选择导出写法调整 ---
|
|
||||||
// Step.4 多sheet处理
|
// Step.4 多sheet处理
|
||||||
List<Map<String, Object>> listMap = new ArrayList<Map<String, Object>>();
|
List<Map<String, Object>> listMap = new ArrayList<Map<String, Object>>();
|
||||||
for (int i = 1; i <=count ; i++) {
|
for (int i = 1; i <=count ; i++) {
|
||||||
@ -142,7 +147,7 @@ public class JeecgController<T, S extends IService<T>> {
|
|||||||
protected ModelAndView exportXlsForBigData(HttpServletRequest request, T object, Class<T> clazz, String title,Integer pageSize) {
|
protected ModelAndView exportXlsForBigData(HttpServletRequest request, T object, Class<T> clazz, String title,Integer pageSize) {
|
||||||
// 组装查询条件
|
// 组装查询条件
|
||||||
QueryWrapper<T> queryWrapper = QueryGenerator.initQueryWrapper(object, request.getParameterMap());
|
QueryWrapper<T> queryWrapper = QueryGenerator.initQueryWrapper(object, request.getParameterMap());
|
||||||
LoginUser sysUser = LoginUserUtils.getSessionUser();
|
LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();
|
||||||
// 计算分页数
|
// 计算分页数
|
||||||
double total = service.count();
|
double total = service.count();
|
||||||
int count = (int) Math.ceil(total / pageSize);
|
int count = (int) Math.ceil(total / pageSize);
|
||||||
@ -220,16 +225,15 @@ public class JeecgController<T, S extends IService<T>> {
|
|||||||
params.setNeedSave(true);
|
params.setNeedSave(true);
|
||||||
try {
|
try {
|
||||||
List<T> list = ExcelImportUtil.importExcel(file.getInputStream(), clazz, params);
|
List<T> list = ExcelImportUtil.importExcel(file.getInputStream(), clazz, params);
|
||||||
//update-begin-author:taoyan date:20190528 for:批量插入数据
|
// 代码逻辑说明: 批量插入数据
|
||||||
long start = System.currentTimeMillis();
|
long start = System.currentTimeMillis();
|
||||||
service.saveBatch(list);
|
service.saveBatch(list);
|
||||||
//400条 saveBatch消耗时间1592毫秒 循环插入消耗时间1947毫秒
|
//400条 saveBatch消耗时间1592毫秒 循环插入消耗时间1947毫秒
|
||||||
//1200条 saveBatch消耗时间3687毫秒 循环插入消耗时间5212毫秒
|
//1200条 saveBatch消耗时间3687毫秒 循环插入消耗时间5212毫秒
|
||||||
log.info("消耗时间" + (System.currentTimeMillis() - start) + "毫秒");
|
log.info("消耗时间" + (System.currentTimeMillis() - start) + "毫秒");
|
||||||
//update-end-author:taoyan date:20190528 for:批量插入数据
|
|
||||||
return Result.ok("文件导入成功!数据行数:" + list.size());
|
return Result.ok("文件导入成功!数据行数:" + list.size());
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
//update-begin-author:taoyan date:20211124 for: 导入数据重复增加提示
|
// 代码逻辑说明: 导入数据重复增加提示
|
||||||
String msg = e.getMessage();
|
String msg = e.getMessage();
|
||||||
log.error(msg, e);
|
log.error(msg, e);
|
||||||
if(msg!=null && msg.indexOf("Duplicate entry")>=0){
|
if(msg!=null && msg.indexOf("Duplicate entry")>=0){
|
||||||
@ -237,7 +241,6 @@ public class JeecgController<T, S extends IService<T>> {
|
|||||||
}else{
|
}else{
|
||||||
return Result.error("文件导入失败:" + e.getMessage());
|
return Result.error("文件导入失败:" + e.getMessage());
|
||||||
}
|
}
|
||||||
//update-end-author:taoyan date:20211124 for: 导入数据重复增加提示
|
|
||||||
} finally {
|
} finally {
|
||||||
try {
|
try {
|
||||||
file.getInputStream().close();
|
file.getInputStream().close();
|
||||||
|
|||||||
@ -13,7 +13,6 @@ import io.swagger.v3.oas.annotations.media.Schema;
|
|||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
import lombok.experimental.Accessors;
|
import lombok.experimental.Accessors;
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @Description: Entity基类
|
* @Description: Entity基类
|
||||||
|
|||||||
@ -97,7 +97,6 @@ public class QueryGenerator {
|
|||||||
return queryWrapper;
|
return queryWrapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
//update-begin---author:chenrui ---date:20240527 for:[TV360X-378]增加自定义字段查询规则功能------------
|
|
||||||
/**
|
/**
|
||||||
* 获取查询条件构造器QueryWrapper实例 通用查询条件已被封装完成
|
* 获取查询条件构造器QueryWrapper实例 通用查询条件已被封装完成
|
||||||
* @param searchObj 查询实体
|
* @param searchObj 查询实体
|
||||||
@ -112,7 +111,6 @@ public class QueryGenerator {
|
|||||||
log.debug("---查询条件构造器初始化完成,耗时:"+(System.currentTimeMillis()-start)+"毫秒----");
|
log.debug("---查询条件构造器初始化完成,耗时:"+(System.currentTimeMillis()-start)+"毫秒----");
|
||||||
return queryWrapper;
|
return queryWrapper;
|
||||||
}
|
}
|
||||||
//update-end---author:chenrui ---date:20240527 for:[TV360X-378]增加自定义字段查询规则功能------------
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 组装Mybatis Plus 查询条件
|
* 组装Mybatis Plus 查询条件
|
||||||
@ -142,7 +140,6 @@ public class QueryGenerator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
String name, type, column;
|
String name, type, column;
|
||||||
// update-begin--Author:taoyan Date:20200923 for:issues/1671 如果字段加注解了@TableField(exist = false),不走DB查询-------
|
|
||||||
//定义实体字段和数据库字段名称的映射 高级查询中 只能获取实体字段 如果设置TableField注解 那么查询条件会出问题
|
//定义实体字段和数据库字段名称的映射 高级查询中 只能获取实体字段 如果设置TableField注解 那么查询条件会出问题
|
||||||
Map<String,String> fieldColumnMap = new HashMap<>(5);
|
Map<String,String> fieldColumnMap = new HashMap<>(5);
|
||||||
for (int i = 0; i < origDescriptors.length; i++) {
|
for (int i = 0; i < origDescriptors.length; i++) {
|
||||||
@ -188,7 +185,7 @@ public class QueryGenerator {
|
|||||||
queryWrapper.and(j -> j.like(field,vals[0]));
|
queryWrapper.and(j -> j.like(field,vals[0]));
|
||||||
}
|
}
|
||||||
}else {
|
}else {
|
||||||
//update-begin---author:chenrui ---date:20240527 for:[TV360X-378]增加自定义字段查询规则功能------------
|
// 代码逻辑说明: [TV360X-378]增加自定义字段查询规则功能------------
|
||||||
QueryRuleEnum rule;
|
QueryRuleEnum rule;
|
||||||
if(null != customRuleMap && customRuleMap.containsKey(name)) {
|
if(null != customRuleMap && customRuleMap.containsKey(name)) {
|
||||||
// 有自定义规则,使用自定义规则.
|
// 有自定义规则,使用自定义规则.
|
||||||
@ -197,7 +194,6 @@ public class QueryGenerator {
|
|||||||
//根据参数值带什么关键字符串判断走什么类型的查询
|
//根据参数值带什么关键字符串判断走什么类型的查询
|
||||||
rule = convert2Rule(value);
|
rule = convert2Rule(value);
|
||||||
}
|
}
|
||||||
//update-end---author:chenrui ---date:20240527 for:[TV360X-378]增加自定义字段查询规则功能------------
|
|
||||||
value = replaceValue(rule,value);
|
value = replaceValue(rule,value);
|
||||||
// add -begin 添加判断为字符串时设为全模糊查询
|
// add -begin 添加判断为字符串时设为全模糊查询
|
||||||
//if( (rule==null || QueryRuleEnum.EQ.equals(rule)) && "class java.lang.String".equals(type)) {
|
//if( (rule==null || QueryRuleEnum.EQ.equals(rule)) && "class java.lang.String".equals(type)) {
|
||||||
@ -217,7 +213,6 @@ public class QueryGenerator {
|
|||||||
|
|
||||||
//高级查询
|
//高级查询
|
||||||
doSuperQuery(queryWrapper, parameterMap, fieldColumnMap);
|
doSuperQuery(queryWrapper, parameterMap, fieldColumnMap);
|
||||||
// update-end--Author:taoyan Date:20200923 for:issues/1671 如果字段加注解了@TableField(exist = false),不走DB查询-------
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -260,16 +255,16 @@ public class QueryGenerator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if(oConvertUtils.isNotEmpty(column)){
|
if(oConvertUtils.isNotEmpty(column)){
|
||||||
log.info("单字段排序规则>> column:" + column + ",排序方式:" + order);
|
log.debug("单字段排序规则>> column:" + column + ",排序方式:" + order);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1. 列表多字段排序优先
|
// 1. 列表多字段排序优先
|
||||||
if(parameterMap!=null&& parameterMap.containsKey("sortInfoString")) {
|
if(parameterMap!=null&& parameterMap.containsKey("sortInfoString")) {
|
||||||
// 多字段排序
|
// 多字段排序
|
||||||
String sortInfoString = parameterMap.get("sortInfoString")[0];
|
String sortInfoString = parameterMap.get("sortInfoString")[0];
|
||||||
log.info("多字段排序规则>> sortInfoString:" + sortInfoString);
|
log.debug("多字段排序规则>> sortInfoString:" + sortInfoString);
|
||||||
List<OrderItem> orderItemList = SqlConcatUtil.getQueryConditionOrders(column, order, sortInfoString);
|
List<OrderItem> orderItemList = SqlConcatUtil.getQueryConditionOrders(column, order, sortInfoString);
|
||||||
log.info(orderItemList.toString());
|
log.debug(orderItemList.toString());
|
||||||
if (orderItemList != null && !orderItemList.isEmpty()) {
|
if (orderItemList != null && !orderItemList.isEmpty()) {
|
||||||
for (OrderItem item : orderItemList) {
|
for (OrderItem item : orderItemList) {
|
||||||
// 一、获取排序数据库字段
|
// 一、获取排序数据库字段
|
||||||
@ -321,13 +316,11 @@ public class QueryGenerator {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
//update-begin-author:scott date:2022-11-07 for:避免用户自定义表无默认字段{创建时间},导致排序报错
|
|
||||||
//TODO 避免用户自定义表无默认字段创建时间,导致排序报错
|
//TODO 避免用户自定义表无默认字段创建时间,导致排序报错
|
||||||
if(DataBaseConstant.CREATE_TIME.equals(column) && !fieldColumnMap.containsKey(DataBaseConstant.CREATE_TIME)){
|
if(DataBaseConstant.CREATE_TIME.equals(column) && !fieldColumnMap.containsKey(DataBaseConstant.CREATE_TIME)){
|
||||||
column = "id";
|
column = "id";
|
||||||
log.warn("检测到实体里没有字段createTime,改成采用ID排序!");
|
log.warn("检测到实体里没有字段createTime,改成采用ID排序!");
|
||||||
}
|
}
|
||||||
//update-end-author:scott date:2022-11-07 for:避免用户自定义表无默认字段{创建时间},导致排序报错
|
|
||||||
|
|
||||||
if (oConvertUtils.isNotEmpty(column) && oConvertUtils.isNotEmpty(order)) {
|
if (oConvertUtils.isNotEmpty(column) && oConvertUtils.isNotEmpty(order)) {
|
||||||
//字典字段,去掉字典翻译文本后缀
|
//字典字段,去掉字典翻译文本后缀
|
||||||
@ -335,15 +328,12 @@ public class QueryGenerator {
|
|||||||
column = column.substring(0, column.lastIndexOf(CommonConstant.DICT_TEXT_SUFFIX));
|
column = column.substring(0, column.lastIndexOf(CommonConstant.DICT_TEXT_SUFFIX));
|
||||||
}
|
}
|
||||||
|
|
||||||
//update-begin-author:taoyan date:2022-5-16 for: issues/3676 获取系统用户列表时,使用SQL注入生效
|
|
||||||
//判断column是不是当前实体的
|
//判断column是不是当前实体的
|
||||||
log.debug("当前字段有:"+ allFields);
|
log.debug("当前字段有:"+ allFields);
|
||||||
if (!allColumnExist(column, allFields)) {
|
if (!allColumnExist(column, allFields)) {
|
||||||
throw new JeecgBootException("请注意,将要排序的列字段不存在:" + column);
|
throw new JeecgBootException("请注意,将要排序的列字段不存在:" + column);
|
||||||
}
|
}
|
||||||
//update-end-author:taoyan date:2022-5-16 for: issues/3676 获取系统用户列表时,使用SQL注入生效
|
|
||||||
|
|
||||||
//update-begin-author:scott date:2022-10-10 for:【jeecg-boot/issues/I5FJU6】doMultiFieldsOrder() 多字段排序方法存在问题
|
|
||||||
//多字段排序方法没有读取 MybatisPlus 注解 @TableField 里 value 的值
|
//多字段排序方法没有读取 MybatisPlus 注解 @TableField 里 value 的值
|
||||||
if (column.contains(",")) {
|
if (column.contains(",")) {
|
||||||
List<String> columnList = Arrays.asList(column.split(","));
|
List<String> columnList = Arrays.asList(column.split(","));
|
||||||
@ -354,12 +344,10 @@ public class QueryGenerator {
|
|||||||
}else{
|
}else{
|
||||||
column = fieldColumnMap.get(column);
|
column = fieldColumnMap.get(column);
|
||||||
}
|
}
|
||||||
//update-end-author:scott date:2022-10-10 for:【jeecg-boot/issues/I5FJU6】doMultiFieldsOrder() 多字段排序方法存在问题
|
|
||||||
|
|
||||||
//SQL注入check
|
//SQL注入check
|
||||||
SqlInjectionUtil.filterContentMulti(column);
|
SqlInjectionUtil.filterContentMulti(column);
|
||||||
|
|
||||||
//update-begin--Author:scott Date:20210531 for:36 多条件排序无效问题修正-------
|
|
||||||
// 排序规则修改
|
// 排序规则修改
|
||||||
// 将现有排序 _ 前端传递排序条件{....,column: 'column1,column2',order: 'desc'} 翻译成sql "column1,column2 desc"
|
// 将现有排序 _ 前端传递排序条件{....,column: 'column1,column2',order: 'desc'} 翻译成sql "column1,column2 desc"
|
||||||
// 修改为 _ 前端传递排序条件{....,column: 'column1,column2',order: 'desc'} 翻译成sql "column1 desc,column2 desc"
|
// 修改为 _ 前端传递排序条件{....,column: 'column1,column2',order: 'desc'} 翻译成sql "column1 desc,column2 desc"
|
||||||
@ -368,11 +356,9 @@ public class QueryGenerator {
|
|||||||
} else {
|
} else {
|
||||||
queryWrapper.orderByDesc(SqlInjectionUtil.getSqlInjectSortFields(column.split(",")));
|
queryWrapper.orderByDesc(SqlInjectionUtil.getSqlInjectSortFields(column.split(",")));
|
||||||
}
|
}
|
||||||
//update-end--Author:scott Date:20210531 for:36 多条件排序无效问题修正-------
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//update-begin-author:taoyan date:2022-5-23 for: issues/3676 获取系统用户列表时,使用SQL注入生效
|
|
||||||
/**
|
/**
|
||||||
* 多字段排序 判断所传字段是否存在
|
* 多字段排序 判断所传字段是否存在
|
||||||
* @return
|
* @return
|
||||||
@ -392,7 +378,6 @@ public class QueryGenerator {
|
|||||||
}
|
}
|
||||||
return exist;
|
return exist;
|
||||||
}
|
}
|
||||||
//update-end-author:taoyan date:2022-5-23 for: issues/3676 获取系统用户列表时,使用SQL注入生效
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 高级查询
|
* 高级查询
|
||||||
@ -405,14 +390,14 @@ public class QueryGenerator {
|
|||||||
String superQueryParams = parameterMap.get(SUPER_QUERY_PARAMS)[0];
|
String superQueryParams = parameterMap.get(SUPER_QUERY_PARAMS)[0];
|
||||||
String superQueryMatchType = parameterMap.get(SUPER_QUERY_MATCH_TYPE) != null ? parameterMap.get(SUPER_QUERY_MATCH_TYPE)[0] : MatchTypeEnum.AND.getValue();
|
String superQueryMatchType = parameterMap.get(SUPER_QUERY_MATCH_TYPE) != null ? parameterMap.get(SUPER_QUERY_MATCH_TYPE)[0] : MatchTypeEnum.AND.getValue();
|
||||||
MatchTypeEnum matchType = MatchTypeEnum.getByValue(superQueryMatchType);
|
MatchTypeEnum matchType = MatchTypeEnum.getByValue(superQueryMatchType);
|
||||||
// update-begin--Author:sunjianlei Date:20200325 for:高级查询的条件要用括号括起来,防止和用户的其他条件冲突 -------
|
// 代码逻辑说明: 高级查询的条件要用括号括起来,防止和用户的其他条件冲突 -------
|
||||||
try {
|
try {
|
||||||
superQueryParams = URLDecoder.decode(superQueryParams, "UTF-8");
|
superQueryParams = URLDecoder.decode(superQueryParams, "UTF-8");
|
||||||
List<QueryCondition> conditions = JSON.parseArray(superQueryParams, QueryCondition.class);
|
List<QueryCondition> conditions = JSON.parseArray(superQueryParams, QueryCondition.class);
|
||||||
if (conditions == null || conditions.size() == 0) {
|
if (conditions == null || conditions.size() == 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// update-begin-author:sunjianlei date:20220119 for: 【JTC-573】 过滤空条件查询,防止 sql 拼接多余的 and
|
// 代码逻辑说明: 【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())
|
||||||
@ -423,7 +408,6 @@ public class QueryGenerator {
|
|||||||
if (filterConditions.size() == 0) {
|
if (filterConditions.size() == 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// update-end-author:sunjianlei date:20220119 for: 【JTC-573】 过滤空条件查询,防止 sql 拼接多余的 and
|
|
||||||
log.debug("---高级查询参数-->" + filterConditions);
|
log.debug("---高级查询参数-->" + filterConditions);
|
||||||
|
|
||||||
queryWrapper.and(andWrapper -> {
|
queryWrapper.and(andWrapper -> {
|
||||||
@ -438,14 +422,14 @@ public class QueryGenerator {
|
|||||||
|
|
||||||
log.debug("SuperQuery ==> " + rule.toString());
|
log.debug("SuperQuery ==> " + rule.toString());
|
||||||
|
|
||||||
//update-begin-author:taoyan date:20201228 for: 【高级查询】 oracle 日期等于查询报错
|
// 代码逻辑说明: 【高级查询】 oracle 日期等于查询报错
|
||||||
Object queryValue = rule.getVal();
|
Object queryValue = rule.getVal();
|
||||||
if("date".equals(rule.getType())){
|
if("date".equals(rule.getType())){
|
||||||
queryValue = DateUtils.str2Date(rule.getVal(),DateUtils.date_sdf.get());
|
queryValue = DateUtils.str2Date(rule.getVal(),DateUtils.date_sdf.get());
|
||||||
}else if("datetime".equals(rule.getType())){
|
}else if("datetime".equals(rule.getType())){
|
||||||
queryValue = DateUtils.str2Date(rule.getVal(), DateUtils.datetimeFormat.get());
|
queryValue = DateUtils.str2Date(rule.getVal(), DateUtils.datetimeFormat.get());
|
||||||
}
|
}
|
||||||
// update-begin--author:sunjianlei date:20210702 for:【/issues/I3VR8E】高级查询没有类型转换,查询参数都是字符串类型 ----
|
// 代码逻辑说明: 【/issues/I3VR8E】高级查询没有类型转换,查询参数都是字符串类型 ----
|
||||||
String dbType = rule.getDbType();
|
String dbType = rule.getDbType();
|
||||||
if (oConvertUtils.isNotEmpty(dbType)) {
|
if (oConvertUtils.isNotEmpty(dbType)) {
|
||||||
try {
|
try {
|
||||||
@ -478,9 +462,8 @@ public class QueryGenerator {
|
|||||||
log.error("高级查询值转换失败:", e);
|
log.error("高级查询值转换失败:", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// update-begin--author:sunjianlei date:20210702 for:【/issues/I3VR8E】高级查询没有类型转换,查询参数都是字符串类型 ----
|
// 代码逻辑说明: 【/issues/I3VR8E】高级查询没有类型转换,查询参数都是字符串类型 ----
|
||||||
addEasyQuery(andWrapper, fieldColumnMap.get(rule.getField()), QueryRuleEnum.getByValue(rule.getRule()), queryValue);
|
addEasyQuery(andWrapper, fieldColumnMap.get(rule.getField()), QueryRuleEnum.getByValue(rule.getRule()), queryValue);
|
||||||
//update-end-author:taoyan date:20201228 for: 【高级查询】 oracle 日期等于查询报错
|
|
||||||
|
|
||||||
// 如果拼接方式是OR,就拼接OR
|
// 如果拼接方式是OR,就拼接OR
|
||||||
if (MatchTypeEnum.OR == matchType && i < (filterConditions.size() - 1)) {
|
if (MatchTypeEnum.OR == matchType && i < (filterConditions.size() - 1)) {
|
||||||
@ -496,7 +479,6 @@ public class QueryGenerator {
|
|||||||
log.error("--高级查询拼接失败:" + e.getMessage());
|
log.error("--高级查询拼接失败:" + e.getMessage());
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
// update-end--Author:sunjianlei Date:20200325 for:高级查询的条件要用括号括起来,防止和用户的其他条件冲突 -------
|
|
||||||
}
|
}
|
||||||
//log.info(" superQuery getCustomSqlSegment: "+ queryWrapper.getCustomSqlSegment());
|
//log.info(" superQuery getCustomSqlSegment: "+ queryWrapper.getCustomSqlSegment());
|
||||||
}
|
}
|
||||||
@ -508,7 +490,7 @@ public class QueryGenerator {
|
|||||||
*/
|
*/
|
||||||
public static QueryRuleEnum convert2Rule(Object value) {
|
public static QueryRuleEnum convert2Rule(Object value) {
|
||||||
// 避免空数据
|
// 避免空数据
|
||||||
// update-begin-author:taoyan date:20210629 for: 查询条件输入空格导致return null后续判断导致抛出null异常
|
// 代码逻辑说明: 查询条件输入空格导致return null后续判断导致抛出null异常
|
||||||
if (value == null) {
|
if (value == null) {
|
||||||
return QueryRuleEnum.EQ;
|
return QueryRuleEnum.EQ;
|
||||||
}
|
}
|
||||||
@ -516,10 +498,8 @@ public class QueryGenerator {
|
|||||||
if (val.length() == 0) {
|
if (val.length() == 0) {
|
||||||
return QueryRuleEnum.EQ;
|
return QueryRuleEnum.EQ;
|
||||||
}
|
}
|
||||||
// update-end-author:taoyan date:20210629 for: 查询条件输入空格导致return null后续判断导致抛出null异常
|
|
||||||
QueryRuleEnum rule =null;
|
QueryRuleEnum rule =null;
|
||||||
|
|
||||||
//update-begin--Author:scott Date:20190724 for:initQueryWrapper组装sql查询条件错误 #284-------------------
|
|
||||||
//TODO 此处规则,只适用于 le lt ge gt
|
//TODO 此处规则,只适用于 le lt ge gt
|
||||||
// step 2 .>= =<
|
// step 2 .>= =<
|
||||||
int length2 = 2;
|
int length2 = 2;
|
||||||
@ -535,14 +515,12 @@ public class QueryGenerator {
|
|||||||
rule = QueryRuleEnum.getByValue(val.substring(0, 1));
|
rule = QueryRuleEnum.getByValue(val.substring(0, 1));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//update-end--Author:scott Date:20190724 for:initQueryWrapper组装sql查询条件错误 #284---------------------
|
|
||||||
|
|
||||||
// step 3 like
|
// step 3 like
|
||||||
//update-begin-author:taoyan for: /issues/3382 默认带*就走模糊,但是如果只有一个*,那么走等于查询
|
// 代码逻辑说明: /issues/3382 默认带*就走模糊,但是如果只有一个*,那么走等于查询
|
||||||
if(rule == null && val.equals(STAR)){
|
if(rule == null && val.equals(STAR)){
|
||||||
rule = QueryRuleEnum.EQ;
|
rule = QueryRuleEnum.EQ;
|
||||||
}
|
}
|
||||||
//update-end-author:taoyan for: /issues/3382 默认带*就走模糊,但是如果只有一个*,那么走等于查询
|
|
||||||
if (rule == null && val.contains(STAR)) {
|
if (rule == null && val.contains(STAR)) {
|
||||||
if (val.startsWith(STAR) && val.endsWith(STAR)) {
|
if (val.startsWith(STAR) && val.endsWith(STAR)) {
|
||||||
rule = QueryRuleEnum.LIKE;
|
rule = QueryRuleEnum.LIKE;
|
||||||
@ -567,12 +545,10 @@ public class QueryGenerator {
|
|||||||
rule = QueryRuleEnum.EQ_WITH_ADD;
|
rule = QueryRuleEnum.EQ_WITH_ADD;
|
||||||
}
|
}
|
||||||
|
|
||||||
//update-begin--Author:taoyan Date:20201229 for:initQueryWrapper组装sql查询条件错误 #284---------------------
|
|
||||||
//特殊处理:Oracle的表达式to_date('xxx','yyyy-MM-dd')含有逗号,会被识别为in查询,转为等于查询
|
//特殊处理:Oracle的表达式to_date('xxx','yyyy-MM-dd')含有逗号,会被识别为in查询,转为等于查询
|
||||||
if(rule == QueryRuleEnum.IN && val.indexOf(YYYY_MM_DD)>=0 && val.indexOf(TO_DATE)>=0){
|
if(rule == QueryRuleEnum.IN && val.indexOf(YYYY_MM_DD)>=0 && val.indexOf(TO_DATE)>=0){
|
||||||
rule = QueryRuleEnum.EQ;
|
rule = QueryRuleEnum.EQ;
|
||||||
}
|
}
|
||||||
//update-end--Author:taoyan Date:20201229 for:initQueryWrapper组装sql查询条件错误 #284---------------------
|
|
||||||
|
|
||||||
return rule != null ? rule : QueryRuleEnum.EQ;
|
return rule != null ? rule : QueryRuleEnum.EQ;
|
||||||
}
|
}
|
||||||
@ -592,11 +568,10 @@ public class QueryGenerator {
|
|||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
String val = (value + "").toString().trim();
|
String val = (value + "").toString().trim();
|
||||||
//update-begin-author:taoyan date:20220302 for: 查询条件的值为等号(=)bug #3443
|
// 代码逻辑说明: 查询条件的值为等号(=)bug #3443
|
||||||
if(QueryRuleEnum.EQ.getValue().equals(val)){
|
if(QueryRuleEnum.EQ.getValue().equals(val)){
|
||||||
return val;
|
return val;
|
||||||
}
|
}
|
||||||
//update-end-author:taoyan date:20220302 for: 查询条件的值为等号(=)bug #3443
|
|
||||||
if (rule == QueryRuleEnum.LIKE) {
|
if (rule == QueryRuleEnum.LIKE) {
|
||||||
value = val.substring(1, val.length() - 1);
|
value = val.substring(1, val.length() - 1);
|
||||||
//mysql 模糊查询之特殊字符下划线 (_、\)
|
//mysql 模糊查询之特殊字符下划线 (_、\)
|
||||||
@ -614,21 +589,19 @@ public class QueryGenerator {
|
|||||||
} else if (rule == QueryRuleEnum.EQ_WITH_ADD) {
|
} else if (rule == QueryRuleEnum.EQ_WITH_ADD) {
|
||||||
value = val.replaceAll("\\+\\+", COMMA);
|
value = val.replaceAll("\\+\\+", COMMA);
|
||||||
}else {
|
}else {
|
||||||
//update-begin--Author:scott Date:20190724 for:initQueryWrapper组装sql查询条件错误 #284-------------------
|
// 代码逻辑说明: initQueryWrapper组装sql查询条件错误 #284-------------------
|
||||||
if(val.startsWith(rule.getValue())){
|
if(val.startsWith(rule.getValue())){
|
||||||
//TODO 此处逻辑应该注释掉-> 如果查询内容中带有查询匹配规则符号,就会被截取的(比如:>=您好)
|
//TODO 此处逻辑应该注释掉-> 如果查询内容中带有查询匹配规则符号,就会被截取的(比如:>=您好)
|
||||||
value = val.replaceFirst(rule.getValue(),"");
|
value = val.replaceFirst(rule.getValue(),"");
|
||||||
}else if(val.startsWith(rule.getCondition()+QUERY_SEPARATE_KEYWORD)){
|
}else if(val.startsWith(rule.getCondition()+QUERY_SEPARATE_KEYWORD)){
|
||||||
value = val.replaceFirst(rule.getCondition()+QUERY_SEPARATE_KEYWORD,"").trim();
|
value = val.replaceFirst(rule.getCondition()+QUERY_SEPARATE_KEYWORD,"").trim();
|
||||||
}
|
}
|
||||||
//update-end--Author:scott Date:20190724 for:initQueryWrapper组装sql查询条件错误 #284-------------------
|
|
||||||
}
|
}
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void addQueryByRule(QueryWrapper<?> queryWrapper,String name,String type,String value,QueryRuleEnum rule) throws ParseException {
|
private static void addQueryByRule(QueryWrapper<?> queryWrapper,String name,String type,String value,QueryRuleEnum rule) throws ParseException {
|
||||||
if(oConvertUtils.isNotEmpty(value)) {
|
if(oConvertUtils.isNotEmpty(value)) {
|
||||||
//update-begin--Author:sunjianlei Date:20220104 for:【JTC-409】修复逗号分割情况下没有转换类型,导致类型严格的数据库查询报错 -------------------
|
|
||||||
// 针对数字类型字段,多值查询
|
// 针对数字类型字段,多值查询
|
||||||
if(value.contains(COMMA)){
|
if(value.contains(COMMA)){
|
||||||
Object[] temp = Arrays.stream(value.split(COMMA)).map(v -> {
|
Object[] temp = Arrays.stream(value.split(COMMA)).map(v -> {
|
||||||
@ -644,7 +617,6 @@ public class QueryGenerator {
|
|||||||
}
|
}
|
||||||
Object temp = QueryGenerator.parseByType(value, type, rule);
|
Object temp = QueryGenerator.parseByType(value, type, rule);
|
||||||
addEasyQuery(queryWrapper, name, rule, temp);
|
addEasyQuery(queryWrapper, name, rule, temp);
|
||||||
//update-end--Author:sunjianlei Date:20220104 for:【JTC-409】修复逗号分割情况下没有转换类型,导致类型严格的数据库查询报错 -------------------
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -759,13 +731,12 @@ public class QueryGenerator {
|
|||||||
}else if(value instanceof String[]) {
|
}else if(value instanceof String[]) {
|
||||||
queryWrapper.in(name, (Object[]) value);
|
queryWrapper.in(name, (Object[]) value);
|
||||||
}
|
}
|
||||||
//update-begin-author:taoyan date:20200909 for:【bug】in 类型多值查询 不适配postgresql #1671
|
// 代码逻辑说明: 【bug】in 类型多值查询 不适配postgresql #1671
|
||||||
else if(value.getClass().isArray()) {
|
else if(value.getClass().isArray()) {
|
||||||
queryWrapper.in(name, (Object[])value);
|
queryWrapper.in(name, (Object[])value);
|
||||||
}else {
|
}else {
|
||||||
queryWrapper.in(name, value);
|
queryWrapper.in(name, value);
|
||||||
}
|
}
|
||||||
//update-end-author:taoyan date:20200909 for:【bug】in 类型多值查询 不适配postgresql #1671
|
|
||||||
break;
|
break;
|
||||||
case LIKE:
|
case LIKE:
|
||||||
queryWrapper.like(name, value);
|
queryWrapper.like(name, value);
|
||||||
@ -782,7 +753,7 @@ public class QueryGenerator {
|
|||||||
case NOT_RIGHT_LIKE:
|
case NOT_RIGHT_LIKE:
|
||||||
queryWrapper.notLikeRight(name, value);
|
queryWrapper.notLikeRight(name, value);
|
||||||
break;
|
break;
|
||||||
//update-begin---author:chenrui ---date:20240527 for:[TV360X-378]下拉多框根据条件查询不出来:增加自定义字段查询规则功能------------
|
// 代码逻辑说明: [TV360X-378]下拉多框根据条件查询不出来:增加自定义字段查询规则功能------------
|
||||||
case LIKE_WITH_OR:
|
case LIKE_WITH_OR:
|
||||||
final String nameFinal = name;
|
final String nameFinal = name;
|
||||||
Object[] vals;
|
Object[] vals;
|
||||||
@ -791,7 +762,7 @@ public class QueryGenerator {
|
|||||||
} else if (value instanceof String[]) {
|
} else if (value instanceof String[]) {
|
||||||
vals = (Object[]) value;
|
vals = (Object[]) value;
|
||||||
}
|
}
|
||||||
//update-begin-author:taoyan date:20200909 for:【bug】in 类型多值查询 不适配postgresql #1671
|
// 代码逻辑说明: 【bug】in 类型多值查询 不适配postgresql #1671
|
||||||
else if (value.getClass().isArray()) {
|
else if (value.getClass().isArray()) {
|
||||||
vals = (Object[]) value;
|
vals = (Object[]) value;
|
||||||
} else {
|
} else {
|
||||||
@ -806,7 +777,6 @@ public class QueryGenerator {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
//update-end---author:chenrui ---date:20240527 for:[TV360X-378]下拉多框根据条件查询不出来:增加自定义字段查询规则功能------------
|
|
||||||
default:
|
default:
|
||||||
log.info("--查询规则未匹配到---");
|
log.info("--查询规则未匹配到---");
|
||||||
break;
|
break;
|
||||||
@ -820,10 +790,8 @@ public class QueryGenerator {
|
|||||||
private static boolean judgedIsUselessField(String name) {
|
private static boolean judgedIsUselessField(String name) {
|
||||||
return "class".equals(name) || "ids".equals(name)
|
return "class".equals(name) || "ids".equals(name)
|
||||||
|| "page".equals(name) || "rows".equals(name)
|
|| "page".equals(name) || "rows".equals(name)
|
||||||
//// update-begin--author:sunjianlei date:20240808 for:【TV360X-2009】取消过滤 sort、order 字段,防止前端排序报错 ------
|
|
||||||
//// https://github.com/jeecgboot/JeecgBoot/issues/6937
|
//// https://github.com/jeecgboot/JeecgBoot/issues/6937
|
||||||
// || "sort".equals(name) || "order".equals(name)
|
// || "sort".equals(name) || "order".equals(name)
|
||||||
//// update-end----author:sunjianlei date:20240808 for:【TV360X-2009】取消过滤 sort、order 字段,防止前端排序报错 ------
|
|
||||||
;
|
;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -836,13 +804,12 @@ public class QueryGenerator {
|
|||||||
public static Map<String, SysPermissionDataRuleModel> getRuleMap() {
|
public static Map<String, SysPermissionDataRuleModel> getRuleMap() {
|
||||||
Map<String, SysPermissionDataRuleModel> ruleMap = new HashMap<>(5);
|
Map<String, SysPermissionDataRuleModel> ruleMap = new HashMap<>(5);
|
||||||
List<SysPermissionDataRuleModel> list = null;
|
List<SysPermissionDataRuleModel> list = null;
|
||||||
//update-begin-author:taoyan date:2023-6-1 for:QQYUN-5441 【简流】获取多个用户/部门/角色 设置部门查询 报错
|
// 代码逻辑说明: QQYUN-5441 【简流】获取多个用户/部门/角色 设置部门查询 报错
|
||||||
try {
|
try {
|
||||||
list = JeecgDataAutorUtils.loadDataSearchConditon();
|
list = JeecgDataAutorUtils.loadDataSearchConditon();
|
||||||
}catch (Exception e){
|
}catch (Exception e){
|
||||||
log.error("根据request对象获取权限数据失败,可能是定时任务中执行的。", e);
|
log.error("根据request对象获取权限数据失败,可能是定时任务中执行的。", e);
|
||||||
}
|
}
|
||||||
//update-end-author:taoyan date:2023-6-1 for:QQYUN-5441 【简流】获取多个用户/部门/角色 设置部门查询 报错
|
|
||||||
if(list != null&&list.size()>0){
|
if(list != null&&list.size()>0){
|
||||||
if(list.get(0)==null){
|
if(list.get(0)==null){
|
||||||
return ruleMap;
|
return ruleMap;
|
||||||
@ -879,9 +846,8 @@ public class QueryGenerator {
|
|||||||
addEasyQuery(queryWrapper, name, rule, DateUtils.str2Date(dateStr,DateUtils.datetimeFormat.get()));
|
addEasyQuery(queryWrapper, name, rule, DateUtils.str2Date(dateStr,DateUtils.datetimeFormat.get()));
|
||||||
}
|
}
|
||||||
}else {
|
}else {
|
||||||
//update-begin---author:chenrui ---date:20241125 for:[issues/7481]多租户模式下 数据权限使用变量:#{tenant_id} 报错------------
|
// 代码逻辑说明: [issues/7481]多租户模式下 数据权限使用变量:#{tenant_id} 报错------------
|
||||||
addEasyQuery(queryWrapper, name, rule, NumberUtils.parseNumber(converRuleValue(dataRule.getRuleValue()), propertyType));
|
addEasyQuery(queryWrapper, name, rule, NumberUtils.parseNumber(converRuleValue(dataRule.getRuleValue()), propertyType));
|
||||||
//update-end---author:chenrui ---date:20241125 for:[issues/7481]多租户模式下 数据权限使用变量:#{tenant_id} 报错------------
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -935,9 +901,8 @@ public class QueryGenerator {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
Set<String> varParams = new HashSet<String>();
|
Set<String> varParams = new HashSet<String>();
|
||||||
//update-begin---author:chenrui ---date:20250108 for:[QQYUN-10785]数据权限,查看自己拥有部门的权限中存在问题 #7288------------
|
// 代码逻辑说明: [QQYUN-10785]数据权限,查看自己拥有部门的权限中存在问题 #7288------------
|
||||||
String regex = "#\\{\\[*\\w+]*}";
|
String regex = "#\\{\\[*\\w+]*}";
|
||||||
//update-end---author:chenrui ---date:20250108 for:[QQYUN-10785]数据权限,查看自己拥有部门的权限中存在问题 #7288------------
|
|
||||||
|
|
||||||
Pattern p = Pattern.compile(regex);
|
Pattern p = Pattern.compile(regex);
|
||||||
Matcher m = p.matcher(sql);
|
Matcher m = p.matcher(sql);
|
||||||
@ -993,9 +958,8 @@ public class QueryGenerator {
|
|||||||
Class propType = origDescriptors[i].getPropertyType();
|
Class propType = origDescriptors[i].getPropertyType();
|
||||||
boolean isString = propType.equals(String.class);
|
boolean isString = propType.equals(String.class);
|
||||||
Object value;
|
Object value;
|
||||||
//update-begin---author:chenrui ---date:20240527 for:[TV360X-539]数据权限,配置日期等于条件时后端报转换错误------------
|
// 代码逻辑说明: [TV360X-539]数据权限,配置日期等于条件时后端报转换错误------------
|
||||||
if(isString || Date.class.equals(propType)) {
|
if(isString || Date.class.equals(propType)) {
|
||||||
//update-end---author:chenrui ---date:20240527 for:[TV360X-539]数据权限,配置日期等于条件时后端报转换错误------------
|
|
||||||
value = converRuleValue(dataRule.getRuleValue());
|
value = converRuleValue(dataRule.getRuleValue());
|
||||||
}else {
|
}else {
|
||||||
value = NumberUtils.parseNumber(dataRule.getRuleValue(),propType);
|
value = NumberUtils.parseNumber(dataRule.getRuleValue(),propType);
|
||||||
|
|||||||
@ -1,101 +1,219 @@
|
|||||||
package org.jeecg.common.system.util;
|
package org.jeecg.common.system.util;
|
||||||
|
|
||||||
import cn.dev33.satoken.stp.StpUtil;
|
import com.alibaba.fastjson.JSON;
|
||||||
|
import com.alibaba.fastjson2.JSONObject;
|
||||||
|
import com.auth0.jwt.JWT;
|
||||||
|
import com.auth0.jwt.JWTVerifier;
|
||||||
|
import com.auth0.jwt.algorithms.Algorithm;
|
||||||
|
import com.auth0.jwt.exceptions.JWTDecodeException;
|
||||||
|
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 com.google.common.base.Joiner;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.Objects;
|
import java.io.OutputStream;
|
||||||
|
import java.util.*;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import jakarta.servlet.ServletResponse;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import jakarta.servlet.http.HttpSession;
|
import jakarta.servlet.http.HttpSession;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.jeecg.common.api.CommonAPI;
|
||||||
|
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.SymbolConstant;
|
import org.jeecg.common.constant.SymbolConstant;
|
||||||
import org.jeecg.common.constant.TenantConstant;
|
import org.jeecg.common.constant.TenantConstant;
|
||||||
import org.jeecg.common.util.LoginUserUtils;
|
|
||||||
import org.jeecg.common.api.vo.Result;
|
|
||||||
import org.jeecg.common.constant.DataBaseConstant;
|
|
||||||
import org.jeecg.common.exception.JeecgBootException;
|
import org.jeecg.common.exception.JeecgBootException;
|
||||||
import org.jeecg.common.system.vo.LoginUser;
|
import org.jeecg.common.system.vo.LoginUser;
|
||||||
import org.jeecg.common.system.vo.SysUserCacheInfo;
|
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
|
||||||
* @Date 2018-07-12 14:23
|
* @Date 2018-07-12 14:23
|
||||||
* @Desc JWT工具类 - 已迁移到Sa-Token,此类作为兼容层保留
|
* @Desc JWT工具类
|
||||||
**/
|
**/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class JwtUtil {
|
public class JwtUtil {
|
||||||
|
|
||||||
|
/**PC端,Token有效期为7天(Token在reids中缓存时间为两倍)*/
|
||||||
|
public static final long EXPIRE_TIME = (7 * 12) * 60 * 60 * 1000L;
|
||||||
|
/**APP端,Token有效期为30天(Token在reids中缓存时间为两倍)*/
|
||||||
|
public static final long APP_EXPIRE_TIME = (30 * 12) * 60 * 60 * 1000L;
|
||||||
static final String WELL_NUMBER = SymbolConstant.WELL_NUMBER + SymbolConstant.LEFT_CURLY_BRACKET;
|
static final String WELL_NUMBER = SymbolConstant.WELL_NUMBER + SymbolConstant.LEFT_CURLY_BRACKET;
|
||||||
|
|
||||||
|
public static final String DEFAULT_CLIENT = "jeecg-client";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 返回错误 JSON 字符串(用于 Sa-Token Filter)
|
*
|
||||||
* @param code 错误码
|
* @param response
|
||||||
* @param errorMsg 错误信息
|
* @param code
|
||||||
* @return JSON 字符串
|
* @param errorMsg
|
||||||
*/
|
*/
|
||||||
public static String responseErrorJson(Integer code, String errorMsg) {
|
public static void responseError(ServletResponse response, Integer code, String errorMsg) {
|
||||||
try {
|
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
|
||||||
|
// issues/I4YH95浏览器显示乱码问题
|
||||||
|
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);
|
||||||
ObjectMapper objectMapper = new ObjectMapper();
|
OutputStream os = null;
|
||||||
return objectMapper.writeValueAsString(jsonResult);
|
try {
|
||||||
|
os = httpServletResponse.getOutputStream();
|
||||||
|
httpServletResponse.setCharacterEncoding("UTF-8");
|
||||||
|
httpServletResponse.setStatus(code);
|
||||||
|
os.write(new ObjectMapper().writeValueAsString(jsonResult).getBytes("UTF-8"));
|
||||||
|
os.flush();
|
||||||
|
os.close();
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
log.error("生成错误 JSON 失败: {}", e.getMessage());
|
log.error(e.getMessage(), e);
|
||||||
// 返回备用的硬编码 JSON
|
|
||||||
return "{\"success\":false,\"message\":\"" + errorMsg + "\",\"code\":" + code + ",\"result\":null,\"timestamp\":" + System.currentTimeMillis() + "}";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 校验token是否正确
|
* 校验token是否正确
|
||||||
* 注意:此方法已废弃,使用Sa-Token自动校验
|
|
||||||
*
|
*
|
||||||
* @param token
|
* @param token 密钥
|
||||||
* @return
|
* @param secret 用户的密码
|
||||||
|
* @return 是否正确
|
||||||
*/
|
*/
|
||||||
@Deprecated
|
public static boolean verify(String token, String username, String secret) {
|
||||||
public static boolean verify(String token){
|
|
||||||
try {
|
try {
|
||||||
// 使用Sa-Token验证
|
// 根据密码生成JWT效验器
|
||||||
return StpUtil.getLoginIdByToken(token) != null;
|
JwtDecoder jwtDecoder = SpringContextUtils.getBean(JwtDecoder.class);
|
||||||
|
// 效验TOKEN
|
||||||
|
jwtDecoder.decode(token);
|
||||||
|
return true;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.warn(e.getMessage(), e);
|
log.warn("Token验证失败:" + e.getMessage(),e);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获得Token中的用户名(不校验token是否有效)
|
* 获得token中的信息无需secret解密也能获得
|
||||||
* <p>注意:现在 loginId 就是 username,直接返回
|
|
||||||
*
|
*
|
||||||
* @param token JWT token
|
* @return token中包含的用户名
|
||||||
* @return 用户名(username),如果 token 无效则返回 null
|
|
||||||
*/
|
*/
|
||||||
public static String getUsername(String token){
|
public static String getUsername(String token) {
|
||||||
try {
|
try {
|
||||||
if(oConvertUtils.isEmpty(token)) {
|
DecodedJWT jwt = JWT.decode(token);
|
||||||
return null;
|
return jwt.getClaim("username").asString();
|
||||||
}
|
} catch (JWTDecodeException e) {
|
||||||
// Sa-Token 的 loginId 现在就是 username,直接返回
|
log.error(e.getMessage(), e);
|
||||||
Object loginId = StpUtil.getLoginIdByToken(token);
|
|
||||||
return loginId != null ? loginId.toString() : null;
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.warn("获取用户名失败: {}", e.getMessage());
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成token
|
||||||
|
*
|
||||||
|
* @param username 用户名
|
||||||
|
* @param secret 用户的密码
|
||||||
|
* @return 加密的token
|
||||||
|
* @deprecated 请使用sign(String username, String secret, String clientType)方法代替
|
||||||
|
*/
|
||||||
|
@Deprecated
|
||||||
|
public static String sign(String username, String secret) {
|
||||||
|
Map<String, Object> additionalParameter = new HashMap<>();
|
||||||
|
additionalParameter.put("username", username);
|
||||||
|
|
||||||
|
RegisteredClientRepository registeredClientRepository = SpringContextUtils.getBean(RegisteredClientRepository.class);
|
||||||
|
SelfAuthenticationProvider selfAuthenticationProvider = SpringContextUtils.getBean(SelfAuthenticationProvider.class);
|
||||||
|
|
||||||
|
OAuth2ClientAuthenticationToken client = new OAuth2ClientAuthenticationToken(Objects.requireNonNull(registeredClientRepository.findByClientId("jeecg-client")), ClientAuthenticationMethod.CLIENT_SECRET_BASIC, null);
|
||||||
|
client.setAuthenticated(true);
|
||||||
|
SelfAuthenticationToken selfAuthenticationToken = new SelfAuthenticationToken(client, additionalParameter);
|
||||||
|
selfAuthenticationToken.setAuthenticated(true);
|
||||||
|
OAuth2AccessTokenAuthenticationToken accessToken = (OAuth2AccessTokenAuthenticationToken) selfAuthenticationProvider.authenticate(selfAuthenticationToken);
|
||||||
|
return accessToken.getAccessToken().getTokenValue();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成签名,根据客户端类型自动选择过期时间
|
||||||
|
* for [JHHB-1030]【鉴权】移动端用户token到期后续期时间变成pc端时长
|
||||||
|
*
|
||||||
|
* @param username 用户名
|
||||||
|
* @param secret 用户的密码
|
||||||
|
* @param clientType 客户端类型(PC或APP)
|
||||||
|
* @return 加密的token
|
||||||
|
*/
|
||||||
|
public static String sign(String username, String secret, String clientType) {
|
||||||
|
Map<String, Object> additionalParameter = new HashMap<>();
|
||||||
|
additionalParameter.put("username", username);
|
||||||
|
additionalParameter.put("clientType", clientType);
|
||||||
|
|
||||||
|
// 根据客户端类型选择对应的过期时间
|
||||||
|
long expireTime = CommonConstant.CLIENT_TYPE_APP.equalsIgnoreCase(clientType)
|
||||||
|
? APP_EXPIRE_TIME
|
||||||
|
: EXPIRE_TIME;
|
||||||
|
additionalParameter.put("expireTime", expireTime);
|
||||||
|
|
||||||
|
RegisteredClientRepository registeredClientRepository = SpringContextUtils.getBean(RegisteredClientRepository.class);
|
||||||
|
SelfAuthenticationProvider selfAuthenticationProvider = SpringContextUtils.getBean(SelfAuthenticationProvider.class);
|
||||||
|
|
||||||
|
OAuth2ClientAuthenticationToken client = new OAuth2ClientAuthenticationToken(
|
||||||
|
Objects.requireNonNull(registeredClientRepository.findByClientId(DEFAULT_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();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从token中获取客户端类型
|
||||||
|
* for [JHHB-1030]【鉴权】移动端用户token到期后续期时间变成pc端时长
|
||||||
|
*
|
||||||
|
* @param token JWT token
|
||||||
|
* @return 客户端类型,如果不存在则返回PC(兼容旧token)
|
||||||
|
*/
|
||||||
|
public static String getClientType(String token) {
|
||||||
|
try {
|
||||||
|
DecodedJWT jwt = JWT.decode(token);
|
||||||
|
String clientType = jwt.getClaim("clientType").asString();
|
||||||
|
// 如果clientType为空,返回默认值PC(兼容旧token)
|
||||||
|
return oConvertUtils.isNotEmpty(clientType) ? clientType : CommonConstant.CLIENT_TYPE_PC;
|
||||||
|
} catch (JWTDecodeException e) {
|
||||||
|
log.warn("解析token中的clientType失败,使用默认值PC:" + e.getMessage());
|
||||||
|
return CommonConstant.CLIENT_TYPE_PC;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据request中的token获取用户账号
|
* 根据request中的token获取用户账号
|
||||||
* 注意:此方法已适配Sa-Token
|
|
||||||
*
|
*
|
||||||
* @param request
|
* @param request
|
||||||
* @return
|
* @return
|
||||||
@ -145,7 +263,7 @@ public class JwtUtil {
|
|||||||
*/
|
*/
|
||||||
public static String getUserSystemData(String key, SysUserCacheInfo user) {
|
public static String getUserSystemData(String key, SysUserCacheInfo user) {
|
||||||
//1.优先获取 SysUserCacheInfo
|
//1.优先获取 SysUserCacheInfo
|
||||||
if (user == null) {
|
if(user==null) {
|
||||||
try {
|
try {
|
||||||
user = JeecgDataAutorUtils.loadUserInfo();
|
user = JeecgDataAutorUtils.loadUserInfo();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
@ -155,7 +273,7 @@ public class JwtUtil {
|
|||||||
//2.通过shiro获取登录用户信息
|
//2.通过shiro获取登录用户信息
|
||||||
LoginUser sysUser = null;
|
LoginUser sysUser = null;
|
||||||
try {
|
try {
|
||||||
sysUser = (LoginUser) LoginUserUtils.getSessionUser();
|
sysUser = SecureUtil.currentUser();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.warn("SecurityUtils.getSubject() 获取用户信息异常:" + e.getMessage());
|
log.warn("SecurityUtils.getSubject() 获取用户信息异常:" + e.getMessage());
|
||||||
}
|
}
|
||||||
@ -163,74 +281,74 @@ public class JwtUtil {
|
|||||||
//#{sys_user_code}%
|
//#{sys_user_code}%
|
||||||
String moshi = "";
|
String moshi = "";
|
||||||
String wellNumber = WELL_NUMBER;
|
String wellNumber = WELL_NUMBER;
|
||||||
if (key.indexOf(SymbolConstant.RIGHT_CURLY_BRACKET) != -1) {
|
if(key.indexOf(SymbolConstant.RIGHT_CURLY_BRACKET)!=-1){
|
||||||
moshi = key.substring(key.indexOf("}") + 1);
|
moshi = key.substring(key.indexOf("}")+1);
|
||||||
}
|
}
|
||||||
String returnValue = null;
|
String returnValue = null;
|
||||||
//针对特殊标示处理#{sysOrgCode},判断替换
|
//针对特殊标示处理#{sysOrgCode},判断替换
|
||||||
if (key.contains(wellNumber)) {
|
if (key.contains(wellNumber)) {
|
||||||
key = key.substring(2, key.indexOf("}"));
|
key = key.substring(2,key.indexOf("}"));
|
||||||
} else {
|
} else {
|
||||||
key = key;
|
key = key;
|
||||||
}
|
}
|
||||||
// 是否存在字符串标志
|
// 是否存在字符串标志
|
||||||
boolean multiStr;
|
boolean multiStr;
|
||||||
if (oConvertUtils.isNotEmpty(key) && key.trim().matches("^\\[\\w+]$")) {
|
if(oConvertUtils.isNotEmpty(key) && key.trim().matches("^\\[\\w+]$")){
|
||||||
key = key.substring(1, key.length() - 1);
|
key = key.substring(1,key.length()-1);
|
||||||
multiStr = true;
|
multiStr = true;
|
||||||
} else {
|
} else {
|
||||||
multiStr = false;
|
multiStr = false;
|
||||||
}
|
}
|
||||||
//替换为当前系统时间(年月日)
|
//替换为当前系统时间(年月日)
|
||||||
if (key.equals(DataBaseConstant.SYS_DATE) || key.toLowerCase().equals(DataBaseConstant.SYS_DATE_TABLE)) {
|
if (key.equals(DataBaseConstant.SYS_DATE)|| key.toLowerCase().equals(DataBaseConstant.SYS_DATE_TABLE)) {
|
||||||
returnValue = DateUtils.formatDate();
|
returnValue = DateUtils.formatDate();
|
||||||
}
|
}
|
||||||
//替换为当前系统时间(年月日时分秒)
|
//替换为当前系统时间(年月日时分秒)
|
||||||
else if (key.equals(DataBaseConstant.SYS_TIME) || key.toLowerCase().equals(DataBaseConstant.SYS_TIME_TABLE)) {
|
else if (key.equals(DataBaseConstant.SYS_TIME)|| key.toLowerCase().equals(DataBaseConstant.SYS_TIME_TABLE)) {
|
||||||
returnValue = DateUtils.now();
|
returnValue = DateUtils.now();
|
||||||
}
|
}
|
||||||
//流程状态默认值(默认未发起)
|
//流程状态默认值(默认未发起)
|
||||||
else if (key.equals(DataBaseConstant.BPM_STATUS) || key.toLowerCase().equals(DataBaseConstant.BPM_STATUS_TABLE)) {
|
else if (key.equals(DataBaseConstant.BPM_STATUS)|| key.toLowerCase().equals(DataBaseConstant.BPM_STATUS_TABLE)) {
|
||||||
returnValue = "1";
|
returnValue = "1";
|
||||||
}
|
}
|
||||||
|
|
||||||
//后台任务获取用户信息异常,导致程序中断
|
//后台任务获取用户信息异常,导致程序中断
|
||||||
if (sysUser == null && user == null) {
|
if(sysUser==null && user==null){
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
//替换为系统登录用户帐号
|
//替换为系统登录用户帐号
|
||||||
if (key.equals(DataBaseConstant.SYS_USER_CODE) || key.toLowerCase().equals(DataBaseConstant.SYS_USER_CODE_TABLE)) {
|
if (key.equals(DataBaseConstant.SYS_USER_CODE)|| key.toLowerCase().equals(DataBaseConstant.SYS_USER_CODE_TABLE)) {
|
||||||
if (user == null) {
|
if(user==null) {
|
||||||
returnValue = sysUser.getUsername();
|
returnValue = sysUser.getUsername();
|
||||||
} else {
|
}else {
|
||||||
returnValue = user.getSysUserCode();
|
returnValue = user.getSysUserCode();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 替换为系统登录用户ID
|
// 替换为系统登录用户ID
|
||||||
else if (key.equals(DataBaseConstant.SYS_USER_ID) || key.equalsIgnoreCase(DataBaseConstant.SYS_USER_ID_TABLE)) {
|
else if (key.equals(DataBaseConstant.SYS_USER_ID) || key.equalsIgnoreCase(DataBaseConstant.SYS_USER_ID_TABLE)) {
|
||||||
if (user == null) {
|
if(user==null) {
|
||||||
returnValue = sysUser.getId();
|
returnValue = sysUser.getId();
|
||||||
} else {
|
}else {
|
||||||
returnValue = user.getSysUserId();
|
returnValue = user.getSysUserId();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//替换为系统登录用户真实名字
|
//替换为系统登录用户真实名字
|
||||||
else if (key.equals(DataBaseConstant.SYS_USER_NAME) || key.toLowerCase().equals(DataBaseConstant.SYS_USER_NAME_TABLE)) {
|
else if (key.equals(DataBaseConstant.SYS_USER_NAME)|| key.toLowerCase().equals(DataBaseConstant.SYS_USER_NAME_TABLE)) {
|
||||||
if (user == null) {
|
if(user==null) {
|
||||||
returnValue = sysUser.getRealname();
|
returnValue = sysUser.getRealname();
|
||||||
} else {
|
}else {
|
||||||
returnValue = user.getSysUserName();
|
returnValue = user.getSysUserName();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//替换为系统用户登录所使用的机构编码
|
//替换为系统用户登录所使用的机构编码
|
||||||
else if (key.equals(DataBaseConstant.SYS_ORG_CODE) || key.toLowerCase().equals(DataBaseConstant.SYS_ORG_CODE_TABLE)) {
|
else if (key.equals(DataBaseConstant.SYS_ORG_CODE)|| key.toLowerCase().equals(DataBaseConstant.SYS_ORG_CODE_TABLE)) {
|
||||||
if (user == null) {
|
if(user==null) {
|
||||||
returnValue = sysUser.getOrgCode();
|
returnValue = sysUser.getOrgCode();
|
||||||
} else {
|
}else {
|
||||||
returnValue = user.getSysOrgCode();
|
returnValue = user.getSysOrgCode();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -245,15 +363,19 @@ public class JwtUtil {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//替换为系统用户所拥有的所有机构编码
|
//替换为系统用户所拥有的所有机构编码
|
||||||
else if (key.equals(DataBaseConstant.SYS_MULTI_ORG_CODE) || key.toLowerCase().equals(DataBaseConstant.SYS_MULTI_ORG_CODE_TABLE)) {
|
else if (key.equals(DataBaseConstant.SYS_MULTI_ORG_CODE)|| key.toLowerCase().equals(DataBaseConstant.SYS_MULTI_ORG_CODE_TABLE)) {
|
||||||
if (user == null) {
|
if(user==null){
|
||||||
|
//TODO 暂时使用用户登录部门,存在逻辑缺陷,不是用户所拥有的部门
|
||||||
returnValue = sysUser.getOrgCode();
|
returnValue = sysUser.getOrgCode();
|
||||||
|
// 代码逻辑说明: [QQYUN-10785]数据权限,查看自己拥有部门的权限中存在问题 #7288------------
|
||||||
returnValue = multiStr ? "'" + returnValue + "'" : returnValue;
|
returnValue = multiStr ? "'" + returnValue + "'" : returnValue;
|
||||||
} else {
|
}else{
|
||||||
if (user.isOneDepart()) {
|
if(user.isOneDepart()) {
|
||||||
returnValue = user.getSysMultiOrgCode().get(0);
|
returnValue = user.getSysMultiOrgCode().get(0);
|
||||||
|
// 代码逻辑说明: [QQYUN-10785]数据权限,查看自己拥有部门的权限中存在问题 #7288------------
|
||||||
returnValue = multiStr ? "'" + returnValue + "'" : returnValue;
|
returnValue = multiStr ? "'" + returnValue + "'" : returnValue;
|
||||||
} else {
|
}else {
|
||||||
|
// 代码逻辑说明: [QQYUN-10785]数据权限,查看自己拥有部门的权限中存在问题 #7288------------
|
||||||
returnValue = user.getSysMultiOrgCode().stream()
|
returnValue = user.getSysMultiOrgCode().stream()
|
||||||
.filter(Objects::nonNull)
|
.filter(Objects::nonNull)
|
||||||
.map(orgCode -> {
|
.map(orgCode -> {
|
||||||
@ -277,17 +399,20 @@ public class JwtUtil {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 多租户ID作为系统变量
|
// 代码逻辑说明: 多租户ID作为系统变量
|
||||||
else if (key.equals(TenantConstant.TENANT_ID) || key.toLowerCase().equals(TenantConstant.TENANT_ID_TABLE)) {
|
else if (key.equals(TenantConstant.TENANT_ID) || key.toLowerCase().equals(TenantConstant.TENANT_ID_TABLE)){
|
||||||
try {
|
try {
|
||||||
returnValue = SpringContextUtils.getHttpServletRequest().getHeader(CommonConstant.TENANT_ID);
|
returnValue = SpringContextUtils.getHttpServletRequest().getHeader(CommonConstant.TENANT_ID);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.warn("获取系统租户异常:" + e.getMessage());
|
log.warn("获取系统租户异常:" + e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (returnValue != null) {
|
if(returnValue!=null){returnValue = returnValue + moshi;}
|
||||||
returnValue = returnValue + moshi;
|
|
||||||
}
|
|
||||||
return returnValue;
|
return returnValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// public static void main(String[] args) {
|
||||||
|
// String token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE1NjUzMzY1MTMsInVzZXJuYW1lIjoiYWRtaW4ifQ.xjhud_tWCNYBOg_aRlMgOdlZoWFFKB_givNElHNw3X0";
|
||||||
|
// System.out.println(JwtUtil.getUsername(token));
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|||||||
@ -67,13 +67,13 @@ public class ResourceUtil {
|
|||||||
synchronized (ResourceUtil.class) {
|
synchronized (ResourceUtil.class) {
|
||||||
if (!initialized) {
|
if (!initialized) {
|
||||||
long startTime = System.currentTimeMillis();
|
long startTime = System.currentTimeMillis();
|
||||||
log.info("【枚举字典加载】开始初始化枚举字典数据...");
|
log.debug("【枚举字典加载】开始初始化枚举字典数据...");
|
||||||
|
|
||||||
initEnumDictData();
|
initEnumDictData();
|
||||||
initialized = true;
|
initialized = true;
|
||||||
|
|
||||||
long endTime = System.currentTimeMillis();
|
long endTime = System.currentTimeMillis();
|
||||||
log.info("【枚举字典加载】枚举字典数据初始化完成,共加载 {} 个字典,总耗时: {}ms", enumDictData.size(), endTime - startTime);
|
log.debug("【枚举字典加载】枚举字典数据初始化完成,共加载 {} 个字典,总耗时: {}ms", enumDictData.size(), endTime - startTime);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -103,7 +103,7 @@ public class ResourceUtil {
|
|||||||
}
|
}
|
||||||
|
|
||||||
long scanEndTime = System.currentTimeMillis();
|
long scanEndTime = System.currentTimeMillis();
|
||||||
log.info("【枚举字典加载】文件扫描完成,总共找到 {} 个枚举类文件,扫描耗时: {}ms", allResources.size(), scanEndTime - scanStartTime);
|
log.debug("【枚举字典加载】文件扫描完成,总共找到 {} 个枚举类文件,扫描耗时: {}ms", allResources.size(), scanEndTime - scanStartTime);
|
||||||
|
|
||||||
MetadataReaderFactory readerFactory = new CachingMetadataReaderFactory(resourcePatternResolver);
|
MetadataReaderFactory readerFactory = new CachingMetadataReaderFactory(resourcePatternResolver);
|
||||||
|
|
||||||
@ -126,7 +126,7 @@ public class ResourceUtil {
|
|||||||
}
|
}
|
||||||
|
|
||||||
long processEndTime = System.currentTimeMillis();
|
long processEndTime = System.currentTimeMillis();
|
||||||
log.info("【枚举字典加载】处理完成,实际处理 {} 个带注解的枚举类,处理耗时: {}ms", processedCount, processEndTime - processStartTime);
|
log.debug("【枚举字典加载】处理完成,实际处理 {} 个带注解的枚举类,处理耗时: {}ms", processedCount, processEndTime - processStartTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -150,7 +150,7 @@ public class SqlConcatUtil {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static String getInConditionValue(Object value,boolean isString) {
|
private static String getInConditionValue(Object value,boolean isString) {
|
||||||
//update-begin-author:taoyan date:20210628 for: 查询条件如果输入,导致sql报错
|
// 代码逻辑说明: 查询条件如果输入,导致sql报错
|
||||||
String[] temp = value.toString().split(",");
|
String[] temp = value.toString().split(",");
|
||||||
if(temp.length==0){
|
if(temp.length==0){
|
||||||
return "('')";
|
return "('')";
|
||||||
@ -168,7 +168,6 @@ public class SqlConcatUtil {
|
|||||||
}else {
|
}else {
|
||||||
return "("+value.toString()+")";
|
return "("+value.toString()+")";
|
||||||
}
|
}
|
||||||
//update-end-author:taoyan date:20210628 for: 查询条件如果输入,导致sql报错
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -215,7 +214,6 @@ public class SqlConcatUtil {
|
|||||||
}
|
}
|
||||||
}else {
|
}else {
|
||||||
|
|
||||||
//update-begin-author:taoyan date:2022-6-30 for: issues/3810 数据权限规则问题
|
|
||||||
// 走到这里说明 value不带有任何模糊查询的标识(*或者%)
|
// 走到这里说明 value不带有任何模糊查询的标识(*或者%)
|
||||||
if (ruleEnum == QueryRuleEnum.LEFT_LIKE) {
|
if (ruleEnum == QueryRuleEnum.LEFT_LIKE) {
|
||||||
if (DataBaseConstant.DB_TYPE_SQLSERVER.equals(getDbType())) {
|
if (DataBaseConstant.DB_TYPE_SQLSERVER.equals(getDbType())) {
|
||||||
@ -236,7 +234,6 @@ public class SqlConcatUtil {
|
|||||||
return "'%" + str + "%'";
|
return "'%" + str + "%'";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//update-end-author:taoyan date:2022-6-30 for: issues/3810 数据权限规则问题
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,13 +1,18 @@
|
|||||||
package org.jeecg.common.system.vo;
|
package org.jeecg.common.system.vo;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson2.JSON;
|
||||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
import lombok.experimental.Accessors;
|
import lombok.experimental.Accessors;
|
||||||
import org.jeecg.common.desensitization.annotation.SensitiveField;
|
import org.jeecg.common.desensitization.annotation.SensitiveField;
|
||||||
import org.springframework.format.annotation.DateTimeFormat;
|
import org.springframework.format.annotation.DateTimeFormat;
|
||||||
|
import org.springframework.security.core.GrantedAuthority;
|
||||||
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <p>
|
* <p>
|
||||||
@ -20,8 +25,10 @@ import java.util.Date;
|
|||||||
@Data
|
@Data
|
||||||
@EqualsAndHashCode(callSuper = false)
|
@EqualsAndHashCode(callSuper = false)
|
||||||
@Accessors(chain = true)
|
@Accessors(chain = true)
|
||||||
public class LoginUser {
|
public class LoginUser implements Serializable {
|
||||||
|
|
||||||
|
|
||||||
|
private static final long serialVersionUID = -7143159031677245866L;
|
||||||
/**
|
/**
|
||||||
* 登录人id
|
* 登录人id
|
||||||
*/
|
*/
|
||||||
@ -148,4 +155,30 @@ public class LoginUser {
|
|||||||
* 主岗位
|
* 主岗位
|
||||||
*/
|
*/
|
||||||
private String mainDepPostId;
|
private String mainDepPostId;
|
||||||
|
|
||||||
|
@SensitiveField
|
||||||
|
private String salt;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
// 重新构建对象过滤一些敏感字段
|
||||||
|
LoginUser loginUser = new LoginUser();
|
||||||
|
loginUser.setId(id);
|
||||||
|
loginUser.setUsername(username);
|
||||||
|
loginUser.setRealname(realname);
|
||||||
|
loginUser.setOrgCode(orgCode);
|
||||||
|
loginUser.setSex(sex);
|
||||||
|
loginUser.setEmail(email);
|
||||||
|
loginUser.setPhone(phone);
|
||||||
|
loginUser.setDelFlag(delFlag);
|
||||||
|
loginUser.setStatus(status);
|
||||||
|
loginUser.setActivitiSync(activitiSync);
|
||||||
|
loginUser.setUserIdentity(userIdentity);
|
||||||
|
loginUser.setDepartIds(departIds);
|
||||||
|
loginUser.setPost(post);
|
||||||
|
loginUser.setTelephone(telephone);
|
||||||
|
loginUser.setRelTenantIds(relTenantIds);
|
||||||
|
loginUser.setClientId(clientId);
|
||||||
|
return JSON.toJSONString(loginUser);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,7 +10,9 @@ import com.aliyuncs.exceptions.ClientException;
|
|||||||
import com.aliyuncs.profile.DefaultProfile;
|
import com.aliyuncs.profile.DefaultProfile;
|
||||||
import com.aliyuncs.profile.IClientProfile;
|
import com.aliyuncs.profile.IClientProfile;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.jeecg.common.constant.CommonConstant;
|
||||||
import org.jeecg.common.constant.enums.DySmsEnum;
|
import org.jeecg.common.constant.enums.DySmsEnum;
|
||||||
|
import org.jeecg.config.JeecgBaseConfig;
|
||||||
import org.jeecg.config.JeecgSmsTemplateConfig;
|
import org.jeecg.config.JeecgSmsTemplateConfig;
|
||||||
import org.jeecg.config.StaticConfig;
|
import org.jeecg.config.StaticConfig;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
@ -61,17 +63,21 @@ public class DySmsHelper {
|
|||||||
|
|
||||||
|
|
||||||
public static boolean sendSms(String phone, JSONObject templateParamJson, DySmsEnum dySmsEnum) throws ClientException {
|
public static boolean sendSms(String phone, JSONObject templateParamJson, DySmsEnum dySmsEnum) throws ClientException {
|
||||||
|
JeecgBaseConfig config = SpringContextUtils.getBean(JeecgBaseConfig.class);
|
||||||
|
String smsSendType = config.getSmsSendType();
|
||||||
|
if(oConvertUtils.isNotEmpty(smsSendType) && CommonConstant.SMS_SEND_TYPE_TENCENT.equals(smsSendType)){
|
||||||
|
return TencentSms.sendTencentSms(phone, templateParamJson, config.getTencent(), dySmsEnum);
|
||||||
|
}
|
||||||
//可自助调整超时时间
|
//可自助调整超时时间
|
||||||
System.setProperty("sun.net.client.defaultConnectTimeout", "10000");
|
System.setProperty("sun.net.client.defaultConnectTimeout", "10000");
|
||||||
System.setProperty("sun.net.client.defaultReadTimeout", "10000");
|
System.setProperty("sun.net.client.defaultReadTimeout", "10000");
|
||||||
|
|
||||||
//update-begin-author:taoyan date:20200811 for:配置类数据获取
|
// 代码逻辑说明: 配置类数据获取
|
||||||
StaticConfig staticConfig = SpringContextUtils.getBean(StaticConfig.class);
|
StaticConfig staticConfig = SpringContextUtils.getBean(StaticConfig.class);
|
||||||
//logger.info("阿里大鱼短信秘钥 accessKeyId:" + staticConfig.getAccessKeyId());
|
//logger.info("阿里大鱼短信秘钥 accessKeyId:" + staticConfig.getAccessKeyId());
|
||||||
//logger.info("阿里大鱼短信秘钥 accessKeySecret:"+ staticConfig.getAccessKeySecret());
|
//logger.info("阿里大鱼短信秘钥 accessKeySecret:"+ staticConfig.getAccessKeySecret());
|
||||||
setAccessKeyId(staticConfig.getAccessKeyId());
|
setAccessKeyId(staticConfig.getAccessKeyId());
|
||||||
setAccessKeySecret(staticConfig.getAccessKeySecret());
|
setAccessKeySecret(staticConfig.getAccessKeySecret());
|
||||||
//update-end-author:taoyan date:20200811 for:配置类数据获取
|
|
||||||
|
|
||||||
//初始化acsClient,暂不支持region化
|
//初始化acsClient,暂不支持region化
|
||||||
IClientProfile profile = DefaultProfile.getProfile("cn-hangzhou", accessKeyId, accessKeySecret);
|
IClientProfile profile = DefaultProfile.getProfile("cn-hangzhou", accessKeyId, accessKeySecret);
|
||||||
@ -81,7 +87,7 @@ public class DySmsHelper {
|
|||||||
//验证json参数
|
//验证json参数
|
||||||
validateParam(templateParamJson,dySmsEnum);
|
validateParam(templateParamJson,dySmsEnum);
|
||||||
|
|
||||||
//update-begin---author:wangshuai---date:2024-11-05---for:【QQYUN-9422】短信模板管理,阿里云---
|
// 代码逻辑说明: 【QQYUN-9422】短信模板管理,阿里云---
|
||||||
String templateCode = dySmsEnum.getTemplateCode();
|
String templateCode = dySmsEnum.getTemplateCode();
|
||||||
JeecgSmsTemplateConfig baseConfig = SpringContextUtils.getBean(JeecgSmsTemplateConfig.class);
|
JeecgSmsTemplateConfig baseConfig = SpringContextUtils.getBean(JeecgSmsTemplateConfig.class);
|
||||||
if(baseConfig != null && CollectionUtil.isNotEmpty(baseConfig.getTemplateCode())){
|
if(baseConfig != null && CollectionUtil.isNotEmpty(baseConfig.getTemplateCode())){
|
||||||
@ -97,7 +103,6 @@ public class DySmsHelper {
|
|||||||
logger.info("yml中读取签名名称{}",baseConfig.getSignature());
|
logger.info("yml中读取签名名称{}",baseConfig.getSignature());
|
||||||
signName = baseConfig.getSignature();
|
signName = baseConfig.getSignature();
|
||||||
}
|
}
|
||||||
//update-end---author:wangshuai---date:2024-11-05---for:【QQYUN-9422】短信模板管理,阿里云---
|
|
||||||
|
|
||||||
//组装请求对象-具体描述见控制台-文档部分内容
|
//组装请求对象-具体描述见控制台-文档部分内容
|
||||||
SendSmsRequest request = new SendSmsRequest();
|
SendSmsRequest request = new SendSmsRequest();
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
package org.jeecg.common.util;
|
package org.jeecg.common.util;
|
||||||
|
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
|
||||||
import cn.hutool.core.io.IoUtil;
|
import cn.hutool.core.io.IoUtil;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
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.ZipArchiveEntry;
|
||||||
import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;
|
import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;
|
||||||
|
|||||||
@ -38,14 +38,12 @@ public class HTMLUtils {
|
|||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
public static String parseMarkdown(String markdownContent) {
|
public static String parseMarkdown(String markdownContent) {
|
||||||
//update-begin---author:wangshuai---date:2024-06-26---for:【TV360X-1344】JDK17 邮箱发送失败,需要换写法---
|
|
||||||
/*PegDownProcessor pdp = new PegDownProcessor();
|
/*PegDownProcessor pdp = new PegDownProcessor();
|
||||||
return pdp.markdownToHtml(markdownContent);*/
|
return pdp.markdownToHtml(markdownContent);*/
|
||||||
Parser parser = Parser.builder().build();
|
Parser parser = Parser.builder().build();
|
||||||
Node document = parser.parse(markdownContent);
|
Node document = parser.parse(markdownContent);
|
||||||
HtmlRenderer renderer = HtmlRenderer.builder().build();
|
HtmlRenderer renderer = HtmlRenderer.builder().build();
|
||||||
return renderer.render(document);
|
return renderer.render(document);
|
||||||
//update-end---author:wangshuai---date:2024-06-26---for:【TV360X-1344】JDK17 邮箱发送失败,需要换写法---
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,175 +0,0 @@
|
|||||||
package org.jeecg.common.util;
|
|
||||||
|
|
||||||
import cn.dev33.satoken.stp.StpUtil;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.jeecg.common.system.vo.LoginUser;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 登录用户工具类
|
|
||||||
* 替代原有的Shiro SecurityUtils工具类
|
|
||||||
* @author jeecg-boot
|
|
||||||
*/
|
|
||||||
@Slf4j
|
|
||||||
public class LoginUserUtils {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Session中存储登录用户信息的key
|
|
||||||
*/
|
|
||||||
private static final String SESSION_KEY_LOGIN_USER = "loginUser";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 执行登录并设置用户信息到Session(推荐)
|
|
||||||
*
|
|
||||||
* <p>此方法会:
|
|
||||||
* <ul>
|
|
||||||
* <li>1. 调用 StpUtil.login(username) 生成token和session</li>
|
|
||||||
* <li>2. 将 LoginUser 存入 Session 缓存(清除不必要的字段(密码等15个字段)</li>
|
|
||||||
* <li>3. 返回生成的 token</li>
|
|
||||||
* </ul>
|
|
||||||
*
|
|
||||||
* @param sysUser 完整的用户对象(从数据库查询得到)
|
|
||||||
* @return 生成的 token
|
|
||||||
*/
|
|
||||||
public static String doLogin(LoginUser sysUser) {
|
|
||||||
if (sysUser == null) {
|
|
||||||
throw new IllegalArgumentException("用户对象不能为空");
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 1. 获取 username
|
|
||||||
String username = sysUser.getUsername();
|
|
||||||
|
|
||||||
if (username == null || username.trim().isEmpty()) {
|
|
||||||
throw new IllegalArgumentException("用户名不能为空");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Sa-Token 登录(使用 username 作为 loginId)
|
|
||||||
StpUtil.login(username);
|
|
||||||
|
|
||||||
// 3. 用户信息到 LoginUser 并存入 Session
|
|
||||||
setSessionUser(sysUser);
|
|
||||||
|
|
||||||
// 4. 返回生成的 token
|
|
||||||
return StpUtil.getTokenValue();
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
throw new RuntimeException("登录失败: " + e.getMessage(), e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取当前登录用户信息
|
|
||||||
*
|
|
||||||
* <p>说明:
|
|
||||||
* <ul>
|
|
||||||
* <li>对于需要认证的接口:Sa-Token Filter 已经校验过登录状态,此方法必然能获取到用户</li>
|
|
||||||
* <li>对于已排除拦截的接口:如果未登录或获取失败则返回 null,由业务代码自行判断处理</li>
|
|
||||||
* </ul>
|
|
||||||
*
|
|
||||||
* @return 登录用户对象,如果未登录或session中没有则返回null
|
|
||||||
*/
|
|
||||||
public static LoginUser getSessionUser() {
|
|
||||||
// 尝试从Sa-Token的Session中获取用户信息
|
|
||||||
Object loginUser = StpUtil.getSession().get(SESSION_KEY_LOGIN_USER);
|
|
||||||
if (loginUser instanceof LoginUser) {
|
|
||||||
return (LoginUser) loginUser;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 根据指定的 token 获取登录用户信息
|
|
||||||
*
|
|
||||||
* <p>适用场景:已排除拦截的接口(如 WebSocket),需要显式传入 token 来获取用户信息
|
|
||||||
*
|
|
||||||
* <p>实现方式:临时切换到该 token 对应的会话,然后获取用户信息
|
|
||||||
*
|
|
||||||
* @param token JWT token
|
|
||||||
* @return 登录用户对象,如果 token 无效或session中没有则返回null
|
|
||||||
*/
|
|
||||||
public static LoginUser getSessionUser(String token) {
|
|
||||||
try {
|
|
||||||
// 根据 token 获取登录ID
|
|
||||||
Object loginId = StpUtil.getLoginIdByToken(token);
|
|
||||||
if (loginId == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 临时切换到该 token 对应的登录会话
|
|
||||||
StpUtil.switchTo(loginId);
|
|
||||||
|
|
||||||
// 直接调用无参方法获取用户信息
|
|
||||||
return getSessionUser();
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.debug("根据token获取用户信息失败: {}", e.getMessage());
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置当前登录用户信息到Session
|
|
||||||
*
|
|
||||||
* <p>为减少 Redis 存储和保障安全,只保留必要的核心字段:
|
|
||||||
* <ul>
|
|
||||||
* <li>id, username, realname - 基础用户信息</li>
|
|
||||||
* <li>orgCode, orgId, departIds - 部门和数据权限</li>
|
|
||||||
* <li>roleCode - 角色权限</li>
|
|
||||||
* <li>loginTenantId, relTenantIds - 多租户</li>
|
|
||||||
* <li>avatar - 用户头像</li>
|
|
||||||
* </ul>
|
|
||||||
*
|
|
||||||
* <p>⚠️ 注意:调用此方法前需要先调用 StpUtil.login()
|
|
||||||
*
|
|
||||||
* @param loginUser 登录用户对象
|
|
||||||
*/
|
|
||||||
public static void setSessionUser(LoginUser loginUser) {
|
|
||||||
if (loginUser == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ⚠️ 安全与性能:清除不必要的字段,减少 Redis 存储
|
|
||||||
loginUser.setPassword(null); // 密码(安全)
|
|
||||||
loginUser.setWorkNo(null); // 工号
|
|
||||||
loginUser.setBirthday(null); // 生日
|
|
||||||
loginUser.setSex(null); // 性别
|
|
||||||
loginUser.setEmail(null); // 邮箱
|
|
||||||
loginUser.setPhone(null); // 手机号
|
|
||||||
loginUser.setStatus(null); // 状态
|
|
||||||
loginUser.setDelFlag(null); // 删除标志
|
|
||||||
loginUser.setActivitiSync(null); // 工作流同步
|
|
||||||
loginUser.setCreateTime(null); // 创建时间
|
|
||||||
loginUser.setUserIdentity(null); // 用户身份
|
|
||||||
loginUser.setPost(null); // 职务
|
|
||||||
loginUser.setTelephone(null); // 座机
|
|
||||||
loginUser.setRelTenantIds(null); // 关联租户
|
|
||||||
loginUser.setMainDepPostId(null); // 主岗位
|
|
||||||
|
|
||||||
StpUtil.getSession().set(SESSION_KEY_LOGIN_USER, loginUser);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取当前登录用户名(推荐使用此方法,语义更清晰)
|
|
||||||
* @return 用户名(username)
|
|
||||||
*/
|
|
||||||
public static String getUsername() {
|
|
||||||
return StpUtil.getLoginIdAsString();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 检查是否已登录
|
|
||||||
* @return true-已登录,false-未登录
|
|
||||||
*/
|
|
||||||
public static boolean isLogin() {
|
|
||||||
return StpUtil.isLogin();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 退出登录
|
|
||||||
*/
|
|
||||||
public static void logout() {
|
|
||||||
StpUtil.logout();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -161,11 +161,10 @@ public class MinioUtil {
|
|||||||
public static String getObjectUrl(String bucketName, String objectName, Integer expires) {
|
public static String getObjectUrl(String bucketName, String objectName, Integer expires) {
|
||||||
initMinio(minioUrl, minioName,minioPass);
|
initMinio(minioUrl, minioName,minioPass);
|
||||||
try{
|
try{
|
||||||
//update-begin---author:liusq Date:20220121 for:获取文件外链报错提示method不能为空,导致文件下载和预览失败----
|
// 代码逻辑说明: 获取文件外链报错提示method不能为空,导致文件下载和预览失败----
|
||||||
GetPresignedObjectUrlArgs objectArgs = GetPresignedObjectUrlArgs.builder().object(objectName)
|
GetPresignedObjectUrlArgs objectArgs = GetPresignedObjectUrlArgs.builder().object(objectName)
|
||||||
.bucket(bucketName)
|
.bucket(bucketName)
|
||||||
.expiry(expires).method(Method.GET).build();
|
.expiry(expires).method(Method.GET).build();
|
||||||
//update-begin---author:liusq Date:20220121 for:获取文件外链报错提示method不能为空,导致文件下载和预览失败----
|
|
||||||
String url = minioClient.getPresignedObjectUrl(objectArgs);
|
String url = minioClient.getPresignedObjectUrl(objectArgs);
|
||||||
return URLDecoder.decode(url,"UTF-8");
|
return URLDecoder.decode(url,"UTF-8");
|
||||||
}catch (Exception e){
|
}catch (Exception e){
|
||||||
|
|||||||
@ -0,0 +1,78 @@
|
|||||||
|
package org.jeecg.common.util;
|
||||||
|
|
||||||
|
import org.apache.commons.fileupload.FileItem;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @date 2025-09-04
|
||||||
|
* @author scott
|
||||||
|
*
|
||||||
|
* 升级springboot3 无法使用 CommonsMultipartFile
|
||||||
|
* 自定义 MultipartFile 实现类,支持从 FileItem 构造
|
||||||
|
*/
|
||||||
|
public class MyCommonsMultipartFile implements MultipartFile {
|
||||||
|
|
||||||
|
private final byte[] fileContent;
|
||||||
|
private final String fileName;
|
||||||
|
private final String contentType;
|
||||||
|
|
||||||
|
// 新增构造方法,支持 FileItem 参数
|
||||||
|
public MyCommonsMultipartFile(FileItem fileItem) throws IOException {
|
||||||
|
this.fileName = fileItem.getName();
|
||||||
|
this.contentType = fileItem.getContentType();
|
||||||
|
try (InputStream inputStream = fileItem.getInputStream()) {
|
||||||
|
this.fileContent = inputStream.readAllBytes();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 现有构造方法
|
||||||
|
public MyCommonsMultipartFile(InputStream inputStream, String fileName, String contentType) throws IOException {
|
||||||
|
this.fileName = fileName;
|
||||||
|
this.contentType = contentType;
|
||||||
|
this.fileContent = inputStream.readAllBytes();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return fileName;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getOriginalFilename() {
|
||||||
|
return fileName;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getContentType() {
|
||||||
|
return contentType;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isEmpty() {
|
||||||
|
return fileContent.length == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getSize() {
|
||||||
|
return fileContent.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte[] getBytes() {
|
||||||
|
return fileContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public InputStream getInputStream() {
|
||||||
|
return new ByteArrayInputStream(fileContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void transferTo(File dest) throws IOException {
|
||||||
|
try (OutputStream os = new FileOutputStream(dest)) {
|
||||||
|
os.write(fileContent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -99,9 +99,8 @@ public class PasswordUtil {
|
|||||||
Cipher cipher = Cipher.getInstance(ALGORITHM);
|
Cipher cipher = Cipher.getInstance(ALGORITHM);
|
||||||
|
|
||||||
cipher.init(Cipher.ENCRYPT_MODE, key, parameterSpec);
|
cipher.init(Cipher.ENCRYPT_MODE, key, parameterSpec);
|
||||||
//update-begin-author:sccott date:20180815 for:中文作为用户名时,加密的密码windows和linux会得到不同的结果 gitee/issues/IZUD7
|
// 代码逻辑说明: 中文作为用户名时,加密的密码windows和linux会得到不同的结果 gitee/issues/IZUD7
|
||||||
encipheredData = cipher.doFinal(plaintext.getBytes("utf-8"));
|
encipheredData = cipher.doFinal(plaintext.getBytes("utf-8"));
|
||||||
//update-end-author:sccott date:20180815 for:中文作为用户名时,加密的密码windows和linux会得到不同的结果 gitee/issues/IZUD7
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
}
|
}
|
||||||
return bytesToHexString(encipheredData);
|
return bytesToHexString(encipheredData);
|
||||||
|
|||||||
@ -56,14 +56,12 @@ public class RestUtil {
|
|||||||
private final static RestTemplate RT;
|
private final static RestTemplate RT;
|
||||||
|
|
||||||
static {
|
static {
|
||||||
//update-begin---author:chenrui ---date:20251011 for:[issues/8859]online表单java增强失效------------
|
// 解决[issues/8859]online表单java增强失效------------
|
||||||
// 使用 Apache HttpClient 避免 JDK HttpURLConnection 的 too many bytes written 问题
|
// 使用 Apache HttpClient 避免 JDK HttpURLConnection 的 too many bytes written 问题
|
||||||
HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory();
|
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增强失效------------
|
|
||||||
// 解决乱码问题(替换 StringHttpMessageConverter 为 UTF-8)
|
// 解决乱码问题(替换 StringHttpMessageConverter 为 UTF-8)
|
||||||
for (int i = 0; i < RT.getMessageConverters().size(); i++) {
|
for (int i = 0; i < RT.getMessageConverters().size(); i++) {
|
||||||
if (RT.getMessageConverters().get(i) instanceof StringHttpMessageConverter) {
|
if (RT.getMessageConverters().get(i) instanceof StringHttpMessageConverter) {
|
||||||
@ -71,7 +69,6 @@ public class RestUtil {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//update-end---author:chenrui ---date:20251011 for:[issues/8859]online表单java增强失效------------
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static RestTemplate getRestTemplate() {
|
public static RestTemplate getRestTemplate() {
|
||||||
@ -226,6 +223,16 @@ public class RestUtil {
|
|||||||
if (variables != null && !variables.isEmpty()) {
|
if (variables != null && !variables.isEmpty()) {
|
||||||
url += ("?" + asUrlVariables(variables));
|
url += ("?" + asUrlVariables(variables));
|
||||||
}
|
}
|
||||||
|
// 解决[issues/8951]从jeecgboot 3.8.2 升级到 3.8.3 在线表单java增强功能报错------------
|
||||||
|
// Content-Length 强制设置(解决可能出现的截断问题)
|
||||||
|
if (StringUtils.isNotEmpty(body)) {
|
||||||
|
int contentLength = body.getBytes(StandardCharsets.UTF_8).length;
|
||||||
|
String current = headers.getFirst(HttpHeaders.CONTENT_LENGTH);
|
||||||
|
if (current == null || !current.equals(String.valueOf(contentLength))) {
|
||||||
|
headers.setContentLength(contentLength);
|
||||||
|
log.info(" RestUtil --- request --- 修正/设置 Content-Length = " + contentLength + (current!=null?" (原值="+current+")":""));
|
||||||
|
}
|
||||||
|
}
|
||||||
// 发送请求
|
// 发送请求
|
||||||
HttpEntity<String> request = new HttpEntity<>(body, headers);
|
HttpEntity<String> request = new HttpEntity<>(body, headers);
|
||||||
return RT.exchange(url, method, request, responseType);
|
return RT.exchange(url, method, request, responseType);
|
||||||
@ -260,13 +267,11 @@ public class RestUtil {
|
|||||||
// 创建自定义RestTemplate(如果需要设置超时)
|
// 创建自定义RestTemplate(如果需要设置超时)
|
||||||
RestTemplate restTemplate = RT;
|
RestTemplate restTemplate = RT;
|
||||||
if (timeout > 0) {
|
if (timeout > 0) {
|
||||||
//update-begin---author:chenrui ---date:20251011 for:[issues/8859]online表单java增强失效------------
|
// 代码逻辑说明: [issues/8859]online表单java增强失效------------
|
||||||
HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory();
|
HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory();
|
||||||
//update-end---author:chenrui ---date:20251011 for:[issues/8859]online表单java增强失效------------
|
|
||||||
requestFactory.setConnectTimeout(timeout);
|
requestFactory.setConnectTimeout(timeout);
|
||||||
requestFactory.setReadTimeout(timeout);
|
requestFactory.setReadTimeout(timeout);
|
||||||
restTemplate = new RestTemplate(requestFactory);
|
restTemplate = new RestTemplate(requestFactory);
|
||||||
//update-begin---author:chenrui ---date:20251011 for:[issues/8859]online表单java增强失效------------
|
|
||||||
// 解决乱码问题(替换 StringHttpMessageConverter 为 UTF-8)
|
// 解决乱码问题(替换 StringHttpMessageConverter 为 UTF-8)
|
||||||
for (int i = 0; i < restTemplate.getMessageConverters().size(); i++) {
|
for (int i = 0; i < restTemplate.getMessageConverters().size(); i++) {
|
||||||
if (restTemplate.getMessageConverters().get(i) instanceof StringHttpMessageConverter) {
|
if (restTemplate.getMessageConverters().get(i) instanceof StringHttpMessageConverter) {
|
||||||
@ -274,7 +279,6 @@ public class RestUtil {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//update-end---author:chenrui ---date:20251011 for:[issues/8859]online表单java增强失效------------
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 请求体
|
// 请求体
|
||||||
@ -292,6 +296,16 @@ public class RestUtil {
|
|||||||
url += ("?" + asUrlVariables(variables));
|
url += ("?" + asUrlVariables(variables));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Content-Length 强制设置(解决可能出现的截断问题)
|
||||||
|
if (StringUtils.isNotEmpty(body) && !headers.containsKey(HttpHeaders.CONTENT_LENGTH)) {
|
||||||
|
int contentLength = body.getBytes(StandardCharsets.UTF_8).length;
|
||||||
|
String current = headers.getFirst(HttpHeaders.CONTENT_LENGTH);
|
||||||
|
if (current == null || !current.equals(String.valueOf(contentLength))) {
|
||||||
|
headers.setContentLength(contentLength);
|
||||||
|
log.info(" RestUtil --- request(timeout) --- 修正/设置 Content-Length = " + contentLength + (current!=null?" (原值="+current+")":""));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 发送请求
|
// 发送请求
|
||||||
HttpEntity<String> request = new HttpEntity<>(body, headers);
|
HttpEntity<String> request = new HttpEntity<>(body, headers);
|
||||||
return restTemplate.exchange(url, method, request, responseType);
|
return restTemplate.exchange(url, method, request, responseType);
|
||||||
|
|||||||
@ -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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -335,13 +335,12 @@ public class SqlInjectionUtil {
|
|||||||
return table;
|
return table;
|
||||||
}
|
}
|
||||||
|
|
||||||
//update-begin---author:scott ---date:2024-05-28 for:表单设计器列表翻译存在表名带条件,导致翻译出问题----
|
// 代码逻辑说明: 表单设计器列表翻译存在表名带条件,导致翻译出问题----
|
||||||
int index = table.toLowerCase().indexOf(" where ");
|
int index = table.toLowerCase().indexOf(" where ");
|
||||||
if (index != -1) {
|
if (index != -1) {
|
||||||
table = table.substring(0, index);
|
table = table.substring(0, index);
|
||||||
log.info("截掉where之后的新表名:" + table);
|
log.info("截掉where之后的新表名:" + table);
|
||||||
}
|
}
|
||||||
//update-end---author:scott ---date::2024-05-28 for:表单设计器列表翻译存在表名带条件,导致翻译出问题----
|
|
||||||
|
|
||||||
table = table.trim();
|
table = table.trim();
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -0,0 +1,184 @@
|
|||||||
|
package org.jeecg.common.util;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson.JSONObject;
|
||||||
|
import com.tencentcloudapi.common.Credential;
|
||||||
|
import com.tencentcloudapi.common.exception.TencentCloudSDKException;
|
||||||
|
import com.tencentcloudapi.common.profile.ClientProfile;
|
||||||
|
import com.tencentcloudapi.common.profile.HttpProfile;
|
||||||
|
import com.tencentcloudapi.sms.v20210111.SmsClient;
|
||||||
|
import com.tencentcloudapi.sms.v20210111.models.SendSmsRequest;
|
||||||
|
import com.tencentcloudapi.sms.v20210111.models.SendSmsResponse;
|
||||||
|
import com.tencentcloudapi.sms.v20210111.models.SendStatus;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.jeecg.common.constant.enums.DySmsEnum;
|
||||||
|
import org.jeecg.config.JeecgSmsTemplateConfig;
|
||||||
|
import org.jeecg.config.tencent.JeecgTencent;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Description: 腾讯发送短信
|
||||||
|
* @author: wangshuai
|
||||||
|
* @date: 2025/11/4 19:27
|
||||||
|
*/
|
||||||
|
public class TencentSms {
|
||||||
|
|
||||||
|
private final static Logger logger = LoggerFactory.getLogger(TencentSms.class);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送腾讯短信
|
||||||
|
*
|
||||||
|
* @param phone
|
||||||
|
* @param templateParamJson
|
||||||
|
* @param tencent
|
||||||
|
* @param dySmsEnum
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public static boolean sendTencentSms(String phone, JSONObject templateParamJson, JeecgTencent tencent, DySmsEnum dySmsEnum) {
|
||||||
|
//获取客户端链接
|
||||||
|
SmsClient client = getSmsClient(tencent);
|
||||||
|
//构建腾讯云短信发送请求
|
||||||
|
SendSmsRequest req = buildSendSmsRequest(phone, templateParamJson, dySmsEnum, tencent);
|
||||||
|
try {
|
||||||
|
//发送短信
|
||||||
|
SendSmsResponse resp = client.SendSms(req);
|
||||||
|
// 处理响应
|
||||||
|
SendStatus[] statusSet = resp.getSendStatusSet();
|
||||||
|
if (statusSet != null && statusSet.length > 0) {
|
||||||
|
SendStatus status = statusSet[0];
|
||||||
|
if ("Ok".equals(status.getCode())) {
|
||||||
|
logger.info("短信发送成功,手机号:{}", phone);
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
logger.error("短信发送失败,手机号:{},错误码:{},错误信息:{}",
|
||||||
|
phone, status.getCode(), status.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (TencentCloudSDKException e) {
|
||||||
|
logger.error("短信发送失败{}", e.getMessage());
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取sms客户端
|
||||||
|
*
|
||||||
|
* @param tencent 腾讯云配置
|
||||||
|
* @return SmsClient对象
|
||||||
|
*/
|
||||||
|
private static SmsClient getSmsClient(JeecgTencent tencent) {
|
||||||
|
Credential cred = new Credential(tencent.getSecretId(), tencent.getSecretKey());
|
||||||
|
// 实例化一个http选项,可选的,没有特殊需求可以跳过
|
||||||
|
HttpProfile httpProfile = new HttpProfile();
|
||||||
|
//指定接入地域域名*/
|
||||||
|
httpProfile.setEndpoint(tencent.getEndpoint());
|
||||||
|
//实例化一个客户端配置对象
|
||||||
|
ClientProfile clientProfile = new ClientProfile();
|
||||||
|
clientProfile.setHttpProfile(httpProfile);
|
||||||
|
//实例化要请求产品的client对象,第二个参数是地域信息
|
||||||
|
return new SmsClient(cred, tencent.getRegion(), clientProfile);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建腾讯云短信发送请求
|
||||||
|
*
|
||||||
|
* @param phone 手机号码
|
||||||
|
* @param templateParamJson 模板参数JSON对象
|
||||||
|
* @param dySmsEnum 短信枚举配置
|
||||||
|
* @param tencent 腾讯云配置
|
||||||
|
* @return 构建好的SendSmsRequest对象
|
||||||
|
*/
|
||||||
|
private static SendSmsRequest buildSendSmsRequest(
|
||||||
|
String phone,
|
||||||
|
JSONObject templateParamJson,
|
||||||
|
DySmsEnum dySmsEnum,
|
||||||
|
JeecgTencent tencent) {
|
||||||
|
|
||||||
|
SendSmsRequest req = new SendSmsRequest();
|
||||||
|
|
||||||
|
// 1. 设置短信应用ID
|
||||||
|
String sdkAppId = tencent.getSdkAppId();
|
||||||
|
req.setSmsSdkAppId(sdkAppId);
|
||||||
|
// 2. 设置短信签名
|
||||||
|
String signName = getSmsSignName(dySmsEnum);
|
||||||
|
req.setSignName(signName);
|
||||||
|
// 3. 设置模板ID
|
||||||
|
String templateId = getSmsTemplateId(dySmsEnum);
|
||||||
|
req.setTemplateId(templateId);
|
||||||
|
// 4. 设置模板参数
|
||||||
|
String[] templateParams = extractTemplateParams(templateParamJson);
|
||||||
|
req.setTemplateParamSet(templateParams);
|
||||||
|
// 5. 设置手机号码
|
||||||
|
String[] phoneNumberSet = { phone };
|
||||||
|
req.setPhoneNumberSet(phoneNumberSet);
|
||||||
|
|
||||||
|
logger.debug("构建短信请求完成 - 应用ID: {}, 签名: {}, 模板ID: {}, 手机号: {}",
|
||||||
|
sdkAppId, signName, templateId, phone);
|
||||||
|
|
||||||
|
return req;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取短信签名名称
|
||||||
|
*
|
||||||
|
* @param dySmsEnum 腾讯云对象
|
||||||
|
*/
|
||||||
|
private static String getSmsSignName(DySmsEnum dySmsEnum) {
|
||||||
|
JeecgSmsTemplateConfig baseConfig = SpringContextUtils.getBean(JeecgSmsTemplateConfig.class);
|
||||||
|
String signName = dySmsEnum.getSignName();
|
||||||
|
|
||||||
|
if (StringUtils.isNotEmpty(baseConfig.getSignature())) {
|
||||||
|
logger.debug("yml中读取签名名称: {}", baseConfig.getSignature());
|
||||||
|
signName = baseConfig.getSignature();
|
||||||
|
}
|
||||||
|
|
||||||
|
return signName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取短信模板ID
|
||||||
|
*
|
||||||
|
* @param dySmsEnum 腾讯云对象
|
||||||
|
*/
|
||||||
|
private static String getSmsTemplateId(DySmsEnum dySmsEnum) {
|
||||||
|
JeecgSmsTemplateConfig baseConfig = SpringContextUtils.getBean(JeecgSmsTemplateConfig.class);
|
||||||
|
String templateCode = dySmsEnum.getTemplateCode();
|
||||||
|
|
||||||
|
if (StringUtils.isNotEmpty(baseConfig.getSignature())) {
|
||||||
|
Map<String, String> smsTemplate = baseConfig.getTemplateCode();
|
||||||
|
if (smsTemplate.containsKey(templateCode) &&
|
||||||
|
StringUtils.isNotEmpty(smsTemplate.get(templateCode))) {
|
||||||
|
templateCode = smsTemplate.get(templateCode);
|
||||||
|
logger.debug("yml中读取短信模板ID: {}", templateCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return templateCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从JSONObject中提取模板参数(按原始顺序)
|
||||||
|
*
|
||||||
|
* @param templateParamJson 模板参数
|
||||||
|
*/
|
||||||
|
private static String[] extractTemplateParams(JSONObject templateParamJson) {
|
||||||
|
if (templateParamJson == null || templateParamJson.isEmpty()) {
|
||||||
|
return new String[0];
|
||||||
|
}
|
||||||
|
List<String> params = new ArrayList<>();
|
||||||
|
for (String key : templateParamJson.keySet()) {
|
||||||
|
Object value = templateParamJson.get(key);
|
||||||
|
if (value != null) {
|
||||||
|
params.add(value.toString());
|
||||||
|
} else {
|
||||||
|
// 处理null值
|
||||||
|
params.add("");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logger.debug("提取模板参数: {}", params);
|
||||||
|
return params.toArray(new String[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,6 +1,6 @@
|
|||||||
package org.jeecg.common.util;
|
package org.jeecg.common.util;
|
||||||
|
|
||||||
import cn.dev33.satoken.stp.StpUtil;
|
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;
|
||||||
@ -12,8 +12,6 @@ import org.jeecg.common.exception.JeecgBoot401Exception;
|
|||||||
import org.jeecg.common.system.util.JwtUtil;
|
import org.jeecg.common.system.util.JwtUtil;
|
||||||
import org.jeecg.common.system.vo.LoginUser;
|
import org.jeecg.common.system.vo.LoginUser;
|
||||||
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @Author scott
|
* @Author scott
|
||||||
* @Date 2019/9/23 14:12
|
* @Date 2019/9/23 14:12
|
||||||
@ -88,42 +86,75 @@ public class TokenUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 验证Token(已重写为Sa-Token实现)
|
* 验证Token
|
||||||
*/
|
*/
|
||||||
public static boolean verifyToken(HttpServletRequest request, CommonAPI commonApi) {
|
public static boolean verifyToken(HttpServletRequest request, CommonAPI commonApi, RedisUtil redisUtil) {
|
||||||
log.debug(" -- url --" + request.getRequestURL());
|
log.debug(" -- url --" + request.getRequestURL());
|
||||||
String token = getTokenByRequest(request);
|
String token = getTokenByRequest(request);
|
||||||
return TokenUtils.verifyToken(token, commonApi);
|
return TokenUtils.verifyToken(token, commonApi, redisUtil);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 验证Token(已重写为Sa-Token实现)
|
* 验证Token
|
||||||
*/
|
*/
|
||||||
public static boolean verifyToken(String token, CommonAPI commonApi) {
|
public static boolean verifyToken(String token, CommonAPI commonApi, RedisUtil redisUtil) {
|
||||||
if (StringUtils.isBlank(token)) {
|
if (StringUtils.isBlank(token)) {
|
||||||
throw new JeecgBoot401Exception("token不能为空!");
|
throw new JeecgBoot401Exception("token不能为空!");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 使用Sa-Token校验token
|
// 解密获得username,用于和数据库进行对比
|
||||||
Object username = StpUtil.getLoginIdByToken(token);
|
String username = JwtUtil.getUsername(token);
|
||||||
if (username == null) {
|
if (username == null) {
|
||||||
throw new JeecgBoot401Exception("token非法无效!");
|
throw new JeecgBoot401Exception("token非法无效!");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 查询用户信息
|
// 查询用户信息
|
||||||
LoginUser user = commonApi.getUserByName(username.toString());
|
//LoginUser user = TokenUtils.getLoginUser(username, commonApi, redisUtil);
|
||||||
|
LoginUser user = commonApi.getUserByName(username);
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
throw new JeecgBoot401Exception("用户不存在!");
|
throw new JeecgBoot401Exception("用户不存在!");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 判断用户状态
|
// 判断用户状态
|
||||||
if (user.getStatus() != 1) {
|
if (user.getStatus() != 1) {
|
||||||
throw new JeecgBoot401Exception("账号已被锁定,请联系管理员!");
|
throw new JeecgBoot401Exception("账号已被锁定,请联系管理员!");
|
||||||
}
|
}
|
||||||
|
// 校验token是否超时失效 & 或者账号密码是否错误
|
||||||
|
if (!jwtTokenRefresh(token, username, user.getPassword(), redisUtil)) {
|
||||||
|
// 用户登录Token过期提示信息
|
||||||
|
String userLoginTokenErrorMsg = oConvertUtils.getString(redisUtil.get(CommonConstant.PREFIX_USER_TOKEN_ERROR_MSG + token));
|
||||||
|
throw new JeecgBoot401Exception(oConvertUtils.isEmpty(userLoginTokenErrorMsg)? CommonConstant.TOKEN_IS_INVALID_MSG: userLoginTokenErrorMsg);
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 刷新token(保证用户在线操作不掉线)
|
||||||
|
* @param token
|
||||||
|
* @param userName
|
||||||
|
* @param passWord
|
||||||
|
* @param redisUtil
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
private static boolean jwtTokenRefresh(String token, String userName, String passWord, RedisUtil redisUtil) {
|
||||||
|
String cacheToken = oConvertUtils.getString(redisUtil.get(CommonConstant.PREFIX_USER_TOKEN + token));
|
||||||
|
if (oConvertUtils.isNotEmpty(cacheToken)) {
|
||||||
|
// 校验token有效性
|
||||||
|
if (!JwtUtil.verify(cacheToken, userName, passWord)) {
|
||||||
|
// 从token中解析客户端类型,保持续期时使用相同的客户端类型
|
||||||
|
String clientType = JwtUtil.getClientType(token);
|
||||||
|
String newAuthorization = JwtUtil.sign(userName, passWord, clientType);
|
||||||
|
// 根据客户端类型设置对应的缓存有效时间
|
||||||
|
long expireTime = CommonConstant.CLIENT_TYPE_APP.equalsIgnoreCase(clientType)
|
||||||
|
? JwtUtil.APP_EXPIRE_TIME * 2 / 1000
|
||||||
|
: JwtUtil.EXPIRE_TIME * 2 / 1000;
|
||||||
|
redisUtil.set(CommonConstant.PREFIX_USER_TOKEN + token, newAuthorization);
|
||||||
|
redisUtil.expire(CommonConstant.PREFIX_USER_TOKEN + token, expireTime);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取登录用户
|
* 获取登录用户
|
||||||
*
|
*
|
||||||
@ -137,10 +168,11 @@ public class TokenUtils {
|
|||||||
//【重要】此处通过redis原生获取缓存用户,是为了解决微服务下system服务挂了,其他服务互调不通问题---
|
//【重要】此处通过redis原生获取缓存用户,是为了解决微服务下system服务挂了,其他服务互调不通问题---
|
||||||
if (redisUtil.hasKey(loginUserKey)) {
|
if (redisUtil.hasKey(loginUserKey)) {
|
||||||
try {
|
try {
|
||||||
loginUser = (LoginUser) redisUtil.get(loginUserKey);
|
Object obj = redisUtil.get(loginUserKey);
|
||||||
|
loginUser = (LoginUser) obj;
|
||||||
//解密用户
|
//解密用户
|
||||||
SensitiveInfoUtil.handlerObject(loginUser, false);
|
SensitiveInfoUtil.handlerObject(loginUser, false);
|
||||||
} catch (IllegalAccessException e) {
|
} catch (Exception e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -149,5 +181,4 @@ public class TokenUtils {
|
|||||||
}
|
}
|
||||||
return loginUser;
|
return loginUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -54,11 +54,10 @@ public class FreemarkerParseFactory {
|
|||||||
//classic_compatible设置,解决报空指针错误
|
//classic_compatible设置,解决报空指针错误
|
||||||
SQL_CONFIG.setClassicCompatible(true);
|
SQL_CONFIG.setClassicCompatible(true);
|
||||||
|
|
||||||
//update-begin-author:taoyan date:2022-8-10 for: freemarker模板注入问题 禁止解析ObjectConstructor,Execute和freemarker.template.utility.JythonRuntime。
|
// 解决freemarker模板注入问题 禁止解析ObjectConstructor,Execute和freemarker.template.utility.JythonRuntime。
|
||||||
//https://ackcent.com/in-depth-freemarker-template-injection/
|
//https://ackcent.com/in-depth-freemarker-template-injection/
|
||||||
TPL_CONFIG.setNewBuiltinClassResolver(TemplateClassResolver.SAFER_RESOLVER);
|
TPL_CONFIG.setNewBuiltinClassResolver(TemplateClassResolver.SAFER_RESOLVER);
|
||||||
SQL_CONFIG.setNewBuiltinClassResolver(TemplateClassResolver.SAFER_RESOLVER);
|
SQL_CONFIG.setNewBuiltinClassResolver(TemplateClassResolver.SAFER_RESOLVER);
|
||||||
//update-end-author:taoyan date:2022-8-10 for: freemarker模板注入问题 禁止解析ObjectConstructor,Execute和freemarker.template.utility.JythonRuntime。
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -73,13 +72,12 @@ public class FreemarkerParseFactory {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
//update-begin--Author:scott Date:20180320 for:解决问题 - 错误提示sql文件不存在,实际问题是sql freemarker用法错误-----
|
// 代码逻辑说明: 解决问题 - 错误提示sql文件不存在,实际问题是sql freemarker用法错误-----
|
||||||
if (e instanceof ParseException) {
|
if (e instanceof ParseException) {
|
||||||
log.error(e.getMessage(), e.fillInStackTrace());
|
log.error(e.getMessage(), e.fillInStackTrace());
|
||||||
throw new Exception(e);
|
throw new Exception(e);
|
||||||
}
|
}
|
||||||
log.debug("----isExistTemplate----" + e.toString());
|
log.debug("----isExistTemplate----" + e.toString());
|
||||||
//update-end--Author:scott Date:20180320 for:解决问题 - 错误提示sql文件不存在,实际问题是sql freemarker用法错误------
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@ -1,123 +1,107 @@
|
|||||||
package org.jeecg.common.util.encryption;
|
package org.jeecg.common.util.encryption;
|
||||||
|
|
||||||
import java.util.Base64;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.jeecg.common.util.oConvertUtils;
|
||||||
import javax.crypto.Cipher;
|
import javax.crypto.Cipher;
|
||||||
import javax.crypto.spec.IvParameterSpec;
|
import javax.crypto.spec.IvParameterSpec;
|
||||||
import javax.crypto.spec.SecretKeySpec;
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.Base64;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @Description: AES 加密
|
* AES 工具 (兼容历史 NoPadding + 新 PKCS5Padding)
|
||||||
* @author: jeecg-boot
|
|
||||||
* @date: 2022/3/30 11:48
|
|
||||||
*/
|
*/
|
||||||
|
@Slf4j
|
||||||
public class AesEncryptUtil {
|
public class AesEncryptUtil {
|
||||||
|
|
||||||
/**
|
private static final String KEY = EncryptedString.key;
|
||||||
* 使用AES-128-CBC加密模式 key和iv可以相同
|
private static final String IV = EncryptedString.iv;
|
||||||
*/
|
|
||||||
private static String KEY = EncryptedString.key;
|
|
||||||
private static String IV = EncryptedString.iv;
|
|
||||||
|
|
||||||
/**
|
/* -------- 新版:CBC + PKCS5Padding (与前端 CryptoJS Pkcs7 兼容) -------- */
|
||||||
* 加密方法
|
private static String decryptPkcs5(String cipherBase64) throws Exception {
|
||||||
* @param data 要加密的数据
|
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
|
||||||
* @param key 加密key
|
SecretKeySpec ks = new SecretKeySpec(KEY.getBytes(StandardCharsets.UTF_8), "AES");
|
||||||
* @param iv 加密iv
|
IvParameterSpec ivSpec = new IvParameterSpec(IV.getBytes(StandardCharsets.UTF_8));
|
||||||
* @return 加密的结果
|
cipher.init(Cipher.DECRYPT_MODE, ks, ivSpec);
|
||||||
* @throws Exception
|
byte[] plain = cipher.doFinal(Base64.getDecoder().decode(cipherBase64));
|
||||||
*/
|
return new String(plain, StandardCharsets.UTF_8);
|
||||||
public static String encrypt(String data, String key, String iv) throws Exception {
|
}
|
||||||
try {
|
|
||||||
|
|
||||||
//"算法/模式/补码方式"NoPadding PkcsPadding
|
/* -------- 旧版:CBC + NoPadding (手工补 0) -------- */
|
||||||
|
private static String decryptLegacyNoPadding(String cipherBase64) throws Exception {
|
||||||
|
Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
|
||||||
|
SecretKeySpec ks = new SecretKeySpec(KEY.getBytes(StandardCharsets.UTF_8), "AES");
|
||||||
|
IvParameterSpec ivSpec = new IvParameterSpec(IV.getBytes(StandardCharsets.UTF_8));
|
||||||
|
cipher.init(Cipher.DECRYPT_MODE, ks, ivSpec);
|
||||||
|
byte[] data = cipher.doFinal(Base64.getDecoder().decode(cipherBase64));
|
||||||
|
return new String(data, StandardCharsets.UTF_8)
|
||||||
|
.replace("\u0000",""); // 旧填充 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------- 兼容入口:登录使用 -------- */
|
||||||
|
public static String resolvePassword(String input){
|
||||||
|
if(oConvertUtils.isEmpty(input)){
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
// 1. 先尝试新版
|
||||||
|
try{
|
||||||
|
String p = decryptPkcs5(input);
|
||||||
|
return clean(p);
|
||||||
|
}catch(Exception ignore){
|
||||||
|
//log.debug("【AES解密】Password not AES PKCS5 cipher, try legacy.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 回退旧版
|
||||||
|
try{
|
||||||
|
String legacy = decryptLegacyNoPadding(input);
|
||||||
|
return clean(legacy);
|
||||||
|
}catch(Exception e){
|
||||||
|
log.debug("【AES解密】Password not AES cipher, raw used.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 视为明文
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------- 可选:统一清理尾部不可见控制字符 -------- */
|
||||||
|
private static String clean(String s){
|
||||||
|
if(s==null) return null;
|
||||||
|
// 去除结尾控制符/空白(不影响中间合法空格)
|
||||||
|
return s.replaceAll("[\\p{Cntrl}]+","").trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------- 若仍需要旧接口,可保留 (不建议再用于新前端) -------- */
|
||||||
|
@Deprecated
|
||||||
|
public static String desEncrypt(String data) throws Exception {
|
||||||
|
return decryptLegacyNoPadding(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 加密(若前端不再使用,可忽略;保留旧实现避免影响历史) */
|
||||||
|
@Deprecated
|
||||||
|
public static String encrypt(String data) throws Exception {
|
||||||
|
try{
|
||||||
Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
|
Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
|
||||||
int blockSize = cipher.getBlockSize();
|
int blockSize = cipher.getBlockSize();
|
||||||
|
byte[] dataBytes = data.getBytes(StandardCharsets.UTF_8);
|
||||||
byte[] dataBytes = data.getBytes();
|
|
||||||
int plaintextLength = dataBytes.length;
|
int plaintextLength = dataBytes.length;
|
||||||
if (plaintextLength % blockSize != 0) {
|
if (plaintextLength % blockSize != 0) {
|
||||||
plaintextLength = plaintextLength + (blockSize - (plaintextLength % blockSize));
|
plaintextLength += (blockSize - (plaintextLength % blockSize));
|
||||||
}
|
}
|
||||||
|
|
||||||
byte[] plaintext = new byte[plaintextLength];
|
byte[] plaintext = new byte[plaintextLength];
|
||||||
System.arraycopy(dataBytes, 0, plaintext, 0, dataBytes.length);
|
System.arraycopy(dataBytes, 0, plaintext, 0, dataBytes.length);
|
||||||
|
SecretKeySpec keyspec = new SecretKeySpec(KEY.getBytes(StandardCharsets.UTF_8), "AES");
|
||||||
SecretKeySpec keyspec = new SecretKeySpec(key.getBytes(), "AES");
|
IvParameterSpec ivspec = new IvParameterSpec(IV.getBytes(StandardCharsets.UTF_8));
|
||||||
IvParameterSpec ivspec = new IvParameterSpec(iv.getBytes());
|
|
||||||
|
|
||||||
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.getEncoder().encodeToString(encrypted);
|
return Base64.getEncoder().encodeToString(encrypted);
|
||||||
|
}catch(Exception e){
|
||||||
} catch (Exception e) {
|
throw new IllegalStateException("legacy encrypt error", e);
|
||||||
e.printStackTrace();
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// public static void main(String[] args) throws Exception {
|
||||||
* 解密方法
|
// // 前端 CBC/Pkcs7 密文测试
|
||||||
* @param data 要解密的数据
|
// String frontCipher = encrypt("sa"); // 仅验证管道是否可用(旧方式)
|
||||||
* @param key 解密key
|
// System.out.println(resolvePassword(frontCipher));
|
||||||
* @param iv 解密iv
|
|
||||||
* @return 解密的结果
|
|
||||||
* @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、解密报错 ---解码失败应该把异常抛出去,在外面处理
|
|
||||||
byte[] encrypted1 = Base64.getDecoder().decode(data);
|
|
||||||
|
|
||||||
Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
|
|
||||||
SecretKeySpec keyspec = new SecretKeySpec(key.getBytes(), "AES");
|
|
||||||
IvParameterSpec ivspec = new IvParameterSpec(iv.getBytes());
|
|
||||||
|
|
||||||
cipher.init(Cipher.DECRYPT_MODE, keyspec, ivspec);
|
|
||||||
|
|
||||||
byte[] original = cipher.doFinal(encrypted1);
|
|
||||||
String originalString = new String(original);
|
|
||||||
//加密解码后的字符串会出现\u0000
|
|
||||||
return originalString.replaceAll("\\u0000", "");
|
|
||||||
//update-end-author:taoyan date:2022-5-23 for:VUEN-1084 【vue3】online表单测试发现的新问题 6、解密报错 ---解码失败应该把异常抛出去,在外面处理
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 使用默认的key和iv加密
|
|
||||||
* @param data
|
|
||||||
* @return
|
|
||||||
* @throws Exception
|
|
||||||
*/
|
|
||||||
public static String encrypt(String data) throws Exception {
|
|
||||||
return encrypt(data, KEY, IV);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 使用默认的key和iv解密
|
|
||||||
* @param data
|
|
||||||
* @return
|
|
||||||
* @throws Exception
|
|
||||||
*/
|
|
||||||
public static String desEncrypt(String data) throws Exception {
|
|
||||||
return desEncrypt(data, KEY, IV);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// /**
|
|
||||||
// * 测试
|
|
||||||
// */
|
|
||||||
// public static void main(String args[]) throws Exception {
|
|
||||||
// String test1 = "sa";
|
|
||||||
// String test =new String(test1.getBytes(),"UTF-8");
|
|
||||||
// String data = null;
|
|
||||||
// String key = KEY;
|
|
||||||
// String iv = IV;
|
|
||||||
// // /g2wzfqvMOeazgtsUVbq1kmJawROa6mcRAzwG1/GeJ4=
|
|
||||||
// data = encrypt(test, key, iv);
|
|
||||||
// System.out.println("数据:"+test);
|
|
||||||
// System.out.println("加密:"+data);
|
|
||||||
// String jiemi =desEncrypt(data, key, iv).trim();
|
|
||||||
// System.out.println("解密:"+jiemi);
|
|
||||||
// }
|
// }
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -194,7 +194,7 @@ public class SsrfFileTypeFilter {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
private static String getFileType(MultipartFile file, String customPath) throws Exception {
|
private static String getFileType(MultipartFile file, String customPath) throws Exception {
|
||||||
//update-begin-author:liusq date:20230404 for: [issue/4672]方法造成的文件被占用,注释掉此方法tomcat就能自动清理掉临时文件
|
// 代码逻辑说明: [issue/4672]方法造成的文件被占用,注释掉此方法tomcat就能自动清理掉临时文件
|
||||||
String fileExtendName = null;
|
String fileExtendName = null;
|
||||||
InputStream is = null;
|
InputStream is = null;
|
||||||
try {
|
try {
|
||||||
@ -234,7 +234,6 @@ public class SsrfFileTypeFilter {
|
|||||||
is.close();
|
is.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//update-end-author:liusq date:20230404 for: [issue/4672]方法造成的文件被占用,注释掉此方法tomcat就能自动清理掉临时文件
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -11,6 +11,10 @@ import org.jeecg.config.mybatis.MybatisPlusSaasConfig;
|
|||||||
import org.springframework.beans.BeanUtils;
|
import org.springframework.beans.BeanUtils;
|
||||||
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import org.springframework.beans.BeanWrapper;
|
||||||
|
import org.springframework.beans.BeanWrapperImpl;
|
||||||
|
|
||||||
|
import java.beans.PropertyDescriptor;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.UnsupportedEncodingException;
|
import java.io.UnsupportedEncodingException;
|
||||||
@ -23,6 +27,7 @@ import java.sql.Date;
|
|||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@ -563,10 +568,8 @@ public class oConvertUtils {
|
|||||||
return "";
|
return "";
|
||||||
} else if (!name.contains(SymbolConstant.UNDERLINE)) {
|
} else if (!name.contains(SymbolConstant.UNDERLINE)) {
|
||||||
// 不含下划线,仅将首字母小写
|
// 不含下划线,仅将首字母小写
|
||||||
//update-begin--Author:zhoujf Date:20180503 for:TASK #2500 【代码生成器】代码生成器开发一通用模板生成功能
|
// 代码逻辑说明: TASK #2500 【代码生成器】代码生成器开发一通用模板生成功能
|
||||||
//update-begin--Author:zhoujf Date:20180503 for:TASK #2500 【代码生成器】代码生成器开发一通用模板生成功能
|
|
||||||
return name.substring(0, 1).toLowerCase() + name.substring(1).toLowerCase();
|
return name.substring(0, 1).toLowerCase() + name.substring(1).toLowerCase();
|
||||||
//update-end--Author:zhoujf Date:20180503 for:TASK #2500 【代码生成器】代码生成器开发一通用模板生成功能
|
|
||||||
}
|
}
|
||||||
// 用下划线将原始字符串分割
|
// 用下划线将原始字符串分割
|
||||||
String[] camels = name.split("_");
|
String[] camels = name.split("_");
|
||||||
@ -611,7 +614,6 @@ public class oConvertUtils {
|
|||||||
return result.substring(0, result.length() - 1);
|
return result.substring(0, result.length() - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
//update-begin--Author:zhoujf Date:20180503 for:TASK #2500 【代码生成器】代码生成器开发一通用模板生成功能
|
|
||||||
/**
|
/**
|
||||||
* 将下划线大写方式命名的字符串转换为驼峰式。(首字母写)
|
* 将下划线大写方式命名的字符串转换为驼峰式。(首字母写)
|
||||||
* 如果转换前的下划线大写方式命名的字符串为空,则返回空字符串。</br>
|
* 如果转换前的下划线大写方式命名的字符串为空,则返回空字符串。</br>
|
||||||
@ -644,7 +646,6 @@ public class oConvertUtils {
|
|||||||
}
|
}
|
||||||
return result.toString();
|
return result.toString();
|
||||||
}
|
}
|
||||||
//update-end--Author:zhoujf Date:20180503 for:TASK #2500 【代码生成器】代码生成器开发一通用模板生成功能
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 将驼峰命名转化成下划线
|
* 将驼峰命名转化成下划线
|
||||||
@ -982,17 +983,18 @@ public class oConvertUtils {
|
|||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 判断 list1中的元素是否在list2中出现
|
* 判断 sourceList中的元素是否在targetList中出现
|
||||||
|
*
|
||||||
* QQYUN-5326【简流】获取组织人员 单/多 筛选条件 没有部门筛选
|
* QQYUN-5326【简流】获取组织人员 单/多 筛选条件 没有部门筛选
|
||||||
* @param list1
|
* @param sourceList 源列表,要检查的元素列表
|
||||||
* @param list2
|
* @param targetList 目标列表,用于匹配的列表
|
||||||
* @return
|
* @return 如果sourceList中有任何元素在targetList中存在则返回true,否则返回false
|
||||||
*/
|
*/
|
||||||
public static boolean isInList(List<String> list1, List<String> list2){
|
public static boolean isInList(List<String> sourceList, List<String> targetList){
|
||||||
for(String str1: list1){
|
for(String sourceItem: sourceList){
|
||||||
boolean flag = false;
|
boolean flag = false;
|
||||||
for(String str2: list2){
|
for(String targetItem: targetList){
|
||||||
if(str1.equals(str2)){
|
if(sourceItem.equals(targetItem)){
|
||||||
flag = true;
|
flag = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -1004,6 +1006,35 @@ public class oConvertUtils {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断 sourceList中的所有元素是否都在targetList中存在
|
||||||
|
* @param sourceList 源列表,要检查的元素列表
|
||||||
|
* @param targetList 目标列表,用于匹配的列表
|
||||||
|
* @return 如果sourceList中的所有元素都在targetList中存在则返回true,否则返回false
|
||||||
|
*/
|
||||||
|
public static boolean isAllInList(List<String> sourceList, List<String> targetList){
|
||||||
|
if(sourceList == null || sourceList.isEmpty()){
|
||||||
|
return true; // 空列表视为所有元素都存在
|
||||||
|
}
|
||||||
|
if(targetList == null || targetList.isEmpty()){
|
||||||
|
return false; // 目标列表为空,源列表非空时返回false
|
||||||
|
}
|
||||||
|
|
||||||
|
for(String sourceItem: sourceList){
|
||||||
|
boolean found = false;
|
||||||
|
for(String targetItem: targetList){
|
||||||
|
if(sourceItem.equals(targetItem)){
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(!found){
|
||||||
|
return false; // 有任何一个元素不在目标列表中,返回false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true; // 所有元素都找到了
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 计算文件大小转成MB
|
* 计算文件大小转成MB
|
||||||
* @param uploadCount
|
* @param uploadCount
|
||||||
@ -1169,4 +1200,36 @@ public class oConvertUtils {
|
|||||||
return MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL && isNotEmpty(tenantId) && !("0").equals(tenantId);
|
return MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL && isNotEmpty(tenantId) && !("0").equals(tenantId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 复制源对象的非空属性到目标对象(同名属性)
|
||||||
|
*
|
||||||
|
* @param source 源对象(页面)
|
||||||
|
* @param target 目标对象(数据库实体)
|
||||||
|
*/
|
||||||
|
public static void copyNonNullFields(Object source, Object target) {
|
||||||
|
if (source == null || target == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 获取源对象的非空属性名数组
|
||||||
|
String[] nullPropertyNames = getNullPropertyNames(source);
|
||||||
|
// 复制:忽略源对象的空属性,仅覆盖目标对象的对应非空属性
|
||||||
|
BeanUtils.copyProperties(source, target, nullPropertyNames);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取源对象中值为 null 的属性名数组
|
||||||
|
*
|
||||||
|
* @param source
|
||||||
|
*/
|
||||||
|
private static String[] getNullPropertyNames(Object source) {
|
||||||
|
BeanWrapper beanWrapper = new BeanWrapperImpl(source);
|
||||||
|
//获取类的属性
|
||||||
|
PropertyDescriptor[] propertyDescriptors = beanWrapper.getPropertyDescriptors();
|
||||||
|
// 过滤出值为 null 的属性名
|
||||||
|
return Stream.of(propertyDescriptors)
|
||||||
|
.map(PropertyDescriptor::getName)
|
||||||
|
.filter(name -> beanWrapper.getPropertyValue(name) == null)
|
||||||
|
.toArray(String[]::new);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -124,9 +124,8 @@ public class OssBootUtil {
|
|||||||
if (!fileDir.endsWith(SymbolConstant.SINGLE_SLASH)) {
|
if (!fileDir.endsWith(SymbolConstant.SINGLE_SLASH)) {
|
||||||
fileDir = fileDir.concat(SymbolConstant.SINGLE_SLASH);
|
fileDir = fileDir.concat(SymbolConstant.SINGLE_SLASH);
|
||||||
}
|
}
|
||||||
//update-begin-author:wangshuai date:20201012 for: 过滤上传文件夹名特殊字符,防止攻击
|
// 代码逻辑说明: 过滤上传文件夹名特殊字符,防止攻击
|
||||||
fileDir=StrAttackFilter.filter(fileDir);
|
fileDir=StrAttackFilter.filter(fileDir);
|
||||||
//update-end-author:wangshuai date:20201012 for: 过滤上传文件夹名特殊字符,防止攻击
|
|
||||||
fileUrl = fileUrl.append(fileDir + fileName);
|
fileUrl = fileUrl.append(fileDir + fileName);
|
||||||
|
|
||||||
if (oConvertUtils.isNotEmpty(staticDomain) && staticDomain.toLowerCase().startsWith(CommonConstant.STR_HTTP)) {
|
if (oConvertUtils.isNotEmpty(staticDomain) && staticDomain.toLowerCase().startsWith(CommonConstant.STR_HTTP)) {
|
||||||
@ -263,9 +262,8 @@ public class OssBootUtil {
|
|||||||
newBucket = bucket;
|
newBucket = bucket;
|
||||||
}
|
}
|
||||||
initOss(endPoint, accessKeyId, accessKeySecret);
|
initOss(endPoint, accessKeyId, accessKeySecret);
|
||||||
//update-begin---author:liusq Date:20220120 for:替换objectName前缀,防止key不一致导致获取不到文件----
|
// 代码逻辑说明: 替换objectName前缀,防止key不一致导致获取不到文件----
|
||||||
objectName = OssBootUtil.replacePrefix(objectName,bucket);
|
objectName = OssBootUtil.replacePrefix(objectName,bucket);
|
||||||
//update-end---author:liusq Date:20220120 for:替换objectName前缀,防止key不一致导致获取不到文件----
|
|
||||||
OSSObject ossObject = ossClient.getObject(newBucket,objectName);
|
OSSObject ossObject = ossClient.getObject(newBucket,objectName);
|
||||||
inputStream = new BufferedInputStream(ossObject.getObjectContent());
|
inputStream = new BufferedInputStream(ossObject.getObjectContent());
|
||||||
}catch (Exception e){
|
}catch (Exception e){
|
||||||
@ -293,9 +291,8 @@ public class OssBootUtil {
|
|||||||
public static String getObjectUrl(String bucketName, String objectName, Date expires) {
|
public static String getObjectUrl(String bucketName, String objectName, Date expires) {
|
||||||
initOss(endPoint, accessKeyId, accessKeySecret);
|
initOss(endPoint, accessKeyId, accessKeySecret);
|
||||||
try{
|
try{
|
||||||
//update-begin---author:liusq Date:20220120 for:替换objectName前缀,防止key不一致导致获取不到文件----
|
// 代码逻辑说明: 替换objectName前缀,防止key不一致导致获取不到文件----
|
||||||
objectName = OssBootUtil.replacePrefix(objectName,bucketName);
|
objectName = OssBootUtil.replacePrefix(objectName,bucketName);
|
||||||
//update-end---author:liusq Date:20220120 for:替换objectName前缀,防止key不一致导致获取不到文件----
|
|
||||||
if(ossClient.doesObjectExist(bucketName,objectName)){
|
if(ossClient.doesObjectExist(bucketName,objectName)){
|
||||||
URL url = ossClient.generatePresignedUrl(bucketName,objectName,expires);
|
URL url = ossClient.generatePresignedUrl(bucketName,objectName,expires);
|
||||||
//log.info("原始url : {}", url.toString());
|
//log.info("原始url : {}", url.toString());
|
||||||
|
|||||||
@ -63,7 +63,7 @@ public abstract class AbstractQueryBlackListHandler {
|
|||||||
if(list==null){
|
if(list==null){
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
log.info(" 获取sql信息 :{} ", list.toString());
|
log.debug(" 获取sql信息 :{} ", list.toString());
|
||||||
boolean flag = checkTableAndFieldsName(list);
|
boolean flag = checkTableAndFieldsName(list);
|
||||||
if(flag == false){
|
if(flag == false){
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@ -60,17 +60,15 @@ public class AutoPoiDictConfig implements AutoPoiDictServiceI {
|
|||||||
|
|
||||||
|
|
||||||
for (DictModel t : dictList) {
|
for (DictModel t : dictList) {
|
||||||
//update-begin---author:liusq Date:20230517 for:[issues/4917]excel 导出异常---
|
// 代码逻辑说明: [issues/4917]excel 导出异常---
|
||||||
if(t!=null && t.getText()!=null && t.getValue()!=null){
|
if(t!=null && t.getText()!=null && t.getValue()!=null){
|
||||||
//update-end---author:liusq Date:20230517 for:[issues/4917]excel 导出异常---
|
// 代码逻辑说明: [issues/I4MBB3]@Excel dicText字段的值有下划线时,导入功能不能正确解析---
|
||||||
//update-begin---author:scott Date:20211220 for:[issues/I4MBB3]@Excel dicText字段的值有下划线时,导入功能不能正确解析---
|
|
||||||
if(t.getValue().contains(EXCEL_SPLIT_TAG)){
|
if(t.getValue().contains(EXCEL_SPLIT_TAG)){
|
||||||
String val = t.getValue().replace(EXCEL_SPLIT_TAG,TEMP_EXCEL_SPLIT_TAG);
|
String val = t.getValue().replace(EXCEL_SPLIT_TAG,TEMP_EXCEL_SPLIT_TAG);
|
||||||
dictReplaces.add(t.getText() + EXCEL_SPLIT_TAG + val);
|
dictReplaces.add(t.getText() + EXCEL_SPLIT_TAG + val);
|
||||||
}else{
|
}else{
|
||||||
dictReplaces.add(t.getText() + EXCEL_SPLIT_TAG + t.getValue());
|
dictReplaces.add(t.getText() + EXCEL_SPLIT_TAG + t.getValue());
|
||||||
}
|
}
|
||||||
//update-end---author:20211220 Date:20211220 for:[issues/I4MBB3]@Excel dicText字段的值有下划线时,导入功能不能正确解析---
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (dictReplaces != null && dictReplaces.size() != 0) {
|
if (dictReplaces != null && dictReplaces.size() != 0) {
|
||||||
|
|||||||
@ -1,9 +1,11 @@
|
|||||||
package org.jeecg.config;
|
package org.jeecg.config;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
import org.jeecg.config.tencent.JeecgTencent;
|
||||||
import org.jeecg.config.vo.*;
|
import org.jeecg.config.vo.*;
|
||||||
import org.springframework.beans.factory.config.BeanDefinition;
|
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.context.annotation.Role;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
@ -40,10 +42,6 @@ public class JeecgBaseConfig {
|
|||||||
*/
|
*/
|
||||||
private Firewall firewall;
|
private Firewall firewall;
|
||||||
|
|
||||||
/**
|
|
||||||
* shiro拦截排除
|
|
||||||
*/
|
|
||||||
private Shiro shiro;
|
|
||||||
/**
|
/**
|
||||||
* 上传文件配置
|
* 上传文件配置
|
||||||
*/
|
*/
|
||||||
@ -76,6 +74,34 @@ public class JeecgBaseConfig {
|
|||||||
*/
|
*/
|
||||||
private BaiduApi baiduApi;
|
private BaiduApi baiduApi;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* minio配置
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
private JeecgMinio minio;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* oss配置
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
private JeecgOSS oss;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 短信发送方式 aliyun阿里云短信 tencent腾讯云短信
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
private String smsSendType = "aliyun";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 腾讯配置
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
private JeecgTencent tencent;
|
||||||
|
|
||||||
public String getCustomResourcePrefixPath() {
|
public String getCustomResourcePrefixPath() {
|
||||||
return customResourcePrefixPath;
|
return customResourcePrefixPath;
|
||||||
}
|
}
|
||||||
@ -108,14 +134,6 @@ public class JeecgBaseConfig {
|
|||||||
this.signatureSecret = signatureSecret;
|
this.signatureSecret = signatureSecret;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Shiro getShiro() {
|
|
||||||
return shiro;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setShiro(Shiro shiro) {
|
|
||||||
this.shiro = shiro;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Path getPath() {
|
public Path getPath() {
|
||||||
return path;
|
return path;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -95,13 +95,11 @@
|
|||||||
// List<Parameter> pars = new ArrayList<>();
|
// List<Parameter> pars = new ArrayList<>();
|
||||||
// tokenPar.name(CommonConstant.X_ACCESS_TOKEN).description("token").modelRef(new ModelRef("string")).parameterType("header").required(false).build();
|
// tokenPar.name(CommonConstant.X_ACCESS_TOKEN).description("token").modelRef(new ModelRef("string")).parameterType("header").required(false).build();
|
||||||
// pars.add(tokenPar.build());
|
// pars.add(tokenPar.build());
|
||||||
// //update-begin-author:liusq---date:2024-08-15--for: 开启多租户时,全局参数增加租户id
|
|
||||||
// if(MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL){
|
// if(MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL){
|
||||||
// ParameterBuilder tenantPar = new ParameterBuilder();
|
// ParameterBuilder tenantPar = new ParameterBuilder();
|
||||||
// tenantPar.name(CommonConstant.TENANT_ID).description("租户ID").modelRef(new ModelRef("string")).parameterType("header").required(false).build();
|
// tenantPar.name(CommonConstant.TENANT_ID).description("租户ID").modelRef(new ModelRef("string")).parameterType("header").required(false).build();
|
||||||
// pars.add(tenantPar.build());
|
// pars.add(tenantPar.build());
|
||||||
// }
|
// }
|
||||||
// //update-end-author:liusq---date:2024-08-15--for: 开启多租户时,全局参数增加租户id
|
|
||||||
//
|
//
|
||||||
// return pars;
|
// return pars;
|
||||||
// }
|
// }
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import lombok.extern.slf4j.Slf4j;
|
|||||||
import org.jeecg.common.constant.CommonConstant;
|
import org.jeecg.common.constant.CommonConstant;
|
||||||
import org.springdoc.core.customizers.OperationCustomizer;
|
import org.springdoc.core.customizers.OperationCustomizer;
|
||||||
import org.springdoc.core.filters.GlobalOpenApiMethodFilter;
|
import org.springdoc.core.filters.GlobalOpenApiMethodFilter;
|
||||||
|
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.PropertySource;
|
import org.springframework.context.annotation.PropertySource;
|
||||||
@ -22,18 +23,24 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
|||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author eightmonth
|
* @author eightmonth
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Configuration
|
@Configuration
|
||||||
|
@ConditionalOnProperty(prefix = "knife4j", name = "production", havingValue = "false", matchIfMissing = true)
|
||||||
@PropertySource("classpath:config/default-spring-doc.properties")
|
@PropertySource("classpath:config/default-spring-doc.properties")
|
||||||
public class Swagger3Config implements WebMvcConfigurer {
|
public class Swagger3Config implements WebMvcConfigurer {
|
||||||
|
|
||||||
|
// 路径匹配结果缓存,避免重复计算
|
||||||
|
private static final Map<String, Boolean> EXCLUDED_PATHS_CACHE = new ConcurrentHashMap<>();
|
||||||
// 定义不需要注入安全要求的路径集合
|
// 定义不需要注入安全要求的路径集合
|
||||||
Set<String> excludedPaths = new HashSet<>(Arrays.asList(
|
private static final Set<String> excludedPaths = new HashSet<>(Arrays.asList(
|
||||||
"/sys/randomImage/{key}",
|
"/sys/randomImage/**",
|
||||||
"/sys/login",
|
"/sys/login",
|
||||||
"/sys/phoneLogin",
|
"/sys/phoneLogin",
|
||||||
"/sys/mLogin",
|
"/sys/mLogin",
|
||||||
@ -43,6 +50,19 @@ public class Swagger3Config implements WebMvcConfigurer {
|
|||||||
"/sys/thirdLogin/**",
|
"/sys/thirdLogin/**",
|
||||||
"/sys/user/register"
|
"/sys/user/register"
|
||||||
));
|
));
|
||||||
|
// 预处理通配符模式,提高匹配效率
|
||||||
|
private static final Set<String> wildcardPatterns = new HashSet<>();
|
||||||
|
private static final Set<String> exactPatterns = new HashSet<>();
|
||||||
|
static {
|
||||||
|
// 初始化时分离精确匹配和通配符匹配
|
||||||
|
for (String pattern : excludedPaths) {
|
||||||
|
if (pattern.endsWith("/**")) {
|
||||||
|
wildcardPatterns.add(pattern.substring(0, pattern.length() - 3));
|
||||||
|
} else {
|
||||||
|
exactPatterns.add(pattern);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@ -100,15 +120,14 @@ public class Swagger3Config implements WebMvcConfigurer {
|
|||||||
|
|
||||||
|
|
||||||
private boolean isExcludedPath(String path) {
|
private boolean isExcludedPath(String path) {
|
||||||
return excludedPaths.stream()
|
// 使用缓存避免重复计算
|
||||||
.anyMatch(pattern -> {
|
return EXCLUDED_PATHS_CACHE.computeIfAbsent(path, p -> {
|
||||||
if (pattern.endsWith("/**")) {
|
|
||||||
// 处理通配符匹配
|
|
||||||
String basePath = pattern.substring(0, pattern.length() - 3);
|
|
||||||
return path.startsWith(basePath);
|
|
||||||
}
|
|
||||||
// 精确匹配
|
// 精确匹配
|
||||||
return pattern.equals(path);
|
if (exactPatterns.contains(p)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// 通配符匹配
|
||||||
|
return wildcardPatterns.stream().anyMatch(p::startsWith);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -117,7 +136,7 @@ public class Swagger3Config implements WebMvcConfigurer {
|
|||||||
return new OpenAPI()
|
return new OpenAPI()
|
||||||
.info(new Info()
|
.info(new Info()
|
||||||
.title("JeecgBoot 后台服务API接口文档")
|
.title("JeecgBoot 后台服务API接口文档")
|
||||||
.version("3.8.3")
|
.version("3.9.0")
|
||||||
.contact(new Contact().name("北京国炬信息技术有限公司").url("www.jeccg.com").email("jeecgos@163.com"))
|
.contact(new Contact().name("北京国炬信息技术有限公司").url("www.jeccg.com").email("jeecgos@163.com"))
|
||||||
.description("后台API接口")
|
.description("后台API接口")
|
||||||
.termsOfService("NO terms of service")
|
.termsOfService("NO terms of service")
|
||||||
|
|||||||
@ -1,19 +0,0 @@
|
|||||||
//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);
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
//}
|
|
||||||
@ -142,7 +142,6 @@ public class WebMvcConfiguration implements WebMvcConfigurer {
|
|||||||
return objectMapper;
|
return objectMapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
//update-begin---author:chenrui ---date:20240514 for:[QQYUN-9247]系统监控功能优化------------
|
|
||||||
// /**
|
// /**
|
||||||
// * SpringBootAdmin的Httptrace不见了
|
// * SpringBootAdmin的Httptrace不见了
|
||||||
// * https://blog.csdn.net/u013810234/article/details/110097201
|
// * https://blog.csdn.net/u013810234/article/details/110097201
|
||||||
@ -151,7 +150,6 @@ public class WebMvcConfiguration implements WebMvcConfigurer {
|
|||||||
// public InMemoryHttpTraceRepository getInMemoryHttpTrace(){
|
// public InMemoryHttpTraceRepository getInMemoryHttpTrace(){
|
||||||
// return new InMemoryHttpTraceRepository();
|
// return new InMemoryHttpTraceRepository();
|
||||||
// }
|
// }
|
||||||
//update-end---author:chenrui ---date:20240514 for:[QQYUN-9247]系统监控功能优化------------
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -165,7 +163,7 @@ public class WebMvcConfiguration implements WebMvcConfigurer {
|
|||||||
// 确保在应用启动早期就配置MeterFilter,避免警告
|
// 确保在应用启动早期就配置MeterFilter,避免警告
|
||||||
if (null != meterRegistryPostProcessor && null != prometheusMeterRegistry) {
|
if (null != meterRegistryPostProcessor && null != prometheusMeterRegistry) {
|
||||||
meterRegistryPostProcessor.postProcessAfterInitialization(prometheusMeterRegistry, "prometheusMeterRegistry");
|
meterRegistryPostProcessor.postProcessAfterInitialization(prometheusMeterRegistry, "prometheusMeterRegistry");
|
||||||
log.info("PrometheusMeterRegistry配置完成");
|
log.info("PrometheusMeterRegistry 配置完成");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -24,18 +24,23 @@ public class WebsocketFilter implements Filter {
|
|||||||
|
|
||||||
private static CommonAPI commonApi;
|
private static CommonAPI commonApi;
|
||||||
|
|
||||||
|
private static RedisUtil redisUtil;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
|
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
|
||||||
if (commonApi == null) {
|
if (commonApi == null) {
|
||||||
commonApi = SpringContextUtils.getBean(CommonAPI.class);
|
commonApi = SpringContextUtils.getBean(CommonAPI.class);
|
||||||
}
|
}
|
||||||
|
if (redisUtil == null) {
|
||||||
|
redisUtil = SpringContextUtils.getBean(RedisUtil.class);
|
||||||
|
}
|
||||||
HttpServletRequest request = (HttpServletRequest)servletRequest;
|
HttpServletRequest request = (HttpServletRequest)servletRequest;
|
||||||
String token = request.getHeader(TOKEN_KEY);
|
String token = request.getHeader(TOKEN_KEY);
|
||||||
|
|
||||||
log.debug("Websocket连接 Token安全校验,Path = {},token:{}", request.getRequestURI(), token);
|
log.debug("Websocket连接 Token安全校验,Path = {},token:{}", request.getRequestURI(), token);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
TokenUtils.verifyToken(token, commonApi);
|
TokenUtils.verifyToken(token, commonApi, redisUtil);
|
||||||
} catch (Exception exception) {
|
} catch (Exception exception) {
|
||||||
//log.error("Websocket连接 Token安全校验失败,IP:{}, Token:{}, Path = {},异常:{}", oConvertUtils.getIpAddrByRequest(request), token, request.getRequestURI(), exception.getMessage());
|
//log.error("Websocket连接 Token安全校验失败,IP:{}, Token:{}, Path = {},异常:{}", oConvertUtils.getIpAddrByRequest(request), token, request.getRequestURI(), exception.getMessage());
|
||||||
log.debug("Websocket连接 Token安全校验失败,IP:{}, Token:{}, Path = {},异常:{}", oConvertUtils.getIpAddrByRequest(request), token, request.getRequestURI(), exception.getMessage());
|
log.debug("Websocket连接 Token安全校验失败,IP:{}, Token:{}, Path = {},异常:{}", oConvertUtils.getIpAddrByRequest(request), token, request.getRequestURI(), exception.getMessage());
|
||||||
|
|||||||
@ -1,8 +1,11 @@
|
|||||||
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.jeecg.common.util.LoginUserUtils;
|
import org.apache.shiro.SecurityUtils;
|
||||||
import org.jeecg.common.api.CommonAPI;
|
import org.jeecg.common.api.CommonAPI;
|
||||||
import org.jeecg.common.api.vo.Result;
|
import org.jeecg.common.api.vo.Result;
|
||||||
import org.jeecg.common.constant.CommonConstant;
|
import org.jeecg.common.constant.CommonConstant;
|
||||||
@ -14,9 +17,6 @@ import org.jeecg.config.JeecgBaseConfig;
|
|||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.web.servlet.HandlerInterceptor;
|
import org.springframework.web.servlet.HandlerInterceptor;
|
||||||
|
|
||||||
import jakarta.annotation.Resource;
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.PrintWriter;
|
import java.io.PrintWriter;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
@ -68,7 +68,7 @@ public class LowCodeModeInterceptor implements HandlerInterceptor {
|
|||||||
if (jeecgBaseConfig.getFirewall()!=null && LowCodeModeInterceptor.LOW_CODE_MODE_PROD.equals(jeecgBaseConfig.getFirewall().getLowCodeMode())) {
|
if (jeecgBaseConfig.getFirewall()!=null && LowCodeModeInterceptor.LOW_CODE_MODE_PROD.equals(jeecgBaseConfig.getFirewall().getLowCodeMode())) {
|
||||||
String requestURI = request.getRequestURI().substring(request.getContextPath().length());
|
String requestURI = request.getRequestURI().substring(request.getContextPath().length());
|
||||||
log.info("低代码模式,拦截请求路径:" + requestURI);
|
log.info("低代码模式,拦截请求路径:" + requestURI);
|
||||||
LoginUser loginUser = LoginUserUtils.getSessionUser();
|
LoginUser loginUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();
|
||||||
Set<String> hasRoles = null;
|
Set<String> hasRoles = null;
|
||||||
if (loginUser == null) {
|
if (loginUser == null) {
|
||||||
loginUser = commonAPI.getUserByName(JwtUtil.getUserNameByToken(SpringContextUtils.getHttpServletRequest()));
|
loginUser = commonAPI.getUserByName(JwtUtil.getUserNameByToken(SpringContextUtils.getHttpServletRequest()));
|
||||||
|
|||||||
@ -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.jeecg.common.util.LoginUserUtils;
|
|
||||||
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;
|
||||||
@ -129,20 +129,18 @@ public class MybatisInterceptor implements Interceptor {
|
|||||||
Field[] fields = null;
|
Field[] fields = null;
|
||||||
if (parameter instanceof ParamMap) {
|
if (parameter instanceof ParamMap) {
|
||||||
ParamMap<?> p = (ParamMap<?>) parameter;
|
ParamMap<?> p = (ParamMap<?>) parameter;
|
||||||
//update-begin-author:scott date:20190729 for:批量更新报错issues/IZA3Q--
|
// 代码逻辑说明: 批量更新报错issues/IZA3Q--
|
||||||
String et = "et";
|
String et = "et";
|
||||||
if (p.containsKey(et)) {
|
if (p.containsKey(et)) {
|
||||||
parameter = p.get(et);
|
parameter = p.get(et);
|
||||||
} else {
|
} else {
|
||||||
parameter = p.get("param1");
|
parameter = p.get("param1");
|
||||||
}
|
}
|
||||||
//update-end-author:scott date:20190729 for:批量更新报错issues/IZA3Q-
|
|
||||||
|
|
||||||
//update-begin-author:scott date:20190729 for:更新指定字段时报错 issues/#516-
|
// 代码逻辑说明: 更新指定字段时报错 issues/#516-
|
||||||
if (parameter == null) {
|
if (parameter == null) {
|
||||||
return invocation.proceed();
|
return invocation.proceed();
|
||||||
}
|
}
|
||||||
//update-end-author:scott date:20190729 for:更新指定字段时报错 issues/#516-
|
|
||||||
|
|
||||||
fields = oConvertUtils.getAllFields(parameter);
|
fields = oConvertUtils.getAllFields(parameter);
|
||||||
} else {
|
} else {
|
||||||
@ -184,7 +182,6 @@ public class MybatisInterceptor implements Interceptor {
|
|||||||
// TODO Auto-generated method stub
|
// TODO Auto-generated method stub
|
||||||
}
|
}
|
||||||
|
|
||||||
//update-begin--Author:scott Date:20191213 for:关于使用Quzrtz 开启线程任务, #465
|
|
||||||
/**
|
/**
|
||||||
* 获取登录用户
|
* 获取登录用户
|
||||||
* @return
|
* @return
|
||||||
@ -192,13 +189,12 @@ public class MybatisInterceptor implements Interceptor {
|
|||||||
private LoginUser getLoginUser() {
|
private LoginUser getLoginUser() {
|
||||||
LoginUser sysUser = null;
|
LoginUser sysUser = null;
|
||||||
try {
|
try {
|
||||||
sysUser = LoginUserUtils.getSessionUser() != null ? LoginUserUtils.getSessionUser() : null;
|
sysUser = SecureUtil.currentUser() != null ? SecureUtil.currentUser() : null;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
//e.printStackTrace();
|
//e.printStackTrace();
|
||||||
sysUser = null;
|
sysUser = null;
|
||||||
}
|
}
|
||||||
return sysUser;
|
return sysUser;
|
||||||
}
|
}
|
||||||
//update-end--Author:scott Date:20191213 for:关于使用Quzrtz 开启线程任务, #465
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,420 +0,0 @@
|
|||||||
package org.jeecg.config.satoken;
|
|
||||||
|
|
||||||
import cn.dev33.satoken.context.SaHolder;
|
|
||||||
import cn.dev33.satoken.context.model.SaRequest;
|
|
||||||
import cn.dev33.satoken.exception.NotLoginException;
|
|
||||||
import cn.dev33.satoken.filter.SaServletFilter;
|
|
||||||
import cn.dev33.satoken.interceptor.SaInterceptor;
|
|
||||||
import cn.dev33.satoken.jwt.StpLogicJwtForSimple;
|
|
||||||
import cn.dev33.satoken.router.SaHttpMethod;
|
|
||||||
import cn.dev33.satoken.router.SaRouter;
|
|
||||||
import cn.dev33.satoken.stp.StpLogic;
|
|
||||||
import cn.dev33.satoken.stp.StpUtil;
|
|
||||||
import jakarta.annotation.Resource;
|
|
||||||
import jakarta.servlet.DispatcherType;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.jeecg.common.api.CommonAPI;
|
|
||||||
import org.jeecg.common.config.TenantContext;
|
|
||||||
import org.jeecg.common.constant.CacheConstant;
|
|
||||||
import org.jeecg.common.constant.CommonConstant;
|
|
||||||
import org.jeecg.common.system.vo.LoginUser;
|
|
||||||
import org.jeecg.common.util.*;
|
|
||||||
import org.jeecg.config.JeecgBaseConfig;
|
|
||||||
import org.jeecg.config.mybatis.MybatisPlusSaasConfig;
|
|
||||||
import org.jeecg.config.satoken.ignore.InMemoryIgnoreAuth;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.beans.factory.config.BeanDefinition;
|
|
||||||
import org.springframework.boot.web.servlet.FilterRegistrationBean;
|
|
||||||
import org.springframework.context.annotation.Bean;
|
|
||||||
import org.springframework.context.annotation.Configuration;
|
|
||||||
import org.springframework.context.annotation.Primary;
|
|
||||||
import org.springframework.context.annotation.Role;
|
|
||||||
import org.springframework.core.env.Environment;
|
|
||||||
import org.springframework.http.HttpHeaders;
|
|
||||||
import org.springframework.http.HttpStatus;
|
|
||||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
|
||||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @author: jeecg-boot
|
|
||||||
* @description: Sa-Token 配置类
|
|
||||||
*/
|
|
||||||
@Slf4j
|
|
||||||
@Configuration
|
|
||||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
|
||||||
public class SaTokenConfig implements WebMvcConfigurer {
|
|
||||||
|
|
||||||
@Resource
|
|
||||||
private JeecgBaseConfig jeecgBaseConfig;
|
|
||||||
@Autowired
|
|
||||||
private Environment env;
|
|
||||||
@Autowired
|
|
||||||
private CommonAPI commonAPI;
|
|
||||||
@Autowired
|
|
||||||
private RedisUtil redisUtil;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sa-Token 整合 jwt (Simple 模式)
|
|
||||||
* 使用JWT-Simple模式生成标准JWT格式的token
|
|
||||||
* 并支持从URL参数"token"读取token(兼容原系统)
|
|
||||||
*/
|
|
||||||
@Bean
|
|
||||||
@Primary
|
|
||||||
public StpLogic getStpLogicJwt() {
|
|
||||||
return new StpLogicJwtForSimple() {
|
|
||||||
/**
|
|
||||||
* 获取当前请求的 Token 值
|
|
||||||
* 优先级:Header > URL参数token > URL参数X-Access-Token
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public String getTokenValue() {
|
|
||||||
try {
|
|
||||||
SaRequest request = SaHolder.getRequest();
|
|
||||||
|
|
||||||
// 1. 优先从Header中获取
|
|
||||||
String tokenValue = request.getHeader(getConfigOrGlobal().getTokenName());
|
|
||||||
if (oConvertUtils.isNotEmpty(tokenValue)) {
|
|
||||||
return tokenValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. 从URL参数"token"获取(兼容原系统)
|
|
||||||
tokenValue = request.getParam("token");
|
|
||||||
if (oConvertUtils.isNotEmpty(tokenValue)) {
|
|
||||||
return tokenValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. 从URL参数"X-Access-Token"获取
|
|
||||||
tokenValue = request.getParam(getConfigOrGlobal().getTokenName());
|
|
||||||
if (oConvertUtils.isNotEmpty(tokenValue)) {
|
|
||||||
return tokenValue;
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.debug("获取token失败: {}", e.getMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. 如果都没有,使用默认逻辑
|
|
||||||
return super.getTokenValue();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 注册 Sa-Token 拦截器,打开注解式鉴权功能
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void addInterceptors(InterceptorRegistry registry) {
|
|
||||||
// 注册 Sa-Token 拦截器,打开注解式鉴权功能
|
|
||||||
registry.addInterceptor(new SaInterceptor()).addPathPatterns("/**");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 注册 Sa-Token 全局过滤器
|
|
||||||
*/
|
|
||||||
@Bean
|
|
||||||
public SaServletFilter getSaServletFilter() {
|
|
||||||
return new SaServletFilter()
|
|
||||||
// 指定 [拦截路由] 与 [放行路由]
|
|
||||||
.addInclude("/**")
|
|
||||||
.setExcludeList(getExcludeUrls())
|
|
||||||
// 认证函数: 每次请求执行
|
|
||||||
.setAuth(obj -> {
|
|
||||||
// 检查是否是免认证路径
|
|
||||||
String servletPath = SaHolder.getRequest().getRequestPath();
|
|
||||||
if (InMemoryIgnoreAuth.contains(servletPath)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 校验 token:如果请求中带有 token,先切换到对应的登录会话再校验
|
|
||||||
try {
|
|
||||||
String token = StpUtil.getTokenValue();
|
|
||||||
if (oConvertUtils.isNotEmpty(token)) {
|
|
||||||
// 根据 token 获取 loginId 并切换到对应的登录会话
|
|
||||||
Object loginId = StpUtil.getLoginIdByToken(token);
|
|
||||||
if (loginId != null) {
|
|
||||||
StpUtil.switchTo(loginId);
|
|
||||||
|
|
||||||
// 需要手工自动续签,默认参数auto-renew:true 不好使
|
|
||||||
long activeTimeout = StpUtil.stpLogic.getConfigOrGlobal().getActiveTimeout();
|
|
||||||
if (activeTimeout > 0) {
|
|
||||||
// 获取当前token的活跃剩余时间
|
|
||||||
long tokenActiveTimeout = StpUtil.getTokenActiveTimeout();
|
|
||||||
|
|
||||||
// 如果剩余活跃时间少于总活跃时间的一半,进行续签
|
|
||||||
if (tokenActiveTimeout > 0 && tokenActiveTimeout < (activeTimeout / 2)) {
|
|
||||||
StpUtil.stpLogic.updateLastActiveToNow(token);
|
|
||||||
log.info("【Sa-Token拦截器】Token续签成功,剩余活跃时间: {}秒", tokenActiveTimeout);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
// 如果获取 loginId 失败,说明 token 无效或未登录,让 checkLogin 抛出异常
|
|
||||||
log.debug("切换登录会话失败: {}", e.getMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
// 最终校验登录状态
|
|
||||||
StpUtil.checkLogin();
|
|
||||||
|
|
||||||
// 租户校验逻辑
|
|
||||||
checkTenantAuthorization();
|
|
||||||
})
|
|
||||||
// 异常处理函数:每次认证函数发生异常时执行此函数
|
|
||||||
.setError(e -> {
|
|
||||||
log.warn("Sa-Token 认证失败:用户未登录或token无效");
|
|
||||||
log.warn("请求路径: {}, Method: {},Token: {}", SaHolder.getRequest().getRequestPath(), SaHolder.getRequest().getMethod(), StpUtil.getTokenValue());
|
|
||||||
|
|
||||||
// 返回401状态码
|
|
||||||
SaHolder.getResponse().setStatus(401).setHeader("Content-Type", "application/json;charset=UTF-8");
|
|
||||||
return org.jeecg.common.system.util.JwtUtil.responseErrorJson(401, CommonConstant.TOKEN_IS_INVALID_MSG);
|
|
||||||
})
|
|
||||||
// 前置函数:在每次认证函数之前执行(BeforeAuth 不受 includeList 与 excludeList 的限制,所有请求都会进入)
|
|
||||||
.setBeforeAuth(r -> {
|
|
||||||
// 设置跨域配置
|
|
||||||
Object cloudServer = env.getProperty(CommonConstant.CLOUD_SERVER_KEY);
|
|
||||||
// 如果cloudServer为空 则说明是单体 需要加载跨域配置【微服务跨域切换】
|
|
||||||
if (cloudServer == null) {
|
|
||||||
SaHolder.getResponse()
|
|
||||||
// 允许指定域访问跨域资源
|
|
||||||
.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, SaHolder.getRequest().getHeader(HttpHeaders.ORIGIN))
|
|
||||||
// 允许所有请求方式
|
|
||||||
.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS, "GET, POST, PUT, DELETE, OPTIONS")
|
|
||||||
// 有效时间
|
|
||||||
.setHeader(HttpHeaders.ACCESS_CONTROL_MAX_AGE, "3600")
|
|
||||||
// 允许的header参数
|
|
||||||
.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS, SaHolder.getRequest().getHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS))
|
|
||||||
// 允许携带凭证
|
|
||||||
.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true");
|
|
||||||
}
|
|
||||||
|
|
||||||
// OPTIONS预检请求,直接返回
|
|
||||||
SaRouter.match(SaHttpMethod.OPTIONS).free(r2 -> {
|
|
||||||
SaHolder.getResponse().setStatus(HttpStatus.OK.value());
|
|
||||||
});
|
|
||||||
|
|
||||||
// 设置当前线程上下文的租户ID
|
|
||||||
String tenantId = SaHolder.getRequest().getHeader(CommonConstant.TENANT_ID);
|
|
||||||
TenantContext.setTenant(tenantId);
|
|
||||||
log.info("===【TenantContext 线程设置】=== 请求路径: {}, 租户ID: {}", SaHolder.getRequest().getRequestPath(), tenantId);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* spring过滤装饰器 <br/>
|
|
||||||
* 支持异步请求的过滤器装饰
|
|
||||||
*/
|
|
||||||
@Bean
|
|
||||||
public FilterRegistrationBean<SaServletFilter> saTokenFilterRegistration() {
|
|
||||||
FilterRegistrationBean<SaServletFilter> registration = new FilterRegistrationBean<>();
|
|
||||||
registration.setFilter(getSaServletFilter());
|
|
||||||
registration.setName("SaServletFilter");
|
|
||||||
// 支持异步请求
|
|
||||||
registration.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ASYNC);
|
|
||||||
// 拦截所有请求
|
|
||||||
registration.addUrlPatterns("/*");
|
|
||||||
registration.setOrder(1);
|
|
||||||
registration.setAsyncSupported(true); // 支持异步请求
|
|
||||||
return registration;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取排除URL列表
|
|
||||||
*/
|
|
||||||
private List<String> getExcludeUrls() {
|
|
||||||
List<String> excludeUrls = new ArrayList<>();
|
|
||||||
|
|
||||||
// 支持yml方式,配置拦截排除
|
|
||||||
if (jeecgBaseConfig != null && jeecgBaseConfig.getShiro() != null) {
|
|
||||||
String shiroExcludeUrls = jeecgBaseConfig.getShiro().getExcludeUrls();
|
|
||||||
if (oConvertUtils.isNotEmpty(shiroExcludeUrls)) {
|
|
||||||
String[] permissionUrl = shiroExcludeUrls.split(",");
|
|
||||||
excludeUrls.addAll(Arrays.asList(permissionUrl));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加默认排除路径
|
|
||||||
excludeUrls.addAll(Arrays.asList(
|
|
||||||
"/sys/cas/client/validateLogin", // cas验证登录
|
|
||||||
"/sys/randomImage/**", // 登录验证码接口排除
|
|
||||||
"/sys/checkCaptcha", // 登录验证码接口排除
|
|
||||||
"/sys/smsCheckCaptcha", // 短信次数发送太多验证码排除
|
|
||||||
"/sys/login", // 登录接口排除
|
|
||||||
"/sys/mLogin", // 登录接口排除
|
|
||||||
"/sys/logout", // 登出接口排除
|
|
||||||
"/sys/thirdLogin/**", // 第三方登录
|
|
||||||
"/sys/getEncryptedString", // 获取加密串
|
|
||||||
"/sys/sms", // 短信验证码
|
|
||||||
"/sys/phoneLogin", // 手机登录
|
|
||||||
"/sys/user/checkOnlyUser", // 校验用户是否存在
|
|
||||||
"/sys/user/register", // 用户注册
|
|
||||||
"/sys/user/phoneVerification", // 用户忘记密码验证手机号
|
|
||||||
"/sys/user/passwordChange", // 用户更改密码
|
|
||||||
"/auth/2step-code", // 登录验证码
|
|
||||||
"/sys/common/static/**", // 图片预览 & 下载文件不限制token
|
|
||||||
"/sys/common/pdf/**", // pdf预览
|
|
||||||
"/generic/**", // pdf预览需要文件
|
|
||||||
"/sys/getLoginQrcode/**", // 登录二维码
|
|
||||||
"/sys/getQrcodeToken/**", // 监听扫码
|
|
||||||
"/sys/checkAuth", // 授权接口排除
|
|
||||||
"/openapi/call/**", // 开放平台接口排除
|
|
||||||
|
|
||||||
// 排除静态资源后缀
|
|
||||||
"/",
|
|
||||||
"/doc.html",
|
|
||||||
"**/*.js",
|
|
||||||
"**/*.css",
|
|
||||||
"**/*.html",
|
|
||||||
"**/*.svg",
|
|
||||||
"**/*.pdf",
|
|
||||||
"**/*.jpg",
|
|
||||||
"**/*.png",
|
|
||||||
"**/*.gif",
|
|
||||||
"**/*.ico",
|
|
||||||
"**/*.ttf",
|
|
||||||
"**/*.woff",
|
|
||||||
"**/*.woff2",
|
|
||||||
"**/*.glb",
|
|
||||||
"**/*.wasm",
|
|
||||||
"**/*.js.map",
|
|
||||||
"**/*.css.map",
|
|
||||||
|
|
||||||
"/druid/**",
|
|
||||||
"/swagger-ui.html",
|
|
||||||
"/swagger*/**",
|
|
||||||
"/webjars/**",
|
|
||||||
"/v3/**",
|
|
||||||
|
|
||||||
// 排除消息通告查看详情页面(用于第三方APP)
|
|
||||||
"/sys/annountCement/show/**",
|
|
||||||
|
|
||||||
// 积木报表和积木BI排除
|
|
||||||
"/jmreport/**",
|
|
||||||
"/drag/lib/**",
|
|
||||||
"/drag/list/**",
|
|
||||||
"/drag/favicon.ico",
|
|
||||||
"/drag/view",
|
|
||||||
"/drag/page/queryById",
|
|
||||||
"/drag/page/addVisitsNumber",
|
|
||||||
"/drag/page/queryTemplateList",
|
|
||||||
"/drag/share/view/**",
|
|
||||||
"/drag/onlDragDatasetHead/getAllChartData",
|
|
||||||
"/drag/onlDragDatasetHead/getTotalData",
|
|
||||||
"/drag/onlDragDatasetHead/getMapDataByCode",
|
|
||||||
"/drag/onlDragDatasetHead/getTotalDataByCompId",
|
|
||||||
"/drag/mock/json/**",
|
|
||||||
"/drag/onlDragDatasetHead/getDictByCodes",
|
|
||||||
"/drag/onlDragDatasetHead/queryAllById",
|
|
||||||
"/jimubi/view",
|
|
||||||
"/jimubi/share/view/**",
|
|
||||||
|
|
||||||
// 大屏模板例子
|
|
||||||
"/test/bigScreen/**",
|
|
||||||
"/bigscreen/template1/**",
|
|
||||||
"/bigscreen/template2/**",
|
|
||||||
|
|
||||||
// websocket排除
|
|
||||||
"/websocket/**", // 系统通知和公告
|
|
||||||
"/newsWebsocket/**", // CMS模块
|
|
||||||
"/vxeSocket/**", // JVxeTable无痕刷新示例
|
|
||||||
"/dragChannelSocket/**", // 仪表盘(按钮通信)
|
|
||||||
|
|
||||||
// App vue3版本查询版本接口
|
|
||||||
"/sys/version/app3version",
|
|
||||||
|
|
||||||
// 测试模块排除
|
|
||||||
"/test/seata/**",
|
|
||||||
|
|
||||||
// 错误路径排除
|
|
||||||
"/error",
|
|
||||||
|
|
||||||
// 企业微信证书排除
|
|
||||||
"/WW_verify*"
|
|
||||||
));
|
|
||||||
|
|
||||||
return excludeUrls;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 校验用户的tenant_id和前端传过来的是否一致
|
|
||||||
*
|
|
||||||
* <p>实现逻辑:
|
|
||||||
* <ul>
|
|
||||||
* <li>1. 获取当前登录用户信息</li>
|
|
||||||
* <li>2. 检查用户是否配置了租户信息</li>
|
|
||||||
* <li>3. 获取前端请求头中的租户ID</li>
|
|
||||||
* <li>4. 校验用户所属租户中是否包含当前请求的租户ID</li>
|
|
||||||
* <li>5. 如果校验失败,从数据库重新查询用户信息并再次校验</li>
|
|
||||||
* <li>6. 最终校验失败则抛出异常</li>
|
|
||||||
* </ul>
|
|
||||||
*
|
|
||||||
* @throws NotLoginException 租户授权变更异常
|
|
||||||
*/
|
|
||||||
private void checkTenantAuthorization() {
|
|
||||||
log.debug("------ 租户校验开始 ------");
|
|
||||||
// 如果未开启租户控制,直接返回
|
|
||||||
if (!MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 获取当前登录用户信息
|
|
||||||
LoginUser loginUser = TokenUtils.getLoginUser(LoginUserUtils.getUsername(), commonAPI, redisUtil);
|
|
||||||
if (loginUser == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
String username = loginUser.getUsername();
|
|
||||||
String userTenantIds = loginUser.getRelTenantIds();
|
|
||||||
|
|
||||||
// 如果用户未配置租户信息,直接返回
|
|
||||||
if (oConvertUtils.isEmpty(userTenantIds)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取前端请求头中的租户ID
|
|
||||||
String loginTenantId = TokenUtils.getTenantIdByRequest(SpringContextUtils.getHttpServletRequest());
|
|
||||||
log.info("登录租户:{}", loginTenantId);
|
|
||||||
log.info("用户拥有那些租户:{}", userTenantIds);
|
|
||||||
|
|
||||||
// 登录用户无租户,前端header中租户ID值为 0
|
|
||||||
String str = "0";
|
|
||||||
if (oConvertUtils.isEmpty(loginTenantId) || str.equals(loginTenantId)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
String[] userTenantIdsArray = userTenantIds.split(",");
|
|
||||||
if (!oConvertUtils.isIn(loginTenantId, userTenantIdsArray)) {
|
|
||||||
boolean isAuthorization = false;
|
|
||||||
|
|
||||||
//========================================================================
|
|
||||||
// 查询用户信息(如果租户不匹配从数据库中重新查询一次用户信息)
|
|
||||||
String loginUserKey = CacheConstant.SYS_USERS_CACHE + "::" + username;
|
|
||||||
redisUtil.del(loginUserKey);
|
|
||||||
|
|
||||||
LoginUser loginUserFromDb = commonAPI.getUserByName(username);
|
|
||||||
LoginUserUtils.setSessionUser(loginUserFromDb);
|
|
||||||
if (loginUserFromDb != null && oConvertUtils.isNotEmpty(loginUserFromDb.getRelTenantIds())) {
|
|
||||||
String[] newArray = loginUserFromDb.getRelTenantIds().split(",");
|
|
||||||
if (oConvertUtils.isIn(loginTenantId, newArray)) {
|
|
||||||
isAuthorization = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//========================================================================
|
|
||||||
|
|
||||||
if (!isAuthorization) {
|
|
||||||
log.info("租户异常——登录租户:{}", loginTenantId);
|
|
||||||
log.info("租户异常——用户拥有租户组:{}", userTenantIds);
|
|
||||||
throw new NotLoginException("登录租户授权变更,请重新登陆!", StpUtil.TYPE, NotLoginException.KICK_OUT);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}catch (Exception e) {
|
|
||||||
log.error("租户校验异常:{}", e.getMessage(), e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -1,174 +0,0 @@
|
|||||||
package org.jeecg.config.satoken;
|
|
||||||
|
|
||||||
import cn.dev33.satoken.dao.SaTokenDao;
|
|
||||||
import cn.dev33.satoken.SaManager;
|
|
||||||
import cn.dev33.satoken.stp.StpInterface;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.jeecg.common.api.CommonAPI;
|
|
||||||
import org.springframework.context.annotation.Lazy;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
import jakarta.annotation.Resource;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description: Sa-Token 权限认证接口实现(带缓存)
|
|
||||||
*
|
|
||||||
* <p>⚠️ 重要说明:</p>
|
|
||||||
* <ul>
|
|
||||||
* <li><strong>Sa-Token 的 StpInterface 默认不提供缓存能力</strong>,需要自己实现缓存逻辑</li>
|
|
||||||
* <li>本实现采用 <strong>[账号id -> 权限/角色列表]</strong> 缓存模型</li>
|
|
||||||
* <li>缓存键格式:
|
|
||||||
* <ul>
|
|
||||||
* <li>用户权限缓存:satoken:user-permission:{username}</li>
|
|
||||||
* <li>用户角色缓存:satoken:user-role:{username}</li>
|
|
||||||
* </ul>
|
|
||||||
* </li>
|
|
||||||
* <li>缓存过期时间:30天</li>
|
|
||||||
* <li>⚠️ 当修改用户的角色或权限时,需要手动清除缓存</li>
|
|
||||||
* </ul>
|
|
||||||
*
|
|
||||||
* <p>清除缓存示例:</p>
|
|
||||||
* <pre>
|
|
||||||
* // 清除单个用户的权限和角色缓存
|
|
||||||
* StpInterfaceImpl.clearUserCache("admin");
|
|
||||||
*
|
|
||||||
* // 清除多个用户的缓存
|
|
||||||
* StpInterfaceImpl.clearUserCache(Arrays.asList("admin", "user1", "user2"));
|
|
||||||
* </pre>
|
|
||||||
*/
|
|
||||||
@Component
|
|
||||||
@Slf4j
|
|
||||||
public class StpInterfaceImpl implements StpInterface {
|
|
||||||
|
|
||||||
@Lazy
|
|
||||||
@Resource
|
|
||||||
private CommonAPI commonApi;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 缓存过期时间(秒):30天
|
|
||||||
*/
|
|
||||||
private static final long CACHE_TIMEOUT = 60 * 60 * 24 * 30;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 权限缓存键前缀
|
|
||||||
*/
|
|
||||||
private static final String PERMISSION_CACHE_PREFIX = "satoken:user-permission:";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 角色缓存键前缀
|
|
||||||
*/
|
|
||||||
private static final String ROLE_CACHE_PREFIX = "satoken:user-role:";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 返回一个账号所拥有的权限码集合(带缓存)
|
|
||||||
*
|
|
||||||
* @param loginId 账号id(这里是 username)
|
|
||||||
* @param loginType 账号类型
|
|
||||||
* @return 权限码集合
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
public List<String> getPermissionList(Object loginId, String loginType) {
|
|
||||||
String username = loginId.toString();
|
|
||||||
String cacheKey = PERMISSION_CACHE_PREFIX + username;
|
|
||||||
|
|
||||||
SaTokenDao dao = SaManager.getSaTokenDao();
|
|
||||||
|
|
||||||
// 1. 先从缓存获取
|
|
||||||
List<String> permissionList = (List<String>) dao.getObject(cacheKey);
|
|
||||||
|
|
||||||
if (permissionList == null) {
|
|
||||||
// 2. 缓存不存在,从数据库查询
|
|
||||||
log.warn("权限缓存未命中,查询数据库 [ username={} ]", username);
|
|
||||||
|
|
||||||
String userId = commonApi.getUserIdByName(username);
|
|
||||||
if (userId == null) {
|
|
||||||
log.warn("用户不存在: {}", username);
|
|
||||||
return new ArrayList<>();
|
|
||||||
}
|
|
||||||
|
|
||||||
Set<String> permissionSet = commonApi.queryUserAuths(userId);
|
|
||||||
permissionList = new ArrayList<>(permissionSet);
|
|
||||||
|
|
||||||
// 3. 将结果缓存起来
|
|
||||||
dao.setObject(cacheKey, permissionList, CACHE_TIMEOUT);
|
|
||||||
log.info("权限已缓存 [ username={}, permissions={} ]", username, permissionList.size());
|
|
||||||
} else {
|
|
||||||
log.debug("权限缓存命中 [ username={}, permissions={} ]", username, permissionList.size());
|
|
||||||
}
|
|
||||||
|
|
||||||
return permissionList;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 返回一个账号所拥有的角色标识集合(带缓存)
|
|
||||||
*
|
|
||||||
* @param loginId 账号id(这里是 username)
|
|
||||||
* @param loginType 账号类型
|
|
||||||
* @return 角色标识集合
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
public List<String> getRoleList(Object loginId, String loginType) {
|
|
||||||
String username = loginId.toString();
|
|
||||||
String cacheKey = ROLE_CACHE_PREFIX + username;
|
|
||||||
|
|
||||||
SaTokenDao dao = SaManager.getSaTokenDao();
|
|
||||||
|
|
||||||
// 1. 先从缓存获取
|
|
||||||
List<String> roleList = (List<String>) dao.getObject(cacheKey);
|
|
||||||
|
|
||||||
if (roleList == null) {
|
|
||||||
// 2. 缓存不存在,从数据库查询
|
|
||||||
log.warn("角色缓存未命中,查询数据库 [ username={} ]", username);
|
|
||||||
|
|
||||||
String userId = commonApi.getUserIdByName(username);
|
|
||||||
if (userId == null) {
|
|
||||||
log.warn("用户不存在: {}", username);
|
|
||||||
return new ArrayList<>();
|
|
||||||
}
|
|
||||||
|
|
||||||
Set<String> roleSet = commonApi.queryUserRolesById(userId);
|
|
||||||
roleList = new ArrayList<>(roleSet);
|
|
||||||
|
|
||||||
// 3. 将结果缓存起来
|
|
||||||
dao.setObject(cacheKey, roleList, CACHE_TIMEOUT);
|
|
||||||
log.info("角色已缓存 [ username={}, roles={} ]", username, roleList.size());
|
|
||||||
} else {
|
|
||||||
log.debug("角色缓存命中 [ username={}, roles={} ]", username, roleList.size());
|
|
||||||
}
|
|
||||||
|
|
||||||
return roleList;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 清除单个用户的权限和角色缓存
|
|
||||||
* <p>使用场景:修改用户的角色分配后</p>
|
|
||||||
*
|
|
||||||
* @param username 用户名
|
|
||||||
*/
|
|
||||||
public static void clearUserCache(String username) {
|
|
||||||
SaTokenDao dao = SaManager.getSaTokenDao();
|
|
||||||
dao.deleteObject(PERMISSION_CACHE_PREFIX + username);
|
|
||||||
dao.deleteObject(ROLE_CACHE_PREFIX + username);
|
|
||||||
log.info("已清除用户缓存 [ username={} ]", username);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 批量清除多个用户的权限和角色缓存
|
|
||||||
* <p>使用场景:修改角色权限后,清除拥有该角色的所有用户的缓存</p>
|
|
||||||
*
|
|
||||||
* @param usernameList 用户名列表
|
|
||||||
*/
|
|
||||||
public static void clearUserCache(List<String> usernameList) {
|
|
||||||
SaTokenDao dao = SaManager.getSaTokenDao();
|
|
||||||
for (String username : usernameList) {
|
|
||||||
dao.deleteObject(PERMISSION_CACHE_PREFIX + username);
|
|
||||||
dao.deleteObject(ROLE_CACHE_PREFIX + username);
|
|
||||||
}
|
|
||||||
log.info("已批量清除用户缓存 [ count={} ]", usernameList.size());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -0,0 +1,90 @@
|
|||||||
|
package org.jeecg.config.security;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||||
|
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.settings.OAuth2TokenFormat;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.settings.TokenSettings;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* spring authorization server 注册客户端便捷工具类
|
||||||
|
* @author eightmonth@qq.com
|
||||||
|
* @date 2024/3/7 11:22
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class ClientService {
|
||||||
|
|
||||||
|
private RegisteredClientRepository registeredClientRepository;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 修改客户端token有效期
|
||||||
|
* 认证码、设备码有效期与accessToken有效期保持一致
|
||||||
|
*/
|
||||||
|
public void updateTokenValidation(String clientId, Long accessTokenValidation, Long refreshTokenValidation){
|
||||||
|
RegisteredClient registeredClient = findByClientId(clientId);
|
||||||
|
RegisteredClient.Builder builder = RegisteredClient.from(registeredClient);
|
||||||
|
TokenSettings tokenSettings = TokenSettings.builder()
|
||||||
|
.idTokenSignatureAlgorithm(SignatureAlgorithm.RS256)
|
||||||
|
.accessTokenTimeToLive(Duration.ofSeconds(accessTokenValidation))
|
||||||
|
.accessTokenFormat(OAuth2TokenFormat.SELF_CONTAINED)
|
||||||
|
.reuseRefreshTokens(true)
|
||||||
|
.refreshTokenTimeToLive(Duration.ofSeconds(refreshTokenValidation))
|
||||||
|
.authorizationCodeTimeToLive(Duration.ofSeconds(accessTokenValidation))
|
||||||
|
.deviceCodeTimeToLive(Duration.ofSeconds(accessTokenValidation))
|
||||||
|
.build();
|
||||||
|
builder.tokenSettings(tokenSettings);
|
||||||
|
registeredClientRepository.save(builder.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 修改客户端授权类型
|
||||||
|
* @param clientId
|
||||||
|
* @param grantTypes
|
||||||
|
*/
|
||||||
|
public void updateGrantType(String clientId, Set<AuthorizationGrantType> grantTypes) {
|
||||||
|
RegisteredClient registeredClient = findByClientId(clientId);
|
||||||
|
RegisteredClient.Builder builder = RegisteredClient.from(registeredClient);
|
||||||
|
for (AuthorizationGrantType grantType : grantTypes) {
|
||||||
|
builder.authorizationGrantType(grantType);
|
||||||
|
}
|
||||||
|
registeredClientRepository.save(builder.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 修改客户端重定向uri
|
||||||
|
* @param clientId
|
||||||
|
* @param redirectUris
|
||||||
|
*/
|
||||||
|
public void updateRedirectUris(String clientId, String redirectUris) {
|
||||||
|
RegisteredClient registeredClient = findByClientId(clientId);
|
||||||
|
RegisteredClient.Builder builder = RegisteredClient.from(registeredClient);
|
||||||
|
builder.redirectUri(redirectUris);
|
||||||
|
registeredClientRepository.save(builder.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 修改客户端授权范围
|
||||||
|
* @param clientId
|
||||||
|
* @param scopes
|
||||||
|
*/
|
||||||
|
public void updateScopes(String clientId, Set<String> scopes) {
|
||||||
|
RegisteredClient registeredClient = findByClientId(clientId);
|
||||||
|
RegisteredClient.Builder builder = RegisteredClient.from(registeredClient);
|
||||||
|
for (String scope : scopes) {
|
||||||
|
builder.scope(scope);
|
||||||
|
}
|
||||||
|
registeredClientRepository.save(builder.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
public RegisteredClient findByClientId(String clientId) {
|
||||||
|
return registeredClientRepository.findByClientId(clientId);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,80 @@
|
|||||||
|
package org.jeecg.config.security;
|
||||||
|
|
||||||
|
import jakarta.servlet.FilterChain;
|
||||||
|
import jakarta.servlet.ServletException;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletRequestWrapper;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.core.annotation.Order;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
import org.springframework.web.filter.OncePerRequestFilter;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 仪盘表请求query体携带的token
|
||||||
|
* @author eightmonth
|
||||||
|
* @date 2024/7/3 14:04
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
@Order(value = Integer.MIN_VALUE)
|
||||||
|
public class CopyTokenFilter extends OncePerRequestFilter {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
|
||||||
|
throws ServletException, IOException {
|
||||||
|
// 以下为undertow定制代码,如切换其它servlet容器,需要同步更换
|
||||||
|
String token = request.getHeader("Authorization");
|
||||||
|
String bearerToken = request.getParameter("token");
|
||||||
|
String headerBearerToken = request.getHeader("X-Access-Token");
|
||||||
|
String finalToken;
|
||||||
|
|
||||||
|
log.debug("【仪盘表请求query体携带的token】CopyTokenFilter token: {}, bearerToken: {}, headerBearerToken: {}", token, bearerToken, headerBearerToken);
|
||||||
|
|
||||||
|
if (StringUtils.hasText(token)) {
|
||||||
|
finalToken = "bearer " + token;
|
||||||
|
} else if (StringUtils.hasText(bearerToken)) {
|
||||||
|
finalToken = "bearer " + bearerToken;
|
||||||
|
} else if (StringUtils.hasText(headerBearerToken)) {
|
||||||
|
finalToken = "bearer " + headerBearerToken;
|
||||||
|
} else {
|
||||||
|
finalToken = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (finalToken != null) {
|
||||||
|
HttpServletRequestWrapper wrapper = new HttpServletRequestWrapper(request) {
|
||||||
|
@Override
|
||||||
|
public String getHeader(String name) {
|
||||||
|
if ("Authorization".equalsIgnoreCase(name)) {
|
||||||
|
return finalToken;
|
||||||
|
}
|
||||||
|
return super.getHeader(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Enumeration<String> getHeaders(String name) {
|
||||||
|
if ("Authorization".equalsIgnoreCase(name)) {
|
||||||
|
return Collections.enumeration(Collections.singleton(finalToken));
|
||||||
|
}
|
||||||
|
return super.getHeaders(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Enumeration<String> getHeaderNames() {
|
||||||
|
List<String> names = Collections.list(super.getHeaderNames());
|
||||||
|
if (!names.contains("Authorization")) {
|
||||||
|
names.add("Authorization");
|
||||||
|
}
|
||||||
|
return Collections.enumeration(names);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
filterChain.doFilter(wrapper, response);
|
||||||
|
} else {
|
||||||
|
filterChain.doFilter(request, response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,34 @@
|
|||||||
|
package org.jeecg.config.security;
|
||||||
|
|
||||||
|
import org.jeecg.common.api.CommonAPI;
|
||||||
|
import org.jeecg.common.system.vo.LoginUser;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.context.annotation.Lazy;
|
||||||
|
import org.springframework.core.convert.converter.Converter;
|
||||||
|
import org.springframework.security.authentication.AbstractAuthenticationToken;
|
||||||
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||||
|
import org.springframework.security.oauth2.jwt.Jwt;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* token只存储用户名与过期时间
|
||||||
|
* 这里通过取用户名转全量用户信息存储到Security中
|
||||||
|
* @author eightmonth@qq.com
|
||||||
|
* @date 2024/7/15 11:05
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class JeecgAuthenticationConvert implements Converter<Jwt, AbstractAuthenticationToken> {
|
||||||
|
|
||||||
|
@Lazy
|
||||||
|
@Autowired
|
||||||
|
private CommonAPI commonAPI;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AbstractAuthenticationToken convert(Jwt source) {
|
||||||
|
String username = source.getClaims().get("username").toString();
|
||||||
|
LoginUser loginUser = commonAPI.getUserByName(username);
|
||||||
|
return new UsernamePasswordAuthenticationToken(loginUser, null, new ArrayList<>());
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,135 @@
|
|||||||
|
package org.jeecg.config.security;
|
||||||
|
|
||||||
|
import org.jeecg.common.system.util.JwtUtil;
|
||||||
|
import org.springframework.lang.Nullable;
|
||||||
|
import org.springframework.security.oauth2.core.ClaimAccessor;
|
||||||
|
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||||
|
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||||
|
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
|
||||||
|
import org.springframework.security.oauth2.jwt.JwsHeader;
|
||||||
|
import org.springframework.security.oauth2.jwt.JwtClaimsSet;
|
||||||
|
import org.springframework.security.oauth2.jwt.JwtEncoder;
|
||||||
|
import org.springframework.security.oauth2.jwt.JwtEncoderParameters;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationGrantAuthenticationToken;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.token.*;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
import org.springframework.util.CollectionUtils;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.temporal.Temporal;
|
||||||
|
import java.time.temporal.TemporalUnit;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author eightmonth@qq.com
|
||||||
|
* @date 2024/7/11 17:10
|
||||||
|
*/
|
||||||
|
public class JeecgOAuth2AccessTokenGenerator implements OAuth2TokenGenerator<OAuth2AccessToken> {
|
||||||
|
private final JwtEncoder jwtEncoder;
|
||||||
|
|
||||||
|
private OAuth2TokenCustomizer<OAuth2TokenClaimsContext> accessTokenCustomizer;
|
||||||
|
|
||||||
|
public JeecgOAuth2AccessTokenGenerator(JwtEncoder jwtEncoder) {
|
||||||
|
this.jwtEncoder = jwtEncoder;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
public OAuth2AccessToken generate(OAuth2TokenContext context) {
|
||||||
|
if (!OAuth2TokenType.ACCESS_TOKEN.equals(context.getTokenType())) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String issuer = null;
|
||||||
|
if (context.getAuthorizationServerContext() != null) {
|
||||||
|
issuer = context.getAuthorizationServerContext().getIssuer();
|
||||||
|
}
|
||||||
|
RegisteredClient registeredClient = context.getRegisteredClient();
|
||||||
|
|
||||||
|
Instant issuedAt = Instant.now();
|
||||||
|
Instant expiresAt = issuedAt.plusMillis(JwtUtil.EXPIRE_TIME);
|
||||||
|
|
||||||
|
OAuth2TokenClaimsSet.Builder claimsBuilder = OAuth2TokenClaimsSet.builder();
|
||||||
|
if (StringUtils.hasText(issuer)) {
|
||||||
|
claimsBuilder.issuer(issuer);
|
||||||
|
}
|
||||||
|
claimsBuilder
|
||||||
|
.subject(context.getPrincipal().getName())
|
||||||
|
.audience(Collections.singletonList(registeredClient.getClientId()))
|
||||||
|
.issuedAt(issuedAt)
|
||||||
|
.expiresAt(expiresAt)
|
||||||
|
.notBefore(issuedAt)
|
||||||
|
.id(UUID.randomUUID().toString());
|
||||||
|
if (!CollectionUtils.isEmpty(context.getAuthorizedScopes())) {
|
||||||
|
claimsBuilder.claim(OAuth2ParameterNames.SCOPE, context.getAuthorizedScopes());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.accessTokenCustomizer != null) {
|
||||||
|
OAuth2TokenClaimsContext.Builder accessTokenContextBuilder = OAuth2TokenClaimsContext.with(claimsBuilder)
|
||||||
|
.registeredClient(context.getRegisteredClient())
|
||||||
|
.principal(context.getPrincipal())
|
||||||
|
.authorizationServerContext(context.getAuthorizationServerContext())
|
||||||
|
.authorizedScopes(context.getAuthorizedScopes())
|
||||||
|
.tokenType(context.getTokenType())
|
||||||
|
.authorizationGrantType(context.getAuthorizationGrantType());
|
||||||
|
if (context.getAuthorization() != null) {
|
||||||
|
accessTokenContextBuilder.authorization(context.getAuthorization());
|
||||||
|
}
|
||||||
|
if (context.getAuthorizationGrant() != null) {
|
||||||
|
accessTokenContextBuilder.authorizationGrant(context.getAuthorizationGrant());
|
||||||
|
}
|
||||||
|
|
||||||
|
OAuth2TokenClaimsContext accessTokenContext = accessTokenContextBuilder.build();
|
||||||
|
this.accessTokenCustomizer.customize(accessTokenContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
OAuth2TokenClaimsSet accessTokenClaimsSet = claimsBuilder.build();
|
||||||
|
OAuth2AuthorizationGrantAuthenticationToken oAuth2ResourceOwnerBaseAuthenticationToken = context.getAuthorizationGrant();
|
||||||
|
String username = (String) oAuth2ResourceOwnerBaseAuthenticationToken.getAdditionalParameters().get("username");
|
||||||
|
String tokenValue = jwtEncoder.encode(JwtEncoderParameters.from(JwsHeader.with(SignatureAlgorithm.ES256).keyId("jeecg").build(),
|
||||||
|
JwtClaimsSet.builder().claim("username", username).expiresAt(expiresAt).build())).getTokenValue();
|
||||||
|
|
||||||
|
//此处可以做改造将tokenValue随机数换成用户信息,方便后续多系统token互通认证(通过解密token得到username)
|
||||||
|
return new OAuth2AccessTokenClaims(OAuth2AccessToken.TokenType.BEARER, tokenValue,
|
||||||
|
accessTokenClaimsSet.getIssuedAt(), accessTokenClaimsSet.getExpiresAt(), context.getAuthorizedScopes(),
|
||||||
|
accessTokenClaimsSet.getClaims());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the {@link OAuth2TokenCustomizer} that customizes the
|
||||||
|
* {@link OAuth2TokenClaimsContext#getClaims() claims} for the
|
||||||
|
* {@link OAuth2AccessToken}.
|
||||||
|
* @param accessTokenCustomizer the {@link OAuth2TokenCustomizer} that customizes the
|
||||||
|
* claims for the {@code OAuth2AccessToken}
|
||||||
|
*/
|
||||||
|
public void setAccessTokenCustomizer(OAuth2TokenCustomizer<OAuth2TokenClaimsContext> accessTokenCustomizer) {
|
||||||
|
Assert.notNull(accessTokenCustomizer, "accessTokenCustomizer cannot be null");
|
||||||
|
this.accessTokenCustomizer = accessTokenCustomizer;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class OAuth2AccessTokenClaims extends OAuth2AccessToken implements ClaimAccessor {
|
||||||
|
|
||||||
|
private final Map<String, Object> claims;
|
||||||
|
|
||||||
|
private OAuth2AccessTokenClaims(TokenType tokenType, String tokenValue, Instant issuedAt, Instant expiresAt,
|
||||||
|
Set<String> scopes, Map<String, Object> claims) {
|
||||||
|
super(tokenType, tokenValue, issuedAt, expiresAt, scopes);
|
||||||
|
this.claims = claims;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String, Object> getClaims() {
|
||||||
|
return this.claims;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,104 @@
|
|||||||
|
package org.jeecg.config.security;
|
||||||
|
|
||||||
|
import cn.hutool.core.util.ArrayUtil;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.jeecg.common.api.CommonAPI;
|
||||||
|
import org.jeecg.common.system.vo.LoginUser;
|
||||||
|
import org.jeecg.common.util.RedisUtil;
|
||||||
|
import org.jeecg.config.security.utils.SecureUtil;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.context.annotation.Lazy;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.util.PatternMatchUtils;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* spring authorization server自定义权限处理,根据@PreAuthorize注解,判断当前用户是否具备权限
|
||||||
|
* @author EightMonth
|
||||||
|
* @date 2024/1/10 17:00
|
||||||
|
*/
|
||||||
|
@Service("jps")
|
||||||
|
@Slf4j
|
||||||
|
public class JeecgPermissionService {
|
||||||
|
private final String SPLIT = "::";
|
||||||
|
private final String PERM_PREFIX = "jps" + SPLIT;
|
||||||
|
|
||||||
|
@Lazy
|
||||||
|
@Autowired
|
||||||
|
private CommonAPI commonAPI;
|
||||||
|
@Autowired
|
||||||
|
private RedisUtil redisUtil;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断接口是否有任意xxx,xxx权限
|
||||||
|
* @param permissions 权限
|
||||||
|
* @return {boolean}
|
||||||
|
*/
|
||||||
|
public boolean requiresPermissions(String... permissions) {
|
||||||
|
if (ArrayUtil.isEmpty(permissions)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
LoginUser loginUser = SecureUtil.currentUser();
|
||||||
|
|
||||||
|
Object cache = redisUtil.get(buildKey("permission", loginUser.getId()));
|
||||||
|
Set<String> permissionList;
|
||||||
|
if (Objects.nonNull(cache)) {
|
||||||
|
permissionList = (Set<String>) cache;
|
||||||
|
} else {
|
||||||
|
permissionList = commonAPI.queryUserAuths(loginUser.getId());
|
||||||
|
redisUtil.set(buildKey("permission", loginUser.getId()), permissionList);
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean pass = permissionList.stream().filter(StringUtils::hasText)
|
||||||
|
.anyMatch(x -> PatternMatchUtils.simpleMatch(permissions, x));
|
||||||
|
if (!pass) {
|
||||||
|
log.error("权限不足,缺少权限:"+ Arrays.toString(permissions));
|
||||||
|
}
|
||||||
|
return pass;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断接口是否有任意xxx,xxx角色
|
||||||
|
* @param roles 角色
|
||||||
|
* @return {boolean}
|
||||||
|
*/
|
||||||
|
public boolean requiresRoles(String... roles) {
|
||||||
|
if (ArrayUtil.isEmpty(roles)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
LoginUser loginUser = SecureUtil.currentUser();
|
||||||
|
|
||||||
|
Object cache = redisUtil.get(buildKey("role", loginUser.getUsername()));
|
||||||
|
Set<String> roleList;
|
||||||
|
if (Objects.nonNull(cache)) {
|
||||||
|
roleList = (Set<String>) cache;
|
||||||
|
} else {
|
||||||
|
roleList = commonAPI.queryUserRoles(loginUser.getUsername());
|
||||||
|
redisUtil.set(buildKey("role", loginUser.getUsername()), roleList);
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean pass = roleList.stream().filter(StringUtils::hasText)
|
||||||
|
.anyMatch(x -> PatternMatchUtils.simpleMatch(roles, x));
|
||||||
|
if (!pass) {
|
||||||
|
log.error("权限不足,缺少角色:" + Arrays.toString(roles));
|
||||||
|
}
|
||||||
|
return pass;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 由于缓存key是以人的维度,角色列表、权限列表在值中,jeecg是以权限列表绑定在角色上,形成的权限集合
|
||||||
|
* 权限发生变更时,需要清理全部人的权限缓存
|
||||||
|
*/
|
||||||
|
public void clearCache() {
|
||||||
|
redisUtil.removeAll(PERM_PREFIX);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String buildKey(String type, String username) {
|
||||||
|
return PERM_PREFIX + type + SPLIT + username;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,54 @@
|
|||||||
|
package org.jeecg.config.security;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.data.redis.core.RedisTemplate;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsent;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* spring authorization server 自定义redis保存授权范围信息
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class JeecgRedisOAuth2AuthorizationConsentService implements OAuth2AuthorizationConsentService {
|
||||||
|
|
||||||
|
private final RedisTemplate<String, Object> redisTemplate;
|
||||||
|
|
||||||
|
private final static Long TIMEOUT = 10L;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void save(OAuth2AuthorizationConsent authorizationConsent) {
|
||||||
|
Assert.notNull(authorizationConsent, "authorizationConsent cannot be null");
|
||||||
|
|
||||||
|
redisTemplate.opsForValue().set(buildKey(authorizationConsent), authorizationConsent, TIMEOUT,
|
||||||
|
TimeUnit.MINUTES);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void remove(OAuth2AuthorizationConsent authorizationConsent) {
|
||||||
|
Assert.notNull(authorizationConsent, "authorizationConsent cannot be null");
|
||||||
|
redisTemplate.delete(buildKey(authorizationConsent));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public OAuth2AuthorizationConsent findById(String registeredClientId, String principalName) {
|
||||||
|
Assert.hasText(registeredClientId, "registeredClientId cannot be empty");
|
||||||
|
Assert.hasText(principalName, "principalName cannot be empty");
|
||||||
|
return (OAuth2AuthorizationConsent) redisTemplate.opsForValue()
|
||||||
|
.get(buildKey(registeredClientId, principalName));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String buildKey(String registeredClientId, String principalName) {
|
||||||
|
return "token:consent:" + registeredClientId + ":" + principalName;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String buildKey(OAuth2AuthorizationConsent authorizationConsent) {
|
||||||
|
return buildKey(authorizationConsent.getRegisteredClientId(), authorizationConsent.getPrincipalName());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,192 @@
|
|||||||
|
package org.jeecg.config.security;
|
||||||
|
|
||||||
|
import cn.hutool.core.collection.CollUtil;
|
||||||
|
import jakarta.annotation.PostConstruct;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||||
|
import org.springframework.data.redis.core.RedisTemplate;
|
||||||
|
import org.springframework.data.redis.serializer.RedisSerializer;
|
||||||
|
import org.springframework.lang.Nullable;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||||
|
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
|
||||||
|
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationCode;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
|
||||||
|
import java.time.temporal.ChronoUnit;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* spring authorization server自定义redis保存认证信息
|
||||||
|
* @author EightMonth
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class JeecgRedisOAuth2AuthorizationService implements OAuth2AuthorizationService{
|
||||||
|
|
||||||
|
private final static Long TIMEOUT = 10L;
|
||||||
|
|
||||||
|
private static final String AUTHORIZATION = "token";
|
||||||
|
|
||||||
|
private final RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private RedisConnectionFactory redisConnectionFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 因为保存sas的认证信息至redis,无法使用jeecg对redisTemplate的某些设置。
|
||||||
|
* 如果在使用时修改redisTemplate属性,会发生线程安全问题,最终容易引起系统无法正常运行。
|
||||||
|
* 所以重新建了一个redis client给到sas操作redis,并且该redis实例不注入spring 容器中
|
||||||
|
*/
|
||||||
|
@PostConstruct
|
||||||
|
public void initSasRedis() {
|
||||||
|
redisTemplate.setValueSerializer(RedisSerializer.java());
|
||||||
|
redisTemplate.setConnectionFactory(redisConnectionFactory);
|
||||||
|
redisTemplate.afterPropertiesSet();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void save(OAuth2Authorization authorization) {
|
||||||
|
Assert.notNull(authorization, "authorization cannot be null");
|
||||||
|
|
||||||
|
if (isState(authorization)) {
|
||||||
|
String token = authorization.getAttribute("state");
|
||||||
|
redisTemplate.opsForValue().set(buildKey(OAuth2ParameterNames.STATE, token), authorization, TIMEOUT,
|
||||||
|
TimeUnit.MINUTES);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isCode(authorization)) {
|
||||||
|
OAuth2Authorization.Token<OAuth2AuthorizationCode> authorizationCode = authorization
|
||||||
|
.getToken(OAuth2AuthorizationCode.class);
|
||||||
|
OAuth2AuthorizationCode authorizationCodeToken = authorizationCode.getToken();
|
||||||
|
long between = ChronoUnit.MINUTES.between(authorizationCodeToken.getIssuedAt(),
|
||||||
|
authorizationCodeToken.getExpiresAt());
|
||||||
|
redisTemplate.opsForValue().set(buildKey(OAuth2ParameterNames.CODE, authorizationCodeToken.getTokenValue()),
|
||||||
|
authorization, between, TimeUnit.MINUTES);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isRefreshToken(authorization)) {
|
||||||
|
OAuth2RefreshToken refreshToken = authorization.getRefreshToken().getToken();
|
||||||
|
long between = ChronoUnit.SECONDS.between(refreshToken.getIssuedAt(), refreshToken.getExpiresAt());
|
||||||
|
redisTemplate.opsForValue().set(buildKey(OAuth2ParameterNames.REFRESH_TOKEN, refreshToken.getTokenValue()),
|
||||||
|
authorization, between, TimeUnit.SECONDS);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isAccessToken(authorization)) {
|
||||||
|
OAuth2AccessToken accessToken = authorization.getAccessToken().getToken();
|
||||||
|
long between = ChronoUnit.SECONDS.between(accessToken.getIssuedAt(), accessToken.getExpiresAt());
|
||||||
|
redisTemplate.opsForValue().set(buildKey(OAuth2ParameterNames.ACCESS_TOKEN, accessToken.getTokenValue()),
|
||||||
|
authorization, between, TimeUnit.SECONDS);
|
||||||
|
|
||||||
|
// 扩展记录 access-token 、username 的关系 1::token::username::admin::xxx
|
||||||
|
String tokenUsername = String.format("%s::%s::%s", AUTHORIZATION, authorization.getPrincipalName(), accessToken.getTokenValue());
|
||||||
|
redisTemplate.opsForValue().set(tokenUsername, accessToken.getTokenValue(), between, TimeUnit.SECONDS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void remove(OAuth2Authorization authorization) {
|
||||||
|
Assert.notNull(authorization, "authorization cannot be null");
|
||||||
|
|
||||||
|
List<String> keys = new ArrayList<>();
|
||||||
|
if (isState(authorization)) {
|
||||||
|
String token = authorization.getAttribute("state");
|
||||||
|
keys.add(buildKey(OAuth2ParameterNames.STATE, token));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isCode(authorization)) {
|
||||||
|
OAuth2Authorization.Token<OAuth2AuthorizationCode> authorizationCode = authorization
|
||||||
|
.getToken(OAuth2AuthorizationCode.class);
|
||||||
|
OAuth2AuthorizationCode authorizationCodeToken = authorizationCode.getToken();
|
||||||
|
keys.add(buildKey(OAuth2ParameterNames.CODE, authorizationCodeToken.getTokenValue()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isRefreshToken(authorization)) {
|
||||||
|
OAuth2RefreshToken refreshToken = authorization.getRefreshToken().getToken();
|
||||||
|
keys.add(buildKey(OAuth2ParameterNames.REFRESH_TOKEN, refreshToken.getTokenValue()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isAccessToken(authorization)) {
|
||||||
|
OAuth2AccessToken accessToken = authorization.getAccessToken().getToken();
|
||||||
|
keys.add(buildKey(OAuth2ParameterNames.ACCESS_TOKEN, accessToken.getTokenValue()));
|
||||||
|
|
||||||
|
// 扩展记录 access-token 、username 的关系 1::token::username::admin::xxx
|
||||||
|
String key = String.format("%s::%s::%s", AUTHORIZATION, authorization.getPrincipalName(), accessToken.getTokenValue());
|
||||||
|
keys.add(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
redisTemplate.delete(keys);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Nullable
|
||||||
|
public OAuth2Authorization findById(String id) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Nullable
|
||||||
|
public OAuth2Authorization findByToken(String token, @Nullable OAuth2TokenType tokenType) {
|
||||||
|
Assert.hasText(token, "token cannot be empty");
|
||||||
|
Assert.notNull(tokenType, "tokenType cannot be empty");
|
||||||
|
return (OAuth2Authorization) redisTemplate.opsForValue().get(buildKey(tokenType.getValue(), token));
|
||||||
|
}
|
||||||
|
|
||||||
|
private String buildKey(String type, String id) {
|
||||||
|
return String.format("%s::%s::%s", AUTHORIZATION, type, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isState(OAuth2Authorization authorization) {
|
||||||
|
return Objects.nonNull(authorization.getAttribute("state"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isCode(OAuth2Authorization authorization) {
|
||||||
|
OAuth2Authorization.Token<OAuth2AuthorizationCode> authorizationCode = authorization
|
||||||
|
.getToken(OAuth2AuthorizationCode.class);
|
||||||
|
return Objects.nonNull(authorizationCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isRefreshToken(OAuth2Authorization authorization) {
|
||||||
|
return Objects.nonNull(authorization.getRefreshToken());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isAccessToken(OAuth2Authorization authorization) {
|
||||||
|
return Objects.nonNull(authorization.getAccessToken());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 扩展方法根据 username 查询是否存在存储的
|
||||||
|
* @param authentication
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public void removeByUsername(Authentication authentication) {
|
||||||
|
// 根据 username查询对应access-token
|
||||||
|
String authenticationName = authentication.getName();
|
||||||
|
|
||||||
|
// 扩展记录 access-token 、username 的关系 1::token::username::admin::xxx
|
||||||
|
String tokenUsernameKey = String.format("%s::%s::*", AUTHORIZATION, authenticationName);
|
||||||
|
Set<String> keys = redisTemplate.keys(tokenUsernameKey);
|
||||||
|
if (CollUtil.isEmpty(keys)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Object> tokenList = redisTemplate.opsForValue().multiGet(keys);
|
||||||
|
|
||||||
|
for (Object token : tokenList) {
|
||||||
|
// 根据token 查询存储的 OAuth2Authorization
|
||||||
|
OAuth2Authorization authorization = this.findByToken((String) token, OAuth2TokenType.ACCESS_TOKEN);
|
||||||
|
// 根据 OAuth2Authorization 删除相关令牌
|
||||||
|
this.remove(authorization);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,38 @@
|
|||||||
|
package org.jeecg.config.security;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 登录模式
|
||||||
|
* @author EightMonth
|
||||||
|
* @date 2024/1/10 17:43
|
||||||
|
*/
|
||||||
|
public class LoginType {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 密码模式
|
||||||
|
*/
|
||||||
|
public static final String PASSWORD = "password";
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 手机号+验证码模式
|
||||||
|
*/
|
||||||
|
public static final String PHONE = "phone";
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* app登录
|
||||||
|
*/
|
||||||
|
public static final String APP = "app";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 扫码登录
|
||||||
|
*/
|
||||||
|
public static final String SCAN = "scan";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 所有联合登录,比如github\钉钉\企业微信\微信
|
||||||
|
*/
|
||||||
|
public static final String SOCIAL = "social";
|
||||||
|
|
||||||
|
public static final String SELF = "self";
|
||||||
|
}
|
||||||
@ -0,0 +1,49 @@
|
|||||||
|
package org.jeecg.config.security;
|
||||||
|
|
||||||
|
import jakarta.servlet.FilterChain;
|
||||||
|
import jakarta.servlet.ServletException;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import org.jeecg.common.system.util.JwtUtil;
|
||||||
|
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||||
|
import org.springframework.security.oauth2.jwt.JwtDecoder;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
|
||||||
|
import org.springframework.security.oauth2.server.resource.BearerTokenErrors;
|
||||||
|
import org.springframework.security.oauth2.server.resource.web.DefaultBearerTokenResolver;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.web.filter.OncePerRequestFilter;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当用户被强退时,使客户端token失效
|
||||||
|
* @author eightmonth@qq.com
|
||||||
|
* @date 2024/3/7 17:30
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class RedisTokenValidationFilter extends OncePerRequestFilter {
|
||||||
|
private OAuth2AuthorizationService authorizationService;
|
||||||
|
private JwtDecoder jwtDecoder;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
|
||||||
|
// 从请求中获取token
|
||||||
|
DefaultBearerTokenResolver defaultBearerTokenResolver = new DefaultBearerTokenResolver();
|
||||||
|
String token = defaultBearerTokenResolver.resolve(request);
|
||||||
|
|
||||||
|
|
||||||
|
if (Objects.nonNull(token)) {
|
||||||
|
// 检查认证信息是否已被清除,如果已被清除,则令该token失效
|
||||||
|
OAuth2Authorization oAuth2Authorization = authorizationService.findByToken(token, OAuth2TokenType.ACCESS_TOKEN);
|
||||||
|
if (Objects.isNull(oAuth2Authorization)) {
|
||||||
|
throw new OAuth2AuthenticationException(BearerTokenErrors.invalidToken("认证信息已失效,请重新登录"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
filterChain.doFilter(request, response);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,310 @@
|
|||||||
|
package org.jeecg.config.security;
|
||||||
|
|
||||||
|
import com.nimbusds.jose.jwk.Curve;
|
||||||
|
import com.nimbusds.jose.jwk.ECKey;
|
||||||
|
import com.nimbusds.jose.jwk.JWKSet;
|
||||||
|
import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
|
||||||
|
import com.nimbusds.jose.jwk.source.JWKSource;
|
||||||
|
import com.nimbusds.jose.proc.SecurityContext;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.SneakyThrows;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.jeecg.common.system.util.JwtUtil;
|
||||||
|
import org.jeecg.config.security.app.AppGrantAuthenticationConvert;
|
||||||
|
import org.jeecg.config.security.app.AppGrantAuthenticationProvider;
|
||||||
|
import org.jeecg.config.security.password.PasswordGrantAuthenticationConvert;
|
||||||
|
import org.jeecg.config.security.password.PasswordGrantAuthenticationProvider;
|
||||||
|
import org.jeecg.config.security.phone.PhoneGrantAuthenticationConvert;
|
||||||
|
import org.jeecg.config.security.phone.PhoneGrantAuthenticationProvider;
|
||||||
|
import org.jeecg.config.security.social.SocialGrantAuthenticationConvert;
|
||||||
|
import org.jeecg.config.security.social.SocialGrantAuthenticationProvider;
|
||||||
|
import org.jeecg.config.shiro.ignore.InMemoryIgnoreAuth;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.core.annotation.Order;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.jdbc.core.JdbcTemplate;
|
||||||
|
import org.springframework.security.config.Customizer;
|
||||||
|
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
|
||||||
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||||
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||||
|
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||||
|
import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer;
|
||||||
|
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
|
import org.springframework.security.oauth2.jwt.JwtDecoder;
|
||||||
|
import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.token.*;
|
||||||
|
import org.springframework.security.web.SecurityFilterChain;
|
||||||
|
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
|
||||||
|
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||||
|
import org.springframework.security.web.util.matcher.MediaTypeRequestMatcher;
|
||||||
|
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||||
|
import org.springframework.util.CollectionUtils;
|
||||||
|
import org.springframework.web.cors.CorsConfiguration;
|
||||||
|
|
||||||
|
import java.security.KeyPair;
|
||||||
|
import java.security.KeyPairGenerator;
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
import java.security.interfaces.ECPrivateKey;
|
||||||
|
import java.security.interfaces.ECPublicKey;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* spring authorization server核心配置
|
||||||
|
* @author eightmonth@qq.com
|
||||||
|
* @date 2024/1/2 9:29
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
@EnableWebSecurity
|
||||||
|
@EnableMethodSecurity
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Slf4j
|
||||||
|
public class SecurityConfig {
|
||||||
|
|
||||||
|
private JdbcTemplate jdbcTemplate;
|
||||||
|
private OAuth2AuthorizationService authorizationService;
|
||||||
|
private JeecgAuthenticationConvert jeecgAuthenticationConvert;
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@Order(1)
|
||||||
|
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http)
|
||||||
|
throws Exception {
|
||||||
|
// 使用新的配置方式替代弃用的applyDefaultSecurity
|
||||||
|
http.securityMatcher(new AntPathRequestMatcher("/oauth2/**"))
|
||||||
|
.authorizeHttpRequests(authorize ->
|
||||||
|
authorize.anyRequest().authenticated()
|
||||||
|
)
|
||||||
|
.csrf(csrf -> csrf.disable())
|
||||||
|
.with(new OAuth2AuthorizationServerConfigurer(), oauth2 -> {
|
||||||
|
oauth2
|
||||||
|
.tokenEndpoint(tokenEndpoint -> tokenEndpoint
|
||||||
|
.accessTokenRequestConverter(new PasswordGrantAuthenticationConvert())
|
||||||
|
.authenticationProvider(new PasswordGrantAuthenticationProvider(authorizationService, tokenGenerator()))
|
||||||
|
)
|
||||||
|
.tokenEndpoint(tokenEndpoint -> tokenEndpoint
|
||||||
|
.accessTokenRequestConverter(new PhoneGrantAuthenticationConvert())
|
||||||
|
.authenticationProvider(new PhoneGrantAuthenticationProvider(authorizationService, tokenGenerator()))
|
||||||
|
)
|
||||||
|
.tokenEndpoint(tokenEndpoint -> tokenEndpoint
|
||||||
|
.accessTokenRequestConverter(new AppGrantAuthenticationConvert())
|
||||||
|
.authenticationProvider(new AppGrantAuthenticationProvider(authorizationService, tokenGenerator()))
|
||||||
|
)
|
||||||
|
.tokenEndpoint(tokenEndpoint -> tokenEndpoint
|
||||||
|
.accessTokenRequestConverter(new SocialGrantAuthenticationConvert())
|
||||||
|
.authenticationProvider(new SocialGrantAuthenticationProvider(authorizationService, tokenGenerator()))
|
||||||
|
)
|
||||||
|
//开启OpenID Connect 1.0(其中oidc为OpenID Connect的缩写)。 访问 /.well-known/openid-configuration即可获取认证信息
|
||||||
|
.oidc(Customizer.withDefaults());
|
||||||
|
});
|
||||||
|
|
||||||
|
//请求接口异常处理:无Token和Token无效的情况
|
||||||
|
http.exceptionHandling(exceptions -> exceptions
|
||||||
|
.authenticationEntryPoint((request, response, authException) -> {
|
||||||
|
// 记录详细的异常信息 - 未认证
|
||||||
|
log.error("接口访问失败(未认证),请求路径:{},错误信息:{}", request.getRequestURI(), authException.getMessage(), authException);
|
||||||
|
JwtUtil.responseError(response, 401, "Token无效或已过期");
|
||||||
|
})
|
||||||
|
.accessDeniedHandler((request, response, accessDeniedException) -> {
|
||||||
|
// 记录详细的异常信息 - token无效或权限不足
|
||||||
|
log.error("接口访问失败(token无效或权限不足),请求路径:{},错误信息:{}", request.getRequestURI(), accessDeniedException.getMessage(), accessDeniedException);
|
||||||
|
JwtUtil.responseError(response, 403, "权限不足");
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return http.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@Order(2)
|
||||||
|
public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http)
|
||||||
|
throws Exception {
|
||||||
|
http
|
||||||
|
//设置所有请求都需要认证,未认证的请求都被重定向到login页面进行登录
|
||||||
|
.authorizeHttpRequests((authorize) -> authorize
|
||||||
|
.requestMatchers(InMemoryIgnoreAuth.get().stream().map(AntPathRequestMatcher::antMatcher).toList().toArray(new AntPathRequestMatcher[0])).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/sys/cas/client/validateLogin")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/sys/randomImage/**")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/sys/checkCaptcha")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/sys/login")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/sys/mLogin")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/sys/logout")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/sys/thirdLogin/**")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/sys/getEncryptedString")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/sys/sms")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/sys/phoneLogin")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/sys/user/checkOnlyUser")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/sys/user/register")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/sys/user/phoneVerification")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/sys/user/passwordChange")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/auth/2step-code")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/sys/common/static/**")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/sys/common/pdf/**")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/generic/**")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/sys/getLoginQrcode/**")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/sys/getQrcodeToken/**")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/sys/checkAuth")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/doc.html")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/**/*.js")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/**/*.css")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/**/*.html")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/**/*.svg")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/**/*.pdf")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/**/*.jpg")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/**/*.png")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/**/*.gif")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/**/*.ico")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/**/*.ttf")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/**/*.woff")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/**/*.woff2")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/druid/**")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/swagger-ui.html")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/swagger**/**")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/webjars/**")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/v3/**")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/WW_verify*")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/sys/annountCement/show/**")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/api/getUserInfo")).permitAll()
|
||||||
|
|
||||||
|
//积木报表排除
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/jmreport/**")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/**/*.js.map")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/**/*.css.map")).permitAll()
|
||||||
|
//积木BI大屏和仪表盘排除
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/drag/view")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/drag/onlDragDatasetHead/getLoginUser")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/drag/page/queryById")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/drag/page/addVisitsNumber")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/drag/page/queryTemplateList")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/drag/share/view/**")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/drag/onlDragDatasetHead/getAllChartData")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/drag/onlDragDatasetHead/getTotalData")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/drag/mock/json/**")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/jimubi/view")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/jimubi/share/view/**")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/drag/onlDragDatasetHead/getMapDataByCode")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/drag/onlDragDatasetHead/getTotalDataByCompId")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/drag/onlDragDatasetHead/queryAllById")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/drag/onlDragDatasetHead/getDictByCodes")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/dragChannelSocket/**")).permitAll()
|
||||||
|
//大屏模板例子
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/test/bigScreen/**")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/bigscreen/template1/**")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/bigscreen/template1/**")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/websocket/**")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/newsWebsocket/**")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/vxeSocket/**")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/test/seata/**")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/error")).permitAll()
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/openapi/call/**")).permitAll()
|
||||||
|
// APP版本信息
|
||||||
|
.requestMatchers(AntPathRequestMatcher.antMatcher("/sys/version/app3version")).permitAll()
|
||||||
|
.anyRequest().authenticated()
|
||||||
|
)
|
||||||
|
.headers(headers -> headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable))
|
||||||
|
.cors(cors -> cors
|
||||||
|
.configurationSource(req -> {
|
||||||
|
CorsConfiguration config = new CorsConfiguration();
|
||||||
|
config.applyPermitDefaultValues();
|
||||||
|
config.setAllowedMethods(Arrays.asList("HEAD", "GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"));
|
||||||
|
return config;
|
||||||
|
}))
|
||||||
|
.csrf(AbstractHttpConfigurer::disable)
|
||||||
|
// 配置OAuth2资源服务器,并添加JWT异常处理
|
||||||
|
.oauth2ResourceServer(oauth2 -> oauth2
|
||||||
|
.jwt(jwt -> jwt.jwtAuthenticationConverter(jeecgAuthenticationConvert))
|
||||||
|
.authenticationEntryPoint((request, response, authException) -> {
|
||||||
|
// 处理JWT解析失败的情况
|
||||||
|
log.error("JWT验证失败,请求路径:{},错误信息:{}", request.getRequestURI(), authException.getMessage(), authException);
|
||||||
|
JwtUtil.responseError(response, 401, "Token无效或已过期");
|
||||||
|
})
|
||||||
|
.accessDeniedHandler((request, response, accessDeniedException) -> {
|
||||||
|
// 处理权限不足的情况
|
||||||
|
log.error("权限验证失败,请求路径:{},错误信息:{}", request.getRequestURI(), accessDeniedException.getMessage(), accessDeniedException);
|
||||||
|
JwtUtil.responseError(response, 403, "权限不足");
|
||||||
|
})
|
||||||
|
)
|
||||||
|
// 全局异常处理
|
||||||
|
.exceptionHandling(exceptions -> exceptions
|
||||||
|
.authenticationEntryPoint((request, response, authException) -> {
|
||||||
|
// 记录详细的异常信息 - 未认证
|
||||||
|
log.error("接口访问失败(未认证),请求路径:{},错误信息:{}", request.getRequestURI(), authException.getMessage(), authException);
|
||||||
|
JwtUtil.responseError(response, 401, "Token无效或已过期");
|
||||||
|
})
|
||||||
|
.accessDeniedHandler((request, response, accessDeniedException) -> {
|
||||||
|
// 记录详细的异常信息 - token无效或权限不足
|
||||||
|
log.error("接口访问失败(token无效或权限不足),请求路径:{},错误信息:{}", request.getRequestURI(), accessDeniedException.getMessage(), accessDeniedException);
|
||||||
|
JwtUtil.responseError(response, 403, "权限不足");
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return http.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数据库保存注册客户端信息
|
||||||
|
*/
|
||||||
|
@Bean
|
||||||
|
public RegisteredClientRepository registeredClientRepository() {
|
||||||
|
return new JdbcRegisteredClientRepository(jdbcTemplate);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*配置 JWK,为JWT(id_token)提供加密密钥,用于加密/解密或签名/验签
|
||||||
|
* JWK详细见:https://datatracker.ietf.org/doc/html/draft-ietf-jose-json-web-key-41
|
||||||
|
*/
|
||||||
|
@Bean
|
||||||
|
@SneakyThrows
|
||||||
|
public JWKSource<SecurityContext> jwkSource() {
|
||||||
|
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("EC");
|
||||||
|
// 如果不设置secureRandom,会存在一个问题,当应用重启后,原有的token将会全部失效,因为重启的keyPair与之前已经不同
|
||||||
|
SecureRandom secureRandom = SecureRandom.getInstance("SHA1PRNG");
|
||||||
|
// 重要!生产环境需要修改!
|
||||||
|
secureRandom.setSeed("jeecg".getBytes());
|
||||||
|
keyPairGenerator.initialize(256, secureRandom);
|
||||||
|
KeyPair keyPair = keyPairGenerator.generateKeyPair();
|
||||||
|
ECPublicKey publicKey = (ECPublicKey) keyPair.getPublic();
|
||||||
|
ECPrivateKey privateKey = (ECPrivateKey) keyPair.getPrivate();
|
||||||
|
|
||||||
|
ECKey jwk = new ECKey.Builder(Curve.P_256, publicKey)
|
||||||
|
.privateKey(privateKey)
|
||||||
|
.keyID("jeecg")
|
||||||
|
.build();
|
||||||
|
JWKSet jwkSet = new JWKSet(jwk);
|
||||||
|
return new ImmutableJWKSet<>(jwkSet);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public PasswordEncoder passwordEncoder() {
|
||||||
|
return NoOpPasswordEncoder.getInstance();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 配置jwt解析器
|
||||||
|
*/
|
||||||
|
@Bean
|
||||||
|
public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
|
||||||
|
return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*配置token生成器
|
||||||
|
*/
|
||||||
|
@Bean
|
||||||
|
OAuth2TokenGenerator<?> tokenGenerator() {
|
||||||
|
JwtGenerator jwtGenerator = new JwtGenerator(new NimbusJwtEncoder(jwkSource()));
|
||||||
|
OAuth2AccessTokenGenerator accessTokenGenerator = new OAuth2AccessTokenGenerator();
|
||||||
|
OAuth2RefreshTokenGenerator refreshTokenGenerator = new OAuth2RefreshTokenGenerator();
|
||||||
|
return new DelegatingOAuth2TokenGenerator(
|
||||||
|
new JeecgOAuth2AccessTokenGenerator(new NimbusJwtEncoder(jwkSource())),
|
||||||
|
new OAuth2RefreshTokenGenerator()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,81 @@
|
|||||||
|
package org.jeecg.config.security.app;
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import org.jeecg.config.security.LoginType;
|
||||||
|
import org.jeecg.config.security.password.PasswordGrantAuthenticationToken;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
|
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||||
|
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||||
|
import org.springframework.security.web.authentication.AuthenticationConverter;
|
||||||
|
import org.springframework.util.LinkedMultiValueMap;
|
||||||
|
import org.springframework.util.MultiValueMap;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* APP模式认证转换器
|
||||||
|
* @author EightMonth
|
||||||
|
* @date 2024/1/1
|
||||||
|
*/
|
||||||
|
public class AppGrantAuthenticationConvert implements AuthenticationConverter {
|
||||||
|
@Override
|
||||||
|
public Authentication convert(HttpServletRequest request) {
|
||||||
|
|
||||||
|
String grantType = request.getParameter(OAuth2ParameterNames.GRANT_TYPE);
|
||||||
|
if (!LoginType.APP.equals(grantType)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Authentication clientPrincipal = SecurityContextHolder.getContext().getAuthentication();
|
||||||
|
|
||||||
|
//从request中提取请求参数,然后存入MultiValueMap<String, String>
|
||||||
|
MultiValueMap<String, String> parameters = getParameters(request);
|
||||||
|
|
||||||
|
// username (REQUIRED)
|
||||||
|
String username = parameters.getFirst(OAuth2ParameterNames.USERNAME);
|
||||||
|
if (!StringUtils.hasText(username) ||
|
||||||
|
parameters.get(OAuth2ParameterNames.USERNAME).size() != 1) {
|
||||||
|
throw new OAuth2AuthenticationException("无效请求,用户名不能为空!");
|
||||||
|
}
|
||||||
|
String password = parameters.getFirst(OAuth2ParameterNames.PASSWORD);
|
||||||
|
if (!StringUtils.hasText(password) ||
|
||||||
|
parameters.get(OAuth2ParameterNames.PASSWORD).size() != 1) {
|
||||||
|
throw new OAuth2AuthenticationException("无效请求,密码不能为空!");
|
||||||
|
}
|
||||||
|
|
||||||
|
//收集要传入PasswordGrantAuthenticationToken构造方法的参数,
|
||||||
|
//该参数接下来在PasswordGrantAuthenticationProvider中使用
|
||||||
|
Map<String, Object> additionalParameters = new HashMap<>();
|
||||||
|
//遍历从request中提取的参数,排除掉grant_type、client_id、code等字段参数,其他参数收集到additionalParameters中
|
||||||
|
parameters.forEach((key, value) -> {
|
||||||
|
if (!key.equals(OAuth2ParameterNames.GRANT_TYPE) &&
|
||||||
|
!key.equals(OAuth2ParameterNames.CLIENT_ID) &&
|
||||||
|
!key.equals(OAuth2ParameterNames.CODE)) {
|
||||||
|
additionalParameters.put(key, value.get(0));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
//返回自定义的PasswordGrantAuthenticationToken对象
|
||||||
|
return new PasswordGrantAuthenticationToken(clientPrincipal, additionalParameters);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*从request中提取请求参数,然后存入MultiValueMap<String, String>
|
||||||
|
*/
|
||||||
|
private static MultiValueMap<String, String> getParameters(HttpServletRequest request) {
|
||||||
|
Map<String, String[]> parameterMap = request.getParameterMap();
|
||||||
|
MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>(parameterMap.size());
|
||||||
|
parameterMap.forEach((key, values) -> {
|
||||||
|
if (values.length > 0) {
|
||||||
|
for (String value : values) {
|
||||||
|
parameters.add(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return parameters;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,320 @@
|
|||||||
|
package org.jeecg.config.security.app;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson.JSONObject;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.jeecg.common.api.CommonAPI;
|
||||||
|
import org.jeecg.common.constant.CacheConstant;
|
||||||
|
import org.jeecg.common.constant.CommonConstant;
|
||||||
|
import org.jeecg.common.exception.JeecgBootException;
|
||||||
|
import org.jeecg.common.exception.JeecgCaptchaException;
|
||||||
|
import org.jeecg.common.system.vo.LoginUser;
|
||||||
|
import org.jeecg.common.system.vo.SysDepartModel;
|
||||||
|
import org.jeecg.common.util.Md5Util;
|
||||||
|
import org.jeecg.common.util.PasswordUtil;
|
||||||
|
import org.jeecg.common.util.RedisUtil;
|
||||||
|
import org.jeecg.common.util.oConvertUtils;
|
||||||
|
import org.jeecg.config.JeecgBaseConfig;
|
||||||
|
import org.jeecg.config.security.password.PasswordGrantAuthenticationToken;
|
||||||
|
import org.jeecg.modules.base.service.BaseCommonService;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.context.annotation.Lazy;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.security.authentication.AuthenticationProvider;
|
||||||
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.core.AuthenticationException;
|
||||||
|
import org.springframework.security.oauth2.core.*;
|
||||||
|
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AccessTokenAuthenticationToken;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContextHolder;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.token.DefaultOAuth2TokenContext;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenContext;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
|
import java.security.Principal;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* APP模式认证处理器,负责处理该认证模式下的核心逻辑
|
||||||
|
* @author EightMonth
|
||||||
|
* @date 2024/1/1
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
public class AppGrantAuthenticationProvider implements AuthenticationProvider {
|
||||||
|
|
||||||
|
private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-5.2";
|
||||||
|
|
||||||
|
private final OAuth2AuthorizationService authorizationService;
|
||||||
|
private final OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator;
|
||||||
|
@Lazy
|
||||||
|
@Autowired
|
||||||
|
private CommonAPI commonAPI;
|
||||||
|
@Autowired
|
||||||
|
private RedisUtil redisUtil;
|
||||||
|
@Autowired
|
||||||
|
private JeecgBaseConfig jeecgBaseConfig;
|
||||||
|
@Autowired
|
||||||
|
private BaseCommonService baseCommonService;
|
||||||
|
|
||||||
|
public AppGrantAuthenticationProvider(OAuth2AuthorizationService authorizationService, OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator) {
|
||||||
|
Assert.notNull(authorizationService, "authorizationService cannot be null");
|
||||||
|
Assert.notNull(tokenGenerator, "tokenGenerator cannot be null");
|
||||||
|
this.authorizationService = authorizationService;
|
||||||
|
this.tokenGenerator = tokenGenerator;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
|
||||||
|
AppGrantAuthenticationToken appGrantAuthenticationToken = (AppGrantAuthenticationToken) authentication;
|
||||||
|
Map<String, Object> additionalParameter = appGrantAuthenticationToken.getAdditionalParameters();
|
||||||
|
|
||||||
|
// 授权类型
|
||||||
|
AuthorizationGrantType authorizationGrantType = appGrantAuthenticationToken.getGrantType();
|
||||||
|
// 用户名
|
||||||
|
String username = (String) additionalParameter.get(OAuth2ParameterNames.USERNAME);
|
||||||
|
// 密码
|
||||||
|
String password = (String) additionalParameter.get(OAuth2ParameterNames.PASSWORD);
|
||||||
|
//请求参数权限范围
|
||||||
|
String requestScopesStr = (String)additionalParameter.getOrDefault(OAuth2ParameterNames.SCOPE, "*");
|
||||||
|
//请求参数权限范围专场集合
|
||||||
|
Set<String> requestScopeSet = Stream.of(requestScopesStr.split(" ")).collect(Collectors.toSet());
|
||||||
|
// 验证码
|
||||||
|
String captcha = (String) additionalParameter.get("captcha");
|
||||||
|
String checkKey = (String) additionalParameter.get("checkKey");
|
||||||
|
|
||||||
|
OAuth2ClientAuthenticationToken clientPrincipal = getAuthenticatedClientElseThrowInvalidClient(appGrantAuthenticationToken);
|
||||||
|
RegisteredClient registeredClient = clientPrincipal.getRegisteredClient();
|
||||||
|
|
||||||
|
// 检查登录失败次数
|
||||||
|
if(isLoginFailOvertimes(username)){
|
||||||
|
Map<String, Object> map = new HashMap<>();
|
||||||
|
map.put("message", "该用户登录失败次数过多,请于10分钟后再次登录!");
|
||||||
|
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,"fdsafas", Instant.now(), Instant.now().plusNanos(1)), null, map);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(captcha==null){
|
||||||
|
Map<String, Object> map = new HashMap<>();
|
||||||
|
map.put("message", "验证码无效");
|
||||||
|
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,"fdsafas", Instant.now(), Instant.now().plusNanos(1)), null, map);
|
||||||
|
}
|
||||||
|
String lowerCaseCaptcha = captcha.toLowerCase();
|
||||||
|
// 加入密钥作为混淆,避免简单的拼接,被外部利用,用户自定义该密钥即可
|
||||||
|
String origin = lowerCaseCaptcha+checkKey+jeecgBaseConfig.getSignatureSecret();
|
||||||
|
String realKey = Md5Util.md5Encode(origin, "utf-8");
|
||||||
|
Object checkCode = redisUtil.get(realKey);
|
||||||
|
//当进入登录页时,有一定几率出现验证码错误 #1714
|
||||||
|
if(checkCode==null || !checkCode.toString().equals(lowerCaseCaptcha)) {
|
||||||
|
Map<String, Object> map = new HashMap<>();
|
||||||
|
map.put("message", "验证码错误");
|
||||||
|
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,"fdsafas", Instant.now(), Instant.now().plusNanos(1)), null, map);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!registeredClient.getAuthorizationGrantTypes().contains(authorizationGrantType)) {
|
||||||
|
Map<String, Object> map = new HashMap<>();
|
||||||
|
map.put("message", "非法登录");
|
||||||
|
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,"fdsafas", Instant.now(), Instant.now().plusNanos(1)), null, map);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 通过用户名获取用户信息
|
||||||
|
LoginUser loginUser = commonAPI.getUserByName(username);
|
||||||
|
//update-begin---author:eightmonth ---date:2024-04-30 for:【6168】master分支切sas分支登录发生错误-----------
|
||||||
|
if (Objects.isNull(loginUser) || !StringUtils.hasText(loginUser.getSalt())) {
|
||||||
|
redisUtil.del(CacheConstant.SYS_USERS_CACHE+"::"+username);
|
||||||
|
loginUser = commonAPI.getUserByName(username);
|
||||||
|
}
|
||||||
|
//update-end---author:eightmonth ---date::2024-04-30 for:【6168】master分支切sas分支登录发生错误--------------
|
||||||
|
// 检查用户可行性
|
||||||
|
checkUserIsEffective(loginUser);
|
||||||
|
|
||||||
|
// 不使用spring security passwordEncoder针对密码进行匹配,使用自有加密匹配,针对 spring security使用noop传输
|
||||||
|
password = PasswordUtil.encrypt(username, password, loginUser.getSalt());
|
||||||
|
if (!password.equals(loginUser.getPassword())) {
|
||||||
|
addLoginFailOvertimes(username);
|
||||||
|
Map<String, Object> map = new HashMap<>();
|
||||||
|
map.put("message", "用户名或密码不正确");
|
||||||
|
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,"fdsafas", Instant.now(), Instant.now().plusNanos(1)), null, map);
|
||||||
|
}
|
||||||
|
|
||||||
|
//由于在上面已验证过用户名、密码,现在构建一个已认证的对象UsernamePasswordAuthenticationToken
|
||||||
|
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = UsernamePasswordAuthenticationToken.authenticated(loginUser,clientPrincipal,new ArrayList<>());
|
||||||
|
|
||||||
|
DefaultOAuth2TokenContext.Builder tokenContextBuilder = DefaultOAuth2TokenContext.builder()
|
||||||
|
.registeredClient(registeredClient)
|
||||||
|
.principal(usernamePasswordAuthenticationToken)
|
||||||
|
.authorizationServerContext(AuthorizationServerContextHolder.getContext())
|
||||||
|
.authorizationGrantType(authorizationGrantType)
|
||||||
|
.authorizedScopes(requestScopeSet)
|
||||||
|
.authorizationGrant(appGrantAuthenticationToken);
|
||||||
|
|
||||||
|
OAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization.withRegisteredClient(registeredClient)
|
||||||
|
.principalName(clientPrincipal.getName())
|
||||||
|
.authorizedScopes(requestScopeSet)
|
||||||
|
.attribute(Principal.class.getName(), username)
|
||||||
|
.authorizationGrantType(authorizationGrantType);
|
||||||
|
|
||||||
|
|
||||||
|
// ----- Access token -----
|
||||||
|
OAuth2TokenContext tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.ACCESS_TOKEN).build();
|
||||||
|
OAuth2Token generatedAccessToken = this.tokenGenerator.generate(tokenContext);
|
||||||
|
if (generatedAccessToken == null) {
|
||||||
|
Map<String, Object> map = new HashMap<>();
|
||||||
|
map.put("message", "无法生成访问token,请联系管理系。");
|
||||||
|
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,"fdsafas", Instant.now(), Instant.now().plusNanos(1)), null, map);
|
||||||
|
}
|
||||||
|
OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
|
||||||
|
generatedAccessToken.getTokenValue(), generatedAccessToken.getIssuedAt(),
|
||||||
|
generatedAccessToken.getExpiresAt(), tokenContext.getAuthorizedScopes());
|
||||||
|
if (generatedAccessToken instanceof ClaimAccessor) {
|
||||||
|
authorizationBuilder.token(accessToken, (metadata) -> {
|
||||||
|
metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, ((ClaimAccessor) generatedAccessToken).getClaims());
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
authorizationBuilder.accessToken(accessToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- Refresh token -----
|
||||||
|
OAuth2RefreshToken refreshToken = null;
|
||||||
|
if (registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.REFRESH_TOKEN) &&
|
||||||
|
// 不向公共客户端颁发刷新令牌
|
||||||
|
!clientPrincipal.getClientAuthenticationMethod().equals(ClientAuthenticationMethod.NONE)) {
|
||||||
|
|
||||||
|
tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.REFRESH_TOKEN).build();
|
||||||
|
OAuth2Token generatedRefreshToken = this.tokenGenerator.generate(tokenContext);
|
||||||
|
if (!(generatedRefreshToken instanceof OAuth2RefreshToken)) {
|
||||||
|
Map<String, Object> map = new HashMap<>();
|
||||||
|
map.put("message", "无法生成刷新token,请联系管理员。");
|
||||||
|
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,"fdsafas", Instant.now(), Instant.now().plusNanos(1)), null, map);
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshToken = (OAuth2RefreshToken) generatedRefreshToken;
|
||||||
|
authorizationBuilder.refreshToken(refreshToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
OAuth2Authorization authorization = authorizationBuilder.build();
|
||||||
|
|
||||||
|
// 保存认证信息至redis
|
||||||
|
authorizationService.save(authorization);
|
||||||
|
|
||||||
|
// 登录成功,删除redis中的验证码
|
||||||
|
redisUtil.del(realKey);
|
||||||
|
redisUtil.del(CommonConstant.LOGIN_FAIL + username);
|
||||||
|
baseCommonService.addLog("用户名: " + username + ",登录成功!", CommonConstant.LOG_TYPE_1, null,loginUser);
|
||||||
|
|
||||||
|
JSONObject addition = new JSONObject(new LinkedHashMap<>());
|
||||||
|
addition.put("token", accessToken.getTokenValue());
|
||||||
|
// 设置租户
|
||||||
|
JSONObject jsonObject = commonAPI.setLoginTenant(username);
|
||||||
|
addition.putAll(jsonObject.getInnerMap());
|
||||||
|
|
||||||
|
// 设置登录用户信息
|
||||||
|
addition.put("userInfo", loginUser);
|
||||||
|
addition.put("sysAllDictItems", commonAPI.queryAllDictItems());
|
||||||
|
|
||||||
|
List<SysDepartModel> departs = commonAPI.queryUserDeparts(loginUser.getId());
|
||||||
|
addition.put("departs", departs);
|
||||||
|
if (departs == null || departs.size() == 0) {
|
||||||
|
addition.put("multi_depart", 0);
|
||||||
|
} else if (departs.size() == 1) {
|
||||||
|
commonAPI.updateUserDepart(username, departs.get(0).getOrgCode(),null);
|
||||||
|
addition.put("multi_depart", 1);
|
||||||
|
} else {
|
||||||
|
//查询当前是否有登录部门
|
||||||
|
if(oConvertUtils.isEmpty(loginUser.getOrgCode())){
|
||||||
|
commonAPI.updateUserDepart(username, departs.get(0).getOrgCode(),null);
|
||||||
|
}
|
||||||
|
addition.put("multi_depart", 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 兼容原有shiro登录结果处理
|
||||||
|
Map<String, Object> map = new HashMap<>();
|
||||||
|
map.put("result", addition);
|
||||||
|
map.put("code", 200);
|
||||||
|
map.put("success", true);
|
||||||
|
map.put("timestamp", System.currentTimeMillis());
|
||||||
|
|
||||||
|
// 返回access_token、refresh_token以及其它信息给到前端
|
||||||
|
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, accessToken, refreshToken, map);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean supports(Class<?> authentication) {
|
||||||
|
return AppGrantAuthenticationToken.class.isAssignableFrom(authentication);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static OAuth2ClientAuthenticationToken getAuthenticatedClientElseThrowInvalidClient(Authentication authentication) {
|
||||||
|
OAuth2ClientAuthenticationToken clientPrincipal = null;
|
||||||
|
if (OAuth2ClientAuthenticationToken.class.isAssignableFrom(authentication.getPrincipal().getClass())) {
|
||||||
|
clientPrincipal = (OAuth2ClientAuthenticationToken) authentication.getPrincipal();
|
||||||
|
}
|
||||||
|
if (clientPrincipal != null && clientPrincipal.isAuthenticated()) {
|
||||||
|
return clientPrincipal;
|
||||||
|
}
|
||||||
|
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_CLIENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 登录失败超出次数5 返回true
|
||||||
|
* @param username
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
private boolean isLoginFailOvertimes(String username){
|
||||||
|
String key = CommonConstant.LOGIN_FAIL + username;
|
||||||
|
Object failTime = redisUtil.get(key);
|
||||||
|
if(failTime!=null){
|
||||||
|
Integer val = Integer.parseInt(failTime.toString());
|
||||||
|
if(val>5){
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录登录失败次数
|
||||||
|
* @param username
|
||||||
|
*/
|
||||||
|
private void addLoginFailOvertimes(String username){
|
||||||
|
String key = CommonConstant.LOGIN_FAIL + username;
|
||||||
|
Object failTime = redisUtil.get(key);
|
||||||
|
Integer val = 0;
|
||||||
|
if(failTime!=null){
|
||||||
|
val = Integer.parseInt(failTime.toString());
|
||||||
|
}
|
||||||
|
// 10分钟
|
||||||
|
redisUtil.set(key, ++val, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验用户是否有效
|
||||||
|
*/
|
||||||
|
private void checkUserIsEffective(LoginUser loginUser) {
|
||||||
|
//情况1:根据用户信息查询,该用户不存在
|
||||||
|
if (Objects.isNull(loginUser)) {
|
||||||
|
baseCommonService.addLog("用户登录失败,用户不存在!", CommonConstant.LOG_TYPE_1, null);
|
||||||
|
throw new JeecgBootException("该用户不存在,请注册");
|
||||||
|
}
|
||||||
|
//情况2:根据用户信息查询,该用户已注销
|
||||||
|
//update-begin---author:王帅 Date:20200601 for:if条件永远为falsebug------------
|
||||||
|
if (CommonConstant.DEL_FLAG_1.equals(loginUser.getDelFlag())) {
|
||||||
|
//update-end---author:王帅 Date:20200601 for:if条件永远为falsebug------------
|
||||||
|
baseCommonService.addLog("用户登录失败,用户名:" + loginUser.getUsername() + "已注销!", CommonConstant.LOG_TYPE_1, null);
|
||||||
|
throw new JeecgBootException("该用户已注销");
|
||||||
|
}
|
||||||
|
//情况3:根据用户信息查询,该用户已冻结
|
||||||
|
if (CommonConstant.USER_FREEZE.equals(loginUser.getStatus())) {
|
||||||
|
baseCommonService.addLog("用户登录失败,用户名:" + loginUser.getUsername() + "已冻结!", CommonConstant.LOG_TYPE_1, null);
|
||||||
|
throw new JeecgBootException("该用户已冻结");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,21 @@
|
|||||||
|
package org.jeecg.config.security.app;
|
||||||
|
|
||||||
|
import org.jeecg.config.security.LoginType;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationGrantAuthenticationToken;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* APP模式认证专用token类型,方法spring authorization server进行认证流转,配合convert使用
|
||||||
|
* @author EightMonth
|
||||||
|
* @date 2024/1/1
|
||||||
|
*/
|
||||||
|
public class AppGrantAuthenticationToken extends OAuth2AuthorizationGrantAuthenticationToken {
|
||||||
|
|
||||||
|
public AppGrantAuthenticationToken(Authentication clientPrincipal, Map<String, Object> additionalParameters) {
|
||||||
|
super(new AuthorizationGrantType(LoginType.APP), clientPrincipal, additionalParameters);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,82 @@
|
|||||||
|
package org.jeecg.config.security.password;
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import org.jeecg.common.constant.CommonConstant;
|
||||||
|
import org.jeecg.config.security.LoginType;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
|
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||||
|
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||||
|
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||||
|
import org.springframework.security.web.authentication.AuthenticationConverter;
|
||||||
|
import org.springframework.util.LinkedMultiValueMap;
|
||||||
|
import org.springframework.util.MultiValueMap;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 密码模式认证转换器
|
||||||
|
* @author EightMonth
|
||||||
|
* @date 2024/1/1
|
||||||
|
*/
|
||||||
|
public class PasswordGrantAuthenticationConvert implements AuthenticationConverter {
|
||||||
|
@Override
|
||||||
|
public Authentication convert(HttpServletRequest request) {
|
||||||
|
|
||||||
|
String grantType = request.getParameter(OAuth2ParameterNames.GRANT_TYPE);
|
||||||
|
if (!LoginType.PASSWORD.equals(grantType)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Authentication clientPrincipal = SecurityContextHolder.getContext().getAuthentication();
|
||||||
|
|
||||||
|
//从request中提取请求参数,然后存入MultiValueMap<String, String>
|
||||||
|
MultiValueMap<String, String> parameters = getParameters(request);
|
||||||
|
|
||||||
|
// username (REQUIRED)
|
||||||
|
String username = parameters.getFirst(OAuth2ParameterNames.USERNAME);
|
||||||
|
if (!StringUtils.hasText(username) ||
|
||||||
|
parameters.get(OAuth2ParameterNames.USERNAME).size() != 1) {
|
||||||
|
throw new OAuth2AuthenticationException("无效请求,用户名不能为空!");
|
||||||
|
}
|
||||||
|
String password = parameters.getFirst(OAuth2ParameterNames.PASSWORD);
|
||||||
|
if (!StringUtils.hasText(password) ||
|
||||||
|
parameters.get(OAuth2ParameterNames.PASSWORD).size() != 1) {
|
||||||
|
throw new OAuth2AuthenticationException("无效请求,密码不能为空!");
|
||||||
|
}
|
||||||
|
|
||||||
|
//收集要传入PasswordGrantAuthenticationToken构造方法的参数,
|
||||||
|
//该参数接下来在PasswordGrantAuthenticationProvider中使用
|
||||||
|
Map<String, Object> additionalParameters = new HashMap<>();
|
||||||
|
//遍历从request中提取的参数,排除掉grant_type、client_id、code等字段参数,其他参数收集到additionalParameters中
|
||||||
|
parameters.forEach((key, value) -> {
|
||||||
|
if (!key.equals(OAuth2ParameterNames.GRANT_TYPE) &&
|
||||||
|
!key.equals(OAuth2ParameterNames.CLIENT_ID) &&
|
||||||
|
!key.equals(OAuth2ParameterNames.CODE)) {
|
||||||
|
additionalParameters.put(key, value.get(0));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
//返回自定义的PasswordGrantAuthenticationToken对象
|
||||||
|
return new PasswordGrantAuthenticationToken(clientPrincipal, additionalParameters);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*从request中提取请求参数,然后存入MultiValueMap<String, String>
|
||||||
|
*/
|
||||||
|
private static MultiValueMap<String, String> getParameters(HttpServletRequest request) {
|
||||||
|
Map<String, String[]> parameterMap = request.getParameterMap();
|
||||||
|
MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>(parameterMap.size());
|
||||||
|
parameterMap.forEach((key, values) -> {
|
||||||
|
if (values.length > 0) {
|
||||||
|
for (String value : values) {
|
||||||
|
parameters.add(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return parameters;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,319 @@
|
|||||||
|
package org.jeecg.config.security.password;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson.JSONObject;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.jeecg.common.api.CommonAPI;
|
||||||
|
import org.jeecg.common.constant.CacheConstant;
|
||||||
|
import org.jeecg.common.constant.CommonConstant;
|
||||||
|
import org.jeecg.common.exception.JeecgBootException;
|
||||||
|
import org.jeecg.common.exception.JeecgCaptchaException;
|
||||||
|
import org.jeecg.common.system.vo.LoginUser;
|
||||||
|
import org.jeecg.common.system.vo.SysDepartModel;
|
||||||
|
import org.jeecg.common.util.Md5Util;
|
||||||
|
import org.jeecg.common.util.PasswordUtil;
|
||||||
|
import org.jeecg.common.util.RedisUtil;
|
||||||
|
import org.jeecg.common.util.oConvertUtils;
|
||||||
|
import org.jeecg.config.JeecgBaseConfig;
|
||||||
|
import org.jeecg.modules.base.service.BaseCommonService;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.context.annotation.Lazy;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.security.authentication.AuthenticationProvider;
|
||||||
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.core.AuthenticationException;
|
||||||
|
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||||
|
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||||
|
import org.springframework.security.oauth2.core.*;
|
||||||
|
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AccessTokenAuthenticationToken;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContextHolder;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.token.DefaultOAuth2TokenContext;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenContext;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
|
import java.security.Principal;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 密码模式认证处理器,负责处理该认证模式下的核心逻辑
|
||||||
|
* @author EightMonth
|
||||||
|
* @date 2024/1/1
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
public class PasswordGrantAuthenticationProvider implements AuthenticationProvider {
|
||||||
|
|
||||||
|
private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-5.2";
|
||||||
|
|
||||||
|
private final OAuth2AuthorizationService authorizationService;
|
||||||
|
private final OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator;
|
||||||
|
@Lazy
|
||||||
|
@Autowired
|
||||||
|
private CommonAPI commonAPI;
|
||||||
|
@Autowired
|
||||||
|
private RedisUtil redisUtil;
|
||||||
|
@Autowired
|
||||||
|
private JeecgBaseConfig jeecgBaseConfig;
|
||||||
|
@Autowired
|
||||||
|
private BaseCommonService baseCommonService;
|
||||||
|
|
||||||
|
public PasswordGrantAuthenticationProvider(OAuth2AuthorizationService authorizationService, OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator) {
|
||||||
|
Assert.notNull(authorizationService, "authorizationService cannot be null");
|
||||||
|
Assert.notNull(tokenGenerator, "tokenGenerator cannot be null");
|
||||||
|
this.authorizationService = authorizationService;
|
||||||
|
this.tokenGenerator = tokenGenerator;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
|
||||||
|
PasswordGrantAuthenticationToken passwordGrantAuthenticationToken = (PasswordGrantAuthenticationToken) authentication;
|
||||||
|
Map<String, Object> additionalParameter = passwordGrantAuthenticationToken.getAdditionalParameters();
|
||||||
|
|
||||||
|
// 授权类型
|
||||||
|
AuthorizationGrantType authorizationGrantType = passwordGrantAuthenticationToken.getGrantType();
|
||||||
|
// 用户名
|
||||||
|
String username = (String) additionalParameter.get(OAuth2ParameterNames.USERNAME);
|
||||||
|
// 密码
|
||||||
|
String password = (String) additionalParameter.get(OAuth2ParameterNames.PASSWORD);
|
||||||
|
//请求参数权限范围
|
||||||
|
String requestScopesStr = (String)additionalParameter.getOrDefault(OAuth2ParameterNames.SCOPE, "*");
|
||||||
|
//请求参数权限范围专场集合
|
||||||
|
Set<String> requestScopeSet = Stream.of(requestScopesStr.split(" ")).collect(Collectors.toSet());
|
||||||
|
// 验证码
|
||||||
|
String captcha = (String) additionalParameter.get("captcha");
|
||||||
|
String checkKey = (String) additionalParameter.get("checkKey");
|
||||||
|
|
||||||
|
OAuth2ClientAuthenticationToken clientPrincipal = getAuthenticatedClientElseThrowInvalidClient(passwordGrantAuthenticationToken);
|
||||||
|
RegisteredClient registeredClient = clientPrincipal.getRegisteredClient();
|
||||||
|
|
||||||
|
// 检查登录失败次数
|
||||||
|
if(isLoginFailOvertimes(username)){
|
||||||
|
throw new JeecgBootException("该用户登录失败次数过多,请于10分钟后再次登录!");
|
||||||
|
}
|
||||||
|
|
||||||
|
if(captcha==null){
|
||||||
|
throw new JeecgBootException("验证码无效");
|
||||||
|
}
|
||||||
|
String lowerCaseCaptcha = captcha.toLowerCase();
|
||||||
|
// 加入密钥作为混淆,避免简单的拼接,被外部利用,用户自定义该密钥即可
|
||||||
|
String origin = lowerCaseCaptcha+checkKey+jeecgBaseConfig.getSignatureSecret();
|
||||||
|
String realKey = Md5Util.md5Encode(origin, "utf-8");
|
||||||
|
Object checkCode = redisUtil.get(realKey);
|
||||||
|
//当进入登录页时,有一定几率出现验证码错误 #1714
|
||||||
|
if(checkCode==null || !checkCode.toString().equals(lowerCaseCaptcha)) {
|
||||||
|
Map<String, Object> map = new HashMap<>();
|
||||||
|
map.put("message", "验证码错误");
|
||||||
|
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,"fdsafas", Instant.now(), Instant.now().plusNanos(1)), null, map);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!registeredClient.getAuthorizationGrantTypes().contains(authorizationGrantType)) {
|
||||||
|
Map<String, Object> map = new HashMap<>();
|
||||||
|
map.put("message", "非法登录");
|
||||||
|
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,"fdsafas", Instant.now(), Instant.now().plusNanos(1)), null, map);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 通过用户名获取用户信息
|
||||||
|
LoginUser loginUser = commonAPI.getUserByName(username);
|
||||||
|
//update-begin---author:eightmonth ---date:2024-04-30 for:【6168】master分支切sas分支登录发生错误-----------
|
||||||
|
if (Objects.isNull(loginUser) || !StringUtils.hasText(loginUser.getSalt())) {
|
||||||
|
redisUtil.del(CacheConstant.SYS_USERS_CACHE+"::"+username);
|
||||||
|
loginUser = commonAPI.getUserByName(username);
|
||||||
|
}
|
||||||
|
//update-end---author:eightmonth ---date::2024-04-30 for:【6168】master分支切sas分支登录发生错误--------------
|
||||||
|
// 检查用户可行性
|
||||||
|
checkUserIsEffective(loginUser);
|
||||||
|
|
||||||
|
// 不使用spring security passwordEncoder针对密码进行匹配,使用自有加密匹配,针对 spring security使用noop传输
|
||||||
|
password = PasswordUtil.encrypt(username, password, loginUser.getSalt());
|
||||||
|
if (!password.equals(loginUser.getPassword())) {
|
||||||
|
addLoginFailOvertimes(username);
|
||||||
|
|
||||||
|
Map<String, Object> map = new HashMap<>();
|
||||||
|
map.put("message", "用户名或密码不正确");
|
||||||
|
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,"fdsafas", Instant.now(), Instant.now().plusNanos(1)), null, map);
|
||||||
|
}
|
||||||
|
|
||||||
|
//由于在上面已验证过用户名、密码,现在构建一个已认证的对象UsernamePasswordAuthenticationToken
|
||||||
|
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = UsernamePasswordAuthenticationToken.authenticated(loginUser,clientPrincipal,new ArrayList<>());
|
||||||
|
|
||||||
|
DefaultOAuth2TokenContext.Builder tokenContextBuilder = DefaultOAuth2TokenContext.builder()
|
||||||
|
.registeredClient(registeredClient)
|
||||||
|
.principal(usernamePasswordAuthenticationToken)
|
||||||
|
.authorizationServerContext(AuthorizationServerContextHolder.getContext())
|
||||||
|
.authorizationGrantType(authorizationGrantType)
|
||||||
|
.authorizedScopes(requestScopeSet)
|
||||||
|
.authorizationGrant(passwordGrantAuthenticationToken);
|
||||||
|
|
||||||
|
OAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization.withRegisteredClient(registeredClient)
|
||||||
|
.principalName(clientPrincipal.getName())
|
||||||
|
.authorizedScopes(requestScopeSet)
|
||||||
|
.attribute(Principal.class.getName(), username)
|
||||||
|
.authorizationGrantType(authorizationGrantType);
|
||||||
|
|
||||||
|
|
||||||
|
// ----- Access token -----
|
||||||
|
OAuth2TokenContext tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.ACCESS_TOKEN).build();
|
||||||
|
OAuth2Token generatedAccessToken = this.tokenGenerator.generate(tokenContext);
|
||||||
|
if (generatedAccessToken == null) {
|
||||||
|
Map<String, Object> map = new HashMap<>();
|
||||||
|
map.put("message", "无法生成访问token,请联系管理系。");
|
||||||
|
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,"fdsafas", Instant.now(), Instant.now().plusNanos(1)), null, map);
|
||||||
|
}
|
||||||
|
OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
|
||||||
|
generatedAccessToken.getTokenValue(), generatedAccessToken.getIssuedAt(),
|
||||||
|
generatedAccessToken.getExpiresAt(), tokenContext.getAuthorizedScopes());
|
||||||
|
if (generatedAccessToken instanceof ClaimAccessor) {
|
||||||
|
authorizationBuilder.token(accessToken, (metadata) -> {
|
||||||
|
metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, ((ClaimAccessor) generatedAccessToken).getClaims());
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
authorizationBuilder.accessToken(accessToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- Refresh token -----
|
||||||
|
OAuth2RefreshToken refreshToken = null;
|
||||||
|
if (registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.REFRESH_TOKEN) &&
|
||||||
|
// 不向公共客户端颁发刷新令牌
|
||||||
|
!clientPrincipal.getClientAuthenticationMethod().equals(ClientAuthenticationMethod.NONE)) {
|
||||||
|
|
||||||
|
tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.REFRESH_TOKEN).build();
|
||||||
|
OAuth2Token generatedRefreshToken = this.tokenGenerator.generate(tokenContext);
|
||||||
|
if (!(generatedRefreshToken instanceof OAuth2RefreshToken)) {
|
||||||
|
Map<String, Object> map = new HashMap<>();
|
||||||
|
map.put("message", "无法生成访问token,请联系管理系。");
|
||||||
|
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,"fdsafas", Instant.now(), Instant.now().plusNanos(1)), null, map);
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshToken = (OAuth2RefreshToken) generatedRefreshToken;
|
||||||
|
authorizationBuilder.refreshToken(refreshToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
OAuth2Authorization authorization = authorizationBuilder.build();
|
||||||
|
|
||||||
|
// 保存认证信息至redis
|
||||||
|
authorizationService.save(authorization);
|
||||||
|
|
||||||
|
// 登录成功,删除redis中的验证码
|
||||||
|
redisUtil.del(realKey);
|
||||||
|
redisUtil.del(CommonConstant.LOGIN_FAIL + username);
|
||||||
|
baseCommonService.addLog("用户名: " + username + ",登录成功!", CommonConstant.LOG_TYPE_1, null,loginUser);
|
||||||
|
|
||||||
|
JSONObject addition = new JSONObject(new LinkedHashMap<>());
|
||||||
|
addition.put("token", accessToken.getTokenValue());
|
||||||
|
|
||||||
|
// 设置租户
|
||||||
|
JSONObject jsonObject = commonAPI.setLoginTenant(username);
|
||||||
|
addition.putAll(jsonObject.getInnerMap());
|
||||||
|
|
||||||
|
// 设置登录用户信息
|
||||||
|
addition.put("userInfo", loginUser);
|
||||||
|
addition.put("sysAllDictItems", commonAPI.queryAllDictItems());
|
||||||
|
|
||||||
|
List<SysDepartModel> departs = commonAPI.queryUserDeparts(loginUser.getId());
|
||||||
|
addition.put("departs", departs);
|
||||||
|
if (departs == null || departs.size() == 0) {
|
||||||
|
addition.put("multi_depart", 0);
|
||||||
|
} else if (departs.size() == 1) {
|
||||||
|
commonAPI.updateUserDepart(username, departs.get(0).getOrgCode(),null);
|
||||||
|
addition.put("multi_depart", 1);
|
||||||
|
} else {
|
||||||
|
//查询当前是否有登录部门
|
||||||
|
if(oConvertUtils.isEmpty(loginUser.getOrgCode())){
|
||||||
|
commonAPI.updateUserDepart(username, departs.get(0).getOrgCode(),null);
|
||||||
|
}
|
||||||
|
addition.put("multi_depart", 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 兼容原有shiro登录结果处理
|
||||||
|
Map<String, Object> map = new HashMap<>();
|
||||||
|
map.put("result", addition);
|
||||||
|
map.put("code", 200);
|
||||||
|
map.put("success", true);
|
||||||
|
map.put("timestamp", System.currentTimeMillis());
|
||||||
|
|
||||||
|
// 返回access_token、refresh_token以及其它信息给到前端
|
||||||
|
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, accessToken, refreshToken, map);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean supports(Class<?> authentication) {
|
||||||
|
return PasswordGrantAuthenticationToken.class.isAssignableFrom(authentication);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static OAuth2ClientAuthenticationToken getAuthenticatedClientElseThrowInvalidClient(Authentication authentication) {
|
||||||
|
OAuth2ClientAuthenticationToken clientPrincipal = null;
|
||||||
|
if (OAuth2ClientAuthenticationToken.class.isAssignableFrom(authentication.getPrincipal().getClass())) {
|
||||||
|
clientPrincipal = (OAuth2ClientAuthenticationToken) authentication.getPrincipal();
|
||||||
|
}
|
||||||
|
if (clientPrincipal != null && clientPrincipal.isAuthenticated()) {
|
||||||
|
return clientPrincipal;
|
||||||
|
}
|
||||||
|
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_CLIENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 登录失败超出次数5 返回true
|
||||||
|
* @param username
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
private boolean isLoginFailOvertimes(String username){
|
||||||
|
String key = CommonConstant.LOGIN_FAIL + username;
|
||||||
|
Object failTime = redisUtil.get(key);
|
||||||
|
if(failTime!=null){
|
||||||
|
Integer val = Integer.parseInt(failTime.toString());
|
||||||
|
if(val>5){
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录登录失败次数
|
||||||
|
* @param username
|
||||||
|
*/
|
||||||
|
private void addLoginFailOvertimes(String username){
|
||||||
|
String key = CommonConstant.LOGIN_FAIL + username;
|
||||||
|
Object failTime = redisUtil.get(key);
|
||||||
|
Integer val = 0;
|
||||||
|
if(failTime!=null){
|
||||||
|
val = Integer.parseInt(failTime.toString());
|
||||||
|
}
|
||||||
|
// 10分钟
|
||||||
|
redisUtil.set(key, ++val, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验用户是否有效
|
||||||
|
*/
|
||||||
|
private void checkUserIsEffective(LoginUser loginUser) {
|
||||||
|
//情况1:根据用户信息查询,该用户不存在
|
||||||
|
if (Objects.isNull(loginUser)) {
|
||||||
|
baseCommonService.addLog("用户登录失败,用户不存在!", CommonConstant.LOG_TYPE_1, null);
|
||||||
|
throw new JeecgBootException("该用户不存在,请注册");
|
||||||
|
}
|
||||||
|
//情况2:根据用户信息查询,该用户已注销
|
||||||
|
//update-begin---author:王帅 Date:20200601 for:if条件永远为falsebug------------
|
||||||
|
if (CommonConstant.DEL_FLAG_1.equals(loginUser.getDelFlag())) {
|
||||||
|
//update-end---author:王帅 Date:20200601 for:if条件永远为falsebug------------
|
||||||
|
baseCommonService.addLog("用户登录失败,用户名:" + loginUser.getUsername() + "已注销!", CommonConstant.LOG_TYPE_1, null);
|
||||||
|
throw new JeecgBootException("该用户已注销");
|
||||||
|
}
|
||||||
|
//情况3:根据用户信息查询,该用户已冻结
|
||||||
|
if (CommonConstant.USER_FREEZE.equals(loginUser.getStatus())) {
|
||||||
|
baseCommonService.addLog("用户登录失败,用户名:" + loginUser.getUsername() + "已冻结!", CommonConstant.LOG_TYPE_1, null);
|
||||||
|
throw new JeecgBootException("该用户已冻结");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,21 @@
|
|||||||
|
package org.jeecg.config.security.password;
|
||||||
|
|
||||||
|
import org.jeecg.config.security.LoginType;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationGrantAuthenticationToken;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 密码模式认证专用token类型,方法spring authorization server进行认证流转,配合convert使用
|
||||||
|
* @author EightMonth
|
||||||
|
* @date 2024/1/1
|
||||||
|
*/
|
||||||
|
public class PasswordGrantAuthenticationToken extends OAuth2AuthorizationGrantAuthenticationToken {
|
||||||
|
|
||||||
|
public PasswordGrantAuthenticationToken(Authentication clientPrincipal, Map<String, Object> additionalParameters) {
|
||||||
|
super(new AuthorizationGrantType(LoginType.PASSWORD), clientPrincipal, additionalParameters);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,77 @@
|
|||||||
|
package org.jeecg.config.security.phone;
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import org.jeecg.config.security.LoginType;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
|
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||||
|
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||||
|
import org.springframework.security.web.authentication.AuthenticationConverter;
|
||||||
|
import org.springframework.util.LinkedMultiValueMap;
|
||||||
|
import org.springframework.util.MultiValueMap;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 手机号模式认证转换器
|
||||||
|
* @author EightMonth
|
||||||
|
* @date 2024/1/1
|
||||||
|
*/
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class PhoneGrantAuthenticationConvert implements AuthenticationConverter {
|
||||||
|
@Override
|
||||||
|
public Authentication convert(HttpServletRequest request) {
|
||||||
|
|
||||||
|
String grantType = request.getParameter(OAuth2ParameterNames.GRANT_TYPE);
|
||||||
|
if (!LoginType.PHONE.equals(grantType)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Authentication clientPrincipal = SecurityContextHolder.getContext().getAuthentication();
|
||||||
|
|
||||||
|
//从request中提取请求参数,然后存入MultiValueMap<String, String>
|
||||||
|
MultiValueMap<String, String> parameters = getParameters(request);
|
||||||
|
|
||||||
|
// 验证码
|
||||||
|
String captcha = parameters.getFirst("captcha");
|
||||||
|
if (!StringUtils.hasText(captcha)) {
|
||||||
|
throw new OAuth2AuthenticationException("无效请求,验证码不能为空!");
|
||||||
|
}
|
||||||
|
|
||||||
|
//收集要传入PhoneGrantAuthenticationToken构造方法的参数,
|
||||||
|
//该参数接下来在PhoneGrantAuthenticationProvider中使用
|
||||||
|
Map<String, Object> additionalParameters = new HashMap<>();
|
||||||
|
//遍历从request中提取的参数,排除掉grant_type、client_id、code等字段参数,其他参数收集到additionalParameters中
|
||||||
|
parameters.forEach((key, value) -> {
|
||||||
|
if (!key.equals(OAuth2ParameterNames.GRANT_TYPE) &&
|
||||||
|
!key.equals(OAuth2ParameterNames.CLIENT_ID) &&
|
||||||
|
!key.equals(OAuth2ParameterNames.CODE)) {
|
||||||
|
additionalParameters.put(key, value.get(0));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
//返回自定义的PhoneGrantAuthenticationToken对象
|
||||||
|
return new PhoneGrantAuthenticationToken(clientPrincipal, additionalParameters);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*从request中提取请求参数,然后存入MultiValueMap<String, String>
|
||||||
|
*/
|
||||||
|
private static MultiValueMap<String, String> getParameters(HttpServletRequest request) {
|
||||||
|
Map<String, String[]> parameterMap = request.getParameterMap();
|
||||||
|
MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>(parameterMap.size());
|
||||||
|
parameterMap.forEach((key, values) -> {
|
||||||
|
if (values.length > 0) {
|
||||||
|
for (String value : values) {
|
||||||
|
parameters.add(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return parameters;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,292 @@
|
|||||||
|
package org.jeecg.config.security.phone;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson.JSONObject;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.jeecg.common.api.CommonAPI;
|
||||||
|
import org.jeecg.common.constant.CommonConstant;
|
||||||
|
import org.jeecg.common.exception.JeecgBootException;
|
||||||
|
import org.jeecg.common.exception.JeecgCaptchaException;
|
||||||
|
import org.jeecg.common.system.vo.LoginUser;
|
||||||
|
import org.jeecg.common.system.vo.SysDepartModel;
|
||||||
|
import org.jeecg.common.util.Md5Util;
|
||||||
|
import org.jeecg.common.util.PasswordUtil;
|
||||||
|
import org.jeecg.common.util.RedisUtil;
|
||||||
|
import org.jeecg.common.util.oConvertUtils;
|
||||||
|
import org.jeecg.config.JeecgBaseConfig;
|
||||||
|
import org.jeecg.config.security.password.PasswordGrantAuthenticationToken;
|
||||||
|
import org.jeecg.modules.base.service.BaseCommonService;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.context.annotation.Lazy;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.security.authentication.AuthenticationProvider;
|
||||||
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.core.AuthenticationException;
|
||||||
|
import org.springframework.security.oauth2.core.*;
|
||||||
|
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AccessTokenAuthenticationToken;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContextHolder;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.token.DefaultOAuth2TokenContext;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenContext;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
|
||||||
|
import java.security.Principal;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 手机号模式认证处理器,负责处理该认证模式下的核心逻辑
|
||||||
|
* @author EightMonth
|
||||||
|
* @date 2024/1/1
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
public class PhoneGrantAuthenticationProvider implements AuthenticationProvider {
|
||||||
|
|
||||||
|
private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-5.2";
|
||||||
|
|
||||||
|
private final OAuth2AuthorizationService authorizationService;
|
||||||
|
private final OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator;
|
||||||
|
@Lazy
|
||||||
|
@Autowired
|
||||||
|
private CommonAPI commonAPI;
|
||||||
|
@Autowired
|
||||||
|
private RedisUtil redisUtil;
|
||||||
|
@Autowired
|
||||||
|
private JeecgBaseConfig jeecgBaseConfig;
|
||||||
|
@Autowired
|
||||||
|
private BaseCommonService baseCommonService;
|
||||||
|
|
||||||
|
public PhoneGrantAuthenticationProvider(OAuth2AuthorizationService authorizationService, OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator) {
|
||||||
|
Assert.notNull(authorizationService, "authorizationService cannot be null");
|
||||||
|
Assert.notNull(tokenGenerator, "tokenGenerator cannot be null");
|
||||||
|
this.authorizationService = authorizationService;
|
||||||
|
this.tokenGenerator = tokenGenerator;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
|
||||||
|
PhoneGrantAuthenticationToken phoneGrantAuthenticationToken = (PhoneGrantAuthenticationToken) authentication;
|
||||||
|
Map<String, Object> additionalParameter = phoneGrantAuthenticationToken.getAdditionalParameters();
|
||||||
|
|
||||||
|
// 授权类型
|
||||||
|
AuthorizationGrantType authorizationGrantType = phoneGrantAuthenticationToken.getGrantType();
|
||||||
|
// 手机号
|
||||||
|
String phone = (String) additionalParameter.get("mobile");
|
||||||
|
|
||||||
|
if(isLoginFailOvertimes(phone)){
|
||||||
|
throw new JeecgBootException("该用户登录失败次数过多,请于10分钟后再次登录!");
|
||||||
|
}
|
||||||
|
|
||||||
|
//请求参数权限范围
|
||||||
|
String requestScopesStr = (String)additionalParameter.getOrDefault(OAuth2ParameterNames.SCOPE, "*");
|
||||||
|
//请求参数权限范围专场集合
|
||||||
|
Set<String> requestScopeSet = Stream.of(requestScopesStr.split(" ")).collect(Collectors.toSet());
|
||||||
|
// 验证码
|
||||||
|
String captcha = (String) additionalParameter.get("captcha");
|
||||||
|
|
||||||
|
OAuth2ClientAuthenticationToken clientPrincipal = getAuthenticatedClientElseThrowInvalidClient(phoneGrantAuthenticationToken);
|
||||||
|
RegisteredClient registeredClient = clientPrincipal.getRegisteredClient();
|
||||||
|
|
||||||
|
// 通过手机号获取用户信息
|
||||||
|
LoginUser loginUser = commonAPI.getUserByPhone(phone);
|
||||||
|
// 检查用户可行性
|
||||||
|
checkUserIsEffective(loginUser);
|
||||||
|
|
||||||
|
|
||||||
|
String redisKey = CommonConstant.PHONE_REDIS_KEY_PRE+phone;
|
||||||
|
Object code = redisUtil.get(redisKey);
|
||||||
|
|
||||||
|
if (!captcha.equals(code)) {
|
||||||
|
//update-begin-author:taoyan date:2022-11-7 for: issues/4109 平台用户登录失败锁定用户
|
||||||
|
addLoginFailOvertimes(phone);
|
||||||
|
|
||||||
|
Map<String, Object> map = new HashMap<>();
|
||||||
|
map.put("message", "手机验证码错误");
|
||||||
|
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,"fdsafas", Instant.now(), Instant.now().plusNanos(1)), null, map);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!registeredClient.getAuthorizationGrantTypes().contains(authorizationGrantType)) {
|
||||||
|
Map<String, Object> map = new HashMap<>();
|
||||||
|
map.put("message", "非法登录");
|
||||||
|
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,"fdsafas", Instant.now(), Instant.now().plusNanos(1)), null, map);
|
||||||
|
}
|
||||||
|
|
||||||
|
//由于在上面已验证过用户名、密码,现在构建一个已认证的对象UsernamePasswordAuthenticationToken
|
||||||
|
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = UsernamePasswordAuthenticationToken.authenticated(loginUser,clientPrincipal,new ArrayList<>());
|
||||||
|
|
||||||
|
DefaultOAuth2TokenContext.Builder tokenContextBuilder = DefaultOAuth2TokenContext.builder()
|
||||||
|
.registeredClient(registeredClient)
|
||||||
|
.principal(usernamePasswordAuthenticationToken)
|
||||||
|
.authorizationServerContext(AuthorizationServerContextHolder.getContext())
|
||||||
|
.authorizationGrantType(authorizationGrantType)
|
||||||
|
.authorizedScopes(requestScopeSet)
|
||||||
|
.authorizationGrant(phoneGrantAuthenticationToken);
|
||||||
|
|
||||||
|
OAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization.withRegisteredClient(registeredClient)
|
||||||
|
.principalName(clientPrincipal.getName())
|
||||||
|
.authorizedScopes(requestScopeSet)
|
||||||
|
.attribute(Principal.class.getName(), loginUser.getUsername())
|
||||||
|
.authorizationGrantType(authorizationGrantType);
|
||||||
|
|
||||||
|
|
||||||
|
// ----- Access token -----
|
||||||
|
OAuth2TokenContext tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.ACCESS_TOKEN).build();
|
||||||
|
OAuth2Token generatedAccessToken = this.tokenGenerator.generate(tokenContext);
|
||||||
|
if (generatedAccessToken == null) {
|
||||||
|
Map<String, Object> map = new HashMap<>();
|
||||||
|
map.put("message", "无法生成刷新token,请联系管理员。");
|
||||||
|
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,"fdsafas", Instant.now(), Instant.now().plusNanos(1)), null, map);
|
||||||
|
}
|
||||||
|
OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
|
||||||
|
generatedAccessToken.getTokenValue(), generatedAccessToken.getIssuedAt(),
|
||||||
|
generatedAccessToken.getExpiresAt(), tokenContext.getAuthorizedScopes());
|
||||||
|
if (generatedAccessToken instanceof ClaimAccessor) {
|
||||||
|
authorizationBuilder.token(accessToken, (metadata) -> {
|
||||||
|
metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, ((ClaimAccessor) generatedAccessToken).getClaims());
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
authorizationBuilder.accessToken(accessToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- Refresh token -----
|
||||||
|
OAuth2RefreshToken refreshToken = null;
|
||||||
|
if (registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.REFRESH_TOKEN) &&
|
||||||
|
// 不向公共客户端颁发刷新令牌
|
||||||
|
!clientPrincipal.getClientAuthenticationMethod().equals(ClientAuthenticationMethod.NONE)) {
|
||||||
|
|
||||||
|
tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.REFRESH_TOKEN).build();
|
||||||
|
OAuth2Token generatedRefreshToken = this.tokenGenerator.generate(tokenContext);
|
||||||
|
if (!(generatedRefreshToken instanceof OAuth2RefreshToken)) {
|
||||||
|
Map<String, Object> map = new HashMap<>();
|
||||||
|
map.put("message", "无法生成刷新token,请联系管理员。");
|
||||||
|
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,"fdsafas", Instant.now(), Instant.now().plusNanos(1)), null, map);
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshToken = (OAuth2RefreshToken) generatedRefreshToken;
|
||||||
|
authorizationBuilder.refreshToken(refreshToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
OAuth2Authorization authorization = authorizationBuilder.build();
|
||||||
|
|
||||||
|
// 保存认证信息至redis
|
||||||
|
authorizationService.save(authorization);
|
||||||
|
|
||||||
|
baseCommonService.addLog("用户名: " + loginUser.getUsername() + ",登录成功!", CommonConstant.LOG_TYPE_1, null,loginUser);
|
||||||
|
|
||||||
|
JSONObject addition = new JSONObject(new LinkedHashMap<>());
|
||||||
|
addition.put("token", accessToken.getTokenValue());
|
||||||
|
// 设置租户
|
||||||
|
JSONObject jsonObject = commonAPI.setLoginTenant(loginUser.getUsername());
|
||||||
|
addition.putAll(jsonObject.getInnerMap());
|
||||||
|
|
||||||
|
// 设置登录用户信息
|
||||||
|
addition.put("userInfo", loginUser);
|
||||||
|
addition.put("sysAllDictItems", commonAPI.queryAllDictItems());
|
||||||
|
|
||||||
|
List<SysDepartModel> departs = commonAPI.queryUserDeparts(loginUser.getId());
|
||||||
|
addition.put("departs", departs);
|
||||||
|
if (departs == null || departs.size() == 0) {
|
||||||
|
addition.put("multi_depart", 0);
|
||||||
|
} else if (departs.size() == 1) {
|
||||||
|
commonAPI.updateUserDepart(loginUser.getUsername(), departs.get(0).getOrgCode(),null);
|
||||||
|
addition.put("multi_depart", 1);
|
||||||
|
} else {
|
||||||
|
//查询当前是否有登录部门
|
||||||
|
if(oConvertUtils.isEmpty(loginUser.getOrgCode())){
|
||||||
|
commonAPI.updateUserDepart(loginUser.getUsername(), departs.get(0).getOrgCode(),null);
|
||||||
|
}
|
||||||
|
addition.put("multi_depart", 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 兼容原有shiro登录结果处理
|
||||||
|
Map<String, Object> map = new HashMap<>();
|
||||||
|
map.put("result", addition);
|
||||||
|
map.put("code", 200);
|
||||||
|
map.put("success", true);
|
||||||
|
map.put("timestamp", System.currentTimeMillis());
|
||||||
|
|
||||||
|
// 返回access_token、refresh_token以及其它信息给到前端
|
||||||
|
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, accessToken, refreshToken, map);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean supports(Class<?> authentication) {
|
||||||
|
return PhoneGrantAuthenticationToken.class.isAssignableFrom(authentication);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static OAuth2ClientAuthenticationToken getAuthenticatedClientElseThrowInvalidClient(Authentication authentication) {
|
||||||
|
OAuth2ClientAuthenticationToken clientPrincipal = null;
|
||||||
|
if (OAuth2ClientAuthenticationToken.class.isAssignableFrom(authentication.getPrincipal().getClass())) {
|
||||||
|
clientPrincipal = (OAuth2ClientAuthenticationToken) authentication.getPrincipal();
|
||||||
|
}
|
||||||
|
if (clientPrincipal != null && clientPrincipal.isAuthenticated()) {
|
||||||
|
return clientPrincipal;
|
||||||
|
}
|
||||||
|
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_CLIENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 登录失败超出次数5 返回true
|
||||||
|
* @param username
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
private boolean isLoginFailOvertimes(String username){
|
||||||
|
String key = CommonConstant.LOGIN_FAIL + username;
|
||||||
|
Object failTime = redisUtil.get(key);
|
||||||
|
if(failTime!=null){
|
||||||
|
Integer val = Integer.parseInt(failTime.toString());
|
||||||
|
if(val>5){
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录登录失败次数
|
||||||
|
* @param username
|
||||||
|
*/
|
||||||
|
private void addLoginFailOvertimes(String username){
|
||||||
|
String key = CommonConstant.LOGIN_FAIL + username;
|
||||||
|
Object failTime = redisUtil.get(key);
|
||||||
|
Integer val = 0;
|
||||||
|
if(failTime!=null){
|
||||||
|
val = Integer.parseInt(failTime.toString());
|
||||||
|
}
|
||||||
|
// 10分钟
|
||||||
|
redisUtil.set(key, ++val, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验用户是否有效
|
||||||
|
*/
|
||||||
|
private void checkUserIsEffective(LoginUser loginUser) {
|
||||||
|
//情况1:根据用户信息查询,该用户不存在
|
||||||
|
if (Objects.isNull(loginUser)) {
|
||||||
|
baseCommonService.addLog("用户登录失败,用户不存在!", CommonConstant.LOG_TYPE_1, null);
|
||||||
|
throw new JeecgBootException("该用户不存在,请注册");
|
||||||
|
}
|
||||||
|
//情况2:根据用户信息查询,该用户已注销
|
||||||
|
//update-begin---author:王帅 Date:20200601 for:if条件永远为falsebug------------
|
||||||
|
if (CommonConstant.DEL_FLAG_1.equals(loginUser.getDelFlag())) {
|
||||||
|
//update-end---author:王帅 Date:20200601 for:if条件永远为falsebug------------
|
||||||
|
baseCommonService.addLog("用户登录失败,用户名:" + loginUser.getUsername() + "已注销!", CommonConstant.LOG_TYPE_1, null);
|
||||||
|
throw new JeecgBootException("该用户已注销");
|
||||||
|
}
|
||||||
|
//情况3:根据用户信息查询,该用户已冻结
|
||||||
|
if (CommonConstant.USER_FREEZE.equals(loginUser.getStatus())) {
|
||||||
|
baseCommonService.addLog("用户登录失败,用户名:" + loginUser.getUsername() + "已冻结!", CommonConstant.LOG_TYPE_1, null);
|
||||||
|
throw new JeecgBootException("该用户已冻结");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,21 @@
|
|||||||
|
package org.jeecg.config.security.phone;
|
||||||
|
|
||||||
|
import org.jeecg.config.security.LoginType;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationGrantAuthenticationToken;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 手机号模式认证专用token类型,方法spring authorization server进行认证流转,配合convert使用
|
||||||
|
* @author EightMonth
|
||||||
|
* @date 2024/1/1
|
||||||
|
*/
|
||||||
|
public class PhoneGrantAuthenticationToken extends OAuth2AuthorizationGrantAuthenticationToken {
|
||||||
|
|
||||||
|
public PhoneGrantAuthenticationToken(Authentication clientPrincipal, Map<String, Object> additionalParameters) {
|
||||||
|
super(new AuthorizationGrantType(LoginType.PHONE), clientPrincipal, additionalParameters);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,187 @@
|
|||||||
|
package org.jeecg.config.security.self;
|
||||||
|
|
||||||
|
import org.jeecg.common.api.CommonAPI;
|
||||||
|
import org.jeecg.common.constant.CommonConstant;
|
||||||
|
import org.jeecg.common.exception.JeecgBoot401Exception;
|
||||||
|
import org.jeecg.common.exception.JeecgBootException;
|
||||||
|
import org.jeecg.common.system.vo.LoginUser;
|
||||||
|
import org.jeecg.common.util.RedisUtil;
|
||||||
|
import org.jeecg.config.JeecgBaseConfig;
|
||||||
|
import org.jeecg.modules.base.service.BaseCommonService;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.context.annotation.Lazy;
|
||||||
|
import org.springframework.security.authentication.AuthenticationProvider;
|
||||||
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.core.AuthenticationException;
|
||||||
|
import org.springframework.security.oauth2.core.*;
|
||||||
|
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AccessTokenAuthenticationToken;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.token.DefaultOAuth2TokenContext;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenContext;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
|
||||||
|
import java.security.Principal;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自用生成token处理器,不对外开放,外部请求无法通过该方式生成token
|
||||||
|
* @author eightmonth@qq.com
|
||||||
|
* @date 2024/3/19 11:40
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class SelfAuthenticationProvider implements AuthenticationProvider {
|
||||||
|
|
||||||
|
private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-5.2";
|
||||||
|
|
||||||
|
private final OAuth2AuthorizationService authorizationService;
|
||||||
|
private final OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator;
|
||||||
|
@Lazy
|
||||||
|
@Autowired
|
||||||
|
private CommonAPI commonAPI;
|
||||||
|
@Autowired
|
||||||
|
private RedisUtil redisUtil;
|
||||||
|
@Autowired
|
||||||
|
private JeecgBaseConfig jeecgBaseConfig;
|
||||||
|
@Autowired
|
||||||
|
private BaseCommonService baseCommonService;
|
||||||
|
|
||||||
|
public SelfAuthenticationProvider(OAuth2AuthorizationService authorizationService, OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator) {
|
||||||
|
Assert.notNull(authorizationService, "authorizationService cannot be null");
|
||||||
|
Assert.notNull(tokenGenerator, "tokenGenerator cannot be null");
|
||||||
|
this.authorizationService = authorizationService;
|
||||||
|
this.tokenGenerator = tokenGenerator;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
|
||||||
|
SelfAuthenticationToken passwordGrantAuthenticationToken = (SelfAuthenticationToken) authentication;
|
||||||
|
Map<String, Object> additionalParameter = passwordGrantAuthenticationToken.getAdditionalParameters();
|
||||||
|
|
||||||
|
// 授权类型
|
||||||
|
AuthorizationGrantType authorizationGrantType = passwordGrantAuthenticationToken.getGrantType();
|
||||||
|
// 用户名
|
||||||
|
String username = (String) additionalParameter.get(OAuth2ParameterNames.USERNAME);
|
||||||
|
//请求参数权限范围
|
||||||
|
String requestScopesStr = "*";
|
||||||
|
//请求参数权限范围专场集合
|
||||||
|
Set<String> requestScopeSet = Stream.of(requestScopesStr.split(" ")).collect(Collectors.toSet());
|
||||||
|
|
||||||
|
OAuth2ClientAuthenticationToken clientPrincipal = getAuthenticatedClientElseThrowInvalidClient(passwordGrantAuthenticationToken);
|
||||||
|
RegisteredClient registeredClient = clientPrincipal.getRegisteredClient();
|
||||||
|
|
||||||
|
// 通过用户名获取用户信息
|
||||||
|
// LoginUser loginUser = commonAPI.getUserByName(username);
|
||||||
|
// 检查用户可行性
|
||||||
|
// checkUserIsEffective(loginUser);
|
||||||
|
|
||||||
|
//由于在上面已验证过用户名、密码,现在构建一个已认证的对象UsernamePasswordAuthenticationToken
|
||||||
|
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = UsernamePasswordAuthenticationToken.authenticated(username,clientPrincipal,new ArrayList<>());
|
||||||
|
|
||||||
|
DefaultOAuth2TokenContext.Builder tokenContextBuilder = DefaultOAuth2TokenContext.builder()
|
||||||
|
.registeredClient(registeredClient)
|
||||||
|
.principal(usernamePasswordAuthenticationToken)
|
||||||
|
.authorizationGrantType(authorizationGrantType)
|
||||||
|
.authorizedScopes(requestScopeSet)
|
||||||
|
.authorizationGrant(passwordGrantAuthenticationToken);
|
||||||
|
|
||||||
|
OAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization.withRegisteredClient(registeredClient)
|
||||||
|
.principalName(clientPrincipal.getName())
|
||||||
|
.authorizedScopes(requestScopeSet)
|
||||||
|
.attribute(Principal.class.getName(), username)
|
||||||
|
.authorizationGrantType(authorizationGrantType);
|
||||||
|
|
||||||
|
|
||||||
|
// ----- Access token -----
|
||||||
|
OAuth2TokenContext tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.ACCESS_TOKEN).build();
|
||||||
|
OAuth2Token generatedAccessToken = this.tokenGenerator.generate(tokenContext);
|
||||||
|
if (generatedAccessToken == null) {
|
||||||
|
throw new JeecgBoot401Exception("无法生成刷新token,请联系管理员。");
|
||||||
|
}
|
||||||
|
OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
|
||||||
|
generatedAccessToken.getTokenValue(), generatedAccessToken.getIssuedAt(),
|
||||||
|
generatedAccessToken.getExpiresAt(), tokenContext.getAuthorizedScopes());
|
||||||
|
if (generatedAccessToken instanceof ClaimAccessor) {
|
||||||
|
authorizationBuilder.token(accessToken, (metadata) -> {
|
||||||
|
metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, ((ClaimAccessor) generatedAccessToken).getClaims());
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
authorizationBuilder.accessToken(accessToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- Refresh token -----
|
||||||
|
OAuth2RefreshToken refreshToken = null;
|
||||||
|
if (registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.REFRESH_TOKEN) &&
|
||||||
|
// 不向公共客户端颁发刷新令牌
|
||||||
|
!clientPrincipal.getClientAuthenticationMethod().equals(ClientAuthenticationMethod.NONE)) {
|
||||||
|
|
||||||
|
tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.REFRESH_TOKEN).build();
|
||||||
|
OAuth2Token generatedRefreshToken = this.tokenGenerator.generate(tokenContext);
|
||||||
|
if (!(generatedRefreshToken instanceof OAuth2RefreshToken)) {
|
||||||
|
Map<String, Object> map = new HashMap<>();
|
||||||
|
map.put("message", "无法生成刷新token,请联系管理员。");
|
||||||
|
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,"fdsafas", Instant.now(), Instant.now().plusNanos(1)), null, map);
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshToken = (OAuth2RefreshToken) generatedRefreshToken;
|
||||||
|
authorizationBuilder.refreshToken(refreshToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
OAuth2Authorization authorization = authorizationBuilder.build();
|
||||||
|
|
||||||
|
// 保存认证信息至redis
|
||||||
|
authorizationService.save(authorization);
|
||||||
|
|
||||||
|
// 返回access_token、refresh_token以及其它信息给到前端
|
||||||
|
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, accessToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean supports(Class<?> authentication) {
|
||||||
|
return SelfAuthenticationToken.class.isAssignableFrom(authentication);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static OAuth2ClientAuthenticationToken getAuthenticatedClientElseThrowInvalidClient(Authentication authentication) {
|
||||||
|
OAuth2ClientAuthenticationToken clientPrincipal = null;
|
||||||
|
if (OAuth2ClientAuthenticationToken.class.isAssignableFrom(authentication.getPrincipal().getClass())) {
|
||||||
|
clientPrincipal = (OAuth2ClientAuthenticationToken) authentication.getPrincipal();
|
||||||
|
}
|
||||||
|
if (clientPrincipal != null && clientPrincipal.isAuthenticated()) {
|
||||||
|
return clientPrincipal;
|
||||||
|
}
|
||||||
|
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_CLIENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验用户是否有效
|
||||||
|
*/
|
||||||
|
private void checkUserIsEffective(LoginUser loginUser) {
|
||||||
|
//情况1:根据用户信息查询,该用户不存在
|
||||||
|
if (Objects.isNull(loginUser)) {
|
||||||
|
baseCommonService.addLog("用户登录失败,用户不存在!", CommonConstant.LOG_TYPE_1, null);
|
||||||
|
throw new JeecgBootException("该用户不存在,请注册");
|
||||||
|
}
|
||||||
|
//情况2:根据用户信息查询,该用户已注销
|
||||||
|
//update-begin---author:王帅 Date:20200601 for:if条件永远为falsebug------------
|
||||||
|
if (CommonConstant.DEL_FLAG_1.equals(loginUser.getDelFlag())) {
|
||||||
|
//update-end---author:王帅 Date:20200601 for:if条件永远为falsebug------------
|
||||||
|
baseCommonService.addLog("用户登录失败,用户名:" + loginUser.getUsername() + "已注销!", CommonConstant.LOG_TYPE_1, null);
|
||||||
|
throw new JeecgBootException("该用户已注销");
|
||||||
|
}
|
||||||
|
//情况3:根据用户信息查询,该用户已冻结
|
||||||
|
if (CommonConstant.USER_FREEZE.equals(loginUser.getStatus())) {
|
||||||
|
baseCommonService.addLog("用户登录失败,用户名:" + loginUser.getUsername() + "已冻结!", CommonConstant.LOG_TYPE_1, null);
|
||||||
|
throw new JeecgBootException("该用户已冻结");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
package org.jeecg.config.security.self;
|
||||||
|
|
||||||
|
import org.jeecg.config.security.LoginType;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationGrantAuthenticationToken;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自用生成token,不支持对外请求,仅为程序内部生成token
|
||||||
|
* @author eightmonth
|
||||||
|
* @date 2024/3/19 11:37
|
||||||
|
*/
|
||||||
|
public class SelfAuthenticationToken extends OAuth2AuthorizationGrantAuthenticationToken {
|
||||||
|
public SelfAuthenticationToken(Authentication clientPrincipal, Map<String, Object> additionalParameters) {
|
||||||
|
super(new AuthorizationGrantType(LoginType.SELF), clientPrincipal, additionalParameters);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,81 @@
|
|||||||
|
package org.jeecg.config.security.social;
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import org.jeecg.config.security.LoginType;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
|
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||||
|
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||||
|
import org.springframework.security.web.authentication.AuthenticationConverter;
|
||||||
|
import org.springframework.util.LinkedMultiValueMap;
|
||||||
|
import org.springframework.util.MultiValueMap;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 社交模式认证转换器,配合github、企业微信、钉钉、微信登录使用
|
||||||
|
* @author EightMonth
|
||||||
|
* @date 2024/1/1
|
||||||
|
*/
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class SocialGrantAuthenticationConvert implements AuthenticationConverter {
|
||||||
|
@Override
|
||||||
|
public Authentication convert(HttpServletRequest request) {
|
||||||
|
|
||||||
|
String grantType = request.getParameter(OAuth2ParameterNames.GRANT_TYPE);
|
||||||
|
if (!LoginType.SOCIAL.equals(grantType)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Authentication clientPrincipal = SecurityContextHolder.getContext().getAuthentication();
|
||||||
|
|
||||||
|
//从request中提取请求参数,然后存入MultiValueMap<String, String>
|
||||||
|
MultiValueMap<String, String> parameters = getParameters(request);
|
||||||
|
|
||||||
|
String token = parameters.getFirst("token");
|
||||||
|
if (!StringUtils.hasText(token)) {
|
||||||
|
throw new OAuth2AuthenticationException("无效请求,三方token不能为空!");
|
||||||
|
}
|
||||||
|
|
||||||
|
String source = parameters.getFirst("thirdType");
|
||||||
|
if (!StringUtils.hasText(source)) {
|
||||||
|
throw new OAuth2AuthenticationException("无效请求,三方来源不能为空!");
|
||||||
|
}
|
||||||
|
|
||||||
|
//收集要传入PhoneGrantAuthenticationToken构造方法的参数,
|
||||||
|
//该参数接下来在PhoneGrantAuthenticationProvider中使用
|
||||||
|
Map<String, Object> additionalParameters = new HashMap<>();
|
||||||
|
//遍历从request中提取的参数,排除掉grant_type、client_id、code等字段参数,其他参数收集到additionalParameters中
|
||||||
|
parameters.forEach((key, value) -> {
|
||||||
|
if (!key.equals(OAuth2ParameterNames.GRANT_TYPE) &&
|
||||||
|
!key.equals(OAuth2ParameterNames.CLIENT_ID) &&
|
||||||
|
!key.equals(OAuth2ParameterNames.CODE)) {
|
||||||
|
additionalParameters.put(key, value.get(0));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
//返回自定义的PhoneGrantAuthenticationToken对象
|
||||||
|
return new SocialGrantAuthenticationToken(clientPrincipal, additionalParameters);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*从request中提取请求参数,然后存入MultiValueMap<String, String>
|
||||||
|
*/
|
||||||
|
private static MultiValueMap<String, String> getParameters(HttpServletRequest request) {
|
||||||
|
Map<String, String[]> parameterMap = request.getParameterMap();
|
||||||
|
MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>(parameterMap.size());
|
||||||
|
parameterMap.forEach((key, values) -> {
|
||||||
|
if (values.length > 0) {
|
||||||
|
for (String value : values) {
|
||||||
|
parameters.add(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return parameters;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,278 @@
|
|||||||
|
package org.jeecg.config.security.social;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson.JSONObject;
|
||||||
|
import com.auth0.jwt.JWT;
|
||||||
|
import com.auth0.jwt.interfaces.DecodedJWT;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.jeecg.common.api.CommonAPI;
|
||||||
|
import org.jeecg.common.constant.CommonConstant;
|
||||||
|
import org.jeecg.common.exception.JeecgBootException;
|
||||||
|
import org.jeecg.common.system.vo.LoginUser;
|
||||||
|
import org.jeecg.common.system.vo.SysDepartModel;
|
||||||
|
import org.jeecg.common.util.RedisUtil;
|
||||||
|
import org.jeecg.common.util.oConvertUtils;
|
||||||
|
import org.jeecg.config.JeecgBaseConfig;
|
||||||
|
import org.jeecg.config.security.password.PasswordGrantAuthenticationToken;
|
||||||
|
import org.jeecg.modules.base.service.BaseCommonService;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.context.annotation.Lazy;
|
||||||
|
import org.springframework.security.authentication.AuthenticationProvider;
|
||||||
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.core.AuthenticationException;
|
||||||
|
import org.springframework.security.oauth2.core.*;
|
||||||
|
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AccessTokenAuthenticationToken;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContextHolder;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.token.DefaultOAuth2TokenContext;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenContext;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
|
||||||
|
import java.security.Principal;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 社交模式认证处理器,负责处理该认证模式下的核心逻辑,配合github、企业微信、钉钉、微信登录使用
|
||||||
|
* @author EightMonth
|
||||||
|
* @date 2024/1/1
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
public class SocialGrantAuthenticationProvider implements AuthenticationProvider {
|
||||||
|
|
||||||
|
private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-5.2";
|
||||||
|
|
||||||
|
private final OAuth2AuthorizationService authorizationService;
|
||||||
|
private final OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator;
|
||||||
|
@Lazy
|
||||||
|
@Autowired
|
||||||
|
private CommonAPI commonAPI;
|
||||||
|
@Autowired
|
||||||
|
private RedisUtil redisUtil;
|
||||||
|
@Autowired
|
||||||
|
private JeecgBaseConfig jeecgBaseConfig;
|
||||||
|
@Autowired
|
||||||
|
private BaseCommonService baseCommonService;
|
||||||
|
|
||||||
|
public SocialGrantAuthenticationProvider(OAuth2AuthorizationService authorizationService, OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator) {
|
||||||
|
Assert.notNull(authorizationService, "authorizationService cannot be null");
|
||||||
|
Assert.notNull(tokenGenerator, "tokenGenerator cannot be null");
|
||||||
|
this.authorizationService = authorizationService;
|
||||||
|
this.tokenGenerator = tokenGenerator;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
|
||||||
|
SocialGrantAuthenticationToken socialGrantAuthenticationToken = (SocialGrantAuthenticationToken) authentication;
|
||||||
|
Map<String, Object> additionalParameter = socialGrantAuthenticationToken.getAdditionalParameters();
|
||||||
|
|
||||||
|
// 授权类型
|
||||||
|
AuthorizationGrantType authorizationGrantType = socialGrantAuthenticationToken.getGrantType();
|
||||||
|
// 三方token
|
||||||
|
String token = (String) additionalParameter.get("token");
|
||||||
|
// 三方来源
|
||||||
|
String source = (String) additionalParameter.get("thirdType");
|
||||||
|
|
||||||
|
//请求参数权限范围
|
||||||
|
String requestScopesStr = (String)additionalParameter.getOrDefault(OAuth2ParameterNames.SCOPE, "*");
|
||||||
|
//请求参数权限范围专场集合
|
||||||
|
Set<String> requestScopeSet = Stream.of(requestScopesStr.split(" ")).collect(Collectors.toSet());
|
||||||
|
|
||||||
|
DecodedJWT jwt = JWT.decode(token);
|
||||||
|
String username = jwt.getClaim("username").asString();
|
||||||
|
|
||||||
|
// 通过手机号获取用户信息
|
||||||
|
LoginUser loginUser = commonAPI.getUserByName(username);
|
||||||
|
// 检查用户可行性
|
||||||
|
checkUserIsEffective(loginUser);
|
||||||
|
|
||||||
|
OAuth2ClientAuthenticationToken clientPrincipal = getAuthenticatedClientElseThrowInvalidClient(socialGrantAuthenticationToken);
|
||||||
|
RegisteredClient registeredClient = clientPrincipal.getRegisteredClient();
|
||||||
|
|
||||||
|
if (!registeredClient.getAuthorizationGrantTypes().contains(authorizationGrantType)) {
|
||||||
|
Map<String, Object> map = new HashMap<>();
|
||||||
|
map.put("message", "非法登录");
|
||||||
|
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,"fdsafas", Instant.now(), Instant.now().plusNanos(1)), null, map);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
//由于在上面已验证过用户名、密码,现在构建一个已认证的对象UsernamePasswordAuthenticationToken
|
||||||
|
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = UsernamePasswordAuthenticationToken.authenticated(loginUser,clientPrincipal,new ArrayList<>());
|
||||||
|
|
||||||
|
DefaultOAuth2TokenContext.Builder tokenContextBuilder = DefaultOAuth2TokenContext.builder()
|
||||||
|
.registeredClient(registeredClient)
|
||||||
|
.principal(usernamePasswordAuthenticationToken)
|
||||||
|
.authorizationServerContext(AuthorizationServerContextHolder.getContext())
|
||||||
|
.authorizationGrantType(authorizationGrantType)
|
||||||
|
.authorizedScopes(requestScopeSet)
|
||||||
|
.authorizationGrant(socialGrantAuthenticationToken);
|
||||||
|
|
||||||
|
OAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization.withRegisteredClient(registeredClient)
|
||||||
|
.principalName(clientPrincipal.getName())
|
||||||
|
.authorizedScopes(requestScopeSet)
|
||||||
|
.attribute(Principal.class.getName(), loginUser.getUsername())
|
||||||
|
.authorizationGrantType(authorizationGrantType);
|
||||||
|
|
||||||
|
|
||||||
|
// ----- Access token -----
|
||||||
|
OAuth2TokenContext tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.ACCESS_TOKEN).build();
|
||||||
|
OAuth2Token generatedAccessToken = this.tokenGenerator.generate(tokenContext);
|
||||||
|
if (generatedAccessToken == null) {
|
||||||
|
Map<String, Object> map = new HashMap<>();
|
||||||
|
map.put("message", "无法生成访问token,请联系管理系。");
|
||||||
|
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,"fdsafas", Instant.now(), Instant.now().plusNanos(1)), null, map);
|
||||||
|
|
||||||
|
}
|
||||||
|
OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
|
||||||
|
generatedAccessToken.getTokenValue(), generatedAccessToken.getIssuedAt(),
|
||||||
|
generatedAccessToken.getExpiresAt(), tokenContext.getAuthorizedScopes());
|
||||||
|
if (generatedAccessToken instanceof ClaimAccessor) {
|
||||||
|
authorizationBuilder.token(accessToken, (metadata) -> {
|
||||||
|
metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, ((ClaimAccessor) generatedAccessToken).getClaims());
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
authorizationBuilder.accessToken(accessToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- Refresh token -----
|
||||||
|
OAuth2RefreshToken refreshToken = null;
|
||||||
|
if (registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.REFRESH_TOKEN) &&
|
||||||
|
// 不向公共客户端颁发刷新令牌
|
||||||
|
!clientPrincipal.getClientAuthenticationMethod().equals(ClientAuthenticationMethod.NONE)) {
|
||||||
|
|
||||||
|
tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.REFRESH_TOKEN).build();
|
||||||
|
OAuth2Token generatedRefreshToken = this.tokenGenerator.generate(tokenContext);
|
||||||
|
if (!(generatedRefreshToken instanceof OAuth2RefreshToken)) {
|
||||||
|
Map<String, Object> map = new HashMap<>();
|
||||||
|
map.put("message", "无法生成刷新token,请联系管理员。");
|
||||||
|
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,"fdsafas", Instant.now(), Instant.now().plusNanos(1)), null, map);
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshToken = (OAuth2RefreshToken) generatedRefreshToken;
|
||||||
|
authorizationBuilder.refreshToken(refreshToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
OAuth2Authorization authorization = authorizationBuilder.build();
|
||||||
|
|
||||||
|
// 保存认证信息至redis
|
||||||
|
authorizationService.save(authorization);
|
||||||
|
|
||||||
|
baseCommonService.addLog("用户名: " + loginUser.getUsername() + ",登录成功!", CommonConstant.LOG_TYPE_1, null,loginUser);
|
||||||
|
|
||||||
|
JSONObject addition = new JSONObject(new LinkedHashMap<>());
|
||||||
|
addition.put("token", accessToken.getTokenValue());
|
||||||
|
// 设置租户
|
||||||
|
JSONObject jsonObject = commonAPI.setLoginTenant(loginUser.getUsername());
|
||||||
|
addition.putAll(jsonObject.getInnerMap());
|
||||||
|
|
||||||
|
// 设置登录用户信息
|
||||||
|
addition.put("userInfo", loginUser);
|
||||||
|
addition.put("sysAllDictItems", commonAPI.queryAllDictItems());
|
||||||
|
|
||||||
|
List<SysDepartModel> departs = commonAPI.queryUserDeparts(loginUser.getId());
|
||||||
|
addition.put("departs", departs);
|
||||||
|
if (departs == null || departs.size() == 0) {
|
||||||
|
addition.put("multi_depart", 0);
|
||||||
|
} else if (departs.size() == 1) {
|
||||||
|
commonAPI.updateUserDepart(loginUser.getUsername(), departs.get(0).getOrgCode(),null);
|
||||||
|
addition.put("multi_depart", 1);
|
||||||
|
} else {
|
||||||
|
//查询当前是否有登录部门
|
||||||
|
if(oConvertUtils.isEmpty(loginUser.getOrgCode())){
|
||||||
|
commonAPI.updateUserDepart(loginUser.getUsername(), departs.get(0).getOrgCode(),null);
|
||||||
|
}
|
||||||
|
addition.put("multi_depart", 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 兼容原有shiro登录结果处理
|
||||||
|
Map<String, Object> map = new HashMap<>();
|
||||||
|
map.put("result", addition);
|
||||||
|
map.put("code", 200);
|
||||||
|
map.put("success", true);
|
||||||
|
map.put("timestamp", System.currentTimeMillis());
|
||||||
|
|
||||||
|
|
||||||
|
// 返回access_token、refresh_token以及其它信息给到前端
|
||||||
|
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, accessToken, refreshToken, map);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean supports(Class<?> authentication) {
|
||||||
|
return SocialGrantAuthenticationToken.class.isAssignableFrom(authentication);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static OAuth2ClientAuthenticationToken getAuthenticatedClientElseThrowInvalidClient(Authentication authentication) {
|
||||||
|
OAuth2ClientAuthenticationToken clientPrincipal = null;
|
||||||
|
if (OAuth2ClientAuthenticationToken.class.isAssignableFrom(authentication.getPrincipal().getClass())) {
|
||||||
|
clientPrincipal = (OAuth2ClientAuthenticationToken) authentication.getPrincipal();
|
||||||
|
}
|
||||||
|
if (clientPrincipal != null && clientPrincipal.isAuthenticated()) {
|
||||||
|
return clientPrincipal;
|
||||||
|
}
|
||||||
|
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_CLIENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 登录失败超出次数5 返回true
|
||||||
|
* @param username
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
private boolean isLoginFailOvertimes(String username){
|
||||||
|
String key = CommonConstant.LOGIN_FAIL + username;
|
||||||
|
Object failTime = redisUtil.get(key);
|
||||||
|
if(failTime!=null){
|
||||||
|
Integer val = Integer.parseInt(failTime.toString());
|
||||||
|
if(val>5){
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录登录失败次数
|
||||||
|
* @param username
|
||||||
|
*/
|
||||||
|
private void addLoginFailOvertimes(String username){
|
||||||
|
String key = CommonConstant.LOGIN_FAIL + username;
|
||||||
|
Object failTime = redisUtil.get(key);
|
||||||
|
Integer val = 0;
|
||||||
|
if(failTime!=null){
|
||||||
|
val = Integer.parseInt(failTime.toString());
|
||||||
|
}
|
||||||
|
// 10分钟
|
||||||
|
redisUtil.set(key, ++val, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验用户是否有效
|
||||||
|
*/
|
||||||
|
private void checkUserIsEffective(LoginUser loginUser) {
|
||||||
|
//情况1:根据用户信息查询,该用户不存在
|
||||||
|
if (Objects.isNull(loginUser)) {
|
||||||
|
baseCommonService.addLog("用户登录失败,用户不存在!", CommonConstant.LOG_TYPE_1, null);
|
||||||
|
throw new JeecgBootException("该用户不存在,请注册");
|
||||||
|
}
|
||||||
|
//情况2:根据用户信息查询,该用户已注销
|
||||||
|
//update-begin---author:王帅 Date:20200601 for:if条件永远为falsebug------------
|
||||||
|
if (CommonConstant.DEL_FLAG_1.equals(loginUser.getDelFlag())) {
|
||||||
|
//update-end---author:王帅 Date:20200601 for:if条件永远为falsebug------------
|
||||||
|
baseCommonService.addLog("用户登录失败,用户名:" + loginUser.getUsername() + "已注销!", CommonConstant.LOG_TYPE_1, null);
|
||||||
|
throw new JeecgBootException("该用户已注销");
|
||||||
|
}
|
||||||
|
//情况3:根据用户信息查询,该用户已冻结
|
||||||
|
if (CommonConstant.USER_FREEZE.equals(loginUser.getStatus())) {
|
||||||
|
baseCommonService.addLog("用户登录失败,用户名:" + loginUser.getUsername() + "已冻结!", CommonConstant.LOG_TYPE_1, null);
|
||||||
|
throw new JeecgBootException("该用户已冻结");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,21 @@
|
|||||||
|
package org.jeecg.config.security.social;
|
||||||
|
|
||||||
|
import org.jeecg.config.security.LoginType;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationGrantAuthenticationToken;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 社交模式认证专用token类型,方法spring authorization server进行认证流转,配合convert使用,配合github、企业微信、钉钉、微信登录使用
|
||||||
|
* @author EightMonth
|
||||||
|
* @date 2024/1/1
|
||||||
|
*/
|
||||||
|
public class SocialGrantAuthenticationToken extends OAuth2AuthorizationGrantAuthenticationToken {
|
||||||
|
|
||||||
|
public SocialGrantAuthenticationToken(Authentication clientPrincipal, Map<String, Object> additionalParameters) {
|
||||||
|
super(new AuthorizationGrantType(LoginType.SOCIAL), clientPrincipal, additionalParameters);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,26 @@
|
|||||||
|
package org.jeecg.config.security.utils;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson2.JSONObject;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.jeecg.common.system.vo.LoginUser;
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 认证信息工具类
|
||||||
|
* @author EightMonth
|
||||||
|
* @date 2024/1/10 17:03
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
public class SecureUtil {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通过当前认证信息获取用户信息
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public static LoginUser currentUser() {
|
||||||
|
String userInfoJson = SecurityContextHolder.getContext().getAuthentication().getName();
|
||||||
|
//log.info("SecureUtil.currentUser: {}", userInfoJson);
|
||||||
|
return JSONObject.parseObject(userInfoJson, LoginUser.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package org.jeecg.config.satoken;
|
package org.jeecg.config.shiro;
|
||||||
|
|
||||||
import java.lang.annotation.ElementType;
|
import java.lang.annotation.ElementType;
|
||||||
import java.lang.annotation.Retention;
|
import java.lang.annotation.Retention;
|
||||||
@ -16,4 +16,3 @@ import java.lang.annotation.Target;
|
|||||||
@Retention(RetentionPolicy.RUNTIME)
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
public @interface IgnoreAuth {
|
public @interface IgnoreAuth {
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1,13 +1,14 @@
|
|||||||
package org.jeecg.config.satoken.ignore;
|
package org.jeecg.config.shiro.ignore;
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.jeecg.config.satoken.IgnoreAuth;
|
import org.jeecg.config.shiro.IgnoreAuth;
|
||||||
import org.springframework.beans.factory.InitializingBean;
|
import org.springframework.beans.factory.InitializingBean;
|
||||||
import org.springframework.context.annotation.Lazy;
|
import org.springframework.context.annotation.Lazy;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.util.CollectionUtils;
|
import org.springframework.util.CollectionUtils;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
import org.springframework.web.method.HandlerMethod;
|
||||||
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
|
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
|
||||||
|
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package org.jeecg.config.satoken.ignore;
|
package org.jeecg.config.shiro.ignore;
|
||||||
|
|
||||||
import org.springframework.util.AntPathMatcher;
|
import org.springframework.util.AntPathMatcher;
|
||||||
import org.springframework.util.PathMatcher;
|
import org.springframework.util.PathMatcher;
|
||||||
@ -6,8 +6,8 @@ import java.util.ArrayList;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 使用内存存储通过@IgnoreAuth注解的url,配合Sa-Token进行免登录校验
|
* 使用内存存储通过@IgnoreAuth注解的url,配合JwtFilter进行免登录校验
|
||||||
* PS:无法使用ThreadLocal进行存储,因为ThreadLocal装载时,Filter已经初始化完毕,导致该类获取ThreadLocal为空
|
* PS:无法使用ThreadLocal进行存储,因为ThreadLocal装载时,JwtFilter已经初始化完毕,导致该类获取ThreadLocal为空
|
||||||
* @author eightmonth
|
* @author eightmonth
|
||||||
* @date 2024/4/18 15:02
|
* @date 2024/4/18 15:02
|
||||||
*/
|
*/
|
||||||
@ -15,7 +15,6 @@ public class InMemoryIgnoreAuth {
|
|||||||
private static final List<String> IGNORE_AUTH_LIST = new ArrayList<>();
|
private static final List<String> IGNORE_AUTH_LIST = new ArrayList<>();
|
||||||
|
|
||||||
private static PathMatcher MATCHER = new AntPathMatcher();
|
private static PathMatcher MATCHER = new AntPathMatcher();
|
||||||
|
|
||||||
public InMemoryIgnoreAuth() {}
|
public InMemoryIgnoreAuth() {}
|
||||||
|
|
||||||
public static void set(List<String> list) {
|
public static void set(List<String> list) {
|
||||||
@ -32,11 +31,11 @@ public class InMemoryIgnoreAuth {
|
|||||||
|
|
||||||
public static boolean contains(String url) {
|
public static boolean contains(String url) {
|
||||||
for (String ignoreAuth : IGNORE_AUTH_LIST) {
|
for (String ignoreAuth : IGNORE_AUTH_LIST) {
|
||||||
if(MATCHER.match(ignoreAuth, url)){
|
if(MATCHER.match(ignoreAuth,url)){
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -41,7 +41,7 @@ public class SignAuthConfiguration implements WebMvcConfigurer {
|
|||||||
registry.addInterceptor(signAuthInterceptor()).addPathPatterns(signUrlsArray);
|
registry.addInterceptor(signAuthInterceptor()).addPathPatterns(signUrlsArray);
|
||||||
}
|
}
|
||||||
|
|
||||||
//update-begin-author:taoyan date:20220427 for: issues/I53J5E post请求X_SIGN签名拦截校验后报错, request body 为空
|
// 代码逻辑说明: issues/I53J5E post请求X_SIGN签名拦截校验后报错, request body 为空
|
||||||
@Bean
|
@Bean
|
||||||
public RequestBodyReserveFilter requestBodyReserveFilter(){
|
public RequestBodyReserveFilter requestBodyReserveFilter(){
|
||||||
return new RequestBodyReserveFilter();
|
return new RequestBodyReserveFilter();
|
||||||
@ -66,6 +66,5 @@ public class SignAuthConfiguration implements WebMvcConfigurer {
|
|||||||
registration.addUrlPatterns(signUrlsArray);
|
registration.addUrlPatterns(signUrlsArray);
|
||||||
return registration;
|
return registration;
|
||||||
}
|
}
|
||||||
//update-end-author:taoyan date:20220427 for: issues/I53J5E post请求X_SIGN签名拦截校验后报错, request body 为空
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -33,7 +33,7 @@ public class SignAuthInterceptor implements HandlerInterceptor {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
|
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
|
||||||
log.info("Sign Interceptor request URI = " + request.getRequestURI());
|
log.debug("Sign Interceptor request URI = " + request.getRequestURI());
|
||||||
HttpServletRequest requestWrapper = new BodyReaderHttpServletRequestWrapper(request);
|
HttpServletRequest requestWrapper = new BodyReaderHttpServletRequestWrapper(request);
|
||||||
//获取全部参数(包括URL和body上的)
|
//获取全部参数(包括URL和body上的)
|
||||||
SortedMap<String, String> allParams = HttpUtils.getAllParams(requestWrapper);
|
SortedMap<String, String> allParams = HttpUtils.getAllParams(requestWrapper);
|
||||||
@ -79,7 +79,7 @@ public class SignAuthInterceptor implements HandlerInterceptor {
|
|||||||
log.debug("Sign 签名通过!Header Sign : {}",headerSign);
|
log.debug("Sign 签名通过!Header Sign : {}",headerSign);
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
log.info("sign allParams: {}", allParams);
|
log.debug("sign allParams: {}", allParams);
|
||||||
log.error("request URI = " + request.getRequestURI());
|
log.error("request URI = " + request.getRequestURI());
|
||||||
log.error("Sign 签名校验失败!Header Sign : {}",headerSign);
|
log.error("Sign 签名校验失败!Header Sign : {}",headerSign);
|
||||||
//校验失败返回前端
|
//校验失败返回前端
|
||||||
|
|||||||
@ -41,19 +41,19 @@ public class HttpUtils {
|
|||||||
// 获取URL上最后带逗号的参数变量 sys/dict/getDictItems/sys_user,realname,username
|
// 获取URL上最后带逗号的参数变量 sys/dict/getDictItems/sys_user,realname,username
|
||||||
String pathVariable = request.getRequestURI().substring(request.getRequestURI().lastIndexOf("/") + 1);
|
String pathVariable = request.getRequestURI().substring(request.getRequestURI().lastIndexOf("/") + 1);
|
||||||
if (pathVariable.contains(SymbolConstant.COMMA)) {
|
if (pathVariable.contains(SymbolConstant.COMMA)) {
|
||||||
log.info(" pathVariable: {}",pathVariable);
|
log.debug(" pathVariable: {}",pathVariable);
|
||||||
String deString = URLDecoder.decode(pathVariable, "UTF-8");
|
String deString = URLDecoder.decode(pathVariable, "UTF-8");
|
||||||
|
|
||||||
//https://www.52dianzi.com/category/article/37/565371.html
|
//https://www.52dianzi.com/category/article/37/565371.html
|
||||||
if(deString.contains("%")){
|
if(deString.contains("%")){
|
||||||
try {
|
try {
|
||||||
deString = URLDecoder.decode(deString, "UTF-8");
|
deString = URLDecoder.decode(deString, "UTF-8");
|
||||||
log.info("存在%情况下,执行两次解码 — pathVariable decode: {}",deString);
|
log.debug("存在%情况下,执行两次解码 — pathVariable decode: {}",deString);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
//e.printStackTrace();
|
//e.printStackTrace();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
log.info(" pathVariable decode: {}",deString);
|
log.debug(" pathVariable decode: {}",deString);
|
||||||
result.put(SignUtil.X_PATH_VARIABLE, deString);
|
result.put(SignUtil.X_PATH_VARIABLE, deString);
|
||||||
}
|
}
|
||||||
// 获取URL上的参数
|
// 获取URL上的参数
|
||||||
@ -91,15 +91,15 @@ public class HttpUtils {
|
|||||||
// 获取URL上最后带逗号的参数变量 sys/dict/getDictItems/sys_user,realname,username
|
// 获取URL上最后带逗号的参数变量 sys/dict/getDictItems/sys_user,realname,username
|
||||||
String pathVariable = url.substring(url.lastIndexOf("/") + 1);
|
String pathVariable = url.substring(url.lastIndexOf("/") + 1);
|
||||||
if (pathVariable.contains(SymbolConstant.COMMA)) {
|
if (pathVariable.contains(SymbolConstant.COMMA)) {
|
||||||
log.info(" pathVariable: {}",pathVariable);
|
log.debug(" pathVariable: {}",pathVariable);
|
||||||
String deString = URLDecoder.decode(pathVariable, "UTF-8");
|
String deString = URLDecoder.decode(pathVariable, "UTF-8");
|
||||||
|
|
||||||
//https://www.52dianzi.com/category/article/37/565371.html
|
//https://www.52dianzi.com/category/article/37/565371.html
|
||||||
if(deString.contains("%")){
|
if(deString.contains("%")){
|
||||||
deString = URLDecoder.decode(deString, "UTF-8");
|
deString = URLDecoder.decode(deString, "UTF-8");
|
||||||
log.info("存在%情况下,执行两次解码 — pathVariable decode: {}",deString);
|
log.debug("存在%情况下,执行两次解码 — pathVariable decode: {}",deString);
|
||||||
}
|
}
|
||||||
log.info(" pathVariable decode: {}",deString);
|
log.debug(" pathVariable decode: {}",deString);
|
||||||
result.put(SignUtil.X_PATH_VARIABLE, deString);
|
result.put(SignUtil.X_PATH_VARIABLE, deString);
|
||||||
}
|
}
|
||||||
// 获取URL上的参数
|
// 获取URL上的参数
|
||||||
@ -174,11 +174,10 @@ public class HttpUtils {
|
|||||||
String[] params = param.split("&");
|
String[] params = param.split("&");
|
||||||
for (String s : params) {
|
for (String s : params) {
|
||||||
int index = s.indexOf("=");
|
int index = s.indexOf("=");
|
||||||
//update-begin---author:chenrui ---date:20240222 for:[issues/5879]数据查询传ds=“”造成的异常------------
|
// 代码逻辑说明: [issues/5879]数据查询传ds=“”造成的异常------------
|
||||||
if (index != -1) {
|
if (index != -1) {
|
||||||
result.put(s.substring(0, index), s.substring(index + 1));
|
result.put(s.substring(0, index), s.substring(index + 1));
|
||||||
}
|
}
|
||||||
//update-end---author:chenrui ---date:20240222 for:[issues/5879]数据查询传ds=“”造成的异常------------
|
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@ -202,11 +201,10 @@ public class HttpUtils {
|
|||||||
String[] params = param.split("&");
|
String[] params = param.split("&");
|
||||||
for (String s : params) {
|
for (String s : params) {
|
||||||
int index = s.indexOf("=");
|
int index = s.indexOf("=");
|
||||||
//update-begin---author:chenrui ---date:20240222 for:[issues/5879]数据查询传ds=“”造成的异常------------
|
// 代码逻辑说明: [issues/5879]数据查询传ds=“”造成的异常------------
|
||||||
if (index != -1) {
|
if (index != -1) {
|
||||||
result.put(s.substring(0, index), s.substring(index + 1));
|
result.put(s.substring(0, index), s.substring(index + 1));
|
||||||
}
|
}
|
||||||
//update-end---author:chenrui ---date:20240222 for:[issues/5879]数据查询传ds=“”造成的异常------------
|
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -34,7 +34,7 @@ public class SignUtil {
|
|||||||
}
|
}
|
||||||
// 把参数加密
|
// 把参数加密
|
||||||
String paramsSign = getParamsSign(params);
|
String paramsSign = getParamsSign(params);
|
||||||
log.info("Param Sign : {}", paramsSign);
|
log.debug("Param Sign : {}", paramsSign);
|
||||||
return !StringUtils.isEmpty(paramsSign) && headerSign.equals(paramsSign);
|
return !StringUtils.isEmpty(paramsSign) && headerSign.equals(paramsSign);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -47,7 +47,7 @@ public class SignUtil {
|
|||||||
//去掉 Url 里的时间戳
|
//去掉 Url 里的时间戳
|
||||||
params.remove("_t");
|
params.remove("_t");
|
||||||
String paramsJsonStr = JSONObject.toJSONString(params);
|
String paramsJsonStr = JSONObject.toJSONString(params);
|
||||||
log.info("Param paramsJsonStr : {}", paramsJsonStr);
|
log.debug("Param paramsJsonStr : {}", paramsJsonStr);
|
||||||
//设置签名秘钥
|
//设置签名秘钥
|
||||||
JeecgBaseConfig jeecgBaseConfig = SpringContextUtils.getBean(JeecgBaseConfig.class);
|
JeecgBaseConfig jeecgBaseConfig = SpringContextUtils.getBean(JeecgBaseConfig.class);
|
||||||
String signatureSecret = jeecgBaseConfig.getSignatureSecret();
|
String signatureSecret = jeecgBaseConfig.getSignatureSecret();
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user