Compare commits
55 Commits
1d3bde9fe7
...
springboot
| Author | SHA1 | Date | |
|---|---|---|---|
| 3e107d766e | |||
| cac4121209 | |||
| 26087172df | |||
| 281c3ff3c8 | |||
| 38d44c2487 | |||
| 8c88f8adf5 | |||
| 526734c5a5 | |||
| 44b48ad916 | |||
| 1a3ae4f61c | |||
| 859c509f08 | |||
| 0704f187af | |||
| 199d2b439e | |||
| 5f898ed034 | |||
| 5a9cb05c86 | |||
| 98936680d5 | |||
| fc043fd5f3 | |||
| 54531002a7 | |||
| 728a95c00d | |||
| 13c9951c1f | |||
| 8dfaa3c3e1 | |||
| 050c478dce | |||
| 2688e8b6e2 | |||
| a9f30f0ca5 | |||
| 1bf4a0595a | |||
| 74cd57fd99 | |||
| c9ac4c9945 | |||
| 3b3371ee1a | |||
| 3599120c94 | |||
| cf9b407a18 | |||
| a194d4e9b2 | |||
| 21585e4d25 | |||
| 0489d30296 | |||
| ed87ac3bff | |||
| 761dbf0343 | |||
| 23c628057b | |||
| 2ac14709ba | |||
| f9cff08716 | |||
| a6feb2fd9d | |||
| b84eb25d41 | |||
| 4326cecad4 | |||
| ec5810176b | |||
| aff307c3ff | |||
| acfd3bb3e4 | |||
| 52082fb256 | |||
| 736515f63a | |||
| a250163198 | |||
| 1ed1f315a4 | |||
| f7670dca3a | |||
| b24ac544c8 | |||
| c7c31e0945 | |||
| 468af57489 | |||
| c85bb1f62d | |||
| b4fa11a605 | |||
| b2240848e0 | |||
| 4a888a4e19 |
@ -3,9 +3,6 @@ AIGC应用平台介绍
|
|||||||
|
|
||||||
一个全栈式 AI 开发平台,旨在帮助开发者快速构建和部署个性化的 AI 应用。
|
一个全栈式 AI 开发平台,旨在帮助开发者快速构建和部署个性化的 AI 应用。
|
||||||
|
|
||||||
> JDK说明:AI流程编排引擎暂时不支持jdk21,所以目前只能使用jdk8或者jdk17启动项目。
|
|
||||||
|
|
||||||
|
|
||||||
JeecgBoot平台的AIGC功能模块,是一套类似`Dify`的`AIGC应用开发平台`+`知识库问答`,是一款基于LLM大语言模型AI应用平台和 RAG 的知识库问答系统。
|
JeecgBoot平台的AIGC功能模块,是一套类似`Dify`的`AIGC应用开发平台`+`知识库问答`,是一款基于LLM大语言模型AI应用平台和 RAG 的知识库问答系统。
|
||||||
其直观的界面结合了 AI 流程编排、RAG 管道、知识库管理、模型管理、对接向量库、实时运行可观察等,让您可以快速从原型到生产,拥有AI服务能力。
|
其直观的界面结合了 AI 流程编排、RAG 管道、知识库管理、模型管理、对接向量库、实时运行可观察等,让您可以快速从原型到生产,拥有AI服务能力。
|
||||||
|
|
||||||
|
|||||||
@ -1,126 +0,0 @@
|
|||||||
|
|
||||||
JeecgBoot低代码平台(商业版介绍)
|
|
||||||
===============
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
项目介绍
|
|
||||||
-----------------------------------
|
|
||||||
|
|
||||||
<h3 align="center">企业级AI低代码平台</h3>
|
|
||||||
|
|
||||||
|
|
||||||
JeecgBoot是一款集成AI应用的,基于BPM流程的低代码平台,旨在帮助企业快速实现低代码开发和构建个性化AI应用,支持MCP和插件,实现聊天式业务操作(如 “一句话创建用户”)!
|
|
||||||
|
|
||||||
前后端分离架构Ant Design&Vue3,SpringBoot,SpringCloud Alibaba,Mybatis-plus,Shiro。强大的代码生成器让前后端代码一键生成,无需写任何代码! 引领AI低代码开发模式: AI生成->OnlineCoding-> 代码生成-> 手工MERGE, 帮助Java项目解决80%的重复工作,让开发更多关注业务,提高效率、节省成本,同时又不失灵活性!低代码能力:Online表单、表单设计、流程设计、Online报表、大屏/仪表盘设计、报表设计; AI应用平台功能:AI知识库问答、AI模型管理、AI流程编排、AI聊天等,支持含ChatGPT、DeepSeek、Ollama等多种AI大模型
|
|
||||||
|
|
||||||
JeecgBoot 提供了一系列 `低代码能力`,实现`真正的零代码`在线开发:Online表单开发、Online报表、复杂报表设计、打印设计、在线图表设计、仪表盘设计、大屏设计、移动图表能力、表单设计器、在线设计流程、流程自动化配置、插件能力(可插拔)
|
|
||||||
|
|
||||||
`AI赋能低代码:` 目前提供了AI应用、AI模型管理、AI流程编排、AI对话助手,AI建表、AI写文章、AI知识库问答、AI字段建议等功能;支持各种AI大模型ChatGPT、DeepSeek、Ollama、智普、千问等.
|
|
||||||
|
|
||||||
`JEECG宗旨是:` 简单功能由OnlineCoding配置实现,做到`零代码开发`;复杂功能由代码生成器生成进行手工Merge 实现`低代码开发`,既保证了`智能`又兼顾`灵活`;实现了低代码开发的同时又支持灵活编码,解决了当前低代码产品普遍不灵活的弊端!
|
|
||||||
|
|
||||||
`JEECG业务流程:` 采用工作流来实现、扩展出任务接口,供开发编写业务逻辑,表单提供多种解决方案: 表单设计器、online配置表单、编码表单。同时实现了流程与表单的分离设计(松耦合)、并支持任务节点灵活配置,既保证了公司流程的保密性,又减少了开发人员的工作量。
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#### JeecgBoot商业版与同类产品区别
|
|
||||||
-----------------------------------
|
|
||||||
|
|
||||||
- 灵活性:jeecgboot基于开源技术栈,设计初考虑到可插拔性和集成灵活性,确保平台的智能性与灵活性,避免因平台过于庞大而导致的扩展困难。
|
|
||||||
- 流程管理:支持一个表单挂接多个流程,同时一个流程可以连接多个表单,增强了流程的灵活性和复杂性管理。
|
|
||||||
- 符合中国国情的流程:针对中国市场的特定需求,jeecgboot能够实现各种符合中国国情的业务流程。
|
|
||||||
- 强大的表单设计器:jeecgboot的表单设计器与敲敲云共享,具备高质量和智能化的特点,能够满足零代码应用的需求,业内同类产品中不多见。
|
|
||||||
- 报表功能:自主研发的报表工具,拥有独立知识产权,功能上比业内老牌产品如帆软更智能,操作简便。
|
|
||||||
- BI产品整合:提供大屏、仪表盘、门户等功能,完美解决这些需求,并支持移动面板的设计与渲染。
|
|
||||||
- 自主研发的模块:jeecgboot的所有模块均为自主研发,具有独立的知识产权。
|
|
||||||
- 颗粒度和功能细致:在功能细致度和颗粒度上,jeecgboot远超同类产品,尤其在零代码能力方面表现突出。
|
|
||||||
- 零代码应用管理:最新版支持与敲敲云的零代码应用管理能力的集成,使得jeecgboot既具备低代码,又具备零代码的应用能力,业内独一无二。
|
|
||||||
- 强大的代码生成器:作为开源代码生成器的先锋,jeecgboot在代码生成的智能化和在线低代码与代码生成的结合方面,优势明显。
|
|
||||||
- 精细化权限管理:提供行级和列级的数据权限控制,满足企业在ERP和OA领域对权限管理的严格需求。
|
|
||||||
- 多平台支持的APP:目前采用uniapp3实现,支持小程序、H5、App及鸿蒙、鸿蒙Next、Electron桌面应用等多种终端。
|
|
||||||
|
|
||||||
> 综上所述,jeecgboot不仅在功能上具备丰富性和灵活性,还在技术架构、权限管理和用户体验等方面展现出明显的优势,是一个综合性能强大的低代码平台。
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
商业版演示
|
|
||||||
-----------------------------------
|
|
||||||
|
|
||||||
JeecgBoot vs 敲敲云
|
|
||||||
> - JeecgBoot是低代码产品拥有系列低代码能力,比如流程设计、表单设计、大屏设计,代码生成器,适合半开发模式(开发+低代码结合),也可以集成零代码应用管理模块.
|
|
||||||
> - 敲敲云是零代码产品,完全不写代码,通过配置搭建业务系统,其在jeecgboot基础上研发而成,删除了online、代码生成、OA等需要编码功能,只保留应用管理功能和聊天、日程、文件三个OA组件.
|
|
||||||
|
|
||||||
|
|
||||||
- JeecgBoot低代码: https://boot3.jeecg.com
|
|
||||||
- 敲敲云零代码:https://app.qiaoqiaoyun.com
|
|
||||||
- APP演示(多端): http://jeecg.com/appIndex
|
|
||||||
|
|
||||||
|
|
||||||
### 流程视频介绍
|
|
||||||
|
|
||||||
[](https://www.bilibili.com/video/BV1Nk4y1o7Qc)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### 商业版功能简述
|
|
||||||
|
|
||||||
> 详细的功能介绍,[请联系官方](https://jeecg.com/vip)
|
|
||||||
|
|
||||||
```
|
|
||||||
│─更多商业功能
|
|
||||||
│ ├─流程设计器
|
|
||||||
│ ├─简流设计器(类钉钉版)
|
|
||||||
│ ├─门户设计(NEW)
|
|
||||||
│ ├─表单设计器
|
|
||||||
│ ├─大屏设计器
|
|
||||||
│ └─我的任务
|
|
||||||
│ └─历史流程
|
|
||||||
│ └─历史流程
|
|
||||||
│ └─流程实例管理
|
|
||||||
│ └─流程监听管理
|
|
||||||
│ └─流程表达式
|
|
||||||
│ └─我发起的流程
|
|
||||||
│ └─我的抄送
|
|
||||||
│ └─流程委派、抄送、跳转
|
|
||||||
│ └─OA办公组件
|
|
||||||
│ └─零代码应用管理(无需编码,在线搭建应用系统)
|
|
||||||
│ ├─积木报表企业版(含jimureport、jimubi)
|
|
||||||
│ ├─AI流程设计器源码
|
|
||||||
│ ├─Online全模块功能和源码
|
|
||||||
│ ├─AI写文章(CMS)
|
|
||||||
│ ├─AI表单字段建议(表单设计器)
|
|
||||||
│ ├─OA办公协同组件
|
|
||||||
│ ├─在线聊天功能
|
|
||||||
│ ├─设计表单移动适配
|
|
||||||
│ ├─设计表单支持外部填报
|
|
||||||
│ ├─设计表单AI字段建议
|
|
||||||
│ ├─设计表单视图功能(支持多种类型含日历、表格、看板、甘特图)
|
|
||||||
│ └─。。。
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
##### 流程设计
|
|
||||||

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

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

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

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

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

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

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

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

|
|
||||||
22
README.md
@ -19,15 +19,17 @@ JeecgBoot AI低代码平台
|
|||||||
|
|
||||||
<h3 align="center">企业级AI低代码平台</h3>
|
<h3 align="center">企业级AI低代码平台</h3>
|
||||||
|
|
||||||
JeecgBoot 是一款基于BPM流程和代码生成的AI低代码平台,助力企业快速实现低代码开发和构建AI应用,支持MCP和插件,实现聊天式业务操作(如 “一句话创建用户”)!
|
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平台实现简单功能的零代码快速搭建,同时针对复杂功能采用代码生成器生成代码并手工合并,打造智能且灵活的低代码开发模式,有效解决了当前低代码产品普遍缺乏灵活性的问题,提升开发效率的同时兼顾系统的扩展性和定制化能力。
|
||||||
|
|
||||||
@ -230,20 +232,6 @@ JeecgBoot平台提供了一套完善的AI应用管理系统模块,是一套类
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
开源版与企业版区别?
|
|
||||||
-----------------------------------
|
|
||||||
|
|
||||||
- JeecgBoot开源版采用 [Apache-2.0 license](LICENSE) 协议附加补充条款:允许商用使用,不会造成侵权行为,允许基于本平台软件开展业务系统开发(但在任何情况下,您不得使用本软件开发可能被认为与本软件竞争的软件).
|
|
||||||
- 商业版与开源版主要区别在于商业版提供了技术支持 和 更多的企业级功能(例如:Online图表、流程监控、流程设计、流程审批、表单设计器、表单视图、积木报表企业版、OA办公、商业APP、零代码应用、Online模块源码等功能). [更多商业功能介绍,点击查看](README-Enterprise.md)
|
|
||||||
- JeecgBoot未来发展方向是:零代码平台的建设,也就是团队的另外一款产品 [敲敲云零代码](https://www.qiaoqiaoyun.com) ,无需编码即可通过拖拽快速搭建企业级应用,与JeecgBoot低代码平台形成互补,满足从简单业务到复杂系统的全场景开发需求,目前已经开源,[欢迎下载](https://qiaoqiaoyun.com/downloadCode)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### Jeecg Boot 产品功能蓝图
|
### Jeecg Boot 产品功能蓝图
|
||||||

|

|
||||||
|
|
||||||
|
|||||||
368
jeecg-boot/Shiro到Sa-Token迁移指南.md
Normal file
@ -0,0 +1,368 @@
|
|||||||
|
# `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)
|
||||||
BIN
jeecg-boot/db/其他数据库脚本/jeecgboot-oracle11g.dmp
Normal file
31499
jeecg-boot/db/其他数据库脚本/jeecgboot-oracle11g.sql
Normal file
27491
jeecg-boot/db/其他数据库脚本/jeecgboot-postgresql17.sql
Normal file
50451
jeecg-boot/db/其他数据库脚本/jeecgboot-sqlserver2017.sql
Normal file
@ -185,83 +185,23 @@
|
|||||||
<artifactId>spring-boot-starter-quartz</artifactId>
|
<artifactId>spring-boot-starter-quartz</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!--JWT-->
|
<!-- Sa-Token 权限认证,在线文档:https://sa-token.cc -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.auth0</groupId>
|
<groupId>cn.dev33</groupId>
|
||||||
<artifactId>java-jwt</artifactId>
|
<artifactId>sa-token-spring-boot3-starter</artifactId>
|
||||||
<version>${java-jwt.version}</version>
|
<version>${sa-token.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<!-- Sa-Token 整合 Redis (使用 jackson 序列化方式) -->
|
||||||
<!--shiro-->
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.apache.shiro</groupId>
|
<groupId>cn.dev33</groupId>
|
||||||
<artifactId>shiro-spring-boot-starter</artifactId>
|
<artifactId>sa-token-redis-jackson</artifactId>
|
||||||
<classifier>jakarta</classifier>
|
<version>${sa-token.version}</version>
|
||||||
<version>${shiro.version}</version>
|
|
||||||
<exclusions>
|
|
||||||
<exclusion>
|
|
||||||
<groupId>org.apache.shiro</groupId>
|
|
||||||
<artifactId>shiro-spring</artifactId>
|
|
||||||
</exclusion>
|
|
||||||
</exclusions>
|
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<!-- Sa-Token 整合 jwt (Simple模式),保持与原JWT token格式兼容 -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.apache.shiro</groupId>
|
<groupId>cn.dev33</groupId>
|
||||||
<artifactId>shiro-spring</artifactId>
|
<artifactId>sa-token-jwt</artifactId>
|
||||||
<classifier>jakarta</classifier>
|
<version>${sa-token.version}</version>
|
||||||
<version>${shiro.version}</version>
|
|
||||||
<!-- 排除仍使用了javax.servlet的依赖 -->
|
|
||||||
<exclusions>
|
|
||||||
<exclusion>
|
|
||||||
<groupId>org.apache.shiro</groupId>
|
|
||||||
<artifactId>shiro-core</artifactId>
|
|
||||||
</exclusion>
|
|
||||||
<exclusion>
|
|
||||||
<groupId>org.apache.shiro</groupId>
|
|
||||||
<artifactId>shiro-web</artifactId>
|
|
||||||
</exclusion>
|
|
||||||
</exclusions>
|
|
||||||
</dependency>
|
|
||||||
<!-- 引入适配jakarta的依赖包 -->
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.apache.shiro</groupId>
|
|
||||||
<artifactId>shiro-core</artifactId>
|
|
||||||
<classifier>jakarta</classifier>
|
|
||||||
<version>${shiro.version}</version>
|
|
||||||
<exclusions>
|
|
||||||
<exclusion>
|
|
||||||
<groupId>commons-beanutils</groupId>
|
|
||||||
<artifactId>commons-beanutils</artifactId>
|
|
||||||
</exclusion>
|
|
||||||
</exclusions>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.apache.shiro</groupId>
|
|
||||||
<artifactId>shiro-web</artifactId>
|
|
||||||
<classifier>jakarta</classifier>
|
|
||||||
<version>${shiro.version}</version>
|
|
||||||
<exclusions>
|
|
||||||
<exclusion>
|
|
||||||
<groupId>org.apache.shiro</groupId>
|
|
||||||
<artifactId>shiro-core</artifactId>
|
|
||||||
</exclusion>
|
|
||||||
</exclusions>
|
|
||||||
</dependency>
|
|
||||||
<!-- shiro-redis -->
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.crazycake</groupId>
|
|
||||||
<artifactId>shiro-redis</artifactId>
|
|
||||||
<version>${shiro-redis.version}</version>
|
|
||||||
<exclusions>
|
|
||||||
<exclusion>
|
|
||||||
<groupId>org.apache.shiro</groupId>
|
|
||||||
<artifactId>shiro-core</artifactId>
|
|
||||||
</exclusion>
|
|
||||||
<exclusion>
|
|
||||||
<artifactId>checkstyle</artifactId>
|
|
||||||
<groupId>com.puppycrawl.tools</groupId>
|
|
||||||
</exclusion>
|
|
||||||
</exclusions>
|
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
|
|||||||
@ -0,0 +1,21 @@
|
|||||||
|
package org.apache.shiro;
|
||||||
|
|
||||||
|
import org.apache.shiro.subject.Subject;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 兼容处理Online功能使用处理,请勿修改
|
||||||
|
* @author eightmonth@qq.com
|
||||||
|
* @date 2024/4/29 14:05
|
||||||
|
*/
|
||||||
|
public class SecurityUtils {
|
||||||
|
|
||||||
|
|
||||||
|
public static Subject getSubject() {
|
||||||
|
return new Subject() {
|
||||||
|
@Override
|
||||||
|
public Object getPrincipal() {
|
||||||
|
return Subject.super.getPrincipal();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,15 @@
|
|||||||
|
package org.apache.shiro.subject;
|
||||||
|
|
||||||
|
|
||||||
|
import org.jeecg.common.util.LoginUserUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 兼容处理Online功能使用处理,请勿修改
|
||||||
|
* @author eightmonth@qq.com
|
||||||
|
* @date 2024/4/29 14:18
|
||||||
|
*/
|
||||||
|
public interface Subject {
|
||||||
|
default Object getPrincipal() {
|
||||||
|
return LoginUserUtils.getSessionUser();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,7 +2,7 @@ package org.jeecg.common.aspect;
|
|||||||
|
|
||||||
import com.alibaba.fastjson.JSONObject;
|
import com.alibaba.fastjson.JSONObject;
|
||||||
import com.alibaba.fastjson.serializer.PropertyFilter;
|
import com.alibaba.fastjson.serializer.PropertyFilter;
|
||||||
import org.apache.shiro.SecurityUtils;
|
import org.jeecg.common.util.LoginUserUtils;
|
||||||
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;
|
||||||
@ -100,7 +100,7 @@ public class AutoLogAspect {
|
|||||||
//设置IP地址
|
//设置IP地址
|
||||||
dto.setIp(IpUtils.getIpAddr(request));
|
dto.setIp(IpUtils.getIpAddr(request));
|
||||||
//获取登录用户信息
|
//获取登录用户信息
|
||||||
LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();
|
LoginUser sysUser = LoginUserUtils.getSessionUser();
|
||||||
if(sysUser!=null){
|
if(sysUser!=null){
|
||||||
dto.setUserid(sysUser.getUsername());
|
dto.setUserid(sysUser.getUsername());
|
||||||
dto.setUsername(sysUser.getRealname());
|
dto.setUsername(sysUser.getRealname());
|
||||||
@ -243,7 +243,7 @@ public class AutoLogAspect {
|
|||||||
sysLog.setIp(IPUtils.getIpAddr(request));
|
sysLog.setIp(IPUtils.getIpAddr(request));
|
||||||
|
|
||||||
//获取登录用户信息
|
//获取登录用户信息
|
||||||
LoginUser sysUser = (LoginUser)SecurityUtils.getSubject().getPrincipal();
|
LoginUser sysUser = LoginUserUtils.getLoginUser();
|
||||||
if(sysUser!=null){
|
if(sysUser!=null){
|
||||||
sysLog.setUserid(sysUser.getUsername());
|
sysLog.setUserid(sysUser.getUsername());
|
||||||
sysLog.setUsername(sysUser.getRealname());
|
sysLog.setUsername(sysUser.getRealname());
|
||||||
|
|||||||
@ -87,30 +87,6 @@ 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 = "prefix_user_token:";
|
|
||||||
/** 登录用户Token令牌作废提示信息,比如 “不允许同一账号多地同时登录,会往这个变量存提示信息” */
|
|
||||||
String PREFIX_USER_TOKEN_ERROR_MSG = "prefix_user_token:error:msg_";
|
|
||||||
|
|
||||||
/**============================== 【是否允许同一账号多地同时登录】登录客户端类型常量 ==============================*/
|
|
||||||
/** 客户端类型:PC端 */
|
|
||||||
String CLIENT_TYPE_PC = "PC";
|
|
||||||
/** 客户端类型:APP端 */
|
|
||||||
String CLIENT_TYPE_APP = "APP";
|
|
||||||
/** 客户端类型:手机号登录 */
|
|
||||||
String CLIENT_TYPE_PHONE = "PHONE";
|
|
||||||
String PREFIX_USER_TOKEN_PC = "prefix_user_token:single_login:pc:";
|
|
||||||
/** 单点登录:用户在APP端的Token缓存KEY前缀 (username -> token) */
|
|
||||||
String PREFIX_USER_TOKEN_APP = "prefix_user_token:single_login:app:";
|
|
||||||
/** 单点登录:用户在手机号登录的Token缓存KEY前缀 (username -> token) */
|
|
||||||
String PREFIX_USER_TOKEN_PHONE = "prefix_user_token: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:";
|
||||||
@ -741,4 +717,13 @@ public interface CommonConstant {
|
|||||||
* 发送短信方式:阿里云
|
* 发送短信方式:阿里云
|
||||||
*/
|
*/
|
||||||
String SMS_SEND_TYPE_ALI_YUN = "aliyun";
|
String SMS_SEND_TYPE_ALI_YUN = "aliyun";
|
||||||
|
|
||||||
|
/**============================== 【是否允许同一账号多地同时登录】登录客户端类型常量 ==============================*/
|
||||||
|
/** 客户端类型:PC端 */
|
||||||
|
String CLIENT_TYPE_PC = "PC";
|
||||||
|
/** 客户端类型:APP端 */
|
||||||
|
String CLIENT_TYPE_APP = "APP";
|
||||||
|
/** 客户端类型:手机号登录 */
|
||||||
|
String CLIENT_TYPE_PHONE = "PHONE";
|
||||||
|
/**============================== 【是否允许同一账号多地同时登录】登录客户端类型常量 ==============================*/
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,9 +5,10 @@ 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.apache.shiro.SecurityUtils;
|
import org.jeecg.common.util.LoginUserUtils;
|
||||||
import org.apache.shiro.authz.AuthorizationException;
|
import cn.dev33.satoken.exception.NotLoginException;
|
||||||
import org.apache.shiro.authz.UnauthorizedException;
|
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;
|
||||||
@ -112,12 +113,34 @@ public class JeecgBootExceptionHandler {
|
|||||||
return Result.error("数据库中已存在该记录");
|
return Result.error("数据库中已存在该记录");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ExceptionHandler({UnauthorizedException.class, AuthorizationException.class})
|
/**
|
||||||
public Result<?> handleAuthorizationException(AuthorizationException e){
|
* 处理Sa-Token未登录异常
|
||||||
|
*/
|
||||||
|
@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);
|
||||||
@ -263,7 +286,7 @@ public class JeecgBootExceptionHandler {
|
|||||||
|
|
||||||
|
|
||||||
//获取登录用户信息
|
//获取登录用户信息
|
||||||
LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();
|
LoginUser sysUser = LoginUserUtils.getSessionUser();
|
||||||
if(sysUser!=null){
|
if(sysUser!=null){
|
||||||
log.setUserid(sysUser.getUsername());
|
log.setUserid(sysUser.getUsername());
|
||||||
log.setUsername(sysUser.getRealname());
|
log.setUsername(sysUser.getRealname());
|
||||||
|
|||||||
@ -6,10 +6,10 @@ 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.jeecgframework.poi.excel.ExcelImportUtil;
|
import org.jeecgframework.poi.excel.ExcelImportUtil;
|
||||||
@ -52,7 +52,7 @@ public class JeecgController<T, S extends IService<T>> {
|
|||||||
protected ModelAndView exportXls(HttpServletRequest request, T object, Class<T> clazz, String title) {
|
protected ModelAndView exportXls(HttpServletRequest request, T object, Class<T> clazz, String title) {
|
||||||
// Step.1 组装查询条件
|
// Step.1 组装查询条件
|
||||||
QueryWrapper<T> queryWrapper = QueryGenerator.initQueryWrapper(object, request.getParameterMap());
|
QueryWrapper<T> queryWrapper = QueryGenerator.initQueryWrapper(object, request.getParameterMap());
|
||||||
LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();
|
LoginUser sysUser = LoginUserUtils.getSessionUser();
|
||||||
|
|
||||||
// 过滤选中数据
|
// 过滤选中数据
|
||||||
String selections = request.getParameter("selections");
|
String selections = request.getParameter("selections");
|
||||||
@ -94,7 +94,7 @@ public class JeecgController<T, S extends IService<T>> {
|
|||||||
protected ModelAndView exportXlsSheet(HttpServletRequest request, T object, Class<T> clazz, String title,String exportFields,Integer pageNum) {
|
protected ModelAndView exportXlsSheet(HttpServletRequest request, T object, Class<T> clazz, String title,String exportFields,Integer pageNum) {
|
||||||
// Step.1 组装查询条件
|
// Step.1 组装查询条件
|
||||||
QueryWrapper<T> queryWrapper = QueryGenerator.initQueryWrapper(object, request.getParameterMap());
|
QueryWrapper<T> queryWrapper = QueryGenerator.initQueryWrapper(object, request.getParameterMap());
|
||||||
LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();
|
LoginUser sysUser = LoginUserUtils.getSessionUser();
|
||||||
// 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);
|
||||||
@ -144,7 +144,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 = (LoginUser) SecurityUtils.getSubject().getPrincipal();
|
LoginUser sysUser = LoginUserUtils.getSessionUser();
|
||||||
// 计算分页数
|
// 计算分页数
|
||||||
double total = service.count();
|
double total = service.count();
|
||||||
int count = (int) Math.ceil(total / pageSize);
|
int count = (int) Math.ceil(total / pageSize);
|
||||||
|
|||||||
@ -1,31 +1,23 @@
|
|||||||
package org.jeecg.common.system.util;
|
package org.jeecg.common.system.util;
|
||||||
|
|
||||||
import com.auth0.jwt.JWT;
|
import cn.dev33.satoken.stp.StpUtil;
|
||||||
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.io.OutputStream;
|
|
||||||
import java.util.Date;
|
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
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.apache.shiro.SecurityUtils;
|
|
||||||
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;
|
||||||
@ -36,159 +28,74 @@ import org.jeecg.common.util.oConvertUtils;
|
|||||||
/**
|
/**
|
||||||
* @Author Scott
|
* @Author Scott
|
||||||
* @Date 2018-07-12 14:23
|
* @Date 2018-07-12 14:23
|
||||||
* @Desc JWT工具类
|
* @Desc JWT工具类 - 已迁移到Sa-Token,此类作为兼容层保留
|
||||||
**/
|
**/
|
||||||
@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;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
* 返回错误 JSON 字符串(用于 Sa-Token Filter)
|
||||||
* @param response
|
* @param code 错误码
|
||||||
* @param code
|
* @param errorMsg 错误信息
|
||||||
* @param errorMsg
|
* @return JSON 字符串
|
||||||
*/
|
*/
|
||||||
public static void responseError(HttpServletResponse response, Integer code, String errorMsg) {
|
public static String responseErrorJson(Integer code, String errorMsg) {
|
||||||
try {
|
try {
|
||||||
Result jsonResult = new Result(code, errorMsg);
|
Result jsonResult = new Result(code, errorMsg);
|
||||||
jsonResult.setSuccess(false);
|
jsonResult.setSuccess(false);
|
||||||
|
|
||||||
// 设置响应头和内容类型
|
|
||||||
response.setStatus(code);
|
|
||||||
response.setHeader("Content-type", "text/html;charset=UTF-8");
|
|
||||||
response.setContentType("application/json;charset=UTF-8");
|
|
||||||
// 使用 ObjectMapper 序列化为 JSON 字符串
|
|
||||||
ObjectMapper objectMapper = new ObjectMapper();
|
ObjectMapper objectMapper = new ObjectMapper();
|
||||||
String json = objectMapper.writeValueAsString(jsonResult);
|
return objectMapper.writeValueAsString(jsonResult);
|
||||||
response.getWriter().write(json);
|
|
||||||
response.getWriter().flush();
|
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
log.error(e.getMessage(), e);
|
log.error("生成错误 JSON 失败: {}", e.getMessage());
|
||||||
|
// 返回备用的硬编码 JSON
|
||||||
|
return "{\"success\":false,\"message\":\"" + errorMsg + "\",\"code\":" + code + ",\"result\":null,\"timestamp\":" + System.currentTimeMillis() + "}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 校验token是否正确
|
* 校验token是否正确
|
||||||
|
* 注意:此方法已废弃,使用Sa-Token自动校验
|
||||||
*
|
*
|
||||||
* @param token 密钥
|
* @param token
|
||||||
* @param secret 用户的密码
|
* @return
|
||||||
* @return 是否正确
|
|
||||||
*/
|
*/
|
||||||
public static boolean verify(String token, String username, String secret) {
|
@Deprecated
|
||||||
|
public static boolean verify(String token){
|
||||||
try {
|
try {
|
||||||
// 根据密码生成JWT效验器
|
// 使用Sa-Token验证
|
||||||
Algorithm algorithm = Algorithm.HMAC256(secret);
|
return StpUtil.getLoginIdByToken(token) != null;
|
||||||
JWTVerifier verifier = JWT.require(algorithm).withClaim("username", username).build();
|
|
||||||
// 效验TOKEN
|
|
||||||
DecodedJWT jwt = verifier.verify(token);
|
|
||||||
return true;
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.warn("Token验证失败:" + e.getMessage(),e);
|
log.warn(e.getMessage(), e);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获得token中的信息无需secret解密也能获得
|
* 获得Token中的用户名(不校验token是否有效)
|
||||||
|
* <p>注意:现在 loginId 就是 username,直接返回
|
||||||
*
|
*
|
||||||
* @return token中包含的用户名
|
* @param token JWT token
|
||||||
|
* @return 用户名(username),如果 token 无效则返回 null
|
||||||
*/
|
*/
|
||||||
public static String getUsername(String token) {
|
public static String getUsername(String token){
|
||||||
try {
|
try {
|
||||||
DecodedJWT jwt = JWT.decode(token);
|
if(oConvertUtils.isEmpty(token)) {
|
||||||
return jwt.getClaim("username").asString();
|
return null;
|
||||||
} catch (JWTDecodeException e) {
|
}
|
||||||
log.error(e.getMessage(), e);
|
// Sa-Token 的 loginId 现在就是 username,直接返回
|
||||||
|
Object loginId = StpUtil.getLoginIdByToken(token);
|
||||||
|
return loginId != null ? loginId.toString() : null;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("获取用户名失败: {}", e.getMessage());
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 生成签名,5min后过期
|
|
||||||
*
|
|
||||||
* @param username 用户名
|
|
||||||
* @param secret 用户的密码
|
|
||||||
* @return 加密的token
|
|
||||||
* @deprecated 请使用sign(String username, String secret, String clientType)方法代替
|
|
||||||
*/
|
|
||||||
@Deprecated
|
|
||||||
public static String sign(String username, String secret) {
|
|
||||||
Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
|
|
||||||
Algorithm algorithm = Algorithm.HMAC256(secret);
|
|
||||||
// 附带username信息
|
|
||||||
return JWT.create().withClaim("username", username).withExpiresAt(date).sign(algorithm);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 生成签名,5min后过期
|
|
||||||
*
|
|
||||||
* @param username 用户名
|
|
||||||
* @param secret 用户的密码
|
|
||||||
* @param expireTime 过期时间
|
|
||||||
* @return 加密的token
|
|
||||||
* @deprecated 请使用sign(String username, String secret, String clientType)方法代替
|
|
||||||
*/
|
|
||||||
@Deprecated
|
|
||||||
public static String sign(String username, String secret, Long expireTime) {
|
|
||||||
Date date = new Date(System.currentTimeMillis() + expireTime);
|
|
||||||
Algorithm algorithm = Algorithm.HMAC256(secret);
|
|
||||||
// 附带username信息
|
|
||||||
return JWT.create().withClaim("username", username).withExpiresAt(date).sign(algorithm);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 生成签名,根据客户端类型自动选择过期时间
|
|
||||||
* 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) {
|
|
||||||
// 根据客户端类型选择对应的过期时间
|
|
||||||
long expireTime = CommonConstant.CLIENT_TYPE_APP.equalsIgnoreCase(clientType)
|
|
||||||
? APP_EXPIRE_TIME
|
|
||||||
: EXPIRE_TIME;
|
|
||||||
Date date = new Date(System.currentTimeMillis() + expireTime);
|
|
||||||
Algorithm algorithm = Algorithm.HMAC256(secret);
|
|
||||||
// 附带username和clientType信息
|
|
||||||
return JWT.create()
|
|
||||||
.withClaim("username", username)
|
|
||||||
.withClaim("clientType", clientType)
|
|
||||||
.withExpiresAt(date)
|
|
||||||
.sign(algorithm);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 从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
|
||||||
@ -238,7 +145,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) {
|
||||||
@ -248,7 +155,7 @@ public class JwtUtil {
|
|||||||
//2.通过shiro获取登录用户信息
|
//2.通过shiro获取登录用户信息
|
||||||
LoginUser sysUser = null;
|
LoginUser sysUser = null;
|
||||||
try {
|
try {
|
||||||
sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();
|
sysUser = (LoginUser) LoginUserUtils.getSessionUser();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.warn("SecurityUtils.getSubject() 获取用户信息异常:" + e.getMessage());
|
log.warn("SecurityUtils.getSubject() 获取用户信息异常:" + e.getMessage());
|
||||||
}
|
}
|
||||||
@ -256,74 +163,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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -338,19 +245,15 @@ 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 -> {
|
||||||
@ -374,20 +277,17 @@ 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){returnValue = returnValue + moshi;}
|
if (returnValue != null) {
|
||||||
|
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));
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,175 @@
|
|||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -1,37 +0,0 @@
|
|||||||
package org.jeecg.common.util;
|
|
||||||
|
|
||||||
import org.apache.shiro.SecurityUtils;
|
|
||||||
import org.apache.shiro.mgt.SecurityManager;
|
|
||||||
import org.apache.shiro.subject.Subject;
|
|
||||||
import org.apache.shiro.util.ThreadContext;
|
|
||||||
|
|
||||||
import java.util.concurrent.*;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @date 2025-09-04
|
|
||||||
* @author scott
|
|
||||||
*
|
|
||||||
* @Description: 支持shiro的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) {
|
|
||||||
Subject subject = SecurityUtils.getSubject();
|
|
||||||
SecurityManager securityManager = SecurityUtils.getSecurityManager();
|
|
||||||
super.execute(() -> {
|
|
||||||
try {
|
|
||||||
ThreadContext.bind(securityManager);
|
|
||||||
ThreadContext.bind(subject);
|
|
||||||
command.run();
|
|
||||||
} finally {
|
|
||||||
ThreadContext.unbindSubject();
|
|
||||||
ThreadContext.unbindSecurityManager();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,5 +1,6 @@
|
|||||||
package org.jeecg.common.util;
|
package org.jeecg.common.util;
|
||||||
|
|
||||||
|
import cn.dev33.satoken.stp.StpUtil;
|
||||||
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;
|
||||||
@ -87,74 +88,41 @@ public class TokenUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 验证Token
|
* 验证Token(已重写为Sa-Token实现)
|
||||||
*/
|
*/
|
||||||
public static boolean verifyToken(HttpServletRequest request, CommonAPI commonApi, RedisUtil redisUtil) {
|
public static boolean verifyToken(HttpServletRequest request, CommonAPI commonApi) {
|
||||||
log.debug(" -- url --" + request.getRequestURL());
|
log.debug(" -- url --" + request.getRequestURL());
|
||||||
String token = getTokenByRequest(request);
|
String token = getTokenByRequest(request);
|
||||||
return TokenUtils.verifyToken(token, commonApi, redisUtil);
|
return TokenUtils.verifyToken(token, commonApi);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 验证Token
|
* 验证Token(已重写为Sa-Token实现)
|
||||||
*/
|
*/
|
||||||
public static boolean verifyToken(String token, CommonAPI commonApi, RedisUtil redisUtil) {
|
public static boolean verifyToken(String token, CommonAPI commonApi) {
|
||||||
if (StringUtils.isBlank(token)) {
|
if (StringUtils.isBlank(token)) {
|
||||||
throw new JeecgBoot401Exception("token不能为空!");
|
throw new JeecgBoot401Exception("token不能为空!");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 解密获得username,用于和数据库进行对比
|
// 使用Sa-Token校验token
|
||||||
String username = JwtUtil.getUsername(token);
|
Object username = StpUtil.getLoginIdByToken(token);
|
||||||
if (username == null) {
|
if (username == null) {
|
||||||
throw new JeecgBoot401Exception("token非法无效!");
|
throw new JeecgBoot401Exception("token非法无效!");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 查询用户信息
|
// 查询用户信息
|
||||||
LoginUser user = TokenUtils.getLoginUser(username, commonApi, redisUtil);
|
LoginUser user = commonApi.getUserByName(username.toString());
|
||||||
//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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 刷新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 true;
|
||||||
}
|
}
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取登录用户
|
* 获取登录用户
|
||||||
@ -181,4 +149,5 @@ public class TokenUtils {
|
|||||||
}
|
}
|
||||||
return loginUser;
|
return loginUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,12 +1,13 @@
|
|||||||
package org.jeecg.common.util.encryption;
|
package org.jeecg.common.util.encryption;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.shiro.lang.codec.Base64;
|
|
||||||
import org.jeecg.common.util.oConvertUtils;
|
import org.jeecg.common.util.oConvertUtils;
|
||||||
|
|
||||||
import javax.crypto.Cipher;
|
import javax.crypto.Cipher;
|
||||||
import javax.crypto.spec.IvParameterSpec;
|
import javax.crypto.spec.IvParameterSpec;
|
||||||
import javax.crypto.spec.SecretKeySpec;
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.Base64;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AES 工具 (兼容历史 NoPadding + 新 PKCS5Padding)
|
* AES 工具 (兼容历史 NoPadding + 新 PKCS5Padding)
|
||||||
@ -23,7 +24,7 @@ public class AesEncryptUtil {
|
|||||||
SecretKeySpec ks = new SecretKeySpec(KEY.getBytes(StandardCharsets.UTF_8), "AES");
|
SecretKeySpec ks = new SecretKeySpec(KEY.getBytes(StandardCharsets.UTF_8), "AES");
|
||||||
IvParameterSpec ivSpec = new IvParameterSpec(IV.getBytes(StandardCharsets.UTF_8));
|
IvParameterSpec ivSpec = new IvParameterSpec(IV.getBytes(StandardCharsets.UTF_8));
|
||||||
cipher.init(Cipher.DECRYPT_MODE, ks, ivSpec);
|
cipher.init(Cipher.DECRYPT_MODE, ks, ivSpec);
|
||||||
byte[] plain = cipher.doFinal(Base64.decode(cipherBase64));
|
byte[] plain = cipher.doFinal(Base64.getDecoder().decode(cipherBase64));
|
||||||
return new String(plain, StandardCharsets.UTF_8);
|
return new String(plain, StandardCharsets.UTF_8);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -33,7 +34,7 @@ public class AesEncryptUtil {
|
|||||||
SecretKeySpec ks = new SecretKeySpec(KEY.getBytes(StandardCharsets.UTF_8), "AES");
|
SecretKeySpec ks = new SecretKeySpec(KEY.getBytes(StandardCharsets.UTF_8), "AES");
|
||||||
IvParameterSpec ivSpec = new IvParameterSpec(IV.getBytes(StandardCharsets.UTF_8));
|
IvParameterSpec ivSpec = new IvParameterSpec(IV.getBytes(StandardCharsets.UTF_8));
|
||||||
cipher.init(Cipher.DECRYPT_MODE, ks, ivSpec);
|
cipher.init(Cipher.DECRYPT_MODE, ks, ivSpec);
|
||||||
byte[] data = cipher.doFinal(Base64.decode(cipherBase64));
|
byte[] data = cipher.doFinal(Base64.getDecoder().decode(cipherBase64));
|
||||||
return new String(data, StandardCharsets.UTF_8)
|
return new String(data, StandardCharsets.UTF_8)
|
||||||
.replace("\u0000",""); // 旧填充 0
|
.replace("\u0000",""); // 旧填充 0
|
||||||
}
|
}
|
||||||
@ -93,7 +94,7 @@ public class AesEncryptUtil {
|
|||||||
IvParameterSpec ivspec = new IvParameterSpec(IV.getBytes(StandardCharsets.UTF_8));
|
IvParameterSpec ivspec = new IvParameterSpec(IV.getBytes(StandardCharsets.UTF_8));
|
||||||
cipher.init(Cipher.ENCRYPT_MODE, keyspec, ivspec);
|
cipher.init(Cipher.ENCRYPT_MODE, keyspec, ivspec);
|
||||||
byte[] encrypted = cipher.doFinal(plaintext);
|
byte[] encrypted = cipher.doFinal(plaintext);
|
||||||
return Base64.encodeToString(encrypted);
|
return Base64.getEncoder().encodeToString(encrypted);
|
||||||
}catch(Exception e){
|
}catch(Exception e){
|
||||||
throw new IllegalStateException("legacy encrypt error", e);
|
throw new IllegalStateException("legacy encrypt error", e);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -24,23 +24,18 @@ 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, redisUtil);
|
TokenUtils.verifyToken(token, commonApi);
|
||||||
} 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());
|
||||||
|
|||||||
@ -2,7 +2,7 @@ package org.jeecg.config.firewall.interceptor;
|
|||||||
|
|
||||||
import com.alibaba.fastjson.JSON;
|
import com.alibaba.fastjson.JSON;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.shiro.SecurityUtils;
|
import org.jeecg.common.util.LoginUserUtils;
|
||||||
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;
|
||||||
@ -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 = (LoginUser) SecurityUtils.getSubject().getPrincipal();
|
LoginUser loginUser = LoginUserUtils.getSessionUser();
|
||||||
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,7 +6,7 @@ import org.apache.ibatis.executor.Executor;
|
|||||||
import org.apache.ibatis.mapping.MappedStatement;
|
import org.apache.ibatis.mapping.MappedStatement;
|
||||||
import org.apache.ibatis.mapping.SqlCommandType;
|
import org.apache.ibatis.mapping.SqlCommandType;
|
||||||
import org.apache.ibatis.plugin.*;
|
import org.apache.ibatis.plugin.*;
|
||||||
import org.apache.shiro.SecurityUtils;
|
import org.jeecg.common.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;
|
||||||
@ -189,7 +189,7 @@ public class MybatisInterceptor implements Interceptor {
|
|||||||
private LoginUser getLoginUser() {
|
private LoginUser getLoginUser() {
|
||||||
LoginUser sysUser = null;
|
LoginUser sysUser = null;
|
||||||
try {
|
try {
|
||||||
sysUser = SecurityUtils.getSubject().getPrincipal() != null ? (LoginUser) SecurityUtils.getSubject().getPrincipal() : null;
|
sysUser = LoginUserUtils.getSessionUser() != null ? LoginUserUtils.getSessionUser() : null;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
//e.printStackTrace();
|
//e.printStackTrace();
|
||||||
sysUser = null;
|
sysUser = null;
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
package org.jeecg.config.shiro;
|
package org.jeecg.config.satoken;
|
||||||
|
|
||||||
import java.lang.annotation.ElementType;
|
import java.lang.annotation.ElementType;
|
||||||
import java.lang.annotation.Retention;
|
import java.lang.annotation.Retention;
|
||||||
@ -16,3 +16,4 @@ import java.lang.annotation.Target;
|
|||||||
@Retention(RetentionPolicy.RUNTIME)
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
public @interface IgnoreAuth {
|
public @interface IgnoreAuth {
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -0,0 +1,420 @@
|
|||||||
|
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.debug("===【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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -0,0 +1,174 @@
|
|||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,14 +1,13 @@
|
|||||||
package org.jeecg.config.shiro.ignore;
|
package org.jeecg.config.satoken.ignore;
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.jeecg.config.shiro.IgnoreAuth;
|
import org.jeecg.config.satoken.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.shiro.ignore;
|
package org.jeecg.config.satoken.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,配合JwtFilter进行免登录校验
|
* 使用内存存储通过@IgnoreAuth注解的url,配合Sa-Token进行免登录校验
|
||||||
* PS:无法使用ThreadLocal进行存储,因为ThreadLocal装载时,JwtFilter已经初始化完毕,导致该类获取ThreadLocal为空
|
* PS:无法使用ThreadLocal进行存储,因为ThreadLocal装载时,Filter已经初始化完毕,导致该类获取ThreadLocal为空
|
||||||
* @author eightmonth
|
* @author eightmonth
|
||||||
* @date 2024/4/18 15:02
|
* @date 2024/4/18 15:02
|
||||||
*/
|
*/
|
||||||
@ -15,6 +15,7 @@ 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) {
|
||||||
@ -31,11 +32,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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1,28 +0,0 @@
|
|||||||
package org.jeecg.config.shiro;
|
|
||||||
|
|
||||||
import org.apache.shiro.authc.AuthenticationToken;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @Author Scott
|
|
||||||
* @create 2018-07-12 15:19
|
|
||||||
* @desc
|
|
||||||
**/
|
|
||||||
public class JwtToken implements AuthenticationToken {
|
|
||||||
|
|
||||||
private static final long serialVersionUID = 1L;
|
|
||||||
private String token;
|
|
||||||
|
|
||||||
public JwtToken(String token) {
|
|
||||||
this.token = token;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Object getPrincipal() {
|
|
||||||
return token;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Object getCredentials() {
|
|
||||||
return token;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,386 +0,0 @@
|
|||||||
package org.jeecg.config.shiro;
|
|
||||||
|
|
||||||
import jakarta.annotation.Resource;
|
|
||||||
import jakarta.servlet.DispatcherType;
|
|
||||||
import jakarta.servlet.Filter;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
|
|
||||||
import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
|
|
||||||
import org.apache.shiro.mgt.DefaultSubjectDAO;
|
|
||||||
import org.apache.shiro.mgt.SecurityManager;
|
|
||||||
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
|
|
||||||
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
|
|
||||||
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
|
|
||||||
import org.apache.shiro.spring.web.ShiroUrlPathHelper;
|
|
||||||
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
|
|
||||||
import org.crazycake.shiro.*;
|
|
||||||
import org.jeecg.common.constant.CommonConstant;
|
|
||||||
import org.jeecg.common.util.oConvertUtils;
|
|
||||||
import org.jeecg.config.JeecgBaseConfig;
|
|
||||||
import org.jeecg.config.shiro.filters.CustomShiroFilterFactoryBean;
|
|
||||||
import org.jeecg.config.shiro.filters.JwtFilter;
|
|
||||||
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.beans.factory.config.BeanDefinition;
|
|
||||||
import org.springframework.boot.web.servlet.FilterRegistrationBean;
|
|
||||||
import org.springframework.context.annotation.*;
|
|
||||||
import org.springframework.core.annotation.AnnotationUtils;
|
|
||||||
import org.springframework.core.env.Environment;
|
|
||||||
import org.springframework.core.type.filter.AnnotationTypeFilter;
|
|
||||||
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
|
|
||||||
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
|
|
||||||
import org.springframework.util.CollectionUtils;
|
|
||||||
import org.springframework.util.StringUtils;
|
|
||||||
import org.springframework.web.bind.annotation.*;
|
|
||||||
import org.springframework.web.filter.DelegatingFilterProxy;
|
|
||||||
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
|
|
||||||
import redis.clients.jedis.HostAndPort;
|
|
||||||
import redis.clients.jedis.JedisCluster;
|
|
||||||
|
|
||||||
import java.lang.reflect.Method;
|
|
||||||
import java.util.*;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @author: Scott
|
|
||||||
* @date: 2018/2/7
|
|
||||||
* @description: shiro 配置类
|
|
||||||
*/
|
|
||||||
|
|
||||||
@Slf4j
|
|
||||||
@Configuration
|
|
||||||
public class ShiroConfig {
|
|
||||||
|
|
||||||
@Resource
|
|
||||||
private LettuceConnectionFactory lettuceConnectionFactory;
|
|
||||||
@Autowired
|
|
||||||
private Environment env;
|
|
||||||
@Resource
|
|
||||||
private JeecgBaseConfig jeecgBaseConfig;
|
|
||||||
@Autowired(required = false)
|
|
||||||
private RedisProperties redisProperties;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Filter Chain定义说明
|
|
||||||
*
|
|
||||||
* 1、一个URL可以配置多个Filter,使用逗号分隔
|
|
||||||
* 2、当设置多个过滤器时,全部验证通过,才视为通过
|
|
||||||
* 3、部分过滤器可指定参数,如perms,roles
|
|
||||||
*/
|
|
||||||
@Bean("shiroFilterFactoryBean")
|
|
||||||
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
|
|
||||||
CustomShiroFilterFactoryBean shiroFilterFactoryBean = new CustomShiroFilterFactoryBean();
|
|
||||||
shiroFilterFactoryBean.setSecurityManager(securityManager);
|
|
||||||
// 拦截器
|
|
||||||
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
|
|
||||||
|
|
||||||
//支持yml方式,配置拦截排除
|
|
||||||
if(jeecgBaseConfig!=null && jeecgBaseConfig.getShiro()!=null){
|
|
||||||
String shiroExcludeUrls = jeecgBaseConfig.getShiro().getExcludeUrls();
|
|
||||||
if(oConvertUtils.isNotEmpty(shiroExcludeUrls)){
|
|
||||||
String[] permissionUrl = shiroExcludeUrls.split(",");
|
|
||||||
for(String url : permissionUrl){
|
|
||||||
filterChainDefinitionMap.put(url,"anon");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 配置不会被拦截的链接 顺序判断
|
|
||||||
filterChainDefinitionMap.put("/sys/cas/client/validateLogin", "anon"); //cas验证登录
|
|
||||||
filterChainDefinitionMap.put("/sys/randomImage/**", "anon"); //登录验证码接口排除
|
|
||||||
filterChainDefinitionMap.put("/sys/checkCaptcha", "anon"); //登录验证码接口排除
|
|
||||||
filterChainDefinitionMap.put("/sys/smsCheckCaptcha", "anon"); //短信次数发送太多验证码排除
|
|
||||||
filterChainDefinitionMap.put("/sys/login", "anon"); //登录接口排除
|
|
||||||
filterChainDefinitionMap.put("/sys/mLogin", "anon"); //登录接口排除
|
|
||||||
filterChainDefinitionMap.put("/sys/logout", "anon"); //登出接口排除
|
|
||||||
filterChainDefinitionMap.put("/sys/thirdLogin/**", "anon"); //第三方登录
|
|
||||||
filterChainDefinitionMap.put("/sys/getEncryptedString", "anon"); //获取加密串
|
|
||||||
filterChainDefinitionMap.put("/sys/sms", "anon");//短信验证码
|
|
||||||
filterChainDefinitionMap.put("/sys/phoneLogin", "anon");//手机登录
|
|
||||||
filterChainDefinitionMap.put("/sys/user/checkOnlyUser", "anon");//校验用户是否存在
|
|
||||||
filterChainDefinitionMap.put("/sys/user/register", "anon");//用户注册
|
|
||||||
filterChainDefinitionMap.put("/sys/user/phoneVerification", "anon");//用户忘记密码验证手机号
|
|
||||||
filterChainDefinitionMap.put("/sys/user/passwordChange", "anon");//用户更改密码
|
|
||||||
filterChainDefinitionMap.put("/auth/2step-code", "anon");//登录验证码
|
|
||||||
filterChainDefinitionMap.put("/sys/common/static/**", "anon");//图片预览 &下载文件不限制token
|
|
||||||
filterChainDefinitionMap.put("/sys/common/pdf/**", "anon");//pdf预览
|
|
||||||
|
|
||||||
//filterChainDefinitionMap.put("/sys/common/view/**", "anon");//图片预览不限制token
|
|
||||||
//filterChainDefinitionMap.put("/sys/common/download/**", "anon");//文件下载不限制token
|
|
||||||
filterChainDefinitionMap.put("/generic/**", "anon");//pdf预览需要文件
|
|
||||||
|
|
||||||
filterChainDefinitionMap.put("/sys/getLoginQrcode/**", "anon"); //登录二维码
|
|
||||||
filterChainDefinitionMap.put("/sys/getQrcodeToken/**", "anon"); //监听扫码
|
|
||||||
filterChainDefinitionMap.put("/sys/checkAuth", "anon"); //授权接口排除
|
|
||||||
filterChainDefinitionMap.put("/openapi/call/**", "anon"); // 开放平台接口排除
|
|
||||||
|
|
||||||
// 代码逻辑说明: 排除静态资源后缀
|
|
||||||
filterChainDefinitionMap.put("/", "anon");
|
|
||||||
filterChainDefinitionMap.put("/doc.html", "anon");
|
|
||||||
filterChainDefinitionMap.put("/**/*.js", "anon");
|
|
||||||
filterChainDefinitionMap.put("/**/*.css", "anon");
|
|
||||||
filterChainDefinitionMap.put("/**/*.html", "anon");
|
|
||||||
filterChainDefinitionMap.put("/**/*.svg", "anon");
|
|
||||||
filterChainDefinitionMap.put("/**/*.pdf", "anon");
|
|
||||||
filterChainDefinitionMap.put("/**/*.jpg", "anon");
|
|
||||||
filterChainDefinitionMap.put("/**/*.png", "anon");
|
|
||||||
filterChainDefinitionMap.put("/**/*.gif", "anon");
|
|
||||||
filterChainDefinitionMap.put("/**/*.ico", "anon");
|
|
||||||
filterChainDefinitionMap.put("/**/*.ttf", "anon");
|
|
||||||
filterChainDefinitionMap.put("/**/*.woff", "anon");
|
|
||||||
filterChainDefinitionMap.put("/**/*.woff2", "anon");
|
|
||||||
|
|
||||||
filterChainDefinitionMap.put("/**/*.glb", "anon");
|
|
||||||
filterChainDefinitionMap.put("/**/*.wasm", "anon");
|
|
||||||
|
|
||||||
filterChainDefinitionMap.put("/druid/**", "anon");
|
|
||||||
filterChainDefinitionMap.put("/swagger-ui.html", "anon");
|
|
||||||
filterChainDefinitionMap.put("/swagger**/**", "anon");
|
|
||||||
filterChainDefinitionMap.put("/webjars/**", "anon");
|
|
||||||
filterChainDefinitionMap.put("/v3/**", "anon");
|
|
||||||
|
|
||||||
filterChainDefinitionMap.put("/sys/annountCement/show/**", "anon");
|
|
||||||
|
|
||||||
//积木报表排除
|
|
||||||
filterChainDefinitionMap.put("/jmreport/**", "anon");
|
|
||||||
filterChainDefinitionMap.put("/**/*.js.map", "anon");
|
|
||||||
filterChainDefinitionMap.put("/**/*.css.map", "anon");
|
|
||||||
|
|
||||||
//积木BI大屏和仪表盘排除
|
|
||||||
filterChainDefinitionMap.put("/drag/view", "anon");
|
|
||||||
filterChainDefinitionMap.put("/drag/page/queryById", "anon");
|
|
||||||
filterChainDefinitionMap.put("/drag/page/addVisitsNumber", "anon");
|
|
||||||
filterChainDefinitionMap.put("/drag/page/queryTemplateList", "anon");
|
|
||||||
filterChainDefinitionMap.put("/drag/share/view/**", "anon");
|
|
||||||
filterChainDefinitionMap.put("/drag/onlDragDatasetHead/getAllChartData", "anon");
|
|
||||||
filterChainDefinitionMap.put("/drag/onlDragDatasetHead/getTotalData", "anon");
|
|
||||||
filterChainDefinitionMap.put("/drag/onlDragDatasetHead/getMapDataByCode", "anon");
|
|
||||||
filterChainDefinitionMap.put("/drag/onlDragDatasetHead/getTotalDataByCompId", "anon");
|
|
||||||
filterChainDefinitionMap.put("/drag/mock/json/**", "anon");
|
|
||||||
filterChainDefinitionMap.put("/drag/onlDragDatasetHead/getDictByCodes", "anon");
|
|
||||||
filterChainDefinitionMap.put("/drag/onlDragDatasetHead/queryAllById", "anon");
|
|
||||||
filterChainDefinitionMap.put("/jimubi/view", "anon");
|
|
||||||
filterChainDefinitionMap.put("/jimubi/share/view/**", "anon");
|
|
||||||
|
|
||||||
//大屏模板例子
|
|
||||||
filterChainDefinitionMap.put("/test/bigScreen/**", "anon");
|
|
||||||
filterChainDefinitionMap.put("/bigscreen/template1/**", "anon");
|
|
||||||
filterChainDefinitionMap.put("/bigscreen/template2/**", "anon");
|
|
||||||
//filterChainDefinitionMap.put("/test/jeecgDemo/rabbitMqClientTest/**", "anon"); //MQ测试
|
|
||||||
//filterChainDefinitionMap.put("/test/jeecgDemo/html", "anon"); //模板页面
|
|
||||||
//filterChainDefinitionMap.put("/test/jeecgDemo/redis/**", "anon"); //redis测试
|
|
||||||
|
|
||||||
//websocket排除
|
|
||||||
filterChainDefinitionMap.put("/websocket/**", "anon");//系统通知和公告
|
|
||||||
filterChainDefinitionMap.put("/newsWebsocket/**", "anon");//CMS模块
|
|
||||||
filterChainDefinitionMap.put("/vxeSocket/**", "anon");//JVxeTable无痕刷新示例
|
|
||||||
//App vue3版本查询版本接口
|
|
||||||
filterChainDefinitionMap.put("/sys/version/app3version", "anon");
|
|
||||||
//仪表盘(按钮通信)
|
|
||||||
filterChainDefinitionMap.put("/dragChannelSocket/**","anon");
|
|
||||||
|
|
||||||
//性能监控——安全隐患泄露TOEKN(durid连接池也有)
|
|
||||||
//filterChainDefinitionMap.put("/actuator/**", "anon");
|
|
||||||
//测试模块排除
|
|
||||||
filterChainDefinitionMap.put("/test/seata/**", "anon");
|
|
||||||
|
|
||||||
//错误路径排除
|
|
||||||
filterChainDefinitionMap.put("/error", "anon");
|
|
||||||
// 企业微信证书排除
|
|
||||||
filterChainDefinitionMap.put("/WW_verify*", "anon");
|
|
||||||
|
|
||||||
// 添加自己的过滤器并且取名为jwt
|
|
||||||
Map<String, Filter> filterMap = new HashMap<String, Filter>(1);
|
|
||||||
//如果cloudServer为空 则说明是单体 需要加载跨域配置【微服务跨域切换】
|
|
||||||
Object cloudServer = env.getProperty(CommonConstant.CLOUD_SERVER_KEY);
|
|
||||||
filterMap.put("jwt", new JwtFilter(cloudServer==null));
|
|
||||||
shiroFilterFactoryBean.setFilters(filterMap);
|
|
||||||
// <!-- 过滤链定义,从上向下顺序执行,一般将/**放在最为下边
|
|
||||||
filterChainDefinitionMap.put("/**", "jwt");
|
|
||||||
|
|
||||||
// 未授权界面返回JSON
|
|
||||||
shiroFilterFactoryBean.setUnauthorizedUrl("/sys/common/403");
|
|
||||||
shiroFilterFactoryBean.setLoginUrl("/sys/common/403");
|
|
||||||
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
|
|
||||||
return shiroFilterFactoryBean;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* spring过滤装饰器 <br/>
|
|
||||||
* 因为shiro的filter不支持异步请求,导致所有的异步请求都会报错. <br/>
|
|
||||||
* 所以需要用spring的FilterRegistrationBean再代理一下shiro的filter.为他扩展异步支持. <br/>
|
|
||||||
* 后续所有异步的接口都需要再这里增加registration.addUrlPatterns("/xxx/xxx");
|
|
||||||
* @return
|
|
||||||
* @author chenrui
|
|
||||||
* @date 2024/12/3 19:49
|
|
||||||
*/
|
|
||||||
@Bean
|
|
||||||
public FilterRegistrationBean shiroFilterRegistration() {
|
|
||||||
FilterRegistrationBean registration = new FilterRegistrationBean();
|
|
||||||
registration.setFilter(new DelegatingFilterProxy("shiroFilterFactoryBean"));
|
|
||||||
registration.setEnabled(true);
|
|
||||||
// 代码逻辑说明: [issues/7491]运行耗时长,效率慢
|
|
||||||
registration.addUrlPatterns("/test/ai/chat/send");
|
|
||||||
registration.addUrlPatterns("/airag/flow/run");
|
|
||||||
registration.addUrlPatterns("/airag/flow/debug");
|
|
||||||
registration.addUrlPatterns("/airag/chat/send");
|
|
||||||
registration.addUrlPatterns("/airag/app/debug");
|
|
||||||
registration.addUrlPatterns("/airag/app/prompt/generate");
|
|
||||||
registration.addUrlPatterns("/airag/chat/receive/**");
|
|
||||||
//支持异步
|
|
||||||
registration.setAsyncSupported(true);
|
|
||||||
registration.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ASYNC);
|
|
||||||
return registration;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Bean("securityManager")
|
|
||||||
public DefaultWebSecurityManager securityManager(ShiroRealm myRealm) {
|
|
||||||
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
|
|
||||||
securityManager.setRealm(myRealm);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* 关闭shiro自带的session,详情见文档
|
|
||||||
* http://shiro.apache.org/session-management.html#SessionManagement-
|
|
||||||
* StatelessApplications%28Sessionless%29
|
|
||||||
*/
|
|
||||||
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
|
|
||||||
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
|
|
||||||
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
|
|
||||||
subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
|
|
||||||
securityManager.setSubjectDAO(subjectDAO);
|
|
||||||
//自定义缓存实现,使用redis
|
|
||||||
securityManager.setCacheManager(redisCacheManager());
|
|
||||||
return securityManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 下面的代码是添加注解支持
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
@Bean
|
|
||||||
@DependsOn("lifecycleBeanPostProcessor")
|
|
||||||
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
|
|
||||||
DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
|
|
||||||
defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
|
|
||||||
/**
|
|
||||||
* 解决重复代理问题 github#994
|
|
||||||
* 添加前缀判断 不匹配 任何Advisor
|
|
||||||
*/
|
|
||||||
defaultAdvisorAutoProxyCreator.setUsePrefix(true);
|
|
||||||
defaultAdvisorAutoProxyCreator.setAdvisorBeanNamePrefix("_no_advisor");
|
|
||||||
return defaultAdvisorAutoProxyCreator;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
public static LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
|
|
||||||
return new LifecycleBeanPostProcessor();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
|
|
||||||
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
|
|
||||||
advisor.setSecurityManager(securityManager);
|
|
||||||
return advisor;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* cacheManager 缓存 redis实现
|
|
||||||
* 使用的是shiro-redis开源插件
|
|
||||||
*
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
public RedisCacheManager redisCacheManager() {
|
|
||||||
log.info("===============(1)创建缓存管理器RedisCacheManager");
|
|
||||||
RedisCacheManager redisCacheManager = new RedisCacheManager();
|
|
||||||
redisCacheManager.setRedisManager(redisManager());
|
|
||||||
//redis中针对不同用户缓存(此处的id需要对应user实体中的id字段,用于唯一标识)
|
|
||||||
redisCacheManager.setPrincipalIdFieldName("id");
|
|
||||||
//用户权限信息缓存时间
|
|
||||||
redisCacheManager.setExpire(200000);
|
|
||||||
return redisCacheManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* RedisConfig在项目starter项目中
|
|
||||||
* jeecg-boot-starter-github\jeecg-boot-common\src\main\java\org\jeecg\common\modules\redis\config\RedisConfig.java
|
|
||||||
*
|
|
||||||
* 配置shiro redisManager
|
|
||||||
* 使用的是shiro-redis开源插件
|
|
||||||
*
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
@Bean
|
|
||||||
public IRedisManager redisManager() {
|
|
||||||
log.info("===============(2)创建RedisManager,连接Redis..");
|
|
||||||
IRedisManager manager;
|
|
||||||
// sentinel cluster redis(【issues/5569】shiro集成 redis 不支持 sentinel 方式部署的redis集群 #5569)
|
|
||||||
if (Objects.nonNull(redisProperties)
|
|
||||||
&& Objects.nonNull(redisProperties.getSentinel())
|
|
||||||
&& !CollectionUtils.isEmpty(redisProperties.getSentinel().getNodes())) {
|
|
||||||
RedisSentinelManager sentinelManager = new RedisSentinelManager();
|
|
||||||
sentinelManager.setMasterName(redisProperties.getSentinel().getMaster());
|
|
||||||
sentinelManager.setHost(String.join(",", redisProperties.getSentinel().getNodes()));
|
|
||||||
sentinelManager.setPassword(redisProperties.getPassword());
|
|
||||||
sentinelManager.setDatabase(redisProperties.getDatabase());
|
|
||||||
|
|
||||||
return sentinelManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
// redis 单机支持,在集群为空,或者集群无机器时候使用 add by jzyadmin@163.com
|
|
||||||
if (lettuceConnectionFactory.getClusterConfiguration() == null || lettuceConnectionFactory.getClusterConfiguration().getClusterNodes().isEmpty()) {
|
|
||||||
RedisManager redisManager = new RedisManager();
|
|
||||||
redisManager.setHost(lettuceConnectionFactory.getHostName() + ":" + lettuceConnectionFactory.getPort());
|
|
||||||
//(lettuceConnectionFactory.getPort());
|
|
||||||
redisManager.setDatabase(lettuceConnectionFactory.getDatabase());
|
|
||||||
redisManager.setTimeout(0);
|
|
||||||
if (!StringUtils.isEmpty(lettuceConnectionFactory.getPassword())) {
|
|
||||||
redisManager.setPassword(lettuceConnectionFactory.getPassword());
|
|
||||||
}
|
|
||||||
manager = redisManager;
|
|
||||||
}else{
|
|
||||||
// redis集群支持,优先使用集群配置
|
|
||||||
RedisClusterManager redisManager = new RedisClusterManager();
|
|
||||||
Set<HostAndPort> portSet = new HashSet<>();
|
|
||||||
lettuceConnectionFactory.getClusterConfiguration().getClusterNodes().forEach(node -> portSet.add(new HostAndPort(node.getHost() , node.getPort())));
|
|
||||||
//update-begin--Author:scott Date:20210531 for:修改集群模式下未设置redis密码的bug issues/I3QNIC
|
|
||||||
if (oConvertUtils.isNotEmpty(lettuceConnectionFactory.getPassword())) {
|
|
||||||
JedisCluster jedisCluster = new JedisCluster(portSet, 2000, 2000, 5,
|
|
||||||
lettuceConnectionFactory.getPassword(), new GenericObjectPoolConfig());
|
|
||||||
redisManager.setPassword(lettuceConnectionFactory.getPassword());
|
|
||||||
redisManager.setJedisCluster(jedisCluster);
|
|
||||||
} else {
|
|
||||||
JedisCluster jedisCluster = new JedisCluster(portSet);
|
|
||||||
redisManager.setJedisCluster(jedisCluster);
|
|
||||||
}
|
|
||||||
manager = redisManager;
|
|
||||||
}
|
|
||||||
return manager;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 解决 ShiroRequestMappingConfig 获取 requestMappingHandlerMapping Bean 冲突
|
|
||||||
* spring-boot-autoconfigure:3.4.5 和 spring-boot-actuator-autoconfigure:3.4.5
|
|
||||||
*/
|
|
||||||
@Primary
|
|
||||||
@Bean
|
|
||||||
public RequestMappingHandlerMapping overridedRequestMappingHandlerMapping() {
|
|
||||||
RequestMappingHandlerMapping mapping = new RequestMappingHandlerMapping();
|
|
||||||
mapping.setUrlPathHelper(new ShiroUrlPathHelper());
|
|
||||||
return mapping;
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<String> rebuildUrl(String[] bases, String[] uris) {
|
|
||||||
List<String> urls = new ArrayList<>();
|
|
||||||
for (String base : bases) {
|
|
||||||
for (String uri : uris) {
|
|
||||||
urls.add(prefix(base)+prefix(uri));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return urls;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String prefix(String seg) {
|
|
||||||
return seg.startsWith("/") ? seg : "/"+seg;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,239 +0,0 @@
|
|||||||
package org.jeecg.config.shiro;
|
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.apache.shiro.authc.AuthenticationException;
|
|
||||||
import org.apache.shiro.authc.AuthenticationInfo;
|
|
||||||
import org.apache.shiro.authc.AuthenticationToken;
|
|
||||||
import org.apache.shiro.authc.SimpleAuthenticationInfo;
|
|
||||||
import org.apache.shiro.authz.AuthorizationInfo;
|
|
||||||
import org.apache.shiro.authz.SimpleAuthorizationInfo;
|
|
||||||
import org.apache.shiro.realm.AuthorizingRealm;
|
|
||||||
import org.apache.shiro.subject.PrincipalCollection;
|
|
||||||
import org.jeecg.common.api.CommonAPI;
|
|
||||||
import org.jeecg.common.config.TenantContext;
|
|
||||||
import org.jeecg.common.constant.CacheConstant;
|
|
||||||
import org.jeecg.common.constant.CommonConstant;
|
|
||||||
import org.jeecg.common.system.util.JwtUtil;
|
|
||||||
import org.jeecg.common.system.vo.LoginUser;
|
|
||||||
import org.jeecg.common.util.RedisUtil;
|
|
||||||
import org.jeecg.common.util.SpringContextUtils;
|
|
||||||
import org.jeecg.common.util.TokenUtils;
|
|
||||||
import org.jeecg.common.util.oConvertUtils;
|
|
||||||
import org.jeecg.config.mybatis.MybatisPlusSaasConfig;
|
|
||||||
import org.springframework.beans.factory.config.BeanDefinition;
|
|
||||||
import org.springframework.context.annotation.Lazy;
|
|
||||||
import org.springframework.context.annotation.Role;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
import jakarta.annotation.Resource;
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @Description: 用户登录鉴权和获取用户授权
|
|
||||||
* @Author: Scott
|
|
||||||
* @Date: 2019-4-23 8:13
|
|
||||||
* @Version: 1.1
|
|
||||||
*/
|
|
||||||
@Component
|
|
||||||
@Slf4j
|
|
||||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
|
||||||
public class ShiroRealm extends AuthorizingRealm {
|
|
||||||
@Lazy
|
|
||||||
@Resource
|
|
||||||
private CommonAPI commonApi;
|
|
||||||
|
|
||||||
@Lazy
|
|
||||||
@Resource
|
|
||||||
private RedisUtil redisUtil;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 必须重写此方法,不然Shiro会报错
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public boolean supports(AuthenticationToken token) {
|
|
||||||
return token instanceof JwtToken;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 权限信息认证(包括角色以及权限)是用户访问controller的时候才进行验证(redis存储的此处权限信息)
|
|
||||||
* 触发检测用户权限时才会调用此方法,例如checkRole,checkPermission
|
|
||||||
*
|
|
||||||
* @param principals 身份信息
|
|
||||||
* @return AuthorizationInfo 权限信息
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
|
|
||||||
log.debug("===============Shiro权限认证开始============ [ roles、permissions]==========");
|
|
||||||
String username = null;
|
|
||||||
String userId = null;
|
|
||||||
if (principals != null) {
|
|
||||||
LoginUser sysUser = (LoginUser) principals.getPrimaryPrincipal();
|
|
||||||
username = sysUser.getUsername();
|
|
||||||
userId = sysUser.getId();
|
|
||||||
}
|
|
||||||
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
|
|
||||||
|
|
||||||
// 设置用户拥有的角色集合,比如“admin,test”
|
|
||||||
Set<String> roleSet = commonApi.queryUserRolesById(userId);
|
|
||||||
//System.out.println(roleSet.toString());
|
|
||||||
info.setRoles(roleSet);
|
|
||||||
|
|
||||||
// 设置用户拥有的权限集合,比如“sys:role:add,sys:user:add”
|
|
||||||
Set<String> permissionSet = commonApi.queryUserAuths(userId);
|
|
||||||
info.addStringPermissions(permissionSet);
|
|
||||||
//System.out.println(permissionSet);
|
|
||||||
log.debug("===============Shiro权限认证成功==============");
|
|
||||||
return info;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 用户信息认证是在用户进行登录的时候进行验证(不存redis)
|
|
||||||
* 也就是说验证用户输入的账号和密码是否正确,错误抛出异常
|
|
||||||
*
|
|
||||||
* @param auth 用户登录的账号密码信息
|
|
||||||
* @return 返回封装了用户信息的 AuthenticationInfo 实例
|
|
||||||
* @throws AuthenticationException
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException {
|
|
||||||
log.debug("===============Shiro身份认证开始============doGetAuthenticationInfo==========");
|
|
||||||
String token = (String) auth.getCredentials();
|
|
||||||
if (token == null) {
|
|
||||||
HttpServletRequest req = SpringContextUtils.getHttpServletRequest();
|
|
||||||
log.info("————————身份认证失败——————————IP地址: "+ oConvertUtils.getIpAddrByRequest(req) +",URL:"+req.getRequestURI());
|
|
||||||
throw new AuthenticationException("token为空!");
|
|
||||||
}
|
|
||||||
// 校验token有效性
|
|
||||||
LoginUser loginUser = null;
|
|
||||||
try {
|
|
||||||
loginUser = this.checkUserTokenIsEffect(token);
|
|
||||||
} catch (AuthenticationException e) {
|
|
||||||
log.error("—————校验 check token 失败——————————"+ e.getMessage(), e);
|
|
||||||
// 重新抛出异常,让JwtFilter统一处理,避免返回两次错误响应
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
return new SimpleAuthenticationInfo(loginUser, token, getName());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 校验token的有效性
|
|
||||||
*
|
|
||||||
* @param token
|
|
||||||
*/
|
|
||||||
public LoginUser checkUserTokenIsEffect(String token) throws AuthenticationException {
|
|
||||||
// 解密获得username,用于和数据库进行对比
|
|
||||||
String username = JwtUtil.getUsername(token);
|
|
||||||
if (username == null) {
|
|
||||||
throw new AuthenticationException("Token非法无效!");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 查询用户信息
|
|
||||||
log.debug("———校验token是否有效————checkUserTokenIsEffect——————— "+ token);
|
|
||||||
LoginUser loginUser = TokenUtils.getLoginUser(username, commonApi, redisUtil);
|
|
||||||
//LoginUser loginUser = commonApi.getUserByName(username);
|
|
||||||
if (loginUser == null) {
|
|
||||||
throw new AuthenticationException("用户不存在!");
|
|
||||||
}
|
|
||||||
// 判断用户状态
|
|
||||||
if (loginUser.getStatus() != 1) {
|
|
||||||
throw new AuthenticationException("账号已被锁定,请联系管理员!");
|
|
||||||
}
|
|
||||||
// 校验token是否超时失效 & 或者账号密码是否错误
|
|
||||||
if (!jwtTokenRefresh(token, username, loginUser.getPassword())) {
|
|
||||||
// 用户登录Token过期提示信息
|
|
||||||
String userLoginTokenErrorMsg = oConvertUtils.getString(redisUtil.get(CommonConstant.PREFIX_USER_TOKEN_ERROR_MSG + token));
|
|
||||||
throw new AuthenticationException(oConvertUtils.isEmpty(userLoginTokenErrorMsg)? CommonConstant.TOKEN_IS_INVALID_MSG: userLoginTokenErrorMsg);
|
|
||||||
}
|
|
||||||
// 代码逻辑说明: 校验用户的tenant_id和前端传过来的是否一致
|
|
||||||
String userTenantIds = loginUser.getRelTenantIds();
|
|
||||||
if(MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL && oConvertUtils.isNotEmpty(userTenantIds)){
|
|
||||||
String contextTenantId = TenantContext.getTenant();
|
|
||||||
log.debug("登录租户:" + contextTenantId);
|
|
||||||
log.debug("用户拥有那些租户:" + userTenantIds);
|
|
||||||
//登录用户无租户,前端header中租户ID值为 0
|
|
||||||
String str ="0";
|
|
||||||
if(oConvertUtils.isNotEmpty(contextTenantId) && !str.equals(contextTenantId)){
|
|
||||||
// 代码逻辑说明: /issues/I4O14W 用户租户信息变更判断漏洞
|
|
||||||
String[] arr = userTenantIds.split(",");
|
|
||||||
if(!oConvertUtils.isIn(contextTenantId, arr)){
|
|
||||||
boolean isAuthorization = false;
|
|
||||||
//========================================================================
|
|
||||||
// 查询用户信息(如果租户不匹配从数据库中重新查询一次用户信息)
|
|
||||||
String loginUserKey = CacheConstant.SYS_USERS_CACHE + "::" + username;
|
|
||||||
redisUtil.del(loginUserKey);
|
|
||||||
LoginUser loginUserFromDb = commonApi.getUserByName(username);
|
|
||||||
if (oConvertUtils.isNotEmpty(loginUserFromDb.getRelTenantIds())) {
|
|
||||||
String[] newArray = loginUserFromDb.getRelTenantIds().split(",");
|
|
||||||
if (oConvertUtils.isIn(contextTenantId, newArray)) {
|
|
||||||
isAuthorization = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//========================================================================
|
|
||||||
|
|
||||||
//*********************************************
|
|
||||||
if(!isAuthorization){
|
|
||||||
log.info("租户异常——登录租户:" + contextTenantId);
|
|
||||||
log.info("租户异常——用户拥有租户组:" + userTenantIds);
|
|
||||||
throw new AuthenticationException("登录租户授权变更,请重新登陆!");
|
|
||||||
}
|
|
||||||
//*********************************************
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return loginUser;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* JWTToken刷新生命周期 (实现: 用户在线操作不掉线功能)
|
|
||||||
* 1、登录成功后将用户的JWT生成的Token作为k、v存储到cache缓存里面(这时候k、v值一样),缓存有效期设置为Jwt有效时间的2倍
|
|
||||||
* 2、当该用户再次请求时,通过JWTFilter层层校验之后会进入到doGetAuthenticationInfo进行身份验证
|
|
||||||
* 3、当该用户这次请求jwt生成的token值已经超时,但该token对应cache中的k还是存在,则表示该用户一直在操作只是JWT的token失效了,程序会给token对应的k映射的v值重新生成JWTToken并覆盖v值,该缓存生命周期重新计算
|
|
||||||
* 4、当该用户这次请求jwt在生成的token值已经超时,并在cache中不存在对应的k,则表示该用户账户空闲超时,返回用户信息已失效,请重新登录。
|
|
||||||
* 注意: 前端请求Header中设置Authorization保持不变,校验有效性以缓存中的token为准。
|
|
||||||
* 用户过期时间 = Jwt有效时间 * 2。
|
|
||||||
*
|
|
||||||
* @param userName
|
|
||||||
* @param passWord
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
public boolean jwtTokenRefresh(String token, String userName, String passWord) {
|
|
||||||
String cacheToken = String.valueOf(redisUtil.get(CommonConstant.PREFIX_USER_TOKEN + token));
|
|
||||||
if (oConvertUtils.isNotEmpty(cacheToken)) {
|
|
||||||
// 校验token有效性
|
|
||||||
if (!JwtUtil.verify(cacheToken, userName, passWord)) {
|
|
||||||
// 从token中解析客户端类型,保持续期时使用相同的客户端类型
|
|
||||||
String clientType = JwtUtil.getClientType(token);
|
|
||||||
String newAuthorization = JwtUtil.sign(userName, passWord, clientType);
|
|
||||||
// 根据客户端类型设置对应的缓存有效时间
|
|
||||||
long expireTime = CommonConstant.CLIENT_TYPE_APP.equalsIgnoreCase(clientType)
|
|
||||||
? JwtUtil.APP_EXPIRE_TIME * 2 / 1000
|
|
||||||
: JwtUtil.EXPIRE_TIME * 2 / 1000;
|
|
||||||
redisUtil.set(CommonConstant.PREFIX_USER_TOKEN + token, newAuthorization);
|
|
||||||
redisUtil.expire(CommonConstant.PREFIX_USER_TOKEN + token, expireTime);
|
|
||||||
log.debug("——————————用户在线操作,更新token保证不掉线—————————jwtTokenRefresh——————— "+ token);
|
|
||||||
}
|
|
||||||
// else {
|
|
||||||
// // 设置超时时间
|
|
||||||
// redisUtil.set(CommonConstant.PREFIX_USER_TOKEN + token, cacheToken);
|
|
||||||
// redisUtil.expire(CommonConstant.PREFIX_USER_TOKEN + token, JwtUtil.EXPIRE_TIME / 1000);
|
|
||||||
// }
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
//redis中不存在此TOEKN,说明token非法返回false
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 清除当前用户的权限认证缓存
|
|
||||||
*
|
|
||||||
* @param principals 权限信息
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void clearCache(PrincipalCollection principals) {
|
|
||||||
super.clearCache(principals);
|
|
||||||
// 代码逻辑说明: 【TV360X-1320】分配权限必须退出重新登录才生效,造成很多用户困扰---
|
|
||||||
super.clearCachedAuthorizationInfo(principals);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,77 +0,0 @@
|
|||||||
package org.jeecg.config.shiro.filters;
|
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
|
|
||||||
import org.apache.shiro.web.filter.InvalidRequestFilter;
|
|
||||||
import org.apache.shiro.web.filter.mgt.DefaultFilter;
|
|
||||||
import org.apache.shiro.web.filter.mgt.FilterChainManager;
|
|
||||||
import org.apache.shiro.web.filter.mgt.FilterChainResolver;
|
|
||||||
import org.apache.shiro.web.filter.mgt.PathMatchingFilterChainResolver;
|
|
||||||
import org.apache.shiro.web.mgt.WebSecurityManager;
|
|
||||||
import org.apache.shiro.web.servlet.AbstractShiroFilter;
|
|
||||||
import org.apache.shiro.mgt.SecurityManager;
|
|
||||||
import org.springframework.beans.factory.BeanInitializationException;
|
|
||||||
|
|
||||||
import jakarta.servlet.Filter;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 自定义ShiroFilterFactoryBean解决资源中文路径问题
|
|
||||||
* @author: jeecg-boot
|
|
||||||
*/
|
|
||||||
@Slf4j
|
|
||||||
public class CustomShiroFilterFactoryBean extends ShiroFilterFactoryBean {
|
|
||||||
@Override
|
|
||||||
public Class getObjectType() {
|
|
||||||
return MySpringShiroFilter.class;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected AbstractShiroFilter createInstance() throws Exception {
|
|
||||||
|
|
||||||
SecurityManager securityManager = getSecurityManager();
|
|
||||||
if (securityManager == null) {
|
|
||||||
String msg = "SecurityManager property must be set.";
|
|
||||||
throw new BeanInitializationException(msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!(securityManager instanceof WebSecurityManager)) {
|
|
||||||
String msg = "The security manager does not implement the WebSecurityManager interface.";
|
|
||||||
throw new BeanInitializationException(msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
FilterChainManager manager = createFilterChainManager();
|
|
||||||
//Expose the constructed FilterChainManager by first wrapping it in a
|
|
||||||
// FilterChainResolver implementation. The AbstractShiroFilter implementations
|
|
||||||
// do not know about FilterChainManagers - only resolvers:
|
|
||||||
PathMatchingFilterChainResolver chainResolver = new PathMatchingFilterChainResolver();
|
|
||||||
chainResolver.setFilterChainManager(manager);
|
|
||||||
|
|
||||||
Map<String, Filter> filterMap = manager.getFilters();
|
|
||||||
Filter invalidRequestFilter = filterMap.get(DefaultFilter.invalidRequest.name());
|
|
||||||
if (invalidRequestFilter instanceof InvalidRequestFilter) {
|
|
||||||
//此处是关键,设置false跳过URL携带中文400,servletPath中文校验bug
|
|
||||||
((InvalidRequestFilter) invalidRequestFilter).setBlockNonAscii(false);
|
|
||||||
}
|
|
||||||
//Now create a concrete ShiroFilter instance and apply the acquired SecurityManager and built
|
|
||||||
//FilterChainResolver. It doesn't matter that the instance is an anonymous inner class
|
|
||||||
//here - we're just using it because it is a concrete AbstractShiroFilter instance that accepts
|
|
||||||
//injection of the SecurityManager and FilterChainResolver:
|
|
||||||
return new MySpringShiroFilter((WebSecurityManager) securityManager, chainResolver);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final class MySpringShiroFilter extends AbstractShiroFilter {
|
|
||||||
protected MySpringShiroFilter(WebSecurityManager webSecurityManager, FilterChainResolver resolver) {
|
|
||||||
if (webSecurityManager == null) {
|
|
||||||
throw new IllegalArgumentException("WebSecurityManager property cannot be null.");
|
|
||||||
} else {
|
|
||||||
this.setSecurityManager(webSecurityManager);
|
|
||||||
if (resolver != null) {
|
|
||||||
this.setFilterChainResolver(resolver);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,132 +0,0 @@
|
|||||||
package org.jeecg.config.shiro.filters;
|
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.apache.commons.lang.StringUtils;
|
|
||||||
import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;
|
|
||||||
import org.jeecg.common.config.TenantContext;
|
|
||||||
import org.jeecg.common.constant.CommonConstant;
|
|
||||||
import org.jeecg.common.system.util.JwtUtil;
|
|
||||||
import org.jeecg.common.util.oConvertUtils;
|
|
||||||
import org.jeecg.config.shiro.JwtToken;
|
|
||||||
import org.jeecg.config.shiro.ignore.InMemoryIgnoreAuth;
|
|
||||||
import org.springframework.http.HttpHeaders;
|
|
||||||
import org.springframework.http.HttpStatus;
|
|
||||||
import org.springframework.web.bind.annotation.RequestMethod;
|
|
||||||
|
|
||||||
import jakarta.servlet.ServletRequest;
|
|
||||||
import jakarta.servlet.ServletResponse;
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @Description: 鉴权登录拦截器
|
|
||||||
* @Author: Scott
|
|
||||||
* @Date: 2018/10/7
|
|
||||||
**/
|
|
||||||
@Slf4j
|
|
||||||
public class JwtFilter extends BasicHttpAuthenticationFilter {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 默认开启跨域设置(使用单体)
|
|
||||||
* 微服务情况下,此属性设置为false
|
|
||||||
*/
|
|
||||||
private boolean allowOrigin = true;
|
|
||||||
|
|
||||||
public JwtFilter(){}
|
|
||||||
public JwtFilter(boolean allowOrigin){
|
|
||||||
this.allowOrigin = allowOrigin;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 执行登录认证
|
|
||||||
*
|
|
||||||
* @param request
|
|
||||||
* @param response
|
|
||||||
* @param mappedValue
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
|
|
||||||
try {
|
|
||||||
// 判断当前路径是不是注解了@IngoreAuth路径,如果是,则放开验证
|
|
||||||
if (InMemoryIgnoreAuth.contains(((HttpServletRequest) request).getServletPath())) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
executeLogin(request, response);
|
|
||||||
return true;
|
|
||||||
} catch (Exception e) {
|
|
||||||
// 使用异常中的具体错误信息,保留"不允许同一账号多地同时登录"等具体提示
|
|
||||||
String errorMsg = e.getMessage();
|
|
||||||
if (oConvertUtils.isEmpty(errorMsg)) {
|
|
||||||
errorMsg = CommonConstant.TOKEN_IS_INVALID_MSG;
|
|
||||||
}
|
|
||||||
JwtUtil.responseError((HttpServletResponse)response, 401, errorMsg);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
|
|
||||||
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
|
|
||||||
String token = httpServletRequest.getHeader(CommonConstant.X_ACCESS_TOKEN);
|
|
||||||
// 代码逻辑说明: JT-355 OA聊天添加token验证,获取token参数
|
|
||||||
if (oConvertUtils.isEmpty(token)) {
|
|
||||||
token = httpServletRequest.getParameter("token");
|
|
||||||
}
|
|
||||||
|
|
||||||
JwtToken jwtToken = new JwtToken(token);
|
|
||||||
// 提交给realm进行登入,如果错误他会抛出异常并被捕获
|
|
||||||
getSubject(request, response).login(jwtToken);
|
|
||||||
// 如果没有抛出异常则代表登入成功,返回true
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 对跨域提供支持
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
|
|
||||||
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
|
|
||||||
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
|
|
||||||
if(allowOrigin){
|
|
||||||
httpServletResponse.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, httpServletRequest.getHeader(HttpHeaders.ORIGIN));
|
|
||||||
// 允许客户端请求方法
|
|
||||||
httpServletResponse.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS, "GET,POST,OPTIONS,PUT,DELETE");
|
|
||||||
// 允许客户端提交的Header
|
|
||||||
String requestHeaders = httpServletRequest.getHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS);
|
|
||||||
if (StringUtils.isNotEmpty(requestHeaders)) {
|
|
||||||
httpServletResponse.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS, requestHeaders);
|
|
||||||
}
|
|
||||||
// 允许客户端携带凭证信息(是否允许发送Cookie)
|
|
||||||
httpServletResponse.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true");
|
|
||||||
}
|
|
||||||
// 跨域时会首先发送一个option请求,这里我们给option请求直接返回正常状态
|
|
||||||
if (RequestMethod.OPTIONS.name().equalsIgnoreCase(httpServletRequest.getMethod())) {
|
|
||||||
httpServletResponse.setStatus(HttpStatus.OK.value());
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
// 代码逻辑说明: 多租户用到
|
|
||||||
String tenantId = httpServletRequest.getHeader(CommonConstant.TENANT_ID);
|
|
||||||
TenantContext.setTenant(tenantId);
|
|
||||||
|
|
||||||
return super.preHandle(request, response);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* JwtFilter中ThreadLocal需要及时清除 #3634
|
|
||||||
*
|
|
||||||
* @param request
|
|
||||||
* @param response
|
|
||||||
* @param exception
|
|
||||||
* @throws Exception
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void afterCompletion(ServletRequest request, ServletResponse response, Exception exception) throws Exception {
|
|
||||||
//log.info("------清空线程中多租户的ID={}------",TenantContext.getTenant());
|
|
||||||
TenantContext.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,67 +0,0 @@
|
|||||||
package org.jeecg.config.shiro.filters;
|
|
||||||
|
|
||||||
import jakarta.servlet.ServletRequest;
|
|
||||||
import jakarta.servlet.ServletResponse;
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
|
||||||
import org.apache.shiro.subject.Subject;
|
|
||||||
import org.apache.shiro.web.filter.AccessControlFilter;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @Author Scott
|
|
||||||
* @create 2019-02-01 15:56
|
|
||||||
* @desc 鉴权请求URL访问权限拦截器
|
|
||||||
*/
|
|
||||||
@Slf4j
|
|
||||||
public class ResourceCheckFilter extends AccessControlFilter {
|
|
||||||
|
|
||||||
private String errorUrl;
|
|
||||||
|
|
||||||
public String getErrorUrl() {
|
|
||||||
return errorUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setErrorUrl(String errorUrl) {
|
|
||||||
this.errorUrl = errorUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 表示是否允许访问 ,如果允许访问返回true,否则false;
|
|
||||||
*
|
|
||||||
* @param servletRequest
|
|
||||||
* @param servletResponse
|
|
||||||
* @param o 表示写在拦截器中括号里面的字符串 mappedValue 就是 [urls] 配置中拦截器参数部分
|
|
||||||
* @return
|
|
||||||
* @throws Exception
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
protected boolean isAccessAllowed(ServletRequest servletRequest, ServletResponse servletResponse, Object o) throws Exception {
|
|
||||||
Subject subject = getSubject(servletRequest, servletResponse);
|
|
||||||
String url = getPathWithinApplication(servletRequest);
|
|
||||||
log.info("当前用户正在访问的 url => " + url);
|
|
||||||
return subject.isPermitted(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* onAccessDenied:表示当访问拒绝时是否已经处理了; 如果返回 true 表示需要继续处理; 如果返回 false
|
|
||||||
* 表示该拦截器实例已经处理了,将直接返回即可。
|
|
||||||
*
|
|
||||||
* @param servletRequest
|
|
||||||
* @param servletResponse
|
|
||||||
* @return
|
|
||||||
* @throws Exception
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
|
|
||||||
log.info("当 isAccessAllowed 返回 false 的时候,才会执行 method onAccessDenied ");
|
|
||||||
|
|
||||||
HttpServletRequest request = (HttpServletRequest) servletRequest;
|
|
||||||
HttpServletResponse response = (HttpServletResponse) servletResponse;
|
|
||||||
response.sendRedirect(request.getContextPath() + this.errorUrl);
|
|
||||||
|
|
||||||
// 返回 false 表示已经处理,例如页面跳转啥的,表示不在走以下的拦截器了(如果还有配置的话)
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -19,10 +19,6 @@ public class Firewall {
|
|||||||
* 低代码模式(dev:开发模式,prod:发布模式——关闭所有在线开发配置能力)
|
* 低代码模式(dev:开发模式,prod:发布模式——关闭所有在线开发配置能力)
|
||||||
*/
|
*/
|
||||||
private String lowCodeMode;
|
private String lowCodeMode;
|
||||||
/**
|
|
||||||
* 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录)
|
|
||||||
*/
|
|
||||||
private Boolean isConcurrent = true;
|
|
||||||
/**
|
/**
|
||||||
* 是否开启默认密码登录提醒(true 登录后提示必须修改默认密码)
|
* 是否开启默认密码登录提醒(true 登录后提示必须修改默认密码)
|
||||||
*/
|
*/
|
||||||
@ -78,12 +74,4 @@ public class Firewall {
|
|||||||
public void setDisableSelectAll(Boolean disableSelectAll) {
|
public void setDisableSelectAll(Boolean disableSelectAll) {
|
||||||
this.disableSelectAll = disableSelectAll;
|
this.disableSelectAll = disableSelectAll;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Boolean getIsConcurrent() {
|
|
||||||
return isConcurrent;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setIsConcurrent(Boolean isConcurrent) {
|
|
||||||
this.isConcurrent = isConcurrent;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,7 +2,7 @@ package org.jeecg.modules.base.service.impl;
|
|||||||
|
|
||||||
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
|
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.shiro.SecurityUtils;
|
import org.jeecg.common.util.LoginUserUtils;
|
||||||
import org.jeecg.common.api.dto.LogDTO;
|
import org.jeecg.common.api.dto.LogDTO;
|
||||||
import org.jeecg.common.constant.enums.ClientTerminalTypeEnum;
|
import org.jeecg.common.constant.enums.ClientTerminalTypeEnum;
|
||||||
import org.jeecg.common.util.BrowserUtils;
|
import org.jeecg.common.util.BrowserUtils;
|
||||||
@ -74,7 +74,7 @@ public class BaseCommonServiceImpl implements BaseCommonService {
|
|||||||
//获取登录用户信息
|
//获取登录用户信息
|
||||||
if(user==null){
|
if(user==null){
|
||||||
try {
|
try {
|
||||||
user = (LoginUser) SecurityUtils.getSubject().getPrincipal();
|
user = LoginUserUtils.getSessionUser();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
//e.printStackTrace();
|
//e.printStackTrace();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -75,7 +75,7 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.jeecgframework.boot3</groupId>
|
<groupId>org.jeecgframework.boot3</groupId>
|
||||||
<artifactId>jeecg-aiflow</artifactId>
|
<artifactId>jeecg-aiflow</artifactId>
|
||||||
<version>3.9.0</version>
|
<version>3.9.0.1</version>
|
||||||
<exclusions>
|
<exclusions>
|
||||||
<exclusion>
|
<exclusion>
|
||||||
<groupId>commons-io</groupId>
|
<groupId>commons-io</groupId>
|
||||||
@ -85,10 +85,14 @@
|
|||||||
<groupId>commons-beanutils</groupId>
|
<groupId>commons-beanutils</groupId>
|
||||||
<artifactId>commons-beanutils</artifactId>
|
<artifactId>commons-beanutils</artifactId>
|
||||||
</exclusion>
|
</exclusion>
|
||||||
|
<exclusion>
|
||||||
|
<groupId>com.yomahub</groupId>
|
||||||
|
<artifactId>liteflow-script-python</artifactId>
|
||||||
|
</exclusion>
|
||||||
</exclusions>
|
</exclusions>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- beigin 这两个依赖太多每个包50M左右,如果你发布需要使用,请把<scope>provided</scope>删掉 -->
|
<!-- begin 注意:这几个依赖体积较大,每个约50MB。若发布时需要使用,请将 <scope>provided</scope> 删除 -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.jetbrains.kotlin</groupId>
|
<groupId>org.jetbrains.kotlin</groupId>
|
||||||
<artifactId>kotlin-scripting-jsr223</artifactId>
|
<artifactId>kotlin-scripting-jsr223</artifactId>
|
||||||
@ -101,7 +105,13 @@
|
|||||||
<version>${liteflow.version}</version>
|
<version>${liteflow.version}</version>
|
||||||
<scope>provided</scope>
|
<scope>provided</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
<!-- end 这两个依赖太多每个包50M左右,如果你发布需要使用,请把<scope>provided</scope>删掉 -->
|
<dependency>
|
||||||
|
<groupId>com.yomahub</groupId>
|
||||||
|
<artifactId>liteflow-script-python</artifactId>
|
||||||
|
<version>${liteflow.version}</version>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
<!-- end 注意:这几个依赖体积较大,每个约50MB。若发布时需要使用,请将 <scope>provided</scope> 删除 -->
|
||||||
|
|
||||||
<!-- aiflow 脚本依赖 -->
|
<!-- aiflow 脚本依赖 -->
|
||||||
<dependency>
|
<dependency>
|
||||||
@ -110,12 +120,6 @@
|
|||||||
<version>${liteflow.version}</version>
|
<version>${liteflow.version}</version>
|
||||||
<scope>runtime</scope>
|
<scope>runtime</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
|
||||||
<groupId>com.yomahub</groupId>
|
|
||||||
<artifactId>liteflow-script-python</artifactId>
|
|
||||||
<version>${liteflow.version}</version>
|
|
||||||
<scope>runtime</scope>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.yomahub</groupId>
|
<groupId>com.yomahub</groupId>
|
||||||
<artifactId>liteflow-script-kotlin</artifactId>
|
<artifactId>liteflow-script-kotlin</artifactId>
|
||||||
|
|||||||
@ -1,17 +1,17 @@
|
|||||||
package org.jeecg.modules.airag.app.controller;
|
package org.jeecg.modules.airag.app.controller;
|
||||||
|
|
||||||
|
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||||
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 lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
|
||||||
import org.jeecg.common.api.vo.Result;
|
import org.jeecg.common.api.vo.Result;
|
||||||
import org.jeecg.common.system.base.controller.JeecgController;
|
import org.jeecg.common.system.base.controller.JeecgController;
|
||||||
import org.jeecg.common.system.query.QueryGenerator;
|
import org.jeecg.common.system.query.QueryGenerator;
|
||||||
import org.jeecg.common.util.AssertUtils;
|
import org.jeecg.common.util.AssertUtils;
|
||||||
import org.jeecg.common.util.TokenUtils;
|
import org.jeecg.common.util.TokenUtils;
|
||||||
import org.jeecg.config.mybatis.MybatisPlusSaasConfig;
|
import org.jeecg.config.mybatis.MybatisPlusSaasConfig;
|
||||||
import org.jeecg.config.shiro.IgnoreAuth;
|
import org.jeecg.config.satoken.IgnoreAuth;
|
||||||
import org.jeecg.modules.airag.app.consts.AiAppConsts;
|
import org.jeecg.modules.airag.app.consts.AiAppConsts;
|
||||||
import org.jeecg.modules.airag.app.entity.AiragApp;
|
import org.jeecg.modules.airag.app.entity.AiragApp;
|
||||||
import org.jeecg.modules.airag.app.service.IAiragAppService;
|
import org.jeecg.modules.airag.app.service.IAiragAppService;
|
||||||
@ -67,7 +67,7 @@ public class AiragAppController extends JeecgController<AiragApp, IAiragAppServi
|
|||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
@RequestMapping(value = "/edit", method = {RequestMethod.PUT, RequestMethod.POST})
|
@RequestMapping(value = "/edit", method = {RequestMethod.PUT, RequestMethod.POST})
|
||||||
@RequiresPermissions("airag:app:edit")
|
@SaCheckPermission("airag:app:edit")
|
||||||
public Result<String> edit(@RequestBody AiragApp airagApp) {
|
public Result<String> edit(@RequestBody AiragApp airagApp) {
|
||||||
AssertUtils.assertNotEmpty("参数异常", airagApp);
|
AssertUtils.assertNotEmpty("参数异常", airagApp);
|
||||||
AssertUtils.assertNotEmpty("请输入应用名称", airagApp.getName());
|
AssertUtils.assertNotEmpty("请输入应用名称", airagApp.getName());
|
||||||
@ -106,7 +106,7 @@ public class AiragAppController extends JeecgController<AiragApp, IAiragAppServi
|
|||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
@DeleteMapping(value = "/delete")
|
@DeleteMapping(value = "/delete")
|
||||||
@RequiresPermissions("airag:app:delete")
|
@SaCheckPermission("airag:app:delete")
|
||||||
public Result<String> delete(HttpServletRequest request,@RequestParam(name = "id", required = true) String id) {
|
public Result<String> delete(HttpServletRequest request,@RequestParam(name = "id", required = true) String id) {
|
||||||
//update-begin---author:chenrui ---date:20250606 for:[issues/8337]关于ai工作列表的数据权限问题 #8337------------
|
//update-begin---author:chenrui ---date:20250606 for:[issues/8337]关于ai工作列表的数据权限问题 #8337------------
|
||||||
//如果是saas隔离的情况下,判断当前租户id是否是当前租户下的
|
//如果是saas隔离的情况下,判断当前租户id是否是当前租户下的
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import lombok.extern.slf4j.Slf4j;
|
|||||||
import org.jeecg.common.api.vo.Result;
|
import org.jeecg.common.api.vo.Result;
|
||||||
import org.jeecg.common.constant.CommonConstant;
|
import org.jeecg.common.constant.CommonConstant;
|
||||||
import org.jeecg.common.util.CommonUtils;
|
import org.jeecg.common.util.CommonUtils;
|
||||||
import org.jeecg.config.shiro.IgnoreAuth;
|
import org.jeecg.config.satoken.IgnoreAuth;
|
||||||
import org.jeecg.modules.airag.app.service.IAiragChatService;
|
import org.jeecg.modules.airag.app.service.IAiragChatService;
|
||||||
import org.jeecg.modules.airag.app.vo.ChatConversation;
|
import org.jeecg.modules.airag.app.vo.ChatConversation;
|
||||||
import org.jeecg.modules.airag.app.vo.ChatSendParams;
|
import org.jeecg.modules.airag.app.vo.ChatSendParams;
|
||||||
|
|||||||
@ -1306,7 +1306,7 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
|||||||
} else {
|
} else {
|
||||||
token = TokenUtils.getTokenByRequest();
|
token = TokenUtils.getTokenByRequest();
|
||||||
}
|
}
|
||||||
if (TokenUtils.verifyToken(token, sysBaseApi, redisUtil)) {
|
if (TokenUtils.verifyToken(token, sysBaseApi)) {
|
||||||
return JwtUtil.getUsername(token);
|
return JwtUtil.getUsername(token);
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
package org.jeecg.modules.airag.llm.controller;
|
package org.jeecg.modules.airag.llm.controller;
|
||||||
|
|
||||||
|
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||||
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 lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
|
||||||
import org.jeecg.common.api.vo.Result;
|
import org.jeecg.common.api.vo.Result;
|
||||||
import org.jeecg.common.system.query.QueryGenerator;
|
import org.jeecg.common.system.query.QueryGenerator;
|
||||||
import org.jeecg.common.util.AssertUtils;
|
import org.jeecg.common.util.AssertUtils;
|
||||||
@ -77,7 +77,7 @@ public class AiragKnowledgeController {
|
|||||||
* @date 2025/2/18 17:09
|
* @date 2025/2/18 17:09
|
||||||
*/
|
*/
|
||||||
@PostMapping(value = "/add")
|
@PostMapping(value = "/add")
|
||||||
@RequiresPermissions("airag:knowledge:add")
|
@SaCheckPermission("airag:knowledge:add")
|
||||||
public Result<String> add(@RequestBody AiragKnowledge airagKnowledge) {
|
public Result<String> add(@RequestBody AiragKnowledge airagKnowledge) {
|
||||||
airagKnowledge.setStatus(LLMConsts.STATUS_ENABLE);
|
airagKnowledge.setStatus(LLMConsts.STATUS_ENABLE);
|
||||||
airagKnowledgeService.save(airagKnowledge);
|
airagKnowledgeService.save(airagKnowledge);
|
||||||
@ -94,7 +94,7 @@ public class AiragKnowledgeController {
|
|||||||
*/
|
*/
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
@RequestMapping(value = "/edit", method = {RequestMethod.PUT, RequestMethod.POST})
|
@RequestMapping(value = "/edit", method = {RequestMethod.PUT, RequestMethod.POST})
|
||||||
@RequiresPermissions("airag:knowledge:edit")
|
@SaCheckPermission("airag:knowledge:edit")
|
||||||
public Result<String> edit(@RequestBody AiragKnowledge airagKnowledge) {
|
public Result<String> edit(@RequestBody AiragKnowledge airagKnowledge) {
|
||||||
AiragKnowledge airagKnowledgeEntity = airagKnowledgeService.getById(airagKnowledge.getId());
|
AiragKnowledge airagKnowledgeEntity = airagKnowledgeService.getById(airagKnowledge.getId());
|
||||||
if (airagKnowledgeEntity == null) {
|
if (airagKnowledgeEntity == null) {
|
||||||
@ -118,7 +118,7 @@ public class AiragKnowledgeController {
|
|||||||
* @date 2025/3/12 17:05
|
* @date 2025/3/12 17:05
|
||||||
*/
|
*/
|
||||||
@PutMapping(value = "/rebuild")
|
@PutMapping(value = "/rebuild")
|
||||||
@RequiresPermissions("airag:knowledge:rebuild")
|
@SaCheckPermission("airag:knowledge:rebuild")
|
||||||
public Result<?> rebuild(@RequestParam("knowIds") String knowIds) {
|
public Result<?> rebuild(@RequestParam("knowIds") String knowIds) {
|
||||||
String[] knowIdArr = knowIds.split(",");
|
String[] knowIdArr = knowIds.split(",");
|
||||||
for (String knowId : knowIdArr) {
|
for (String knowId : knowIdArr) {
|
||||||
@ -137,7 +137,7 @@ public class AiragKnowledgeController {
|
|||||||
*/
|
*/
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
@DeleteMapping(value = "/delete")
|
@DeleteMapping(value = "/delete")
|
||||||
@RequiresPermissions("airag:knowledge:delete")
|
@SaCheckPermission("airag:knowledge:delete")
|
||||||
public Result<String> delete(HttpServletRequest request, @RequestParam(name = "id", required = true) String id) {
|
public Result<String> delete(HttpServletRequest request, @RequestParam(name = "id", required = true) String id) {
|
||||||
//update-begin---author:chenrui ---date:20250606 for:[issues/8337]关于ai工作列表的数据权限问题 #8337------------
|
//update-begin---author:chenrui ---date:20250606 for:[issues/8337]关于ai工作列表的数据权限问题 #8337------------
|
||||||
//如果是saas隔离的情况下,判断当前租户id是否是当前租户下的
|
//如果是saas隔离的情况下,判断当前租户id是否是当前租户下的
|
||||||
@ -204,7 +204,7 @@ public class AiragKnowledgeController {
|
|||||||
* @date 2025/2/18 15:47
|
* @date 2025/2/18 15:47
|
||||||
*/
|
*/
|
||||||
@PostMapping(value = "/doc/edit")
|
@PostMapping(value = "/doc/edit")
|
||||||
@RequiresPermissions("airag:knowledge:doc:edit")
|
@SaCheckPermission("airag:knowledge:doc:edit")
|
||||||
public Result<?> addDocument(@RequestBody AiragKnowledgeDoc airagKnowledgeDoc) {
|
public Result<?> addDocument(@RequestBody AiragKnowledgeDoc airagKnowledgeDoc) {
|
||||||
return airagKnowledgeDocService.editDocument(airagKnowledgeDoc);
|
return airagKnowledgeDocService.editDocument(airagKnowledgeDoc);
|
||||||
}
|
}
|
||||||
@ -217,7 +217,7 @@ public class AiragKnowledgeController {
|
|||||||
* @date 2025/3/20 11:29
|
* @date 2025/3/20 11:29
|
||||||
*/
|
*/
|
||||||
@PostMapping(value = "/doc/import/zip")
|
@PostMapping(value = "/doc/import/zip")
|
||||||
@RequiresPermissions("airag:knowledge:doc:zip")
|
@SaCheckPermission("airag:knowledge:doc:zip")
|
||||||
public Result<?> importDocumentFromZip(@RequestParam(name = "knowId", required = true) String knowId,
|
public Result<?> importDocumentFromZip(@RequestParam(name = "knowId", required = true) String knowId,
|
||||||
@RequestParam(name = "file", required = true) MultipartFile file) {
|
@RequestParam(name = "file", required = true) MultipartFile file) {
|
||||||
return airagKnowledgeDocService.importDocumentFromZip(knowId,file);
|
return airagKnowledgeDocService.importDocumentFromZip(knowId,file);
|
||||||
@ -244,7 +244,7 @@ public class AiragKnowledgeController {
|
|||||||
* @date 2025/2/18 15:47
|
* @date 2025/2/18 15:47
|
||||||
*/
|
*/
|
||||||
@PutMapping(value = "/doc/rebuild")
|
@PutMapping(value = "/doc/rebuild")
|
||||||
@RequiresPermissions("airag:knowledge:doc:rebuild")
|
@SaCheckPermission("airag:knowledge:doc:rebuild")
|
||||||
public Result<?> rebuildDocument(@RequestParam("docIds") String docIds) {
|
public Result<?> rebuildDocument(@RequestParam("docIds") String docIds) {
|
||||||
return airagKnowledgeDocService.rebuildDocument(docIds);
|
return airagKnowledgeDocService.rebuildDocument(docIds);
|
||||||
}
|
}
|
||||||
@ -259,7 +259,7 @@ public class AiragKnowledgeController {
|
|||||||
*/
|
*/
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
@DeleteMapping(value = "/doc/deleteBatch")
|
@DeleteMapping(value = "/doc/deleteBatch")
|
||||||
@RequiresPermissions("airag:knowledge:doc:deleteBatch")
|
@SaCheckPermission("airag:knowledge:doc:deleteBatch")
|
||||||
public Result<String> deleteDocumentBatch(HttpServletRequest request, @RequestParam(name = "ids", required = true) String ids) {
|
public Result<String> deleteDocumentBatch(HttpServletRequest request, @RequestParam(name = "ids", required = true) String ids) {
|
||||||
List<String> idsList = Arrays.asList(ids.split(","));
|
List<String> idsList = Arrays.asList(ids.split(","));
|
||||||
//update-begin---author:chenrui ---date:20250606 for:[issues/8337]关于ai工作列表的数据权限问题 #8337------------
|
//update-begin---author:chenrui ---date:20250606 for:[issues/8337]关于ai工作列表的数据权限问题 #8337------------
|
||||||
@ -287,7 +287,7 @@ public class AiragKnowledgeController {
|
|||||||
*/
|
*/
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
@DeleteMapping(value = "/doc/deleteAll")
|
@DeleteMapping(value = "/doc/deleteAll")
|
||||||
@RequiresPermissions("airag:knowledge:doc:deleteAll")
|
@SaCheckPermission("airag:knowledge:doc:deleteAll")
|
||||||
public Result<?> deleteDocumentAll(HttpServletRequest request, @RequestParam(name = "knowId") String knowId) {
|
public Result<?> deleteDocumentAll(HttpServletRequest request, @RequestParam(name = "knowId") String knowId) {
|
||||||
//update-begin---author:chenrui ---date:20250606 for:[issues/8337]关于ai工作列表的数据权限问题 #8337------------
|
//update-begin---author:chenrui ---date:20250606 for:[issues/8337]关于ai工作列表的数据权限问题 #8337------------
|
||||||
//如果是saas隔离的情况下,判断当前租户id是否是当前租户下的
|
//如果是saas隔离的情况下,判断当前租户id是否是当前租户下的
|
||||||
|
|||||||
@ -169,7 +169,7 @@ public class AiragMcpController extends JeecgController<AiragMcp, IAiragMcpServi
|
|||||||
* @param request
|
* @param request
|
||||||
* @param airagMcp
|
* @param airagMcp
|
||||||
*/
|
*/
|
||||||
// @RequiresPermissions("llm:airag_mcp:exportXls")
|
// @SaCheckPermission("llm:airag_mcp:exportXls")
|
||||||
@RequestMapping(value = "/exportXls")
|
@RequestMapping(value = "/exportXls")
|
||||||
public ModelAndView exportXls(HttpServletRequest request, AiragMcp airagMcp) {
|
public ModelAndView exportXls(HttpServletRequest request, AiragMcp airagMcp) {
|
||||||
return super.exportXls(request, airagMcp, AiragMcp.class, "MCP");
|
return super.exportXls(request, airagMcp, AiragMcp.class, "MCP");
|
||||||
@ -182,7 +182,7 @@ public class AiragMcpController extends JeecgController<AiragMcp, IAiragMcpServi
|
|||||||
* @param response
|
* @param response
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
// @RequiresPermissions("llm:airag_mcp:importExcel")
|
// @SaCheckPermission("llm:airag_mcp:importExcel")
|
||||||
@RequestMapping(value = "/importExcel", method = RequestMethod.POST)
|
@RequestMapping(value = "/importExcel", method = RequestMethod.POST)
|
||||||
public Result<?> importExcel(HttpServletRequest request, HttpServletResponse response) {
|
public Result<?> importExcel(HttpServletRequest request, HttpServletResponse response) {
|
||||||
return super.importExcel(request, response, AiragMcp.class);
|
return super.importExcel(request, response, AiragMcp.class);
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
package org.jeecg.modules.airag.llm.controller;
|
package org.jeecg.modules.airag.llm.controller;
|
||||||
|
|
||||||
|
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||||
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;
|
||||||
@ -7,7 +8,6 @@ import dev.langchain4j.data.message.UserMessage;
|
|||||||
import dev.langchain4j.model.embedding.EmbeddingModel;
|
import dev.langchain4j.model.embedding.EmbeddingModel;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
|
||||||
import org.jeecg.ai.factory.AiModelFactory;
|
import org.jeecg.ai.factory.AiModelFactory;
|
||||||
import org.jeecg.ai.factory.AiModelOptions;
|
import org.jeecg.ai.factory.AiModelOptions;
|
||||||
import org.jeecg.common.api.vo.Result;
|
import org.jeecg.common.api.vo.Result;
|
||||||
@ -72,7 +72,7 @@ public class AiragModelController extends JeecgController<AiragModel, IAiragMode
|
|||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
@PostMapping(value = "/add")
|
@PostMapping(value = "/add")
|
||||||
@RequiresPermissions("airag:model:add")
|
@SaCheckPermission("airag:model:add")
|
||||||
public Result<String> add(@RequestBody AiragModel airagModel) {
|
public Result<String> add(@RequestBody AiragModel airagModel) {
|
||||||
// 验证 模型名称/模型类型/基础模型
|
// 验证 模型名称/模型类型/基础模型
|
||||||
AssertUtils.assertNotEmpty("模型名称不能为空", airagModel.getName());
|
AssertUtils.assertNotEmpty("模型名称不能为空", airagModel.getName());
|
||||||
@ -95,7 +95,7 @@ public class AiragModelController extends JeecgController<AiragModel, IAiragMode
|
|||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
@RequestMapping(value = "/edit", method = {RequestMethod.PUT, RequestMethod.POST})
|
@RequestMapping(value = "/edit", method = {RequestMethod.PUT, RequestMethod.POST})
|
||||||
@RequiresPermissions("airag:model:edit")
|
@SaCheckPermission("airag:model:edit")
|
||||||
public Result<String> edit(@RequestBody AiragModel airagModel) {
|
public Result<String> edit(@RequestBody AiragModel airagModel) {
|
||||||
airagModelService.updateById(airagModel);
|
airagModelService.updateById(airagModel);
|
||||||
return Result.OK("编辑成功!");
|
return Result.OK("编辑成功!");
|
||||||
@ -108,7 +108,7 @@ public class AiragModelController extends JeecgController<AiragModel, IAiragMode
|
|||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
@DeleteMapping(value = "/delete")
|
@DeleteMapping(value = "/delete")
|
||||||
@RequiresPermissions("airag:model:delete")
|
@SaCheckPermission("airag:model:delete")
|
||||||
public Result<String> delete(HttpServletRequest request, @RequestParam(name = "id", required = true) String id) {
|
public Result<String> delete(HttpServletRequest request, @RequestParam(name = "id", required = true) String id) {
|
||||||
//update-begin---author:chenrui ---date:20250606 for:[issues/8337]关于ai工作列表的数据权限问题 #8337------------
|
//update-begin---author:chenrui ---date:20250606 for:[issues/8337]关于ai工作列表的数据权限问题 #8337------------
|
||||||
//如果是saas隔离的情况下,判断当前租户id是否是当前租户下的
|
//如果是saas隔离的情况下,判断当前租户id是否是当前租户下的
|
||||||
|
|||||||
@ -10,8 +10,6 @@ import io.swagger.v3.oas.annotations.Operation;
|
|||||||
import io.swagger.v3.oas.annotations.Parameter;
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.shiro.SecurityUtils;
|
|
||||||
import org.apache.shiro.mgt.DefaultSecurityManager;
|
|
||||||
import org.jeecg.common.api.vo.Result;
|
import org.jeecg.common.api.vo.Result;
|
||||||
import org.jeecg.common.aspect.annotation.AutoLog;
|
import org.jeecg.common.aspect.annotation.AutoLog;
|
||||||
import org.jeecg.common.aspect.annotation.PermissionData;
|
import org.jeecg.common.aspect.annotation.PermissionData;
|
||||||
@ -21,7 +19,7 @@ import org.jeecg.common.system.query.QueryGenerator;
|
|||||||
import org.jeecg.common.util.DateUtils;
|
import org.jeecg.common.util.DateUtils;
|
||||||
import org.jeecg.common.util.RedisUtil;
|
import org.jeecg.common.util.RedisUtil;
|
||||||
import org.jeecg.common.util.UUIDGenerator;
|
import org.jeecg.common.util.UUIDGenerator;
|
||||||
import org.jeecg.config.shiro.IgnoreAuth;
|
import org.jeecg.config.satoken.IgnoreAuth;
|
||||||
import org.jeecg.modules.demo.test.entity.JeecgDemo;
|
import org.jeecg.modules.demo.test.entity.JeecgDemo;
|
||||||
import org.jeecg.modules.demo.test.service.IJeecgDemoService;
|
import org.jeecg.modules.demo.test.service.IJeecgDemoService;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
@ -477,11 +475,6 @@ public class JeecgDemoController extends JeecgController<JeecgDemo, IJeecgDemoSe
|
|||||||
*/
|
*/
|
||||||
@GetMapping(value ="/test")
|
@GetMapping(value ="/test")
|
||||||
public Mono<String> test() {
|
public Mono<String> test() {
|
||||||
//解决shiro报错No SecurityManager accessible to the calling code, either bound to the org.apache.shiro
|
|
||||||
// https://blog.csdn.net/Japhet_jiu/article/details/131177210
|
|
||||||
DefaultSecurityManager securityManager = new DefaultSecurityManager();
|
|
||||||
SecurityUtils.setSecurityManager(securityManager);
|
|
||||||
|
|
||||||
return Mono.just("测试");
|
return Mono.just("测试");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -8,17 +8,14 @@ import java.util.Map;
|
|||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
import org.apache.shiro.SecurityUtils;
|
|
||||||
import org.jeecg.common.api.vo.Result;
|
import org.jeecg.common.api.vo.Result;
|
||||||
import org.jeecg.common.system.base.controller.JeecgController;
|
import org.jeecg.common.system.base.controller.JeecgController;
|
||||||
import org.jeecg.common.system.query.QueryGenerator;
|
import org.jeecg.common.system.query.QueryGenerator;
|
||||||
import org.jeecg.common.system.vo.LoginUser;
|
import org.jeecg.common.system.vo.LoginUser;
|
||||||
import org.jeecg.common.util.oConvertUtils;
|
import org.jeecg.common.util.LoginUserUtils;
|
||||||
import org.jeecg.modules.demo.test.entity.JeecgDemo;
|
|
||||||
import org.jeecg.modules.demo.test.entity.JeecgOrderCustomer;
|
import org.jeecg.modules.demo.test.entity.JeecgOrderCustomer;
|
||||||
import org.jeecg.modules.demo.test.entity.JeecgOrderMain;
|
import org.jeecg.modules.demo.test.entity.JeecgOrderMain;
|
||||||
import org.jeecg.modules.demo.test.entity.JeecgOrderTicket;
|
import org.jeecg.modules.demo.test.entity.JeecgOrderTicket;
|
||||||
import org.jeecg.modules.demo.test.service.IJeecgDemoService;
|
|
||||||
import org.jeecg.modules.demo.test.service.IJeecgOrderCustomerService;
|
import org.jeecg.modules.demo.test.service.IJeecgOrderCustomerService;
|
||||||
import org.jeecg.modules.demo.test.service.IJeecgOrderMainService;
|
import org.jeecg.modules.demo.test.service.IJeecgOrderMainService;
|
||||||
import org.jeecg.modules.demo.test.service.IJeecgOrderTicketService;
|
import org.jeecg.modules.demo.test.service.IJeecgOrderTicketService;
|
||||||
@ -33,7 +30,6 @@ import org.springframework.beans.factory.annotation.Autowired;
|
|||||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.PutMapping;
|
|
||||||
import org.springframework.web.bind.annotation.RequestBody;
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMethod;
|
import org.springframework.web.bind.annotation.RequestMethod;
|
||||||
@ -184,7 +180,7 @@ public class JeecgOrderMainController extends JeecgController<JeecgOrderMain, IJ
|
|||||||
//Step.2 AutoPoi 导出Excel
|
//Step.2 AutoPoi 导出Excel
|
||||||
ModelAndView mv = new ModelAndView(new JeecgEntityExcelView());
|
ModelAndView mv = new ModelAndView(new JeecgEntityExcelView());
|
||||||
//获取当前用户
|
//获取当前用户
|
||||||
LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();
|
LoginUser sysUser = LoginUserUtils.getSessionUser();
|
||||||
|
|
||||||
List<JeecgOrderMainPage> pageList = new ArrayList<JeecgOrderMainPage>();
|
List<JeecgOrderMainPage> pageList = new ArrayList<JeecgOrderMainPage>();
|
||||||
|
|
||||||
|
|||||||
@ -3,10 +3,10 @@ package org.jeecg.modules.demo.test.service.impl;
|
|||||||
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.impl.ServiceImpl;
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
import org.apache.shiro.SecurityUtils;
|
|
||||||
import org.jeecg.common.constant.CacheConstant;
|
import org.jeecg.common.constant.CacheConstant;
|
||||||
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.modules.demo.test.entity.JeecgDemo;
|
import org.jeecg.modules.demo.test.entity.JeecgDemo;
|
||||||
import org.jeecg.modules.demo.test.mapper.JeecgDemoMapper;
|
import org.jeecg.modules.demo.test.mapper.JeecgDemoMapper;
|
||||||
import org.jeecg.modules.demo.test.service.IJeecgDemoService;
|
import org.jeecg.modules.demo.test.service.IJeecgDemoService;
|
||||||
@ -97,7 +97,7 @@ public class JeecgDemoServiceImpl extends ServiceImpl<JeecgDemoMapper, JeecgDemo
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getExportFields() {
|
public String getExportFields() {
|
||||||
LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();
|
LoginUser sysUser = LoginUserUtils.getSessionUser();
|
||||||
//权限配置列导出示例
|
//权限配置列导出示例
|
||||||
//1.配置前缀与菜单中配置的列前缀一致
|
//1.配置前缀与菜单中配置的列前缀一致
|
||||||
List<String> noAuthList = new ArrayList<>();
|
List<String> noAuthList = new ArrayList<>();
|
||||||
|
|||||||
@ -1,438 +0,0 @@
|
|||||||
package org.jeecg.modules.dlglong.controller;
|
|
||||||
|
|
||||||
import com.alibaba.fastjson.JSON;
|
|
||||||
import com.alibaba.fastjson.JSONArray;
|
|
||||||
import com.alibaba.fastjson.JSONObject;
|
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
|
||||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
|
||||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.apache.commons.io.IOUtils;
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
|
||||||
import org.jeecg.common.api.vo.Result;
|
|
||||||
import org.jeecg.common.system.query.MatchTypeEnum;
|
|
||||||
import org.jeecg.common.system.query.QueryCondition;
|
|
||||||
import org.jeecg.common.system.query.QueryGenerator;
|
|
||||||
import org.jeecg.common.constant.VxeSocketConst;
|
|
||||||
import org.jeecg.modules.demo.mock.vxe.websocket.VxeSocket;
|
|
||||||
import org.jeecg.modules.dlglong.entity.MockEntity;
|
|
||||||
import org.springframework.web.bind.annotation.*;
|
|
||||||
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.net.URLDecoder;
|
|
||||||
import java.util.*;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @Description: DlMockController
|
|
||||||
* @author: jeecg-boot
|
|
||||||
*/
|
|
||||||
@Slf4j
|
|
||||||
@RestController
|
|
||||||
@RequestMapping("/mock/dlglong")
|
|
||||||
public class DlMockController {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 模拟更改状态
|
|
||||||
*
|
|
||||||
* @param id
|
|
||||||
* @param status
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
@GetMapping("/change1")
|
|
||||||
public Result mockChange1(@RequestParam("id") String id, @RequestParam("status") String status) {
|
|
||||||
/* id 为 行的id(rowId),只要获取到rowId,那么只需要调用 VXESocket.sendMessageToAll() 即可 */
|
|
||||||
|
|
||||||
// 封装行数据
|
|
||||||
JSONObject rowData = new JSONObject();
|
|
||||||
// 这个字段就是要更改的行数据ID
|
|
||||||
rowData.put("id", id);
|
|
||||||
// 这个字段就是要更改的列的key和具体的值
|
|
||||||
rowData.put("status", status);
|
|
||||||
// 模拟更改数据
|
|
||||||
this.mockChange(rowData);
|
|
||||||
|
|
||||||
return Result.ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 模拟更改拖轮状态
|
|
||||||
*
|
|
||||||
* @param id
|
|
||||||
* @param tugStatus
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
@GetMapping("/change2")
|
|
||||||
public Result mockChange2(@RequestParam("id") String id, @RequestParam("tug_status") String tugStatus) {
|
|
||||||
/* id 为 行的id(rowId),只要获取到rowId,那么只需要调用 VXESocket.sendMessageToAll() 即可 */
|
|
||||||
|
|
||||||
// 封装行数据
|
|
||||||
JSONObject rowData = new JSONObject();
|
|
||||||
// 这个字段就是要更改的行数据ID
|
|
||||||
rowData.put("id", id);
|
|
||||||
// 这个字段就是要更改的列的key和具体的值
|
|
||||||
JSONObject status = JSON.parseObject(tugStatus);
|
|
||||||
rowData.put("tug_status", status);
|
|
||||||
// 模拟更改数据
|
|
||||||
this.mockChange(rowData);
|
|
||||||
|
|
||||||
return Result.ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 模拟更改进度条状态
|
|
||||||
*
|
|
||||||
* @param id
|
|
||||||
* @param progress
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
@GetMapping("/change3")
|
|
||||||
public Result mockChange3(@RequestParam("id") String id, @RequestParam("progress") String progress) {
|
|
||||||
/* id 为 行的id(rowId),只要获取到rowId,那么只需要调用 VXESocket.sendMessageToAll() 即可 */
|
|
||||||
|
|
||||||
// 封装行数据
|
|
||||||
JSONObject rowData = new JSONObject();
|
|
||||||
// 这个字段就是要更改的行数据ID
|
|
||||||
rowData.put("id", id);
|
|
||||||
// 这个字段就是要更改的列的key和具体的值
|
|
||||||
rowData.put("progress", progress);
|
|
||||||
// 模拟更改数据
|
|
||||||
this.mockChange(rowData);
|
|
||||||
|
|
||||||
return Result.ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void mockChange(JSONObject rowData) {
|
|
||||||
// 封装socket数据
|
|
||||||
JSONObject socketData = new JSONObject();
|
|
||||||
// 这里的 socketKey 必须要和调度计划页面上写的 socketKey 属性保持一致
|
|
||||||
socketData.put("socketKey", "page-dispatch");
|
|
||||||
// 这里的 args 必须得是一个数组,下标0是行数据,下标1是caseId,一般不用传
|
|
||||||
socketData.put("args", new Object[]{rowData, ""});
|
|
||||||
// 封装消息字符串,这里的 type 必须是 VXESocketConst.TYPE_UVT
|
|
||||||
String message = VxeSocket.packageMessage(VxeSocketConst.TYPE_UVT, socketData);
|
|
||||||
// 调用 sendMessageToAll 发送给所有在线的用户
|
|
||||||
VxeSocket.sendMessageToAll(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 模拟更改【大船待审】状态
|
|
||||||
*
|
|
||||||
* @param status
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
@GetMapping("/change4")
|
|
||||||
public Result mockChange4(@RequestParam("status") String status) {
|
|
||||||
// 封装socket数据
|
|
||||||
JSONObject socketData = new JSONObject();
|
|
||||||
// 这里的 key 是前端注册时使用的key,必须保持一致
|
|
||||||
socketData.put("key", "dispatch-dcds-status");
|
|
||||||
// 这里的 args 必须得是一个数组,每一位都是注册方法的参数,按顺序传递
|
|
||||||
socketData.put("args", new Object[]{status});
|
|
||||||
|
|
||||||
// 封装消息字符串,这里的 type 必须是 VXESocketConst.TYPE_UVT
|
|
||||||
String message = VxeSocket.packageMessage(VxeSocketConst.TYPE_CSD, socketData);
|
|
||||||
// 调用 sendMessageToAll 发送给所有在线的用户
|
|
||||||
VxeSocket.sendMessageToAll(message);
|
|
||||||
|
|
||||||
return Result.ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 【模拟】即时保存单行数据
|
|
||||||
*
|
|
||||||
* @param rowData 行数据,实际使用时可以替换成一个实体类
|
|
||||||
*/
|
|
||||||
@PutMapping("/immediateSaveRow")
|
|
||||||
public Result mockImmediateSaveRow(@RequestBody JSONObject rowData) throws Exception {
|
|
||||||
System.out.println("即时保存.rowData:" + rowData.toJSONString());
|
|
||||||
// 延时1.5秒,模拟网慢堵塞真实感
|
|
||||||
Thread.sleep(500);
|
|
||||||
return Result.ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 【模拟】即时保存整个表格的数据
|
|
||||||
*
|
|
||||||
* @param tableData 表格数据(实际使用时可以替换成一个List实体类)
|
|
||||||
*/
|
|
||||||
@PostMapping("/immediateSaveAll")
|
|
||||||
public Result mockImmediateSaveAll(@RequestBody JSONArray tableData) throws Exception {
|
|
||||||
// 【注】:
|
|
||||||
// 1、tableData里包含该页所有的数据
|
|
||||||
// 2、如果你实现了“即时保存”,那么除了新增的数据,其他的都是已经保存过的了,
|
|
||||||
// 不需要再进行一次update操作了,所以可以在前端传数据的时候就遍历判断一下,
|
|
||||||
// 只传新增的数据给后台insert即可,否者将会造成性能上的浪费。
|
|
||||||
// 3、新增的行是没有id的,通过这一点,就可以判断是否是新增的数据
|
|
||||||
|
|
||||||
System.out.println("即时保存.tableData:" + tableData.toJSONString());
|
|
||||||
// 延时1.5秒,模拟网慢堵塞真实感
|
|
||||||
Thread.sleep(1000);
|
|
||||||
return Result.ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取模拟数据
|
|
||||||
*
|
|
||||||
* @param pageNo 页码
|
|
||||||
* @param pageSize 页大小
|
|
||||||
* @param parentId 父ID,不传则查询顶级
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
@GetMapping("/getData")
|
|
||||||
public Result getMockData(
|
|
||||||
@RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo,
|
|
||||||
@RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize,
|
|
||||||
// 父级id,根据父级id查询子级,如果为空则查询顶级
|
|
||||||
@RequestParam(name = "parentId", required = false) String parentId
|
|
||||||
) {
|
|
||||||
// 模拟JSON数据路径
|
|
||||||
String path = "classpath:org/jeecg/modules/dlglong/json/dlglong.json";
|
|
||||||
// 读取JSON数据
|
|
||||||
JSONArray dataList = readJsonData(path);
|
|
||||||
if (dataList == null) {
|
|
||||||
return Result.error("读取数据失败!");
|
|
||||||
}
|
|
||||||
IPage<JSONObject> page = this.queryDataPage(dataList, parentId, pageNo, pageSize);
|
|
||||||
return Result.ok(page);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取模拟“调度计划”页面的数据
|
|
||||||
*
|
|
||||||
* @param pageNo 页码
|
|
||||||
* @param pageSize 页大小
|
|
||||||
* @param parentId 父ID,不传则查询顶级
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
@GetMapping("/getDdjhData")
|
|
||||||
public Result getMockDdjhData(
|
|
||||||
// SpringMVC 会自动将参数注入到实体里
|
|
||||||
MockEntity mockEntity,
|
|
||||||
@RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo,
|
|
||||||
@RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize,
|
|
||||||
// 父级id,根据父级id查询子级,如果为空则查询顶级
|
|
||||||
@RequestParam(name = "parentId", required = false) String parentId,
|
|
||||||
@RequestParam(name = "status", required = false) String status,
|
|
||||||
// 高级查询条件
|
|
||||||
@RequestParam(name = "superQueryParams", required = false) String superQueryParams,
|
|
||||||
// 高级查询模式
|
|
||||||
@RequestParam(name = "superQueryMatchType", required = false) String superQueryMatchType,
|
|
||||||
HttpServletRequest request
|
|
||||||
) {
|
|
||||||
// 获取查询条件(前台传递的查询参数)
|
|
||||||
Map<String, String[]> parameterMap = request.getParameterMap();
|
|
||||||
// 遍历输出到控制台
|
|
||||||
System.out.println("\ngetDdjhData - 普通查询条件:");
|
|
||||||
for (String key : parameterMap.keySet()) {
|
|
||||||
System.out.println("-- " + key + ": " + JSON.toJSONString(parameterMap.get(key)));
|
|
||||||
}
|
|
||||||
// 输出高级查询
|
|
||||||
try {
|
|
||||||
System.out.println("\ngetDdjhData - 高级查询条件:");
|
|
||||||
// 高级查询模式
|
|
||||||
MatchTypeEnum matchType = MatchTypeEnum.getByValue(superQueryMatchType);
|
|
||||||
if (matchType == null) {
|
|
||||||
System.out.println("-- 高级查询模式:不识别(" + superQueryMatchType + ")");
|
|
||||||
} else {
|
|
||||||
System.out.println("-- 高级查询模式:" + matchType.getValue());
|
|
||||||
}
|
|
||||||
superQueryParams = URLDecoder.decode(superQueryParams, "UTF-8");
|
|
||||||
List<QueryCondition> conditions = JSON.parseArray(superQueryParams, QueryCondition.class);
|
|
||||||
if (conditions != null) {
|
|
||||||
for (QueryCondition condition : conditions) {
|
|
||||||
System.out.println("-- " + JSON.toJSONString(condition));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
System.out.println("-- 没有传递任何高级查询条件");
|
|
||||||
}
|
|
||||||
System.out.println();
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("-- 高级查询操作失败:" + superQueryParams, e);
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 注:实际使用中不用写上面那种繁琐的代码,这里只是为了直观的输出到控制台里而写的示例,
|
|
||||||
使用下面这种写法更简洁方便 */
|
|
||||||
|
|
||||||
// 封装成 MyBatisPlus 能识别的 QueryWrapper,可以直接使用这个对象进行SQL筛选条件拼接
|
|
||||||
// 这个方法也会自动封装高级查询条件,但是高级查询参数名必须是superQueryParams和superQueryMatchType
|
|
||||||
QueryWrapper<MockEntity> queryWrapper = QueryGenerator.initQueryWrapper(mockEntity, parameterMap);
|
|
||||||
System.out.println("queryWrapper: " + queryWrapper.getCustomSqlSegment());
|
|
||||||
|
|
||||||
// 模拟JSON数据路径
|
|
||||||
String path = "classpath:org/jeecg/modules/dlglong/json/ddjh.json";
|
|
||||||
String statusValue = "8";
|
|
||||||
if (statusValue.equals(status)) {
|
|
||||||
path = "classpath:org/jeecg/modules/dlglong/json/ddjh_s8.json";
|
|
||||||
}
|
|
||||||
// 读取JSON数据
|
|
||||||
JSONArray dataList = readJsonData(path);
|
|
||||||
if (dataList == null) {
|
|
||||||
return Result.error("读取数据失败!");
|
|
||||||
}
|
|
||||||
|
|
||||||
IPage<JSONObject> page = this.queryDataPage(dataList, parentId, pageNo, pageSize);
|
|
||||||
// 逐行查询子表数据,用于计算拖轮状态
|
|
||||||
List<JSONObject> records = page.getRecords();
|
|
||||||
for (JSONObject record : records) {
|
|
||||||
Map<String, Integer> tugStatusMap = new HashMap<>(5);
|
|
||||||
String id = record.getString("id");
|
|
||||||
// 查询出主表的拖轮
|
|
||||||
String tugMain = record.getString("tug");
|
|
||||||
// 判断是否有值
|
|
||||||
if (StringUtils.isNotBlank(tugMain)) {
|
|
||||||
// 拖轮根据分号分割
|
|
||||||
String[] tugs = tugMain.split(";");
|
|
||||||
// 查询子表数据
|
|
||||||
List<JSONObject> subRecords = this.queryDataPage(dataList, id, null, null).getRecords();
|
|
||||||
// 遍历子表和拖轮数据,找出进行计算反推拖轮状态
|
|
||||||
for (JSONObject subData : subRecords) {
|
|
||||||
String subTug = subData.getString("tug");
|
|
||||||
if (StringUtils.isNotBlank(subTug)) {
|
|
||||||
for (String tug : tugs) {
|
|
||||||
if (tug.equals(subTug)) {
|
|
||||||
// 计算拖轮状态逻辑
|
|
||||||
int statusCode = 0;
|
|
||||||
|
|
||||||
/* 如果有发船时间、作业开始时间、作业结束时间、回船时间,则主表中的拖轮列中的每个拖轮背景色要即时变色 */
|
|
||||||
|
|
||||||
// 有发船时间,状态 +1
|
|
||||||
String departureTime = subData.getString("departure_time");
|
|
||||||
if (StringUtils.isNotBlank(departureTime)) {
|
|
||||||
statusCode += 1;
|
|
||||||
}
|
|
||||||
// 有作业开始时间,状态 +1
|
|
||||||
String workBeginTime = subData.getString("work_begin_time");
|
|
||||||
if (StringUtils.isNotBlank(workBeginTime)) {
|
|
||||||
statusCode += 1;
|
|
||||||
}
|
|
||||||
// 有作业结束时间,状态 +1
|
|
||||||
String workEndTime = subData.getString("work_end_time");
|
|
||||||
if (StringUtils.isNotBlank(workEndTime)) {
|
|
||||||
statusCode += 1;
|
|
||||||
}
|
|
||||||
// 有回船时间,状态 +1
|
|
||||||
String returnTime = subData.getString("return_time");
|
|
||||||
if (StringUtils.isNotBlank(returnTime)) {
|
|
||||||
statusCode += 1;
|
|
||||||
}
|
|
||||||
// 保存拖轮状态,key是拖轮的值,value是状态,前端根据不同的状态码,显示不同的颜色,这个颜色也可以后台计算完之后返回给前端直接使用
|
|
||||||
tugStatusMap.put(tug, statusCode);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 新加一个字段用于保存拖轮状态,不要直接覆盖原来的,这个字段可以不保存到数据库里
|
|
||||||
record.put("tug_status", tugStatusMap);
|
|
||||||
}
|
|
||||||
page.setRecords(records);
|
|
||||||
return Result.ok(page);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 模拟查询数据,可以根据父ID查询,可以分页
|
|
||||||
*
|
|
||||||
* @param dataList 数据列表
|
|
||||||
* @param parentId 父ID
|
|
||||||
* @param pageNo 页码
|
|
||||||
* @param pageSize 页大小
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
private IPage<JSONObject> queryDataPage(JSONArray dataList, String parentId, Integer pageNo, Integer pageSize) {
|
|
||||||
// 根据父级id查询子级
|
|
||||||
JSONArray dataDb = dataList;
|
|
||||||
if (StringUtils.isNotBlank(parentId)) {
|
|
||||||
JSONArray results = new JSONArray();
|
|
||||||
List<String> parentIds = Arrays.asList(parentId.split(","));
|
|
||||||
this.queryByParentId(dataDb, parentIds, results);
|
|
||||||
dataDb = results;
|
|
||||||
}
|
|
||||||
// 模拟分页(实际中应用SQL自带的分页)
|
|
||||||
List<JSONObject> records = new ArrayList<>();
|
|
||||||
IPage<JSONObject> page;
|
|
||||||
long beginIndex, endIndex;
|
|
||||||
// 如果任意一个参数为null,则不分页
|
|
||||||
if (pageNo == null || pageSize == null) {
|
|
||||||
page = new Page<>(0, dataDb.size());
|
|
||||||
beginIndex = 0;
|
|
||||||
endIndex = dataDb.size();
|
|
||||||
} else {
|
|
||||||
page = new Page<>(pageNo, pageSize);
|
|
||||||
beginIndex = page.offset();
|
|
||||||
endIndex = page.offset() + page.getSize();
|
|
||||||
}
|
|
||||||
for (long i = beginIndex; (i < endIndex && i < dataDb.size()); i++) {
|
|
||||||
JSONObject data = dataDb.getJSONObject((int) i);
|
|
||||||
data = JSON.parseObject(data.toJSONString());
|
|
||||||
// 不返回 children
|
|
||||||
data.remove("children");
|
|
||||||
records.add(data);
|
|
||||||
}
|
|
||||||
page.setRecords(records);
|
|
||||||
page.setTotal(dataDb.size());
|
|
||||||
return page;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void queryByParentId(JSONArray dataList, List<String> parentIds, JSONArray results) {
|
|
||||||
for (int i = 0; i < dataList.size(); i++) {
|
|
||||||
JSONObject data = dataList.getJSONObject(i);
|
|
||||||
JSONArray children = data.getJSONArray("children");
|
|
||||||
// 找到了该父级
|
|
||||||
if (parentIds.contains(data.getString("id"))) {
|
|
||||||
if (children != null) {
|
|
||||||
// addAll 的目的是将多个子表的数据合并在一起
|
|
||||||
results.addAll(children);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (children != null) {
|
|
||||||
queryByParentId(children, parentIds, results);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
results.addAll(new JSONArray());
|
|
||||||
}
|
|
||||||
|
|
||||||
private JSONArray readJsonData(String path) {
|
|
||||||
try {
|
|
||||||
InputStream stream = getClass().getClassLoader().getResourceAsStream(path.replace("classpath:", ""));
|
|
||||||
if (stream != null) {
|
|
||||||
String json = IOUtils.toString(stream, "UTF-8");
|
|
||||||
return JSON.parseArray(json);
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
|
||||||
log.error(e.getMessage(), e);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取车辆最后一个位置
|
|
||||||
*
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
@PostMapping("/findLatestCarLngLat")
|
|
||||||
public List findLatestCarLngLat() {
|
|
||||||
// 模拟JSON数据路径
|
|
||||||
String path = "classpath:org/jeecg/modules/dlglong/json/CarLngLat.json";
|
|
||||||
// 读取JSON数据
|
|
||||||
return readJsonData(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取车辆最后一个位置
|
|
||||||
*
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
@PostMapping("/findCarTrace")
|
|
||||||
public List findCarTrace() {
|
|
||||||
// 模拟JSON数据路径
|
|
||||||
String path = "classpath:org/jeecg/modules/dlglong/json/CarTrace.json";
|
|
||||||
// 读取JSON数据
|
|
||||||
return readJsonData(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,27 +0,0 @@
|
|||||||
package org.jeecg.modules.dlglong.entity;
|
|
||||||
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 模拟实体
|
|
||||||
* @author: jeecg-boot
|
|
||||||
*/
|
|
||||||
@Data
|
|
||||||
public class MockEntity {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* id
|
|
||||||
*/
|
|
||||||
private String id;
|
|
||||||
/**
|
|
||||||
* 父级ID
|
|
||||||
*/
|
|
||||||
private String parentId;
|
|
||||||
/**
|
|
||||||
* 状态
|
|
||||||
*/
|
|
||||||
private String status;
|
|
||||||
|
|
||||||
/* -- 省略其他字段 -- */
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"id": "6891ba44421aa907bcb7390c",
|
|
||||||
"alarm": "0",
|
|
||||||
"altitude": "13",
|
|
||||||
"direction": "0",
|
|
||||||
"latitude": "38.918739",
|
|
||||||
"longitude": "117.758737",
|
|
||||||
"speed": "11",
|
|
||||||
"status": "4980739",
|
|
||||||
"timestamp": "2025-08-05T16:01:07",
|
|
||||||
"imei": "18441136860"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 622 KiB |
|
Before Width: | Height: | Size: 1.4 MiB |
|
Before Width: | Height: | Size: 244 KiB After Width: | Height: | Size: 143 KiB |
|
Before Width: | Height: | Size: 247 KiB After Width: | Height: | Size: 143 KiB |
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 6.0 KiB After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 262 B After Width: | Height: | Size: 187 B |
|
Before Width: | Height: | Size: 1001 B After Width: | Height: | Size: 992 B |
|
Before Width: | Height: | Size: 380 B After Width: | Height: | Size: 333 B |
|
Before Width: | Height: | Size: 338 B After Width: | Height: | Size: 259 B |
|
Before Width: | Height: | Size: 280 B After Width: | Height: | Size: 209 B |
|
Before Width: | Height: | Size: 1015 B After Width: | Height: | Size: 1014 B |
|
Before Width: | Height: | Size: 709 B After Width: | Height: | Size: 383 B |
|
Before Width: | Height: | Size: 159 KiB After Width: | Height: | Size: 90 KiB |
|
Before Width: | Height: | Size: 183 B After Width: | Height: | Size: 164 B |
|
Before Width: | Height: | Size: 273 B After Width: | Height: | Size: 186 B |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 251 B After Width: | Height: | Size: 159 B |
|
Before Width: | Height: | Size: 256 B After Width: | Height: | Size: 159 B |
|
Before Width: | Height: | Size: 333 B After Width: | Height: | Size: 307 B |
|
Before Width: | Height: | Size: 529 B After Width: | Height: | Size: 478 B |
|
Before Width: | Height: | Size: 242 B After Width: | Height: | Size: 182 B |
|
Before Width: | Height: | Size: 7.6 KiB After Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 317 B After Width: | Height: | Size: 302 B |
|
Before Width: | Height: | Size: 367 B After Width: | Height: | Size: 335 B |
|
Before Width: | Height: | Size: 499 B After Width: | Height: | Size: 497 B |
|
Before Width: | Height: | Size: 557 B After Width: | Height: | Size: 436 B |
|
Before Width: | Height: | Size: 711 B After Width: | Height: | Size: 512 B |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 182 B After Width: | Height: | Size: 134 B |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.7 KiB |