升级jeecg-boot3.6.3

This commit is contained in:
zhouwentao 2024-04-16 20:29:27 +08:00
parent 0de3fb15ab
commit ac5a285e63
904 changed files with 453220 additions and 9459 deletions

View File

@ -7,13 +7,13 @@
JEECG BOOT 低代码开发平台
===============
当前最新版本: 3.5.3发布日期2023-07-24
当前最新版本: 3.6.3发布日期2024-03-11
[![AUR](https://img.shields.io/badge/license-Apache%20License%202.0-blue.svg)](https://github.com/zhangdaiscott/jeecg-boot/blob/master/LICENSE)
[![](https://img.shields.io/badge/Author-北京敲敲云科技-orange.svg)](http://www.jeecg.com)
[![](https://img.shields.io/badge/Author-北京国炬软件-orange.svg)](http://jeecg.com/aboutusIndex)
[![](https://img.shields.io/badge/Blog-官方博客-blue.svg)](https://jeecg.blog.csdn.net)
[![](https://img.shields.io/badge/version-3.5.3-brightgreen.svg)](https://github.com/zhangdaiscott/jeecg-boot)
[![](https://img.shields.io/badge/version-3.6.3-brightgreen.svg)](https://github.com/zhangdaiscott/jeecg-boot)
[![GitHub stars](https://img.shields.io/github/stars/zhangdaiscott/jeecg-boot.svg?style=social&label=Stars)](https://github.com/zhangdaiscott/jeecg-boot)
[![GitHub forks](https://img.shields.io/github/forks/zhangdaiscott/jeecg-boot.svg?style=social&label=Fork)](https://github.com/zhangdaiscott/jeecg-boot)
@ -44,22 +44,22 @@ Jeecg-Boot低代码开发平台可以应用在任何J2EE项目的开发中
项目源码
-----------------------------------
| 仓库 |前端 Vue3版 | 前端 Vue2版 | 后端源码 |
|-|-|-|-|
| Github | [jeecgboot-vue3](https://github.com/jeecgboot/jeecgboot-vue3) | [ant-design-vue-jeecg](https://github.com/jeecgboot/ant-design-vue-jeecg) | [jeecg-boot](https://github.com/jeecgboot/jeecg-boot) |
| 码云 | [jeecgboot-vue3](https://gitee.com/jeecg/jeecgboot-vue3) | [ant-design-vue-jeecg](https://gitee.com/jeecg/ant-design-vue-jeecg) | [jeecg-boot](https://gitee.com/jeecg/jeecg-boot) |
| 仓库 |前端源码 Vue3版 | 后端JAVA源码 |
|-|-|-|
| Github | [jeecgboot-vue3](https://github.com/jeecgboot/jeecgboot-vue3) | [jeecg-boot](https://github.com/jeecgboot/jeecg-boot) |
| 码云 | [jeecgboot-vue3](https://gitee.com/jeecg/jeecgboot-vue3) | [jeecg-boot](https://gitee.com/jeecg/jeecg-boot) |
> 官方已推出 `SpringBoot3+JDK17版本` [分支源码下载](https://github.com/jeecgboot/jeecg-boot/tree/springboot3) | [升级SpringBoot3博客](https://blog.csdn.net/zhangdaiscott/article/details/134805602)
#### 项目说明
| 项目名 | 说明 |
|--------------------|------------------------|
| `jeecg-boot` | SpringBoot后台源码支持微服务 |
| `jeecgboot-vue3` | Vue3+TS 新版前端源码 |
| `ant-design-vue-jeecg` |Vue2版前端源码 |
| `jeecgboot-vue3` | 前端源码 (Vue3版本) |
| `jeecg-boot` | 后端JAVA源码支持微服务 |
| `jeecg-uniapp` | [APP开发框架一份代码多终端适配同时支持APP、小程序、H5](https://github.com/jeecgboot/jeecg-uniapp) |
| `jeecg-boot-starter` | [Stater依赖项目单独维护点击下载](https://gitee.com/jeecg/jeecg-boot-starter) |
| `更多开源项目` | [更多源码下载](http://jeecg.com/download) |
| `更多开源项目` | [更多底层源码下载](http://jeecg.com/download) |
快速搭建开发环境
@ -83,16 +83,22 @@ Docker快速启动项目
-----------------------------------
- 项目官网: [http://www.jeecg.com](http://www.jeecg.com)
- 开发文档: [http://help.jeecg.com](http://help.jeecg.com)
- 新手指南: [快速入门](http://www.jeecg.com/doc/quickstart) | [常见问题 ](http://www.jeecg.com/doc/qa) | [视频教程](https://space.bilibili.com/454617261/channel/series) | [1分钟低代码体验](https://my.oschina.net/jeecg/blog/3083313)
- 开发文档: [https://help.jeecg.com](https://help.jeecg.com)
- 新手指南: [快速入门](http://www.jeecg.com/doc/quickstart) | [常见问题 ](http://www.jeecg.com/doc/qa) | [视频教程](https://space.bilibili.com/454617261/channel/series) | [1分钟低代码体验](https://my.oschina.net/jeecg/blog/3083313)
- 在线演示 [Vue3演示](http://boot3.jeecg.com) | [Vue2演示](http://boot3.jeecg.com) | [APP演示](http://jeecg.com/appIndex)
> 演示系统的登录账号密码,请点击 [获取账号密码](http://jeecg.com/doc/demo) 获取
- 在线演示 [Vue3演示](http://boot3.jeecg.com) | [APP演示](http://jeecg.com/appIndex) | [敲敲云零代码](https://qiaoqiaoyun.com)
> 演示系统的登录账号密码,请点击 [获取账号密码](http://jeecg.com/doc/demo) 获取
>
- QQ交流群 ⑦791696430、⑥730954414、VUE3群683903138、⑤860162132(满)、④774126647(满)、③816531124(满)、②769925425(满)、①284271917(满)
- QQ交流群 ⑧825232878、⑦791696430(满)、⑥730954414(满)、683903138(满)、⑤860162132(满)、④774126647(满)、③816531124(满)、②769925425(满)、①284271917(满)
> ` 提醒【QQ群是自助服务群建议给帮助您解决问题的同学发送指定红包表示感谢】 `
大龄码农的思考
-----------------------------------
> 作为码农年纪大了写不动代码了怎么办??哎!!
所以我们团队在追求不写代码也可实现复杂业务系统!目前已经做到了,不信你到敲敲云零代码试试(通过流程串联修改业务数据)
- https://www.qiaoqiaoyun.com
技术支持
@ -105,39 +111,32 @@ Docker快速启动项目
=======【VUE2版本专题介绍】============================================
VUE2版本专题介绍
-----------------------------------
#### 项目介绍
#### 项目介绍
- 项目名称ant-design-vue-jeecg
- 说明JeecgBoot前端提供两套解决方案一套VUE2和一套VUE3版本目前vue2版本最新代码只支持到jeecgboot 3.4.3版本,一定注意。
- 更多介绍:[Vue2版演示](http://boot.jeecg.com) |[开发文档](http://doc.jeecg.com)
- [快速启动——Vue2前端](http://doc.jeecg.com/2678320)
- [Docker启动——Vue2前端](http://doc.jeecg.com/3043612)
#### Vue2与Vue3版本区别
> - VUE3版本彻底抛弃IE兼容不兼容IE和低版本浏览器只适配高版本谷歌和Edge
政府、事业类单位项目需要谨慎选择——国产化迁移是一个漫长的过程万一过程中要求IE兼容这个不可逆
> - 所以如果对浏览器有要求的项目请选择VUE2版本。
> - VUE3版是全新的技术栈紧跟主流前端重写各个功能都做了优化拥有更好的体验效果
#### 源码下载
| 源码 | 源码地址 |
|--------------------|------------------------|
| 后端源码 `Vue2版` |https://gitee.com/jeecg/jeecg-boot/tree/v3.4.3last |
| 前端源码 `Vue2版` |https://gitee.com/jeecg/ant-design-vue-jeecg |
| 后端JAVA源码 `Vue2版` |https://gitee.com/jeecg/jeecg-boot/tree/v3.4.3last |
| 前端vue2源码 `Vue2版` |https://gitee.com/jeecg/ant-design-vue-jeecg |
=========【VUE2版本专题介绍】=========================================
#### Vue2与Vue3版本区别
> - VUE3版本彻底抛弃IE兼容不兼容IE和低版本浏览器只适配高版本谷歌和Edge
政府、事业类单位项目需要谨慎选择——国产化迁移是一个漫长的过程万一过程中要求IE兼容这个不可逆
> - 所以如果对浏览器有要求的项目请选择VUE2版本。
> - VUE3版是全新的技术栈紧跟主流前端重写各个功能都做了优化拥有更好的体验效果
#### 技术文档
- 在线演示:[Vue2版演示](http://boot.jeecg.com)
- 开发文档:| [开发文档](http://doc.jeecg.com) | [Vue2前端快速启动](http://doc.jeecg.com/2678320) | [Vue2前端采用Docker启动](http://doc.jeecg.com/3043612)
##### Star走势图
Star走势图
-----------------------------------
[![Star History Chart](https://api.star-history.com/svg?repos=jeecgboot/jeecg-boot&type=Date)](https://star-history.com/#jeecgboot/jeecg-boot)
@ -220,9 +219,9 @@ VUE2版本专题介绍
* 40.权限控制采用 RBACRole-Based Access Control基于角色的访问控制
* 41.提供新行编辑表格JVXETable轻松满足各种复杂ERP布局拥有更高的性能、更灵活的扩展、更强大的功能
技术架构:
-----------------------------------
#### 开发环境
@ -292,7 +291,7 @@ VUE2版本专题介绍
5、熔断降级限流 Sentinel √
6、分布式文件 Minio、阿里OSS √
6、分布式文件 Minio、阿里OSS √
7、统一权限控制 JWT + Shiro √
@ -302,7 +301,7 @@ VUE2版本专题介绍
10、消息中间件 RabbitMQ √
11、分布式任务 xxl-job √
11、分布式任务 xxl-job √
12、分布式事务 Seata
@ -314,7 +313,7 @@ VUE2版本专题介绍
16、路由限流 √
#### 微服务架构图
![微服务架构图](https://jeecgos.oss-cn-beijing.aliyuncs.com/files/jeecgboot_springcloud2022.png "在这里输入图片标题")
@ -468,8 +467,11 @@ VUE2版本专题介绍
```
### 流程引擎推荐
JeecgBoot企业版本默认集成了activiti和flowable两套方案大家在使用本开源项目时如果想进一步集成流程引擎推荐结合贺波老师的书 [《深入Activiti流程引擎核心原理与高阶实战》](https://item.m.jd.com/product/13928958.html?gx=RnAomTM2bmCImZxDqYAkVCoIHuIYVqc)
<img src="https://jeecgos.oss-cn-beijing.aliyuncs.com/files/tuijian20231220161656.png" width="25%" height="auto">
### 系统效果
@ -580,8 +582,8 @@ VUE2版本专题介绍
##### 在线接口文档
![输入图片说明](https://static.oschina.net/uploads/img/201908/27095258_M2Xq.png "在这里输入图片标题")
![输入图片说明](https://static.oschina.net/uploads/img/201904/14160957_hN3X.png "在这里输入图片标题")
## 捐赠
## 捐赠
如果觉得还不错,请作者喝杯咖啡吧 ☺
![](https://static.oschina.net/uploads/img/201903/08155608_0EFX.png)
![](https://static.oschina.net/uploads/img/201903/08155608_0EFX.png)

View File

@ -4,7 +4,7 @@
<parent>
<groupId>org.jeecgframework.boot</groupId>
<artifactId>jeecg-boot-parent</artifactId>
<version>3.5.3</version>
<version>3.6.3</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>jeecg-boot-base-core</artifactId>
@ -145,7 +145,7 @@
<version>${postgresql.version}</version>
<scope>runtime</scope>
</dependency>
<!-- Quartz定时任务 -->
<dependency>
<groupId>org.springframework.boot</groupId>
@ -175,6 +175,10 @@
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
</exclusion>
<exclusion>
<artifactId>checkstyle</artifactId>
<groupId>com.puppycrawl.tools</groupId>
</exclusion>
</exclusions>
</dependency>
@ -252,6 +256,15 @@
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
</dependency>
<!--加载hutool-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-core</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-crypto</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -22,10 +22,10 @@ public interface CommonAPI {
/**
* 2查询用户权限信息
* @param username
* @param userId
* @return
*/
Set<String> queryUserAuths(String username);
Set<String> queryUserAuths(String userId);
/**
* 3根据 id 查询数据库中存储的 DynamicDataSourceModel
@ -102,12 +102,12 @@ public interface CommonAPI {
/**
* 13获取表数据字典
* @param table
* @param tableFilterSql
* @param text
* @param code
* @return
*/
List<DictModel> queryTableDictItemsByCode(String table, String text, String code);
List<DictModel> queryTableDictItemsByCode(String tableFilterSql, String text, String code);
/**
* 14 普通字典的翻译根据多个dictCode和多条数据多个以逗号分割
@ -117,14 +117,17 @@ public interface CommonAPI {
*/
Map<String, List<DictModel>> translateManyDict(String dictCodes, String keys);
//update-begin---author:chenrui ---date:20231221 for[issues/#5643]解决分布式下表字典跨库无法查询问题------------
/**
* 15 字典表的 翻译可批量
* @param table
* @param text
* @param code
* @param keys 多个用逗号分割
* @param dataSource 数据源
* @return
*/
List<DictModel> translateDictFromTableByKeys(String table, String text, String code, String keys);
List<DictModel> translateDictFromTableByKeys(String table, String text, String code, String keys, String dataSource);
//update-end---author:chenrui ---date:20231221 for[issues/#5643]解决分布式下表字典跨库无法查询问题------------
}

View File

@ -17,6 +17,8 @@ public class DataLogDTO {
private String type;
private String createName;
public DataLogDTO(){
}

View File

@ -30,6 +30,13 @@ public class OnlineAuthDTO implements Serializable {
*/
private String onlineFormUrl;
//update-begin---author:chenrui ---date:20240123 for[QQYUN-7992]online工单申请下的online表单未配置online表单开发菜单操作报错无权限------------
/**
* online工单的地址
*/
private String onlineWorkOrderUrl;
//update-end---author:chenrui ---date:20240123 for[QQYUN-7992]online工单申请下的online表单未配置online表单开发菜单操作报错无权限------------
public OnlineAuthDTO(){
}

View File

@ -4,7 +4,7 @@ import lombok.Data;
import org.jeecg.common.constant.CommonConstant;
import java.io.Serializable;
import java.util.Map;
import java.util.*;
/**
* 普通消息
@ -43,14 +43,7 @@ public class MessageDTO implements Serializable {
* 消息类型 1:消息 2:系统消息
*/
protected String category;
//-----------------------------------------------------------------------
//update-begin---author:taoyan ---date:20220705 for支持自定义推送类型邮件钉钉企业微信系统消息-----------
/**
* 模板消息对应的模板编码
*/
protected String templateCode;
/**
* 消息类型org.jeecg.common.constant.enums.MessageTypeEnum
* XT("system", "系统消息")
@ -60,23 +53,38 @@ public class MessageDTO implements Serializable {
*/
protected String type;
//---推送模板相关参数-------------------------------------------------------------
/**
* 是否发送Markdown格式的消息
*/
protected boolean isMarkdown;
/**
* 模板消息对应的模板编码
*/
protected String templateCode;
/**
* 解析模板内容 对应的数据
*/
protected Map<String, Object> data;
//update-end---author:taoyan ---date::20220705 for支持自定义推送类型邮件钉钉企业微信系统消息-----------
//-----------------------------------------------------------------------
//---推送模板相关参数-------------------------------------------------------------
//---邮件相关参数-------------------------------------------------------------
/**
* 抄送人
* 邮件抄送人
*/
private String copyToUser;
/**
* 邮件推送地址
*/
protected Set<String> toEmailList;
/**
* 邮件抄送地址
*/
protected Set<String> ccEmailList;
//---邮件相关参数-------------------------------------------------------------
public MessageDTO(){
}

View File

@ -140,11 +140,15 @@ public class DictAspect {
String code = field.getAnnotation(Dict.class).dicCode();
String text = field.getAnnotation(Dict.class).dicText();
String table = field.getAnnotation(Dict.class).dictTable();
//update-begin---author:chenrui ---date:20231221 for[issues/#5643]解决分布式下表字典跨库无法查询问题------------
String dataSource = field.getAnnotation(Dict.class).ds();
//update-end---author:chenrui ---date:20231221 for[issues/#5643]解决分布式下表字典跨库无法查询问题------------
List<String> dataList;
String dictCode = code;
if (!StringUtils.isEmpty(table)) {
dictCode = String.format("%s,%s,%s", table, text, code);
//update-begin---author:chenrui ---date:20231221 for[issues/#5643]解决分布式下表字典跨库无法查询问题------------
dictCode = String.format("%s,%s,%s,%s", table, text, code, dataSource);
//update-end---author:chenrui ---date:20231221 for[issues/#5643]解决分布式下表字典跨库无法查询问题------------
}
dataList = dataListMap.computeIfAbsent(dictCode, k -> new ArrayList<>());
this.listAddAllDeduplicate(dataList, Arrays.asList(value.split(",")));
@ -169,10 +173,15 @@ public class DictAspect {
String code = field.getAnnotation(Dict.class).dicCode();
String text = field.getAnnotation(Dict.class).dicText();
String table = field.getAnnotation(Dict.class).dictTable();
//update-begin---author:chenrui ---date:20231221 for[issues/#5643]解决分布式下表字典跨库无法查询问题------------
// 自定义的字典表数据源
String dataSource = field.getAnnotation(Dict.class).ds();
//update-end---author:chenrui ---date:20231221 for[issues/#5643]解决分布式下表字典跨库无法查询问题------------
String fieldDictCode = code;
if (!StringUtils.isEmpty(table)) {
fieldDictCode = String.format("%s,%s,%s", table, text, code);
//update-begin---author:chenrui ---date:20231221 for[issues/#5643]解决分布式下表字典跨库无法查询问题------------
fieldDictCode = String.format("%s,%s,%s,%s", table, text, code, dataSource);
//update-end---author:chenrui ---date:20231221 for[issues/#5643]解决分布式下表字典跨库无法查询问题------------
}
String value = record.getString(field.getName());
@ -274,9 +283,25 @@ public class DictAspect {
String[] arr = dictCode.split(",");
String table = arr[0], text = arr[1], code = arr[2];
String values = String.join(",", needTranslDataTable);
//update-begin---author:chenrui ---date:20231221 for[issues/#5643]解决分布式下表字典跨库无法查询问题------------
// 自定义的数据源
String dataSource = null;
if (arr.length > 3) {
dataSource = arr[3];
}
//update-end---author:chenrui ---date:20231221 for[issues/#5643]解决分布式下表字典跨库无法查询问题------------
log.debug("translateDictFromTableByKeys.dictCode:" + dictCode);
log.debug("translateDictFromTableByKeys.values:" + values);
List<DictModel> texts = commonApi.translateDictFromTableByKeys(table, text, code, values);
//update-begin---author:chenrui ---date:20231221 for[issues/#5643]解决分布式下表字典跨库无法查询问题------------
//update-begin---author:wangshuai---date:2024-01-09---for:微服务下为空报错没有参数需要传递空字符串---
if(null == dataSource){
dataSource = "";
}
//update-end---author:wangshuai---date:2024-01-09---for:微服务下为空报错没有参数需要传递空字符串---
List<DictModel> texts = commonApi.translateDictFromTableByKeys(table, text, code, values, dataSource);
//update-end---author:chenrui ---date:20231221 for[issues/#5643]解决分布式下表字典跨库无法查询问题------------
log.debug("translateDictFromTableByKeys.result:" + texts);
List<DictModel> list = translText.computeIfAbsent(dictCode, k -> new ArrayList<>());
list.addAll(texts);

View File

@ -59,8 +59,7 @@ public class PermissionDataAspect {
requestPath = filterUrl(requestPath);
//update-begin-author:taoyan date:20211027 for:JTC-132online报表权限online报表带参数的菜单配置数据权限无效
//先判断是否online报表请求
// TODO 参数顺序调整有隐患
if(requestPath.indexOf(UrlMatchEnum.CGREPORT_DATA.getMatchUrl())>=0){
if(requestPath.indexOf(UrlMatchEnum.CGREPORT_DATA.getMatchUrl())>=0 || requestPath.indexOf(UrlMatchEnum.CGREPORT_ONLY_DATA.getMatchUrl())>=0){
// 获取地址栏参数
String urlParamString = request.getParameter(CommonConstant.ONL_REP_URL_PARAM_STR);
if(oConvertUtils.isNotEmpty(urlParamString)){
@ -68,7 +67,7 @@ public class PermissionDataAspect {
}
}
//update-end-author:taoyan date:20211027 for:JTC-132online报表权限online报表带参数的菜单配置数据权限无效
log.info("拦截请求 >> {} ; 请求类型 >> {} . ", requestPath, requestMethod);
log.debug("拦截请求 >> {} ; 请求类型 >> {} . ", requestPath, requestMethod);
String username = JwtUtil.getUserNameByToken(request);
//查询数据权限信息
//TODO 微服务情况下也得支持缓存机制

View File

@ -14,6 +14,8 @@ public enum UrlMatchEnum {
CGFORM_TREE_DATA("/online/cgform/api/getTreeData/", "/online/cgformList/"),
/**求URL与菜单路由URL转换规则 /online/cgreport/api/getColumnsAndData/ */
CGREPORT_DATA("/online/cgreport/api/getColumnsAndData/", "/online/cgreport/"),
/** 求URL与菜单路由URL转换规则/online/cgreport/api/getData/ 【vue3报表数据请求地址】 */
CGREPORT_ONLY_DATA("/online/cgreport/api/getData/", "/online/cgreport/"),
/**求URL与菜单路由URL转换规则 /online/cgreport/api/exportXls/ */
CGREPORT_EXCEL_DATA("/online/cgreport/api/exportXls/", "/online/cgreport/"),
/**求URL与菜单路由URL转换规则 /online/cgreport/api/exportManySheetXls/ */

View File

@ -1,33 +0,0 @@
package org.jeecg.common.aspect.annotation;
import java.lang.annotation.*;
import org.jeecg.common.constant.enums.LowAppAopEnum;
/**
* 自动注入low_app_id
*
* @Author scott
* @email jeecgos@163.com
* @Date 2022年01月05日
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AutoLowApp {
/**
* 切面类型adddeletedb_import等其他操作
*
* @return
*/
LowAppAopEnum action();
/**
* 业务类型cgform等
*
* @return
*/
String bizType();
}

View File

@ -39,4 +39,16 @@ public @interface Dict {
* @return 返回类型 String
*/
String dictTable() default "";
//update-begin---author:chenrui ---date:20231221 for[issues/#5643]解决分布式下表字典跨库无法查询问题------------
/**
* 方法描述: 数据字典表所在数据源名称
* chenrui
* 2023年12月20日-下午4:58
*
* @return 返回类型 String
*/
String ds() default "";
//update-end---author:chenrui ---date:20231221 for[issues/#5643]解决分布式下表字典跨库无法查询问题------------
}

View File

@ -69,6 +69,8 @@ public interface CommonConstant {
/** {@code 500 Server Error} (HTTP/1.0 - RFC 1945) */
Integer SC_INTERNAL_SERVER_ERROR_500 = 500;
/** {@code 404 Not Found} (HTTP/1.0 - RFC 1945) */
Integer SC_INTERNAL_NOT_FOUND_404 = 404;
/** {@code 200 OK} (HTTP/1.0 - RFC 1945) */
Integer SC_OK_200 = 200;
@ -112,8 +114,8 @@ public interface CommonConstant {
String HAS_CANCLE = "2";
/**阅读状态0未读1已读*/
String HAS_READ_FLAG = "1";
String NO_READ_FLAG = "0";
Integer HAS_READ_FLAG = 1;
Integer NO_READ_FLAG = 0;
/**优先级L低M中H高*/
String PRIORITY_L = "L";
@ -160,6 +162,8 @@ public interface CommonConstant {
/**字典翻译文本后缀*/
String DICT_TEXT_SUFFIX = "_dictText";
/**字典翻译颜色后缀*/
String DICT_COLOR_SUFFIX = "_dictColor";
/**
* 表单设计器主表类型
@ -315,6 +319,8 @@ public interface CommonConstant {
String X_TIMESTAMP = "X-TIMESTAMP";
/** 租户请求头 更名为X-Tenant-Id */
String TENANT_ID = "X-Tenant-Id";
/** 简流接口请求头,用于排除不支持的控件字段 */
String X_MiniFlowExclusionFieldMode = "X-Miniflowexclusionfieldmode";
/**===============================================================================================*/
String TOKEN_IS_INVALID_MSG = "Token失效请重新登录!";
@ -371,6 +377,8 @@ public interface CommonConstant {
/**前端vue3版本Header参数名*/
String VERSION="X-Version";
String VERSION_V3 = "v3";
/**存储在线程变量里的动态表名*/
String DYNAMIC_TABLE_NAME="DYNAMIC_TABLE_NAME";
/**
@ -388,6 +396,7 @@ public interface CommonConstant {
/** 部门表唯一keyorgCode */
String DEPART_KEY_ORG_CODE = "orgCode";
/**======【消息推送相关】==============================================================================*/
/**
* 发消息 会传递一些信息到map
*/
@ -398,6 +407,11 @@ public interface CommonConstant {
*/
String NOTICE_MSG_BUS_ID = "NOTICE_MSG_BUS_ID";
/**
* 发消息 消息业务类型
*/
String NOTICE_MSG_BUS_TYPE = "NOTICE_MSG_BUS_TYPE";
/**
* 邮箱消息中地址登录时地址后携带的token,需要替换成真实的token值
*/
@ -420,6 +434,7 @@ public interface CommonConstant {
/** 消息模板markdown */
String MSG_TEMPLATE_TYPE_MD = "5";
/**========【消息推送相关】==========================================================================*/
/**
* 短信验证码redis-key的前缀
@ -481,6 +496,11 @@ public interface CommonConstant {
*/
String USER_TENANT_REFUSE = "4";
/**
* 用户租户状态(邀请)
*/
String USER_TENANT_INVITE = "5";
/**
* 不是叶子节点
*/
@ -490,4 +510,71 @@ public interface CommonConstant {
* 是叶子节点
*/
Integer IS_LEAF = 1;
/**
* 钉钉
*/
String DINGTALK = "DINGTALK";
/**
* 企业微信
*/
String WECHAT_ENTERPRISE = "WECHAT_ENTERPRISE";
/**
* 系统默认租户id 0
*/
Integer TENANT_ID_DEFAULT_VALUE = 0;
/**
* low-app用 应用级别的复制
*/
String COPY_LEVEL_APP = "app";
/**
* low-app用 菜单级别的复制
*/
String COPY_LEVEL_MENU = "menu";
/**
* low-app用 应用备份
*/
String COPY_LEVEL_BAK = "backup";
/**
* low-app用 从备份还原
*/
String COPY_LEVEL_COVER = "cover";
/** 【QQYUN-6034】关联字段变更历史值缓存半个小时 */
String CACHE_REL_FIELD_OLD_VAL = "sys:cache:desform:relFieldOldVal:";
/**
* 排序类型升序
*/
String ORDER_TYPE_ASC = "ASC";
/**
* 排序类型降序
*/
String ORDER_TYPE_DESC = "DESC";
//update-begin---author:scott ---date:2023-09-10 for积木报表常量----
/**
* 报表允许设计开发的角色
*/
public static String[] allowDevRoles = new String[]{"lowdeveloper", "admin"};
/**
* 对应积木报表的常量
* 数据隔离模式 按照创建人隔离
*/
public static final String SAAS_MODE_CREATED = "created";
/**
* 对应积木报表的常量
* 数据隔离模式 按照租户隔离
*/
public static final String SAAS_MODE_TENANT = "tenant";
//update-end---author:scott ---date::2023-09-10 for积木报表常量----
}

View File

@ -28,12 +28,21 @@ public interface CommonSendStatus {
public static final String APP_SESSION_SUFFIX = "_app";
/**-----【流程相关通知模板code】------------------------------------------------------------*/
/**流程催办——系统通知消息模板*/
public static final String TZMB_BPM_CUIBAN = "bpm_cuiban";
/**流程抄送——系统通知消息模板*/
public static final String TZMB_BPM_CC = "bpm_cc";
/**流程催办——邮件通知消息模板*/
public static final String TZMB_BPM_CUIBAN_EMAIL = "bpm_cuiban_email";
/**标准模板—系统消息通知*/
public static final String TZMB_SYS_TS_NOTE = "sys_ts_note";
/**流程超时提醒——系统通知消息模板*/
public static final String TZMB_BPM_CHAOSHI_TIP = "bpm_chaoshi_tip";
/**-----【流程相关通知模板code】-----------------------------------------------------------*/
/**
* 系统通知拓展参数比如用于流程抄送和催办通知这里额外传递流程跳转页面所需要的路由参数
*/
public static final String MSG_ABSTRACT_JSON = "msg_abstract";
}

View File

@ -17,6 +17,9 @@ public interface DataBaseConstant {
/**postgreSQL达梦数据库*/
public static final String DB_TYPE_POSTGRESQL = "POSTGRESQL";
/**人大金仓数据库*/
public static final String DB_TYPE_KINGBASEES = "KINGBASEES";
/**sqlserver数据库*/
public static final String DB_TYPE_SQLSERVER = "SQLSERVER";

View File

@ -116,4 +116,8 @@ public class SymbolConstant {
*/
public static final String SQUARE_BRACKETS_RIGHT = "]";
/**
* 拼接字符串符号 分号 ;
*/
public static final String SEMICOLON = ";";
}

View File

@ -28,7 +28,7 @@ public enum CgformEnum {
/**
* 多表 (erp风格)
*/
ERP(2, "erp", "/jeecg/code-template-online", "erp.onetomany", "ERP风格" ,new String[]{"vue3","vue"}),
ERP(2, "erp", "/jeecg/code-template-online", "erp.onetomany", "ERP风格" ,new String[]{"vue3","vue","vue3Native"}),
/**
* 多表内嵌子表风格
*/

View File

@ -1,4 +1,4 @@
package org.jeecg.common.util;
package org.jeecg.common.constant.enums;
import org.apache.commons.lang3.StringUtils;
@ -17,7 +17,11 @@ public enum DySmsEnum {
/**会议通知*/
MEET_NOTICE_TEMPLATE_CODE("SMS_201480469","JEECG","username,title,minute,time"),
/**我的计划通知*/
PLAN_NOTICE_TEMPLATE_CODE("SMS_201470515","JEECG","username,title,time");
PLAN_NOTICE_TEMPLATE_CODE("SMS_201470515","JEECG","username,title,time"),
/**支付成功短信通知*/
PAY_SUCCESS_NOTICE_CODE("SMS_461735163","敲敲云","realname,money,endTime"),
/**会员到期通知提醒*/
VIP_EXPIRE_NOTICE_CODE("SMS_461885023","敲敲云","realname,endTime");
/**
* 短信模板编码

View File

@ -0,0 +1,66 @@
package org.jeecg.common.constant.enums;
import org.jeecg.common.util.oConvertUtils;
/**
* 邮件html模板配置地址美剧
*
* @author: liusq
* @Date: 2023-10-13
*/
public enum EmailTemplateEnum {
/**
* 流程催办
*/
BPM_CUIBAN_EMAIL("bpm_cuiban_email", "/templates/email/bpm_cuiban_email.ftl"),
/**
* 流程新任务
*/
BPM_NEW_TASK_EMAIL("bpm_new_task_email", "/templates/email/bpm_new_task_email.ftl"),
/**
* 表单新增记录
*/
DESFORM_NEW_DATA_EMAIL("desform_new_data_email", "/templates/email/desform_new_data_email.ftl");
/**
* 模板名称
*/
private String name;
/**
* 模板地址
*/
private String url;
EmailTemplateEnum(String name, String url) {
this.name = name;
this.url = url;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public static EmailTemplateEnum getByName(String name) {
if (oConvertUtils.isEmpty(name)) {
return null;
}
for (EmailTemplateEnum val : values()) {
if (val.getName().equals(name)) {
return val;
}
}
return null;
}
}

View File

@ -6,7 +6,7 @@ import org.jeecg.common.util.oConvertUtils;
* 文件类型
*/
public enum FileTypeEnum {
// 文档类型folder:文件夹 excel:excel doc:word pp:ppt image:图片 archive:其他文档 video:视频
// 文档类型folder:文件夹 excel:excel doc:word pp:ppt image:图片 archive:其他文档 video:视频 voice:语音
// FOLDER
xls(".xls","excel","excel"),
xlsx(".xlsx","excel","excel"),
@ -26,7 +26,8 @@ public enum FileTypeEnum {
flv(".flv","video","视频"),
mp4(".mp4","video","视频"),
zip(".zip","zip","压缩包"),
pdf(".pdf","pdf","pdf");
pdf(".pdf","pdf","pdf"),
mp3(".mp3","mp3","语音");
private String type;
private String value;

View File

@ -1,30 +0,0 @@
package org.jeecg.common.constant.enums;
/**
* LowApp 切面注解枚举
* @date 2022-1-5
* @author: jeecg-boot
*/
public enum LowAppAopEnum {
/**
* 新增方法
*/
ADD,
/**
* 删除方法包含单个和批量删除
*/
DELETE,
/** 复制表单操作 */
COPY,
/**
* Online表单专用数据库表转Online表单
*/
CGFORM_DB_IMPORT,
/**
* 表单设计器专用子表转工作表
*/
DESFORM_SUB2WORK
}

View File

@ -13,12 +13,16 @@ import java.util.List;
public enum RoleIndexConfigEnum {
/**首页自定义 admin*/
ADMIN("admin", "dashboard/Analysis"),
// ADMIN("admin", "dashboard/Analysis"),
//TEST("test", "dashboard/IndexChart"),
/**首页自定义 hr*/
HR("hr", "dashboard/IndexBdc");
// HR("hr", "dashboard/IndexBdc");
//DM("dm", "dashboard/IndexTask"),
// 此值仅为防止报错无任何实际意义
ROLE_INDEX_CONFIG_ENUM("RoleIndexConfigEnumDefault", "dashboard/Analysis");
/**
* 角色编码
*/

View File

@ -1,4 +1,6 @@
package org.jeecg.common.util;
package org.jeecg.common.constant.enums;
import org.jeecg.common.util.oConvertUtils;
/**
* 系统公告自定义跳转方式
@ -12,7 +14,16 @@ public enum SysAnnmentTypeEnum {
/**
* 流程跳转到我的任务
*/
BPM("bpm", "url", "/bpm/task/MyTaskList");
BPM("bpm", "url", "/bpm/task/MyTaskList"),
/**
* 流程抄送任务
*/
BPM_VIEW("bpm_cc", "url", "/bpm/task/MyTaskList"),
/**
* 邀请用户跳转到个人设置
*/
TENANT_INVITE("tenant_invite", "url", "/system/usersetting");
/**
* 业务类型(email:邮件 bpm:流程)

View File

@ -1,4 +1,4 @@
package org.jeecg.modules.message.enums;
package org.jeecg.common.constant.enums;
import org.jeecg.common.system.annotation.EnumDict;
import org.jeecg.common.system.vo.DictModel;
@ -18,6 +18,16 @@ public enum Vue3MessageHrefEnum {
* 流程催办
*/
BPM("bpm", "/task/myHandleTaskInfo"),
/**
* 系统消息通知
*/
BPM_SYSTEM_MSG("bpm_msg_node", ""),
/**
* 流程抄送任务
*/
BPM_VIEW("bpm_cc", "/task/myHandleTaskInfo"),
/**
* 节点通知

View File

@ -1,5 +1,7 @@
package org.jeecg.common.exception;
import org.jeecg.common.constant.CommonConstant;
/**
* @Description: jeecg-boot自定义异常
* @author: jeecg-boot
@ -7,10 +9,24 @@ package org.jeecg.common.exception;
public class JeecgBootException extends RuntimeException {
private static final long serialVersionUID = 1L;
/**
* 返回给前端的错误code
*/
private int errCode = CommonConstant.SC_INTERNAL_SERVER_ERROR_500;
public JeecgBootException(String message){
super(message);
}
public JeecgBootException(String message, int errCode){
super(message);
this.errCode = errCode;
}
public int getErrCode() {
return errCode;
}
public JeecgBootException(Throwable cause)
{
super(cause);

View File

@ -1,6 +1,7 @@
package org.jeecg.common.exception;
import cn.hutool.core.util.ObjectUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authz.AuthorizationException;
import org.apache.shiro.authz.UnauthorizedException;
import org.jeecg.common.api.vo.Result;
@ -16,8 +17,6 @@ import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.multipart.MaxUploadSizeExceededException;
import org.springframework.web.servlet.NoHandlerFoundException;
import lombok.extern.slf4j.Slf4j;
/**
* 异常处理器
*
@ -34,7 +33,7 @@ public class JeecgBootExceptionHandler {
@ExceptionHandler(JeecgBootException.class)
public Result<?> handleJeecgBootException(JeecgBootException e){
log.error(e.getMessage(), e);
return Result.error(e.getMessage());
return Result.error(e.getErrCode(), e.getMessage());
}
/**
@ -133,4 +132,24 @@ public class JeecgBootExceptionHandler {
return Result.error("Redis 连接异常!");
}
/**
* SQL注入风险全局异常处理
*
* @param exception
* @return
*/
@ExceptionHandler(JeecgSqlInjectionException.class)
public Result<?> handleSQLException(Exception exception) {
String msg = exception.getMessage().toLowerCase();
final String extractvalue = "extractvalue";
final String updatexml = "updatexml";
boolean hasSensitiveInformation = msg.indexOf(extractvalue) >= 0 || msg.indexOf(updatexml) >= 0;
if (msg != null && hasSensitiveInformation) {
log.error("校验失败存在SQL注入风险{}", msg);
return Result.error("校验失败存在SQL注入风险");
}
return Result.error("校验失败存在SQL注入风险" + msg);
}
}

View File

@ -0,0 +1,23 @@
package org.jeecg.common.exception;
/**
* @Description: jeecg-boot自定义SQL注入异常
* @author: jeecg-boot
*/
public class JeecgSqlInjectionException extends RuntimeException {
private static final long serialVersionUID = 1L;
public JeecgSqlInjectionException(String message){
super(message);
}
public JeecgSqlInjectionException(Throwable cause)
{
super(cause);
}
public JeecgSqlInjectionException(String message, Throwable cause)
{
super(message,cause);
}
}

View File

@ -19,7 +19,6 @@ import org.jeecgframework.poi.excel.entity.ImportParams;
import org.jeecgframework.poi.excel.entity.enmus.ExcelType;
import org.jeecgframework.poi.excel.view.JeecgEntityExcelView;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.MultipartHttpServletRequest;
import org.springframework.web.servlet.ModelAndView;
@ -29,7 +28,6 @@ import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.*;
import java.util.stream.Collectors;
/**
* @Description: Controller基类
@ -70,7 +68,7 @@ public class JeecgController<T, S extends IService<T>> {
mv.addObject(NormalExcelConstants.FILE_NAME, title);
mv.addObject(NormalExcelConstants.CLASS, clazz);
//update-begin--Author:liusq Date:20210126 for图片导出报错ImageBasePath未设置--------------------
ExportParams exportParams=new ExportParams(title + "报表", "导出人:" + sysUser.getRealname(), title);
ExportParams exportParams=new ExportParams(title + "报表", "导出人:" + sysUser.getRealname(), title);
exportParams.setImageBasePath(jeecgBaseConfig.getPath().getUpload());
//update-end--Author:liusq Date:20210126 for图片导出报错ImageBasePath未设置----------------------
mv.addObject(NormalExcelConstants.PARAMS,exportParams);
@ -110,7 +108,7 @@ public class JeecgController<T, S extends IService<T>> {
IPage<T> pageList = service.page(page, queryWrapper);
List<T> exportList = pageList.getRecords();
Map<String, Object> map = new HashMap<>(5);
ExportParams exportParams=new ExportParams(title + "报表", "导出人:" + sysUser.getRealname(), title+i,jeecgBaseConfig.getPath().getUpload());
ExportParams exportParams=new ExportParams(title + "报表", "导出人:" + sysUser.getRealname(), title+i,jeecgBaseConfig.getPath().getUpload());
exportParams.setType(ExcelType.XSSF);
//map.put("title",exportParams);
//表格Title

View File

@ -20,6 +20,14 @@ public class QueryCondition implements Serializable {
private String dbType;
private String rule;
private String val;
public QueryCondition(String field, String type, String dbType, String rule, String val) {
this.field = field;
this.type = type;
this.dbType = dbType;
this.rule = rule;
this.val = val;
}
public String getField() {
return field;

View File

@ -19,11 +19,9 @@ import org.jeecg.common.constant.SymbolConstant;
import org.jeecg.common.exception.JeecgBootException;
import org.jeecg.common.system.util.JeecgDataAutorUtils;
import org.jeecg.common.system.util.JwtUtil;
import org.jeecg.common.system.util.SqlConcatUtil;
import org.jeecg.common.system.vo.SysPermissionDataRuleModel;
import org.jeecg.common.util.CommonUtils;
import org.jeecg.common.util.DateUtils;
import org.jeecg.common.util.SqlInjectionUtil;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.common.util.*;
import org.springframework.util.NumberUtils;
import com.alibaba.fastjson.JSON;
@ -143,7 +141,7 @@ public class QueryGenerator {
}
Object value = PropertyUtils.getSimpleProperty(searchObj, name);
column = getTableFieldName(searchObj.getClass(), name);
column = ReflectHelper.getTableFieldName(searchObj.getClass(), name);
if(column==null){
//column为null只有一种情况 那就是 添加了注解@TableField(exist = false) 后续都不用处理了
continue;
@ -283,15 +281,9 @@ public class QueryGenerator {
// 将现有排序 _ 前端传递排序条件{....,column: 'column1,column2',order: 'desc'} 翻译成sql "column1,column2 desc"
// 修改为 _ 前端传递排序条件{....,column: 'column1,column2',order: 'desc'} 翻译成sql "column1 desc,column2 desc"
if (order.toUpperCase().indexOf(ORDER_TYPE_ASC)>=0) {
//queryWrapper.orderByAsc(oConvertUtils.camelToUnderline(column));
String columnStr = oConvertUtils.camelToUnderline(column);
String[] columnArray = columnStr.split(",");
queryWrapper.orderByAsc(Arrays.asList(columnArray));
queryWrapper.orderByAsc(SqlInjectionUtil.getSqlInjectSortFields(column.split(",")));
} else {
//queryWrapper.orderByDesc(oConvertUtils.camelToUnderline(column));
String columnStr = oConvertUtils.camelToUnderline(column);
String[] columnArray = columnStr.split(",");
queryWrapper.orderByDesc(Arrays.asList(columnArray));
queryWrapper.orderByDesc(SqlInjectionUtil.getSqlInjectSortFields(column.split(",")));
}
//update-end--Author:scott Date:20210531 for36 多条件排序无效问题修正-------
}
@ -347,7 +339,7 @@ public class QueryGenerator {
return;
}
// update-end-author:sunjianlei date:20220119 for: JTC-573 过滤空条件查询防止 sql 拼接多余的 and
log.info("---高级查询参数-->" + filterConditions);
log.debug("---高级查询参数-->" + filterConditions);
queryWrapper.and(andWrapper -> {
for (int i = 0; i < filterConditions.size(); i++) {
@ -641,11 +633,11 @@ public class QueryGenerator {
* @param value 查询条件值
*/
public static void addEasyQuery(QueryWrapper<?> queryWrapper, String name, QueryRuleEnum rule, Object value) {
if (value == null || rule == null || oConvertUtils.isEmpty(value)) {
if (name==null || value == null || rule == null || oConvertUtils.isEmpty(value)) {
return;
}
name = oConvertUtils.camelToUnderline(name);
log.info("---查询过滤器Query规则---field:{}, rule:{}, value:{}",name,rule.getValue(),value);
log.debug("---高级查询 Query规则---field:{} , rule:{} , value:{}",name,rule.getValue(),value);
switch (rule) {
case GT:
queryWrapper.gt(name, value);
@ -713,7 +705,14 @@ public class QueryGenerator {
*/
public static Map<String, SysPermissionDataRuleModel> getRuleMap() {
Map<String, SysPermissionDataRuleModel> ruleMap = new HashMap<>(5);
List<SysPermissionDataRuleModel> list =JeecgDataAutorUtils.loadDataSearchConditon();
List<SysPermissionDataRuleModel> list = null;
//update-begin-author:taoyan date:2023-6-1 for:QQYUN-5441 简流获取多个用户/部门/角色 设置部门查询 报错
try {
list = JeecgDataAutorUtils.loadDataSearchConditon();
}catch (Exception e){
log.error("根据request对象获取权限数据失败可能是定时任务中执行的。", e);
}
//update-end-author:taoyan date:2023-6-1 for:QQYUN-5441 简流获取多个用户/部门/角色 设置部门查询 报错
if(list != null&&list.size()>0){
if(list.get(0)==null){
return ruleMap;
@ -821,223 +820,7 @@ public class QueryGenerator {
* @return
*/
public static String getSingleQueryConditionSql(String field,String alias,Object value,boolean isString) {
return getSingleQueryConditionSql(field, alias, value, isString,null);
}
/**
* 报表获取查询条件 支持多数据源
* @param field
* @param alias
* @param value
* @param isString
* @param dataBaseType
* @return
*/
public static String getSingleQueryConditionSql(String field,String alias,Object value,boolean isString, String dataBaseType) {
if (value == null) {
return "";
}
field = alias+oConvertUtils.camelToUnderline(field);
QueryRuleEnum rule = QueryGenerator.convert2Rule(value);
return getSingleSqlByRule(rule, field, value, isString, dataBaseType);
}
/**
* 获取单个查询条件的值
* @param rule
* @param field
* @param value
* @param isString
* @param dataBaseType
* @return
*/
private static String getSingleSqlByRule(QueryRuleEnum rule,String field,Object value,boolean isString, String dataBaseType) {
String res = "";
switch (rule) {
case GT:
res =field+rule.getValue()+getFieldConditionValue(value, isString, dataBaseType);
break;
case GE:
res = field+rule.getValue()+getFieldConditionValue(value, isString, dataBaseType);
break;
case LT:
res = field+rule.getValue()+getFieldConditionValue(value, isString, dataBaseType);
break;
case LE:
res = field+rule.getValue()+getFieldConditionValue(value, isString, dataBaseType);
break;
case EQ:
res = field+rule.getValue()+getFieldConditionValue(value, isString, dataBaseType);
break;
case EQ_WITH_ADD:
res = field+" = "+getFieldConditionValue(value, isString, dataBaseType);
break;
case NE:
res = field+" <> "+getFieldConditionValue(value, isString, dataBaseType);
break;
case IN:
res = field + " in "+getInConditionValue(value, isString);
break;
case LIKE:
res = field + " like "+getLikeConditionValue(value, QueryRuleEnum.LIKE);
break;
case LEFT_LIKE:
res = field + " like "+getLikeConditionValue(value, QueryRuleEnum.LEFT_LIKE);
break;
case RIGHT_LIKE:
res = field + " like "+getLikeConditionValue(value, QueryRuleEnum.RIGHT_LIKE);
break;
default:
res = field+" = "+getFieldConditionValue(value, isString, dataBaseType);
break;
}
return res;
}
/**
* 获取单个查询条件的值
* @param rule
* @param field
* @param value
* @param isString
* @return
*/
private static String getSingleSqlByRule(QueryRuleEnum rule,String field,Object value,boolean isString) {
return getSingleSqlByRule(rule, field, value, isString, null);
}
/**
* 获取查询条件的值
* @param value
* @param isString
* @param dataBaseType
* @return
*/
private static String getFieldConditionValue(Object value,boolean isString, String dataBaseType) {
String str = value.toString().trim();
if(str.startsWith(SymbolConstant.EXCLAMATORY_MARK)) {
str = str.substring(1);
}else if(str.startsWith(QueryRuleEnum.GE.getValue())) {
str = str.substring(2);
}else if(str.startsWith(QueryRuleEnum.LE.getValue())) {
str = str.substring(2);
}else if(str.startsWith(QueryRuleEnum.GT.getValue())) {
str = str.substring(1);
}else if(str.startsWith(QueryRuleEnum.LT.getValue())) {
str = str.substring(1);
}else if(str.indexOf(QUERY_COMMA_ESCAPE)>0) {
str = str.replaceAll("\\+\\+", COMMA);
}
if(dataBaseType==null){
dataBaseType = getDbType();
}
if(isString) {
if(DataBaseConstant.DB_TYPE_SQLSERVER.equals(dataBaseType)){
return " N'"+str+"' ";
}else{
return " '"+str+"' ";
}
}else {
// 如果不是字符串 有一种特殊情况 popup调用都走这个逻辑 参数传递的可能是admin这种格式的
if(DataBaseConstant.DB_TYPE_SQLSERVER.equals(dataBaseType) && str.endsWith(SymbolConstant.SINGLE_QUOTATION_MARK) && str.startsWith(SymbolConstant.SINGLE_QUOTATION_MARK)){
return " N"+str;
}
return value.toString();
}
}
private static String getInConditionValue(Object value,boolean isString) {
//update-begin-author:taoyan date:20210628 for: 查询条件如果输入,导致sql报错
String[] temp = value.toString().split(",");
if(temp.length==0){
return "('')";
}
if(isString) {
List<String> res = new ArrayList<>();
for (String string : temp) {
if(DataBaseConstant.DB_TYPE_SQLSERVER.equals(getDbType())){
res.add("N'"+string+"'");
}else{
res.add("'"+string+"'");
}
}
return "("+String.join("," ,res)+")";
}else {
return "("+value.toString()+")";
}
//update-end-author:taoyan date:20210628 for: 查询条件如果输入,导致sql报错
}
/**
* 先根据值判断 走左模糊还是右模糊
* 最后如果值不带任何标识(*或者%)则再根据ruleEnum判断
* @param value
* @param ruleEnum
* @return
*/
private static String getLikeConditionValue(Object value, QueryRuleEnum ruleEnum) {
String str = value.toString().trim();
if(str.startsWith(SymbolConstant.ASTERISK) && str.endsWith(SymbolConstant.ASTERISK)) {
if(DataBaseConstant.DB_TYPE_SQLSERVER.equals(getDbType())){
return "N'%"+str.substring(1,str.length()-1)+"%'";
}else{
return "'%"+str.substring(1,str.length()-1)+"%'";
}
}else if(str.startsWith(SymbolConstant.ASTERISK)) {
if(DataBaseConstant.DB_TYPE_SQLSERVER.equals(getDbType())){
return "N'%"+str.substring(1)+"'";
}else{
return "'%"+str.substring(1)+"'";
}
}else if(str.endsWith(SymbolConstant.ASTERISK)) {
if(DataBaseConstant.DB_TYPE_SQLSERVER.equals(getDbType())){
return "N'"+str.substring(0,str.length()-1)+"%'";
}else{
return "'"+str.substring(0,str.length()-1)+"%'";
}
}else {
if(str.indexOf(SymbolConstant.PERCENT_SIGN)>=0) {
if(DataBaseConstant.DB_TYPE_SQLSERVER.equals(getDbType())){
if(str.startsWith(SymbolConstant.SINGLE_QUOTATION_MARK) && str.endsWith(SymbolConstant.SINGLE_QUOTATION_MARK)){
return "N"+str;
}else{
return "N"+"'"+str+"'";
}
}else{
if(str.startsWith(SymbolConstant.SINGLE_QUOTATION_MARK) && str.endsWith(SymbolConstant.SINGLE_QUOTATION_MARK)){
return str;
}else{
return "'"+str+"'";
}
}
}else {
//update-begin-author:taoyan date:2022-6-30 for: issues/3810 数据权限规则问题
// 走到这里说明 value不带有任何模糊查询的标识(*或者%)
if (ruleEnum == QueryRuleEnum.LEFT_LIKE) {
if (DataBaseConstant.DB_TYPE_SQLSERVER.equals(getDbType())) {
return "N'%" + str + "'";
} else {
return "'%" + str + "'";
}
} else if (ruleEnum == QueryRuleEnum.RIGHT_LIKE) {
if (DataBaseConstant.DB_TYPE_SQLSERVER.equals(getDbType())) {
return "N'" + str + "%'";
} else {
return "'" + str + "%'";
}
} else {
if (DataBaseConstant.DB_TYPE_SQLSERVER.equals(getDbType())) {
return "N'%" + str + "%'";
} else {
return "'%" + str + "%'";
}
}
//update-end-author:taoyan date:2022-6-30 for: issues/3810 数据权限规则问题
}
}
return SqlConcatUtil.getSingleQueryConditionSql(field, alias, value, isString,null);
}
/**
@ -1064,7 +847,7 @@ public class QueryGenerator {
continue;
}
if(ruleMap.containsKey(name)) {
column = getTableFieldName(clazz, name);
column = ReflectHelper.getTableFieldName(clazz, name);
if(column==null){
continue;
}
@ -1078,7 +861,7 @@ public class QueryGenerator {
}else {
value = NumberUtils.parseNumber(dataRule.getRuleValue(),propType);
}
String filedSql = getSingleSqlByRule(rule, oConvertUtils.camelToUnderline(column), value,isString);
String filedSql = SqlConcatUtil.getSingleSqlByRule(rule, oConvertUtils.camelToUnderline(column), value,isString);
sb.append(sqlAnd+filedSql);
}
}
@ -1107,7 +890,7 @@ public class QueryGenerator {
if (judgedIsUselessField(name)) {
continue;
}
column = getTableFieldName(clazz, name);
column = ReflectHelper.getTableFieldName(clazz, name);
if(column==null){
continue;
}
@ -1126,42 +909,6 @@ public class QueryGenerator {
return getSqlRuleValue(sql);
}
/**
* 获取所有配置的权限 返回sql字符串 不受字段限制 配置什么就拿到什么
* @return
*/
public static String getAllConfigAuth() {
StringBuffer sb = new StringBuffer();
//权限查询
Map<String,SysPermissionDataRuleModel> ruleMap = getRuleMap();
String sqlAnd = " and ";
for (String c : ruleMap.keySet()) {
SysPermissionDataRuleModel dataRule = ruleMap.get(c);
String ruleValue = dataRule.getRuleValue();
if(oConvertUtils.isEmpty(ruleValue)){
continue;
}
if(oConvertUtils.isNotEmpty(c) && c.startsWith(SQL_RULES_COLUMN)){
sb.append(sqlAnd+getSqlRuleValue(ruleValue));
}else{
boolean isString = false;
ruleValue = ruleValue.trim();
if(ruleValue.startsWith("'") && ruleValue.endsWith("'")){
isString = true;
ruleValue = ruleValue.substring(1,ruleValue.length()-1);
}
QueryRuleEnum rule = QueryRuleEnum.getByValue(dataRule.getRuleConditions());
String value = converRuleValue(ruleValue);
String filedSql = getSingleSqlByRule(rule, c, value,isString);
sb.append(sqlAnd+filedSql);
}
}
log.info("query auth sql is = "+sb.toString());
return sb.toString();
}
/**
* 获取系统数据库类型
*/
@ -1169,71 +916,6 @@ public class QueryGenerator {
return CommonUtils.getDatabaseType();
}
/**
* 获取class的 包括父类的
* @param clazz
* @return
*/
private static List<Field> getClassFields(Class<?> clazz) {
List<Field> list = new ArrayList<Field>();
Field[] fields;
do{
fields = clazz.getDeclaredFields();
for(int i = 0;i<fields.length;i++){
list.add(fields[i]);
}
clazz = clazz.getSuperclass();
}while(clazz!= Object.class&&clazz!=null);
return list;
}
/**
* 获取表字段名
* @param clazz
* @param name
* @return
*/
private static String getTableFieldName(Class<?> clazz, String name) {
try {
//如果字段加注解了@TableField(exist = false),不走DB查询
Field field = null;
try {
field = clazz.getDeclaredField(name);
} catch (NoSuchFieldException e) {
//e.printStackTrace();
}
//如果为空则去父类查找字段
if (field == null) {
List<Field> allFields = getClassFields(clazz);
List<Field> searchFields = allFields.stream().filter(a -> a.getName().equals(name)).collect(Collectors.toList());
if(searchFields!=null && searchFields.size()>0){
field = searchFields.get(0);
}
}
if (field != null) {
TableField tableField = field.getAnnotation(TableField.class);
if (tableField != null){
if(tableField.exist() == false){
//如果设置了TableField false 这个字段不需要处理
return null;
}else{
String column = tableField.value();
//如果设置了TableField value 这个字段是实体字段
if(!"".equals(column)){
return column;
}
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
return name;
}
/**
* mysql 模糊查询之特殊字符下划线 _\
*

View File

@ -25,12 +25,19 @@ public enum QueryRuleEnum {
IN("IN","in","包含"),
/**查询规则 全模糊*/
LIKE("LIKE","like","全模糊"),
/**查询规则 不模糊包含*/
NOT_LIKE("NOT_LIKE","not_like","不模糊包含"),
/**查询规则 左模糊*/
LEFT_LIKE("LEFT_LIKE","left_like","左模糊"),
/**查询规则 右模糊*/
RIGHT_LIKE("RIGHT_LIKE","right_like","右模糊"),
/**查询规则 带加号等于*/
EQ_WITH_ADD("EQWITHADD","eq_with_add","带加号等于"),
/**查询规则 多词模糊匹配*/
LIKE_WITH_AND("LIKEWITHAND","like_with_and","多词模糊匹配————暂时未用上"),
/**查询规则 自定义SQL片段*/
SQL_RULES("USE_SQL_RULES","ext","自定义SQL片段"),
// ------- 当前表单设计器内专用 -------
/** 值为空 */
EMPTY("EMPTY","empty","值为空"),
@ -38,15 +45,12 @@ public enum QueryRuleEnum {
NOT_EMPTY("NOT_EMPTY","not_empty","值不为空"),
/**查询规则 不包含*/
NOT_IN("NOT_IN","not_in","不包含"),
// ------- 当前表单设计器内专用 -------
/**查询规则 多词模糊匹配*/
LIKE_WITH_AND("LIKEWITHAND","like_with_and","多词模糊匹配————暂时未用上"),
/**查询规则 自定义SQL片段*/
SQL_RULES("USE_SQL_RULES","ext","自定义SQL片段"),
/**查询规则 多词匹配*/
ELE_MATCH("ELE_MATCH","elemMatch","多词匹配"),
/**查询规则 范围查询*/
RANGE("RANGE","range","范围查询");
RANGE("RANGE","range","范围查询"),
NOT_RANGE("NOT_RANGE","not_range","不在范围查询");
// ------- 当前表单设计器内专用 -------
private String value;
@ -89,7 +93,7 @@ public enum QueryRuleEnum {
return null;
}
for(QueryRuleEnum val :values()){
if (val.getValue().equals(value) || val.getCondition().equals(value)){
if (val.getValue().equals(value) || val.getCondition().equalsIgnoreCase(value)){
return val;
}
}

View File

@ -16,6 +16,7 @@ import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.SecurityUtils;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.constant.CommonConstant;
@ -34,6 +35,7 @@ import org.jeecg.common.util.oConvertUtils;
* @Date 2018-07-12 14:23
* @Desc JWT工具类
**/
@Slf4j
public class JwtUtil {
/**Token有效期为7天Token在reids中缓存时间为两倍*/
@ -163,15 +165,24 @@ public class JwtUtil {
* @param user
* @return
*/
public static String getUserSystemData(String key,SysUserCacheInfo user) {
public static String getUserSystemData(String key, SysUserCacheInfo user) {
//1.优先获取 SysUserCacheInfo
if(user==null) {
user = JeecgDataAutorUtils.loadUserInfo();
try {
user = JeecgDataAutorUtils.loadUserInfo();
} catch (Exception e) {
log.warn("获取用户信息异常:" + e.getMessage());
}
}
//2.通过shiro获取登录用户信息
LoginUser sysUser = null;
try {
sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();
} catch (Exception e) {
log.warn("SecurityUtils.getSubject() 获取用户信息异常:" + e.getMessage());
}
//#{sys_user_code}%
// 获取登录用户信息
LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();
String moshi = "";
String wellNumber = WELL_NUMBER;
if(key.indexOf(SymbolConstant.RIGHT_CURLY_BRACKET)!=-1){
@ -184,6 +195,24 @@ public class JwtUtil {
} else {
key = key;
}
//替换为当前系统时间(年月日)
if (key.equals(DataBaseConstant.SYS_DATE)|| key.toLowerCase().equals(DataBaseConstant.SYS_DATE_TABLE)) {
returnValue = DateUtils.formatDate();
}
//替换为当前系统时间年月日时分秒
else if (key.equals(DataBaseConstant.SYS_TIME)|| key.toLowerCase().equals(DataBaseConstant.SYS_TIME_TABLE)) {
returnValue = DateUtils.now();
}
//流程状态默认值默认未发起
else if (key.equals(DataBaseConstant.BPM_STATUS)|| key.toLowerCase().equals(DataBaseConstant.BPM_STATUS_TABLE)) {
returnValue = "1";
}
//后台任务获取用户信息异常导致程序中断
if(sysUser==null && user==null){
return null;
}
//替换为系统登录用户帐号
if (key.equals(DataBaseConstant.SYS_USER_CODE)|| key.toLowerCase().equals(DataBaseConstant.SYS_USER_CODE_TABLE)) {
if(user==null) {
@ -222,21 +251,13 @@ public class JwtUtil {
}
}
}
//替换为当前系统时间(年月日)
else if (key.equals(DataBaseConstant.SYS_DATE)|| key.toLowerCase().equals(DataBaseConstant.SYS_DATE_TABLE)) {
returnValue = DateUtils.formatDate();
}
//替换为当前系统时间年月日时分秒
else if (key.equals(DataBaseConstant.SYS_TIME)|| key.toLowerCase().equals(DataBaseConstant.SYS_TIME_TABLE)) {
returnValue = DateUtils.now();
}
//流程状态默认值默认未发起
else if (key.equals(DataBaseConstant.BPM_STATUS)|| key.toLowerCase().equals(DataBaseConstant.BPM_STATUS_TABLE)) {
returnValue = "1";
}
//update-begin-author:taoyan date:20210330 for:多租户ID作为系统变量
else if (key.equals(TenantConstant.TENANT_ID) || key.toLowerCase().equals(TenantConstant.TENANT_ID_TABLE)){
returnValue = SpringContextUtils.getHttpServletRequest().getHeader(CommonConstant.TENANT_ID);
try {
returnValue = SpringContextUtils.getHttpServletRequest().getHeader(CommonConstant.TENANT_ID);
} catch (Exception e) {
log.warn("获取系统租户异常:" + e.getMessage());
}
}
//update-end-author:taoyan date:20210330 for:多租户ID作为系统变量
if(returnValue!=null){returnValue = returnValue + moshi;}

View File

@ -0,0 +1,243 @@
package org.jeecg.common.system.util;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.constant.DataBaseConstant;
import org.jeecg.common.constant.SymbolConstant;
import org.jeecg.common.system.query.QueryGenerator;
import org.jeecg.common.system.query.QueryRuleEnum;
import org.jeecg.common.util.CommonUtils;
import org.jeecg.common.util.oConvertUtils;
import java.util.ArrayList;
import java.util.List;
/**
* @Description: 查询过滤器SQL拼接写法拆成独立工具类
* @author:qinfeng
* @date 20230904
*/
@Slf4j
public class SqlConcatUtil {
/**
* 获取单个查询条件的值
* @param rule
* @param field
* @param value
* @param isString
* @return
*/
public static String getSingleSqlByRule(QueryRuleEnum rule,String field,Object value,boolean isString) {
return getSingleSqlByRule(rule, field, value, isString, null);
}
/**
* 报表获取查询条件 支持多数据源
* @param field
* @param alias
* @param value
* @param isString
* @param dataBaseType
* @return
*/
public static String getSingleQueryConditionSql(String field,String alias,Object value,boolean isString, String dataBaseType) {
if (value == null) {
return "";
}
field = alias+oConvertUtils.camelToUnderline(field);
QueryRuleEnum rule = QueryGenerator.convert2Rule(value);
return getSingleSqlByRule(rule, field, value, isString, dataBaseType);
}
/**
* 获取单个查询条件的值
* @param rule
* @param field
* @param value
* @param isString
* @param dataBaseType
* @return
*/
private static String getSingleSqlByRule(QueryRuleEnum rule,String field,Object value,boolean isString, String dataBaseType) {
String res = "";
switch (rule) {
case GT:
res =field+rule.getValue()+getFieldConditionValue(value, isString, dataBaseType);
break;
case GE:
res = field+rule.getValue()+getFieldConditionValue(value, isString, dataBaseType);
break;
case LT:
res = field+rule.getValue()+getFieldConditionValue(value, isString, dataBaseType);
break;
case LE:
res = field+rule.getValue()+getFieldConditionValue(value, isString, dataBaseType);
break;
case EQ:
res = field+rule.getValue()+getFieldConditionValue(value, isString, dataBaseType);
break;
case EQ_WITH_ADD:
res = field+" = "+getFieldConditionValue(value, isString, dataBaseType);
break;
case NE:
res = field+" <> "+getFieldConditionValue(value, isString, dataBaseType);
break;
case IN:
res = field + " in "+getInConditionValue(value, isString);
break;
case LIKE:
res = field + " like "+getLikeConditionValue(value, QueryRuleEnum.LIKE);
break;
case LEFT_LIKE:
res = field + " like "+getLikeConditionValue(value, QueryRuleEnum.LEFT_LIKE);
break;
case RIGHT_LIKE:
res = field + " like "+getLikeConditionValue(value, QueryRuleEnum.RIGHT_LIKE);
break;
default:
res = field+" = "+getFieldConditionValue(value, isString, dataBaseType);
break;
}
return res;
}
/**
* 获取查询条件的值
* @param value
* @param isString
* @param dataBaseType
* @return
*/
private static String getFieldConditionValue(Object value,boolean isString, String dataBaseType) {
String str = value.toString().trim();
if(str.startsWith(SymbolConstant.EXCLAMATORY_MARK)) {
str = str.substring(1);
}else if(str.startsWith(QueryRuleEnum.GE.getValue())) {
str = str.substring(2);
}else if(str.startsWith(QueryRuleEnum.LE.getValue())) {
str = str.substring(2);
}else if(str.startsWith(QueryRuleEnum.GT.getValue())) {
str = str.substring(1);
}else if(str.startsWith(QueryRuleEnum.LT.getValue())) {
str = str.substring(1);
}else if(str.indexOf(QueryGenerator.QUERY_COMMA_ESCAPE)>0) {
str = str.replaceAll("\\+\\+", SymbolConstant.COMMA);
}
if(dataBaseType==null){
dataBaseType = getDbType();
}
if(isString) {
if(DataBaseConstant.DB_TYPE_SQLSERVER.equals(dataBaseType)){
return " N'"+str+"' ";
}else{
return " '"+str+"' ";
}
}else {
// 如果不是字符串 有一种特殊情况 popup调用都走这个逻辑 参数传递的可能是admin这种格式的
if(DataBaseConstant.DB_TYPE_SQLSERVER.equals(dataBaseType) && str.endsWith(SymbolConstant.SINGLE_QUOTATION_MARK) && str.startsWith(SymbolConstant.SINGLE_QUOTATION_MARK)){
return " N"+str;
}
return value.toString();
}
}
private static String getInConditionValue(Object value,boolean isString) {
//update-begin-author:taoyan date:20210628 for: 查询条件如果输入,导致sql报错
String[] temp = value.toString().split(",");
if(temp.length==0){
return "('')";
}
if(isString) {
List<String> res = new ArrayList<>();
for (String string : temp) {
if(DataBaseConstant.DB_TYPE_SQLSERVER.equals(getDbType())){
res.add("N'"+string+"'");
}else{
res.add("'"+string+"'");
}
}
return "("+String.join("," ,res)+")";
}else {
return "("+value.toString()+")";
}
//update-end-author:taoyan date:20210628 for: 查询条件如果输入,导致sql报错
}
/**
* 先根据值判断 走左模糊还是右模糊
* 最后如果值不带任何标识(*或者%)则再根据ruleEnum判断
* @param value
* @param ruleEnum
* @return
*/
private static String getLikeConditionValue(Object value, QueryRuleEnum ruleEnum) {
String str = value.toString().trim();
if(str.startsWith(SymbolConstant.ASTERISK) && str.endsWith(SymbolConstant.ASTERISK)) {
if(DataBaseConstant.DB_TYPE_SQLSERVER.equals(getDbType())){
return "N'%"+str.substring(1,str.length()-1)+"%'";
}else{
return "'%"+str.substring(1,str.length()-1)+"%'";
}
}else if(str.startsWith(SymbolConstant.ASTERISK)) {
if(DataBaseConstant.DB_TYPE_SQLSERVER.equals(getDbType())){
return "N'%"+str.substring(1)+"'";
}else{
return "'%"+str.substring(1)+"'";
}
}else if(str.endsWith(SymbolConstant.ASTERISK)) {
if(DataBaseConstant.DB_TYPE_SQLSERVER.equals(getDbType())){
return "N'"+str.substring(0,str.length()-1)+"%'";
}else{
return "'"+str.substring(0,str.length()-1)+"%'";
}
}else {
if(str.indexOf(SymbolConstant.PERCENT_SIGN)>=0) {
if(DataBaseConstant.DB_TYPE_SQLSERVER.equals(getDbType())){
if(str.startsWith(SymbolConstant.SINGLE_QUOTATION_MARK) && str.endsWith(SymbolConstant.SINGLE_QUOTATION_MARK)){
return "N"+str;
}else{
return "N"+"'"+str+"'";
}
}else{
if(str.startsWith(SymbolConstant.SINGLE_QUOTATION_MARK) && str.endsWith(SymbolConstant.SINGLE_QUOTATION_MARK)){
return str;
}else{
return "'"+str+"'";
}
}
}else {
//update-begin-author:taoyan date:2022-6-30 for: issues/3810 数据权限规则问题
// 走到这里说明 value不带有任何模糊查询的标识(*或者%)
if (ruleEnum == QueryRuleEnum.LEFT_LIKE) {
if (DataBaseConstant.DB_TYPE_SQLSERVER.equals(getDbType())) {
return "N'%" + str + "'";
} else {
return "'%" + str + "'";
}
} else if (ruleEnum == QueryRuleEnum.RIGHT_LIKE) {
if (DataBaseConstant.DB_TYPE_SQLSERVER.equals(getDbType())) {
return "N'" + str + "%'";
} else {
return "'" + str + "%'";
}
} else {
if (DataBaseConstant.DB_TYPE_SQLSERVER.equals(getDbType())) {
return "N'%" + str + "%'";
} else {
return "'%" + str + "%'";
}
}
//update-end-author:taoyan date:2022-6-30 for: issues/3810 数据权限规则问题
}
}
}
/**
* 获取系统数据库类型
*/
private static String getDbType() {
return CommonUtils.getDatabaseType();
}
}

View File

@ -2,6 +2,7 @@ package org.jeecg.common.system.vo;
import java.io.Serializable;
import com.alibaba.fastjson.JSONObject;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Data;
@ -26,7 +27,13 @@ public class DictModel implements Serializable{
this.value = value;
this.text = text;
}
public DictModel(String value, String text, String color) {
this.value = value;
this.text = text;
this.color = color;
}
/**
* 字典value
*/
@ -35,6 +42,10 @@ public class DictModel implements Serializable{
* 字典文本
*/
private String text;
/**
* 字典颜色
*/
private String color;
/**
* 特殊用途 JgEditableTable
@ -50,4 +61,11 @@ public class DictModel implements Serializable{
return this.text;
}
/**
* 用于表单设计器 关联记录表数据存储
* QQYUN-5595表单设计器他表字段 导入没有翻译
*/
private JSONObject jsonObject;
}

View File

@ -1,17 +1,14 @@
package org.jeecg.common.system.vo;
import java.util.Date;
import com.fasterxml.jackson.annotation.JsonIgnore;
import io.swagger.annotations.ApiModelProperty;
import org.jeecg.common.desensitization.annotation.SensitiveField;
import org.springframework.format.annotation.DateTimeFormat;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import org.jeecg.common.desensitization.annotation.SensitiveField;
import org.springframework.format.annotation.DateTimeFormat;
import java.util.Date;
/**
* <p>
@ -53,6 +50,7 @@ public class LoginUser {
/**
* 当前登录部门code
*/
@SensitiveField
private String orgCode;
/**
* 头像
@ -63,7 +61,6 @@ public class LoginUser {
/**
* 生日
*/
@SensitiveField
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd")
@DateTimeFormat(pattern = "yyyy-MM-dd")
private Date birthday;
@ -109,6 +106,7 @@ public class LoginUser {
/**
* 管理部门ids
*/
@SensitiveField
private String departIds;
/**
@ -124,6 +122,7 @@ public class LoginUser {
private String telephone;
/** 多租户ids临时用不持久化数据库(数据库字段不存在) */
@SensitiveField
private String relTenantIds;
/**设备id uniapp推送用*/
@ -131,7 +130,7 @@ public class LoginUser {
private String openId;
@ApiModelProperty(value = "是否是vip")
/**是否是vip*/
@ApiModelProperty(value = "是否是vip")
private boolean vipFlag=false;
}

View File

@ -19,6 +19,8 @@ public class SysFilesModel {
private String storeType;
/**文件大小kb*/
private Double fileSize;
/**租户id*/
private String tenantId;
public String getId() {
return id;
@ -67,4 +69,12 @@ public class SysFilesModel {
public void setFileSize(Double fileSize) {
this.fileSize = fileSize;
}
public String getTenantId() {
return tenantId;
}
public void setTenantId(String tenantId) {
this.tenantId = tenantId;
}
}

View File

@ -0,0 +1,61 @@
package org.jeecg.common.system.vo;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import org.jeecg.common.desensitization.annotation.SensitiveField;
import org.springframework.format.annotation.DateTimeFormat;
import java.util.Date;
/**
* <p>
* 在线用户信息
* </p>
*
* @Author scott
* @since 2023-08-16
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
public class UserAccountInfo {
/**
* 登录人id
*/
private String id;
/**
* 登录人账号
*/
private String username;
/**
* 登录人名字
*/
private String realname;
/**
* 电子邮件
*/
private String email;
/**
* 头像
*/
@SensitiveField
private String avatar;
/**
* 同步工作流引擎1同步0不同步
*/
private Integer activitiSync;
/**
* 电话
*/
@SensitiveField
private String phone;
}

View File

@ -1,16 +1,18 @@
package org.jeecg.common.util;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DataSourceProperty;
import com.baomidou.dynamic.datasource.creator.DataSourceProperty;
import com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DynamicDataSourceProperties;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.toolkit.JdbcUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.jeecg.common.constant.CommonConstant;
import org.jeecg.common.constant.DataBaseConstant;
import org.jeecg.common.constant.ServiceNameConstants;
import org.jeecg.common.constant.SymbolConstant;
import org.jeecg.common.util.filter.FileTypeFilter;
import org.jeecg.common.exception.JeecgBootException;
import org.jeecg.common.util.filter.SsrfFileTypeFilter;
import org.jeecg.common.util.oss.OssBootUtil;
import org.jeecgframework.poi.util.PoiPublicUtil;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
@ -26,7 +28,9 @@ import java.io.InputStream;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.SQLException;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@ -136,6 +140,7 @@ public class CommonUtils {
}
} catch (Exception e) {
log.error(e.getMessage(), e);
throw new JeecgBootException(e.getMessage());
}
return url;
}
@ -148,7 +153,7 @@ public class CommonUtils {
public static String uploadLocal(MultipartFile mf,String bizPath,String uploadpath){
try {
//update-begin-author:liusq date:20210809 for: 过滤上传文件类型
FileTypeFilter.fileTypeFilter(mf);
SsrfFileTypeFilter.checkUploadFileType(mf);
//update-end-author:liusq date:20210809 for: 过滤上传文件类型
String fileName = null;
File file = new File(uploadpath + File.separator + bizPath + File.separator );
@ -299,7 +304,7 @@ public class CommonUtils {
DB_TYPE = DataBaseConstant.DB_TYPE_ORACLE;
}else if(dbType.indexOf(DataBaseConstant.DB_TYPE_SQLSERVER)>=0||dbType.indexOf(sqlserver)>=0) {
DB_TYPE = DataBaseConstant.DB_TYPE_SQLSERVER;
}else if(dbType.indexOf(DataBaseConstant.DB_TYPE_POSTGRESQL)>=0) {
}else if(dbType.indexOf(DataBaseConstant.DB_TYPE_POSTGRESQL)>=0 || dbType.indexOf(DataBaseConstant.DB_TYPE_KINGBASEES)>=0) {
DB_TYPE = DataBaseConstant.DB_TYPE_POSTGRESQL;
}else if(dbType.indexOf(DataBaseConstant.DB_TYPE_MARIADB)>=0) {
DB_TYPE = DataBaseConstant.DB_TYPE_MARIADB;
@ -392,4 +397,91 @@ public class CommonUtils {
return target;
}
/**
* 将list集合以分割符的方式进行分割
* @param list String类型的集合文本
* @param separator 分隔符
* @return
*/
public static String getSplitText(List<String> list, String separator) {
if (null != list && list.size() > 0) {
return StringUtils.join(list, separator);
}
return "";
}
/**
* 通过table的条件SQL
*
* @param tableSql sys_user where name = '1212'
* @return name = '1212'
*/
public static String getFilterSqlByTableSql(String tableSql) {
if(oConvertUtils.isEmpty(tableSql)){
return null;
}
if (tableSql.toLowerCase().indexOf(DataBaseConstant.SQL_WHERE) > 0) {
String[] arr = tableSql.split(" (?i)where ");
if (arr != null && oConvertUtils.isNotEmpty(arr[1])) {
return arr[1];
}
}
return "";
}
/**
* 通过table获取表名
*
* @param tableSql sys_user where name = '1212'
* @return sys_user
*/
public static String getTableNameByTableSql(String tableSql) {
if(oConvertUtils.isEmpty(tableSql)){
return null;
}
if (tableSql.toLowerCase().indexOf(DataBaseConstant.SQL_WHERE) > 0) {
String[] arr = tableSql.split(" (?i)where ");
return arr[0].trim();
} else {
return tableSql;
}
}
/**
* 判断两个数组是否存在交集
* @param set1
* @param arr2
* @return
*/
public static boolean hasIntersection(Set<String> set1, String[] arr2) {
if (set1 == null) {
return false;
}
if(set1.size()>0){
for (String str : arr2) {
if (set1.contains(str)) {
return true;
}
}
}
return false;
}
/**
* 输出info日志会捕获异常防止因为日志问题导致程序异常
*
* @param msg
* @param objects
*/
public static void logInfo(String msg, Object... objects) {
try {
log.info(msg, objects);
} catch (Exception e) {
log.warn("{} —— {}", msg, e.getMessage());
}
}
}

View File

@ -1,9 +1,5 @@
package org.jeecg.common.util;
import org.jeecg.config.StaticConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.alibaba.fastjson.JSONObject;
import com.aliyuncs.DefaultAcsClient;
import com.aliyuncs.IAcsClient;
@ -12,6 +8,10 @@ import com.aliyuncs.dysmsapi.model.v20170525.SendSmsResponse;
import com.aliyuncs.exceptions.ClientException;
import com.aliyuncs.profile.DefaultProfile;
import com.aliyuncs.profile.IClientProfile;
import org.jeecg.common.constant.enums.DySmsEnum;
import org.jeecg.config.StaticConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Created on 17/6/7.
@ -55,15 +55,15 @@ public class DySmsHelper {
}
public static boolean sendSms(String phone,JSONObject templateParamJson,DySmsEnum dySmsEnum) throws ClientException {
public static boolean sendSms(String phone, JSONObject templateParamJson, DySmsEnum dySmsEnum) throws ClientException {
//可自助调整超时时间
System.setProperty("sun.net.client.defaultConnectTimeout", "10000");
System.setProperty("sun.net.client.defaultReadTimeout", "10000");
//update-begin-authortaoyan date:20200811 for:配置类数据获取
StaticConfig staticConfig = SpringContextUtils.getBean(StaticConfig.class);
logger.info("阿里大鱼短信秘钥 accessKeyId" + staticConfig.getAccessKeyId());
logger.info("阿里大鱼短信秘钥 accessKeySecret"+ staticConfig.getAccessKeySecret());
//logger.info("阿里大鱼短信秘钥 accessKeyId" + staticConfig.getAccessKeyId());
//logger.info("阿里大鱼短信秘钥 accessKeySecret"+ staticConfig.getAccessKeySecret());
setAccessKeyId(staticConfig.getAccessKeyId());
setAccessKeySecret(staticConfig.getAccessKeySecret());
//update-end-authortaoyan date:20200811 for:配置类数据获取

View File

@ -4,7 +4,7 @@ import io.minio.*;
import io.minio.http.Method;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.constant.SymbolConstant;
import org.jeecg.common.util.filter.FileTypeFilter;
import org.jeecg.common.util.filter.SsrfFileTypeFilter;
import org.jeecg.common.util.filter.StrAttackFilter;
import org.springframework.web.multipart.MultipartFile;
@ -60,7 +60,7 @@ public class MinioUtil {
//update-end-author:wangshuai date:20201012 for: 过滤上传文件夹名特殊字符防止攻击
//update-begin-author:liusq date:20210809 for: 过滤上传文件类型
FileTypeFilter.fileTypeFilter(file);
SsrfFileTypeFilter.checkUploadFileType(file);
//update-end-author:liusq date:20210809 for: 过滤上传文件类型
String newBucket = bucketName;

View File

@ -1,5 +1,6 @@
package org.jeecg.common.util;
import com.baomidou.mybatisplus.annotation.TableField;
import lombok.extern.slf4j.Slf4j;
import java.lang.reflect.Field;
@ -7,6 +8,7 @@ import java.lang.reflect.Method;
import java.util.*;
import java.util.Map.Entry;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
/**
* @author 张代浩
@ -252,4 +254,86 @@ public class ReflectHelper {
return value;
}
/**
* 判断给定的字段是不是类中的属性
* @param field 字段名
* @param clazz 类对象
* @return
*/
public static boolean isClassField(String field, Class clazz){
Field[] fields = clazz.getDeclaredFields();
for(int i=0;i<fields.length;i++){
String fieldName = fields[i].getName();
String tableColumnName = oConvertUtils.camelToUnderline(fieldName);
if(fieldName.equalsIgnoreCase(field) || tableColumnName.equalsIgnoreCase(field)){
return true;
}
}
return false;
}
/**
* 获取class的 包括父类的
* @param clazz
* @return
*/
public static List<Field> getClassFields(Class<?> clazz) {
List<Field> list = new ArrayList<Field>();
Field[] fields;
do{
fields = clazz.getDeclaredFields();
for(int i = 0;i<fields.length;i++){
list.add(fields[i]);
}
clazz = clazz.getSuperclass();
}while(clazz!= Object.class&&clazz!=null);
return list;
}
/**
* 获取表字段名
* @param clazz
* @param name
* @return
*/
public static String getTableFieldName(Class<?> clazz, String name) {
try {
//如果字段加注解了@TableField(exist = false),不走DB查询
Field field = null;
try {
field = clazz.getDeclaredField(name);
} catch (NoSuchFieldException e) {
//e.printStackTrace();
}
//如果为空则去父类查找字段
if (field == null) {
List<Field> allFields = getClassFields(clazz);
List<Field> searchFields = allFields.stream().filter(a -> a.getName().equals(name)).collect(Collectors.toList());
if(searchFields!=null && searchFields.size()>0){
field = searchFields.get(0);
}
}
if (field != null) {
TableField tableField = field.getAnnotation(TableField.class);
if (tableField != null){
if(tableField.exist() == false){
//如果设置了TableField false 这个字段不需要处理
return null;
}else{
String column = tableField.value();
//如果设置了TableField value 这个字段是实体字段
if(!"".equals(column)){
return column;
}
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
return name;
}
}

View File

@ -1,11 +1,12 @@
package org.jeecg.common.util;
import cn.hutool.crypto.SecureUtil;
import cn.hutool.core.util.ReUtil;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.exception.JeecgBootException;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Field;
import java.util.Set;
import org.jeecg.common.constant.CommonConstant;
import org.jeecg.common.constant.SymbolConstant;
import org.jeecg.common.exception.JeecgSqlInjectionException;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@ -15,61 +16,74 @@ import java.util.regex.Pattern;
* @author zhoujf
*/
@Slf4j
public class SqlInjectionUtil {
public class SqlInjectionUtil {
/**
* sign 用于表字典加签的盐值SQL漏洞
* 上线修改值 20200501同步修改前端的盐值
* 默认sql注入关键词
*/
private final static String TABLE_DICT_SIGN_SALT = "20200501";
private final static String XSS_STR = "and |extractvalue|updatexml|geohash|gtid_subset|gtid_subtract|exec |insert |select |delete |update |drop |count |chr |mid |master |truncate |char |declare |;|or |+|user()";
private final static String XSS_STR = "and |exec |peformance_schema|information_schema|extractvalue|updatexml|geohash|gtid_subset|gtid_subtract|insert |select |delete |update |drop |count |chr |mid |master |truncate |char |declare |;|or |+|--";
/**
* 正则 user() 匹配更严谨
* online报表专用sql注入关键词
*/
private final static String REGULAR_EXPRE_USER = "user[\\s]*\\([\\s]*\\)";
/**正则 show tables*/
private final static String SHOW_TABLES = "show\\s+tables";
private static String specialReportXssStr = "exec |peformance_schema|information_schema|extractvalue|updatexml|geohash|gtid_subset|gtid_subtract|insert |alter |delete |grant |update |drop |master |truncate |declare |--";
/**
* sleep函数
* 字典专用sql注入关键词
*/
private final static Pattern FUN_SLEEP = Pattern.compile("sleep\\([\\d\\.]*\\)", Pattern.CASE_INSENSITIVE);
private static String specialDictSqlXssStr = "exec |peformance_schema|information_schema|extractvalue|updatexml|geohash|gtid_subset|gtid_subtract|insert |select |delete |update |drop |count |chr |mid |master |truncate |char |declare |;|+|--";
/**
* 完整匹配的key不需要考虑前空格
*/
private static List<String> FULL_MATCHING_KEYWRODS = new ArrayList<>();
static {
FULL_MATCHING_KEYWRODS.add(";");
FULL_MATCHING_KEYWRODS.add("+");
FULL_MATCHING_KEYWRODS.add("--");
}
/**
* sql注入风险的 正则关键字
*
* 函数匹配需要用正则模式
*/
private final static String[] XSS_REGULAR_STR_ARRAY = new String[]{
"chr\\s*\\(",
"mid\\s*\\(",
" char\\s*\\(",
"sleep\\s*\\(",
"user\\s*\\(",
"show\\s+tables",
"user[\\s]*\\([\\s]*\\)",
"show\\s+databases",
"sleep\\(\\d*\\)",
"sleep\\(.*\\)",
};
/**
* sql注释的正则
*/
private final static Pattern SQL_ANNOTATION = Pattern.compile("/\\*[\\s\\S]*\\*/");
private final static String SQL_ANNOTATION2 = "--";
/**
* 针对表字典进行额外的sign签名校验增加安全机制
* @param dictCode:
* @param sign:
* @param request:
* @Return: void
* sql注入提示语
*/
public static void checkDictTableSign(String dictCode, String sign, HttpServletRequest request) {
//表字典SQL注入漏洞,签名校验
String accessToken = request.getHeader("X-Access-Token");
String signStr = dictCode + SqlInjectionUtil.TABLE_DICT_SIGN_SALT + accessToken;
String javaSign = SecureUtil.md5(signStr);
if (!javaSign.equals(sign)) {
log.error("表字典SQL注入漏洞签名校验失败 " + sign + "!=" + javaSign+ ",dictCode=" + dictCode);
throw new JeecgBootException("无权限访问!");
}
log.info(" 表字典SQL注入漏洞签名校验成功sign=" + sign + ",dictCode=" + dictCode);
}
private final static String SQL_INJECTION_KEYWORD_TIP = "请注意存在SQL注入关键词---> {}";
private final static String SQL_INJECTION_TIP = "请注意值可能存在SQL注入风险!--->";
private final static String SQL_INJECTION_TIP_VARIABLE = "请注意值可能存在SQL注入风险!---> {}";
/**
* sql注入过滤处理遇到注入关键字抛异常
* @param value
* @param values
*/
public static void filterContent(String value) {
filterContent(value, null);
public static void filterContent(String... values) {
filterContent(values, null);
}
/**
* sql注入过滤处理遇到注入关键字抛异常
* 校验比较严格
*
* sql注入过滤处理遇到注入关键字抛异常
*
* @param value
* @return
*/
@ -77,47 +91,83 @@ public class SqlInjectionUtil {
if (value == null || "".equals(value)) {
return;
}
// 校验sql注释 不允许有sql注释
// 校验sql注释 不允许有sql注释
checkSqlAnnotation(value);
// 统一转为小写
value = value.toLowerCase();
//SQL注入检测存在绕过风险 https://gitee.com/jeecg/jeecg-boot/issues/I4NZGE
//value = value.replaceAll("/\\*.*\\*/","");
// 转为小写进行后续比较
value = value.toLowerCase().trim();
// SQL注入检测存在绕过风险 (普通文本校验)
//https://gitee.com/jeecg/jeecg-boot/issues/I4NZGE
String[] xssArr = XSS_STR.split("\\|");
for (int i = 0; i < xssArr.length; i++) {
if (value.indexOf(xssArr[i]) > -1) {
log.error("请注意存在SQL注入关键词---> {}", xssArr[i]);
log.error("请注意值可能存在SQL注入风险!---> {}", value);
throw new RuntimeException("请注意值可能存在SQL注入风险!--->" + value);
log.error(SqlInjectionUtil.SQL_INJECTION_KEYWORD_TIP, xssArr[i]);
log.error(SqlInjectionUtil.SQL_INJECTION_TIP_VARIABLE, value);
throw new JeecgSqlInjectionException(SqlInjectionUtil.SQL_INJECTION_TIP + value);
}
}
//update-begin-author:taoyan date:2022-7-13 for: 除了XSS_STR这些提前设置好的还需要额外的校验比如 单引号
// SQL注入检测存在绕过风险 (自定义传入普通文本校验)
if (customXssString != null) {
String[] xssArr2 = customXssString.split("\\|");
for (int i = 0; i < xssArr2.length; i++) {
if (value.indexOf(xssArr2[i]) > -1) {
log.error("请注意存在SQL注入关键词---> {}", xssArr2[i]);
log.error("请注意值可能存在SQL注入风险!---> {}", value);
throw new RuntimeException("请注意值可能存在SQL注入风险!--->" + value);
log.error(SqlInjectionUtil.SQL_INJECTION_KEYWORD_TIP, xssArr2[i]);
log.error(SqlInjectionUtil.SQL_INJECTION_TIP_VARIABLE, value);
throw new JeecgSqlInjectionException(SqlInjectionUtil.SQL_INJECTION_TIP + value);
}
}
}
//update-end-author:taoyan date:2022-7-13 for: 除了XSS_STR这些提前设置好的还需要额外的校验比如 单引号
if(Pattern.matches(SHOW_TABLES, value) || Pattern.matches(REGULAR_EXPRE_USER, value)){
throw new RuntimeException("请注意值可能存在SQL注入风险!--->" + value);
// SQL注入检测存在绕过风险 (正则校验)
for (String regularOriginal : XSS_REGULAR_STR_ARRAY) {
String regular = ".*" + regularOriginal + ".*";
if (Pattern.matches(regular, value)) {
log.error(SqlInjectionUtil.SQL_INJECTION_KEYWORD_TIP, regularOriginal);
log.error(SqlInjectionUtil.SQL_INJECTION_TIP_VARIABLE, value);
throw new JeecgSqlInjectionException(SqlInjectionUtil.SQL_INJECTION_TIP + value);
}
}
return;
}
/**
* sql注入过滤处理遇到注入关键字抛异常
* @param values
* 判断是否存在SQL注入关键词字符串
*
* @param keyword
* @return
*/
public static void filterContent(String[] values) {
filterContent(values, null);
@SuppressWarnings("AlibabaUndefineMagicConstant")
private static boolean isExistSqlInjectKeyword(String sql, String keyword) {
if (sql.startsWith(keyword.trim())) {
return true;
} else if (sql.contains(keyword)) {
// 需要匹配的sql注入关键词
String matchingText = " " + keyword;
if(FULL_MATCHING_KEYWRODS.contains(keyword)){
matchingText = keyword;
}
if (sql.contains(matchingText)) {
return true;
} else {
String regularStr = "\\s+\\S+" + keyword;
List<String> resultFindAll = ReUtil.findAll(regularStr, sql, 0, new ArrayList<String>());
for (String res : resultFindAll) {
log.info("isExistSqlInjectKeyword —- 匹配到的SQL注入关键词{}", res);
/**
* SQL注入中可以替换空格的字符(%09 %0A %0D +都可以替代空格)
* http://blog.chinaunix.net/uid-12501104-id-2932639.html
* https://www.cnblogs.com/Vinson404/p/7253255.html
* */
if (res.contains("%") || res.contains("+") || res.contains("#") || res.contains("/") || res.contains(")")) {
return true;
}
}
}
}
return false;
}
/**
* sql注入过滤处理遇到注入关键字抛异常
*
@ -125,40 +175,11 @@ public class SqlInjectionUtil {
* @return
*/
public static void filterContent(String[] values, String customXssString) {
String[] xssArr = XSS_STR.split("\\|");
for (String value : values) {
if (value == null || "".equals(value)) {
for (String val : values) {
if (oConvertUtils.isEmpty(val)) {
return;
}
// 校验sql注释 不允许有sql注释
checkSqlAnnotation(value);
// 统一转为小写
value = value.toLowerCase();
//SQL注入检测存在绕过风险 https://gitee.com/jeecg/jeecg-boot/issues/I4NZGE
//value = value.replaceAll("/\\*.*\\*/","");
for (int i = 0; i < xssArr.length; i++) {
if (value.indexOf(xssArr[i]) > -1) {
log.error("请注意存在SQL注入关键词---> {}", xssArr[i]);
log.error("请注意值可能存在SQL注入风险!---> {}", value);
throw new RuntimeException("请注意值可能存在SQL注入风险!--->" + value);
}
}
//update-begin-author:taoyan date:2022-7-13 for: 除了XSS_STR这些提前设置好的还需要额外的校验比如 单引号
if (customXssString != null) {
String[] xssArr2 = customXssString.split("\\|");
for (int i = 0; i < xssArr2.length; i++) {
if (value.indexOf(xssArr2[i]) > -1) {
log.error("请注意存在SQL注入关键词---> {}", xssArr2[i]);
log.error("请注意值可能存在SQL注入风险!---> {}", value);
throw new RuntimeException("请注意值可能存在SQL注入风险!--->" + value);
}
}
}
//update-end-author:taoyan date:2022-7-13 for: 除了XSS_STR这些提前设置好的还需要额外的校验比如 单引号
if(Pattern.matches(SHOW_TABLES, value) || Pattern.matches(REGULAR_EXPRE_USER, value)){
throw new RuntimeException("请注意值可能存在SQL注入风险!--->" + value);
}
filterContent(val, customXssString);
}
return;
}
@ -170,130 +191,230 @@ public class SqlInjectionUtil {
* @param value
* @return
*/
//@Deprecated
public static void specialFilterContentForDictSql(String value) {
String specialXssStr = " exec |extractvalue|updatexml|geohash|gtid_subset|gtid_subtract| insert | select | delete | update | drop | count | chr | mid | master | truncate | char | declare |;|+|user()";
String[] xssArr = specialXssStr.split("\\|");
String[] xssArr = specialDictSqlXssStr.split("\\|");
if (value == null || "".equals(value)) {
return;
}
// 校验sql注释 不允许有sql注释
// 校验sql注释 不允许有sql注释
checkSqlAnnotation(value);
// 统一转为小写
value = value.toLowerCase();
//SQL注入检测存在绕过风险 https://gitee.com/jeecg/jeecg-boot/issues/I4NZGE
//value = value.replaceAll("/\\*.*\\*/","");
value = value.toLowerCase().trim();
// SQL注入检测存在绕过风险 (普通文本校验)
for (int i = 0; i < xssArr.length; i++) {
if (value.indexOf(xssArr[i]) > -1 || value.startsWith(xssArr[i].trim())) {
log.error("请注意存在SQL注入关键词---> {}", xssArr[i]);
log.error("请注意值可能存在SQL注入风险!---> {}", value);
throw new RuntimeException("请注意值可能存在SQL注入风险!--->" + value);
if (isExistSqlInjectKeyword(value, xssArr[i])) {
log.error(SqlInjectionUtil.SQL_INJECTION_KEYWORD_TIP, xssArr[i]);
log.error(SqlInjectionUtil.SQL_INJECTION_TIP_VARIABLE, value);
throw new JeecgSqlInjectionException(SqlInjectionUtil.SQL_INJECTION_TIP + value);
}
}
if(Pattern.matches(SHOW_TABLES, value) || Pattern.matches(REGULAR_EXPRE_USER, value)){
throw new RuntimeException("请注意值可能存在SQL注入风险!--->" + value);
// SQL注入检测存在绕过风险 (正则校验)
for (String regularOriginal : XSS_REGULAR_STR_ARRAY) {
String regular = ".*" + regularOriginal + ".*";
if (Pattern.matches(regular, value)) {
log.error(SqlInjectionUtil.SQL_INJECTION_KEYWORD_TIP, regularOriginal);
log.error(SqlInjectionUtil.SQL_INJECTION_TIP_VARIABLE, value);
throw new JeecgSqlInjectionException(SqlInjectionUtil.SQL_INJECTION_TIP + value);
}
}
return;
}
/**
* 提醒不通用
* 仅用于Online报表SQL解析注入过滤
* @param value
* @return
*/
//@Deprecated
public static void specialFilterContentForOnlineReport(String value) {
String specialXssStr = " exec |extractvalue|updatexml|geohash|gtid_subset|gtid_subtract| insert | delete | update | drop | chr | mid | master | truncate | char | declare |user()";
String[] xssArr = specialXssStr.split("\\|");
String[] xssArr = specialReportXssStr.split("\\|");
if (value == null || "".equals(value)) {
return;
}
// 校验sql注释 不允许有sql注释
// 校验sql注释 不允许有sql注释
checkSqlAnnotation(value);
// 统一转为小写
value = value.toLowerCase();
//SQL注入检测存在绕过风险 https://gitee.com/jeecg/jeecg-boot/issues/I4NZGE
//value = value.replaceAll("/\\*.*\\*/"," ");
value = value.toLowerCase().trim();
// SQL注入检测存在绕过风险 (普通文本校验)
for (int i = 0; i < xssArr.length; i++) {
if (value.indexOf(xssArr[i]) > -1 || value.startsWith(xssArr[i].trim())) {
log.error("请注意存在SQL注入关键词---> {}", xssArr[i]);
log.error("请注意值可能存在SQL注入风险!---> {}", value);
throw new RuntimeException("请注意值可能存在SQL注入风险!--->" + value);
if (isExistSqlInjectKeyword(value, xssArr[i])) {
log.error(SqlInjectionUtil.SQL_INJECTION_KEYWORD_TIP, xssArr[i]);
log.error(SqlInjectionUtil.SQL_INJECTION_TIP_VARIABLE, value);
throw new JeecgSqlInjectionException(SqlInjectionUtil.SQL_INJECTION_TIP + value);
}
}
if(Pattern.matches(SHOW_TABLES, value) || Pattern.matches(REGULAR_EXPRE_USER, value)){
throw new RuntimeException("请注意值可能存在SQL注入风险!--->" + value);
// SQL注入检测存在绕过风险 (正则校验)
for (String regularOriginal : XSS_REGULAR_STR_ARRAY) {
String regular = ".*" + regularOriginal + ".*";
if (Pattern.matches(regular, value)) {
log.error(SqlInjectionUtil.SQL_INJECTION_KEYWORD_TIP, regularOriginal);
log.error(SqlInjectionUtil.SQL_INJECTION_TIP_VARIABLE, value);
throw new JeecgSqlInjectionException(SqlInjectionUtil.SQL_INJECTION_TIP + value);
}
}
return;
}
/**
* 判断给定的字段是不是类中的属性
* @param field 字段名
* @param clazz 类对象
* @return
*/
public static boolean isClassField(String field, Class clazz){
Field[] fields = clazz.getDeclaredFields();
for(int i=0;i<fields.length;i++){
String fieldName = fields[i].getName();
String tableColumnName = oConvertUtils.camelToUnderline(fieldName);
if(fieldName.equalsIgnoreCase(field) || tableColumnName.equalsIgnoreCase(field)){
return true;
}
}
return false;
}
/**
* 判断给定的多个字段是不是类中的属性
* @param fieldSet 字段名set
* @param clazz 类对象
* @return
*/
public static boolean isClassField(Set<String> fieldSet, Class clazz){
Field[] fields = clazz.getDeclaredFields();
for(String field: fieldSet){
boolean exist = false;
for(int i=0;i<fields.length;i++){
String fieldName = fields[i].getName();
String tableColumnName = oConvertUtils.camelToUnderline(fieldName);
if(fieldName.equalsIgnoreCase(field) || tableColumnName.equalsIgnoreCase(field)){
exist = true;
break;
}
}
if(!exist){
return false;
}
}
return true;
}
/**
* 校验是否有sql注释
* @return
*/
public static void checkSqlAnnotation(String str){
if(str.contains(SQL_ANNOTATION2)){
String error = "请注意SQL中不允许含注释有安全风险";
log.error(error);
throw new RuntimeException(error);
}
Matcher matcher = SQL_ANNOTATION.matcher(str);
if(matcher.find()){
String error = "请注意值可能存在SQL注入风险---> \\*.*\\";
log.error(error);
throw new RuntimeException(error);
}
// issues/4737 sys/duplicate/check SQL注入 #4737
Matcher sleepMatcher = FUN_SLEEP.matcher(str);
if(sleepMatcher.find()){
String error = "请注意值可能存在SQL注入风险---> sleep";
log.error(error);
throw new RuntimeException(error);
throw new JeecgSqlInjectionException(error);
}
}
/**
* 返回查询表名
* <p>
* sql注入过滤处理遇到注入关键字抛异常
*
* @param table
*/
private static Pattern tableNamePattern = Pattern.compile("^[a-zA-Z][a-zA-Z0-9_\\$]{0,63}$");
public static String getSqlInjectTableName(String table) {
if(oConvertUtils.isEmpty(table)){
return table;
}
table = table.trim();
/**
* 检验表名是否合法
*
* 表名只能由字母数字和下划线组成
* 表名必须以字母开头
* 表名长度通常有限制例如最多为 64 个字符
*/
boolean isValidTableName = tableNamePattern.matcher(table).matches();
if (!isValidTableName) {
String errorMsg = "表名不合法存在SQL注入风险!--->" + table;
log.error(errorMsg);
throw new JeecgSqlInjectionException(errorMsg);
}
//进一步验证是否存在SQL注入风险
filterContent(table);
return table;
}
/**
* 返回查询字段
* <p>
* sql注入过滤处理遇到注入关键字抛异常
*
* @param field
*/
static final Pattern fieldPattern = Pattern.compile("^[a-zA-Z0-9_]+$");
public static String getSqlInjectField(String field) {
if(oConvertUtils.isEmpty(field)){
return field;
}
field = field.trim();
if (field.contains(SymbolConstant.COMMA)) {
return getSqlInjectField(field.split(SymbolConstant.COMMA));
}
/**
* 校验表字段是否有效
*
* 字段定义只能是是字母 数字 下划线的组合不允许有空格转义字符串等
*/
boolean isValidField = fieldPattern.matcher(field).matches();
if (!isValidField) {
String errorMsg = "字段不合法存在SQL注入风险!--->" + field;
log.error(errorMsg);
throw new JeecgSqlInjectionException(errorMsg);
}
//进一步验证是否存在SQL注入风险
filterContent(field);
return field;
}
/**
* 获取多个字段
* 返回: 逗号拼接
*
* @param fields
* @return
*/
public static String getSqlInjectField(String... fields) {
for (String s : fields) {
getSqlInjectField(s);
}
return String.join(SymbolConstant.COMMA, fields);
}
/**
* 获取排序字段
* 返回字符串
*
* 1.将驼峰命名转化成下划线
* 2.限制sql注入
* @param sortField 排序字段
* @return
*/
public static String getSqlInjectSortField(String sortField) {
String field = SqlInjectionUtil.getSqlInjectField(oConvertUtils.camelToUnderline(sortField));
return field;
}
/**
* 获取多个排序字段
* 返回数组
*
* 1.将驼峰命名转化成下划线
* 2.限制sql注入
* @param sortFields 多个排序字段
* @return
*/
public static List getSqlInjectSortFields(String... sortFields) {
List list = new ArrayList<String>();
for (String sortField : sortFields) {
list.add(getSqlInjectSortField(sortField));
}
return list;
}
/**
* 获取 orderBy type
* 返回字符串
* <p>
* 1.检测是否为 asc desc 其中的一个
* 2.限制sql注入
*
* @param orderType
* @return
*/
public static String getSqlInjectOrderType(String orderType) {
if (orderType == null) {
return null;
}
orderType = orderType.trim();
if (CommonConstant.ORDER_TYPE_ASC.equalsIgnoreCase(orderType)) {
return CommonConstant.ORDER_TYPE_ASC;
} else {
return CommonConstant.ORDER_TYPE_DESC;
}
}
}

View File

@ -34,6 +34,21 @@ public class TokenUtils {
}
return token;
}
/**
* 获取 request 里传递的 token
* @return
*/
public static String getTokenByRequest() {
String token = null;
try {
HttpServletRequest request = SpringContextUtils.getHttpServletRequest();
token = TokenUtils.getTokenByRequest(request);
} catch (Exception e) {
//e.printStackTrace();
}
return token;
}
/**
* 获取 request 里传递的 tenantId (租户ID)

View File

@ -169,7 +169,7 @@ public class FreemarkerParseFactory {
//"where and"
String whereAnd = DataBaseConstant.SQL_WHERE+" and";
//", where"
String commaWhere = SymbolConstant.COMMA+" "+ DataBaseConstant.SQL_WHERE;
String commaWhere = SymbolConstant.COMMA+" "+DataBaseConstant.SQL_WHERE;
//", "
String commaSpace = SymbolConstant.COMMA + " ";
if (sql.endsWith(DataBaseConstant.SQL_WHERE) || sql.endsWith(whereSpace)) {

View File

@ -4,27 +4,72 @@ import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
/**
* @Description: 校验上传文件敏感后缀
* @Description: 校验文件敏感后缀
* @author: lsq
* @date: 2021年08月09 15:29
* @date: 2023年09月12 15:29
*/
@Slf4j
public class FileTypeFilter {
/**文件后缀*/
private static String[] forbidType = {"jsp","php"};
public class SsrfFileTypeFilter {
/**
* 允许操作文件类型白名单
*/
private final static List<String> FILE_TYPE_WHITE_LIST = new ArrayList<>();
/**初始化文件头类型,不够的自行补充*/
final static HashMap<String, String> FILE_TYPE_MAP = new HashMap<>();
static {
//图片文件
FILE_TYPE_WHITE_LIST.add("jpg");
FILE_TYPE_WHITE_LIST.add("jpeg");
FILE_TYPE_WHITE_LIST.add("png");
FILE_TYPE_WHITE_LIST.add("gif");
FILE_TYPE_WHITE_LIST.add("bmp");
FILE_TYPE_WHITE_LIST.add("svg");
FILE_TYPE_WHITE_LIST.add("ico");
//文本文件
FILE_TYPE_WHITE_LIST.add("txt");
FILE_TYPE_WHITE_LIST.add("doc");
FILE_TYPE_WHITE_LIST.add("docx");
FILE_TYPE_WHITE_LIST.add("pdf");
FILE_TYPE_WHITE_LIST.add("csv");
// FILE_TYPE_WHITE_LIST.add("xml");
//音视频文件
FILE_TYPE_WHITE_LIST.add("mp4");
FILE_TYPE_WHITE_LIST.add("avi");
FILE_TYPE_WHITE_LIST.add("mov");
FILE_TYPE_WHITE_LIST.add("wmv");
FILE_TYPE_WHITE_LIST.add("mp3");
FILE_TYPE_WHITE_LIST.add("wav");
//表格文件
FILE_TYPE_WHITE_LIST.add("xls");
FILE_TYPE_WHITE_LIST.add("xlsx");
//压缩文件
FILE_TYPE_WHITE_LIST.add("zip");
FILE_TYPE_WHITE_LIST.add("rar");
FILE_TYPE_WHITE_LIST.add("7z");
FILE_TYPE_WHITE_LIST.add("tar");
//app文件后缀
FILE_TYPE_WHITE_LIST.add("apk");
FILE_TYPE_WHITE_LIST.add("wgt");
//设置禁止文件的头部标记
FILE_TYPE_MAP.put("3c25402070616765206c", "jsp");
FILE_TYPE_MAP.put("3c3f7068700a0a2f2a2a0a202a205048", "php");
FILE_TYPE_MAP.put("cafebabe0000002e0041", "class");
FILE_TYPE_MAP.put("494e5345525420494e54", "sql");
/* fileTypeMap.put("ffd8ffe000104a464946", "jpg");
fileTypeMap.put("89504e470d0a1a0a0000", "png");
fileTypeMap.put("47494638396126026f01", "gif");
@ -89,17 +134,38 @@ public class FileTypeFilter {
return fileName.substring(fileName.lastIndexOf(".") + 1, fileName.length());
}
/**
* 文件类型过滤
* 下载文件类型过滤
*
* @param filePath
*/
public static void checkDownloadFileType(String filePath) throws IOException {
//文件后缀
String suffix = getFileTypeBySuffix(filePath);
log.info("suffix:{}", suffix);
boolean isAllowExtension = FILE_TYPE_WHITE_LIST.contains(suffix.toLowerCase());
//是否允许下载的文件
if (!isAllowExtension) {
throw new IOException("下载失败,存在非法文件类型:" + suffix);
}
}
/**
* 上传文件类型过滤
*
* @param file
*/
public static void fileTypeFilter(MultipartFile file) throws Exception {
public static void checkUploadFileType(MultipartFile file) throws Exception {
//获取文件真是后缀
String suffix = getFileType(file);
for (String type : forbidType) {
if (type.contains(suffix)) {
throw new Exception("上传失败,非法文件类型:" + suffix);
}
log.info("suffix:{}", suffix);
boolean isAllowExtension = FILE_TYPE_WHITE_LIST.contains(suffix.toLowerCase());
//是否允许下载的文件
if (!isAllowExtension) {
throw new Exception("上传失败,存在非法文件类型:" + suffix);
}
}

View File

@ -168,6 +168,17 @@ public class oConvertUtils {
}
}
public static Integer getInteger(Object object, Integer defval) {
if (isEmpty(object)) {
return (defval);
}
try {
return (Integer.parseInt(object.toString()));
} catch (NumberFormatException e) {
return (defval);
}
}
public static Integer getInt(Object object) {
if (isEmpty(object)) {
return null;
@ -702,9 +713,20 @@ public class oConvertUtils {
if (isArray(oldVal)) {
return equalityOfArrays((Object[]) oldVal, (Object[]) newVal);
}else if(oldVal instanceof JSONArray){
return equalityOfJSONArray((JSONArray) oldVal, (JSONArray) newVal);
if(newVal instanceof JSONArray){
return equalityOfJSONArray((JSONArray) oldVal, (JSONArray) newVal);
}else{
if (isEmpty(newVal) && (oldVal == null || ((JSONArray) oldVal).size() == 0)) {
return true;
}
List<Object> arrayStr = Arrays.asList(newVal.toString().split(","));
JSONArray newValArray = new JSONArray(arrayStr);
return equalityOfJSONArray((JSONArray) oldVal, newValArray);
}
}else{
return oldVal.equals(newVal);
}
return oldVal.equals(newVal);
} else {
if (oldVal == null && newVal == null) {
return true;
@ -742,7 +764,7 @@ public class oConvertUtils {
Object[] newValArray = newVal.toArray();
return equalityOfArrays(oldValArray,newValArray);
} else {
if (oldVal == null && newVal == null) {
if ((oldVal == null || oldVal.size() == 0) && (newVal == null || newVal.size() == 0)) {
return true;
} else {
return false;
@ -750,6 +772,38 @@ public class oConvertUtils {
}
}
/**
* 比较带逗号的字符串
* QQYUN-5212简流按日期触发 多选 人员组件 选择顺序不一致时 不触发应该是统一问题 包括多选部门组件
* @param oldVal
* @param newVal
* @return
*/
public static boolean equalityOfStringArrays(String oldVal, String newVal) {
if(oldVal.equals(newVal)){
return true;
}
if(oldVal.indexOf(",")>=0 && newVal.indexOf(",")>=0){
String[] arr1 = oldVal.split(",");
String[] arr2 = newVal.split(",");
if(arr1.length == arr2.length){
boolean flag = true;
Map<String, Integer> map = new HashMap<>();
for(String s1: arr1){
map.put(s1, 1);
}
for(String s2: arr2){
if(map.get(s2) == null){
flag = false;
break;
}
}
return flag;
}
}
return false;
}
/**
* 判断两个数组是否相等数组元素不分顺序
*
@ -763,7 +817,7 @@ public class oConvertUtils {
Arrays.sort(newVal);
return Arrays.equals(oldVal, newVal);
} else {
if (oldVal == null && newVal == null) {
if ((oldVal == null || oldVal.length == 0) && (newVal == null || newVal.length == 0)) {
return true;
} else {
return false;
@ -807,4 +861,85 @@ public class oConvertUtils {
}
return json;
}
/**
* 将List 转成 JSONArray
* @return
*/
public static JSONArray list2JSONArray(List<String> list){
if(list==null || list.size()==0){
return null;
}
JSONArray array = new JSONArray();
for(String str: list){
array.add(str);
}
return array;
}
/**
* 判断两个list中的元素是否完全一致
* QQYUN-5326简流获取组织人员 / 筛选条件 没有部门筛选
* @return
*/
public static boolean isEqList(List<String> list1, List<String> list2){
if(list1.size() != list2.size()){
return false;
}
for(String str1: list1){
boolean flag = false;
for(String str2: list2){
if(str1.equals(str2)){
flag = true;
break;
}
}
if(!flag){
return false;
}
}
return true;
}
/**
* 判断 list1中的元素是否在list2中出现
* QQYUN-5326简流获取组织人员 / 筛选条件 没有部门筛选
* @param list1
* @param list2
* @return
*/
public static boolean isInList(List<String> list1, List<String> list2){
for(String str1: list1){
boolean flag = false;
for(String str2: list2){
if(str1.equals(str2)){
flag = true;
break;
}
}
if(flag){
return true;
}
}
return false;
}
/**
* 计算文件大小转成MB
* @param uploadCount
* @return
*/
public static Double calculateFileSizeToMb(Long uploadCount){
double count = 0.0;
if(uploadCount>0) {
BigDecimal bigDecimal = new BigDecimal(uploadCount);
//换算成MB
BigDecimal divide = bigDecimal.divide(new BigDecimal(1048576));
count = divide.setScale(2, BigDecimal.ROUND_HALF_UP).doubleValue();
return count;
}
return count;
}
}

View File

@ -11,7 +11,7 @@ import org.apache.commons.fileupload.FileItemStream;
import org.jeecg.common.constant.CommonConstant;
import org.jeecg.common.constant.SymbolConstant;
import org.jeecg.common.util.CommonUtils;
import org.jeecg.common.util.filter.FileTypeFilter;
import org.jeecg.common.util.filter.SsrfFileTypeFilter;
import org.jeecg.common.util.filter.StrAttackFilter;
import org.jeecg.common.util.oConvertUtils;
import org.springframework.web.multipart.MultipartFile;
@ -98,7 +98,7 @@ public class OssBootUtil {
*/
public static String upload(MultipartFile file, String fileDir,String customBucket) throws Exception {
//update-begin-author:liusq date:20210809 for: 过滤上传文件类型
FileTypeFilter.fileTypeFilter(file);
SsrfFileTypeFilter.checkUploadFileType(file);
//update-end-author:liusq date:20210809 for: 过滤上传文件类型
String filePath = null;

View File

@ -2,6 +2,7 @@ package org.jeecg.common.util.security;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.jeecg.common.exception.JeecgSqlInjectionException;
import java.util.*;
import java.util.regex.Matcher;
@ -81,6 +82,12 @@ public abstract class AbstractQueryBlackListHandler {
}
}
// 返回黑名单校验结果不合法直接抛出异常
if(!flag){
log.error(this.getError());
throw new JeecgSqlInjectionException(this.getError());
}
return flag;
}

View File

@ -0,0 +1,33 @@
package org.jeecg.common.util.sqlInjection;
import net.sf.jsqlparser.parser.CCJSqlParserDefaultVisitor;
import net.sf.jsqlparser.parser.SimpleNode;
import net.sf.jsqlparser.statement.select.UnionOp;
import org.jeecg.common.exception.JeecgSqlInjectionException;
/**
* 基于抽象语法树(AST)的注入攻击分析实现
*
* @author guyadong
*/
public class InjectionAstNodeVisitor extends CCJSqlParserDefaultVisitor {
public InjectionAstNodeVisitor() {
}
/**
* 处理禁止联合查询
*
* @param node
* @param data
* @return
*/
@Override
public Object visit(SimpleNode node, Object data) {
Object value = node.jjtGetValue();
if (value instanceof UnionOp) {
throw new JeecgSqlInjectionException("DISABLE UNION");
}
return super.visit(node, data);
}
}

View File

@ -0,0 +1,172 @@
package org.jeecg.common.util.sqlInjection;
import net.sf.jsqlparser.expression.BinaryExpression;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.Function;
import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
import net.sf.jsqlparser.expression.operators.conditional.OrExpression;
import net.sf.jsqlparser.expression.operators.relational.ComparisonOperator;
import net.sf.jsqlparser.schema.Column;
import net.sf.jsqlparser.statement.select.Join;
import net.sf.jsqlparser.statement.select.OrderByElement;
import net.sf.jsqlparser.statement.select.PlainSelect;
import net.sf.jsqlparser.statement.select.SelectItem;
import net.sf.jsqlparser.statement.select.SubSelect;
import net.sf.jsqlparser.statement.select.WithItem;
import net.sf.jsqlparser.util.TablesNamesFinder;
import org.jeecg.common.exception.JeecgSqlInjectionException;
import org.jeecg.common.util.sqlInjection.parse.ConstAnalyzer;
import org.jeecg.common.util.sqlInjection.parse.ParserSupport;
/**
* 基于SQL语法对象的SQL注入攻击分析实现
*
* @author guyadong
*/
public class InjectionSyntaxObjectAnalyzer extends TablesNamesFinder {
/**
* 危险函数名
*/
private static final String DANGROUS_FUNCTIONS = "(sleep|benchmark|extractvalue|updatexml|ST_LatFromGeoHash|ST_LongFromGeoHash|GTID_SUBSET|GTID_SUBTRACT|floor|ST_Pointfromgeohash"
+ "|geometrycollection|multipoint|polygon|multipolygon|linestring|multilinestring)";
private static ThreadLocal<Boolean> disableSubselect = new ThreadLocal<Boolean>() {
@Override
protected Boolean initialValue() {
return true;
}
};
private ConstAnalyzer constAnalyzer = new ConstAnalyzer();
public InjectionSyntaxObjectAnalyzer() {
super();
init(true);
}
@Override
public void visitBinaryExpression(BinaryExpression binaryExpression) {
if (binaryExpression instanceof ComparisonOperator) {
if (isConst(binaryExpression.getLeftExpression()) && isConst(binaryExpression.getRightExpression())) {
/** 禁用恒等式 */
throw new JeecgSqlInjectionException("DISABLE IDENTICAL EQUATION " + binaryExpression);
}
}
super.visitBinaryExpression(binaryExpression);
}
@Override
public void visit(AndExpression andExpression) {
super.visit(andExpression);
checkConstExpress(andExpression.getLeftExpression());
checkConstExpress(andExpression.getRightExpression());
}
@Override
public void visit(OrExpression orExpression) {
super.visit(orExpression);
checkConstExpress(orExpression.getLeftExpression());
checkConstExpress(orExpression.getRightExpression());
}
@Override
public void visit(Function function) {
if (function.getName().matches(DANGROUS_FUNCTIONS)) {
/** 禁用危险函数 */
throw new JeecgSqlInjectionException("DANGROUS FUNCTION: " + function.getName());
}
super.visit(function);
}
@Override
public void visit(WithItem withItem) {
try {
/** 允许 WITH 语句中的子查询 */
disableSubselect.set(false);
super.visit(withItem);
} finally {
disableSubselect.set(true);
}
}
@Override
public void visit(SubSelect subSelect) {
try {
/** 允许语句中的子查询 */
disableSubselect.set(false);
super.visit(subSelect);
} finally {
disableSubselect.set(true);
}
// if (disableSubselect.get()) {
// // 禁用子查询
// throw new JeecgSqlInjectionException("DISABLE subselect " + subSelect);
// }
}
@Override
public void visit(Column tableColumn) {
if (ParserSupport.isBoolean(tableColumn)) {
throw new JeecgSqlInjectionException("DISABLE CONST BOOL " + tableColumn);
}
super.visit(tableColumn);
}
@Override
public void visit(PlainSelect plainSelect) {
if (plainSelect.getSelectItems() != null) {
for (SelectItem item : plainSelect.getSelectItems()) {
item.accept(this);
}
}
if (plainSelect.getFromItem() != null) {
plainSelect.getFromItem().accept(this);
}
if (plainSelect.getJoins() != null) {
for (Join join : plainSelect.getJoins()) {
join.getRightItem().accept(this);
for (Expression e : join.getOnExpressions()) {
e.accept(this);
}
}
}
if (plainSelect.getWhere() != null) {
plainSelect.getWhere().accept(this);
checkConstExpress(plainSelect.getWhere());
}
if (plainSelect.getHaving() != null) {
plainSelect.getHaving().accept(this);
}
if (plainSelect.getOracleHierarchical() != null) {
plainSelect.getOracleHierarchical().accept(this);
}
if (plainSelect.getOrderByElements() != null) {
for (OrderByElement orderByElement : plainSelect.getOrderByElements()) {
orderByElement.getExpression().accept(this);
}
}
if (plainSelect.getGroupBy() != null) {
for (Expression expression : plainSelect.getGroupBy().getGroupByExpressionList().getExpressions()) {
expression.accept(this);
}
}
}
private boolean isConst(Expression expression) {
return constAnalyzer.isConstExpression(expression);
}
private void checkConstExpress(Expression expression) {
if (constAnalyzer.isConstExpression(expression)) {
/** 禁用常量表达式 */
throw new JeecgSqlInjectionException("DISABLE CONST EXPRESSION " + expression);
}
}
}

View File

@ -0,0 +1,65 @@
package org.jeecg.common.util.sqlInjection;
import org.jeecg.common.exception.JeecgSqlInjectionException;
import org.jeecg.common.util.sqlInjection.parse.ParserSupport;
;
/**
* SQL注入攻击分析器
*
* @author guyadong
* 参考:
* https://blog.csdn.net/10km/article/details/127767358
* https://gitee.com/l0km/sql2java/tree/dev/sql2java-manager/src/main/java/gu/sql2java/parser
*/
public class SqlInjectionAnalyzer {
//启用/关闭注入攻击检查
private boolean injectCheckEnable = true;
//防止SQL注入攻击分析实现
private final InjectionSyntaxObjectAnalyzer injectionChecker;
private final InjectionAstNodeVisitor injectionVisitor;
public SqlInjectionAnalyzer() {
this.injectionChecker = new InjectionSyntaxObjectAnalyzer();
this.injectionVisitor = new InjectionAstNodeVisitor();
}
/**
* 启用/关闭注入攻击检查,默认启动
*
* @param enable
* @return
*/
public SqlInjectionAnalyzer injectCheckEnable(boolean enable) {
injectCheckEnable = enable;
return this;
}
/**
* 对解析后的SQL对象执行注入攻击分析有注入攻击的危险则抛出异常{@link JeecgSqlInjectionException}
*
* @param sqlParserInfo
* @throws JeecgSqlInjectionException
*/
public ParserSupport.SqlParserInfo injectAnalyse(ParserSupport.SqlParserInfo sqlParserInfo) throws JeecgSqlInjectionException {
if (null != sqlParserInfo && injectCheckEnable) {
/** SQL注入攻击检查 */
sqlParserInfo.statement.accept(injectionChecker);
sqlParserInfo.simpleNode.jjtAccept(injectionVisitor, null);
}
return sqlParserInfo;
}
/**
* sql校验
*/
public static void checkSql(String sql,boolean check){
SqlInjectionAnalyzer sqlInjectionAnalyzer = new SqlInjectionAnalyzer();
sqlInjectionAnalyzer.injectCheckEnable(check);
ParserSupport.SqlParserInfo sqlParserInfo = ParserSupport.parse0(sql, null,null);
sqlInjectionAnalyzer.injectAnalyse(sqlParserInfo);
}
}

View File

@ -0,0 +1,601 @@
package org.jeecg.common.util.sqlInjection.parse;
import net.sf.jsqlparser.expression.AllValue;
import net.sf.jsqlparser.expression.AnalyticExpression;
import net.sf.jsqlparser.expression.AnyComparisonExpression;
import net.sf.jsqlparser.expression.ArrayConstructor;
import net.sf.jsqlparser.expression.ArrayExpression;
import net.sf.jsqlparser.expression.BinaryExpression;
import net.sf.jsqlparser.expression.CaseExpression;
import net.sf.jsqlparser.expression.CastExpression;
import net.sf.jsqlparser.expression.CollateExpression;
import net.sf.jsqlparser.expression.ConnectByRootOperator;
import net.sf.jsqlparser.expression.DateTimeLiteralExpression;
import net.sf.jsqlparser.expression.DateValue;
import net.sf.jsqlparser.expression.DoubleValue;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.ExpressionVisitor;
import net.sf.jsqlparser.expression.ExtractExpression;
import net.sf.jsqlparser.expression.Function;
import net.sf.jsqlparser.expression.HexValue;
import net.sf.jsqlparser.expression.IntervalExpression;
import net.sf.jsqlparser.expression.JdbcNamedParameter;
import net.sf.jsqlparser.expression.JdbcParameter;
import net.sf.jsqlparser.expression.JsonAggregateFunction;
import net.sf.jsqlparser.expression.JsonExpression;
import net.sf.jsqlparser.expression.JsonFunction;
import net.sf.jsqlparser.expression.JsonFunctionExpression;
import net.sf.jsqlparser.expression.KeepExpression;
import net.sf.jsqlparser.expression.LongValue;
import net.sf.jsqlparser.expression.MySQLGroupConcat;
import net.sf.jsqlparser.expression.NextValExpression;
import net.sf.jsqlparser.expression.NotExpression;
import net.sf.jsqlparser.expression.NullValue;
import net.sf.jsqlparser.expression.NumericBind;
import net.sf.jsqlparser.expression.OracleHierarchicalExpression;
import net.sf.jsqlparser.expression.OracleHint;
import net.sf.jsqlparser.expression.OracleNamedFunctionParameter;
import net.sf.jsqlparser.expression.Parenthesis;
import net.sf.jsqlparser.expression.RowConstructor;
import net.sf.jsqlparser.expression.RowGetExpression;
import net.sf.jsqlparser.expression.SignedExpression;
import net.sf.jsqlparser.expression.StringValue;
import net.sf.jsqlparser.expression.TimeKeyExpression;
import net.sf.jsqlparser.expression.TimeValue;
import net.sf.jsqlparser.expression.TimestampValue;
import net.sf.jsqlparser.expression.TimezoneExpression;
import net.sf.jsqlparser.expression.TryCastExpression;
import net.sf.jsqlparser.expression.UserVariable;
import net.sf.jsqlparser.expression.ValueListExpression;
import net.sf.jsqlparser.expression.VariableAssignment;
import net.sf.jsqlparser.expression.WhenClause;
import net.sf.jsqlparser.expression.XMLSerializeExpr;
import net.sf.jsqlparser.expression.operators.arithmetic.Addition;
import net.sf.jsqlparser.expression.operators.arithmetic.BitwiseAnd;
import net.sf.jsqlparser.expression.operators.arithmetic.BitwiseLeftShift;
import net.sf.jsqlparser.expression.operators.arithmetic.BitwiseOr;
import net.sf.jsqlparser.expression.operators.arithmetic.BitwiseRightShift;
import net.sf.jsqlparser.expression.operators.arithmetic.BitwiseXor;
import net.sf.jsqlparser.expression.operators.arithmetic.Concat;
import net.sf.jsqlparser.expression.operators.arithmetic.Division;
import net.sf.jsqlparser.expression.operators.arithmetic.IntegerDivision;
import net.sf.jsqlparser.expression.operators.arithmetic.Modulo;
import net.sf.jsqlparser.expression.operators.arithmetic.Multiplication;
import net.sf.jsqlparser.expression.operators.arithmetic.Subtraction;
import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
import net.sf.jsqlparser.expression.operators.conditional.OrExpression;
import net.sf.jsqlparser.expression.operators.conditional.XorExpression;
import net.sf.jsqlparser.expression.operators.relational.Between;
import net.sf.jsqlparser.expression.operators.relational.EqualsTo;
import net.sf.jsqlparser.expression.operators.relational.ExistsExpression;
import net.sf.jsqlparser.expression.operators.relational.ExpressionList;
import net.sf.jsqlparser.expression.operators.relational.FullTextSearch;
import net.sf.jsqlparser.expression.operators.relational.GeometryDistance;
import net.sf.jsqlparser.expression.operators.relational.GreaterThan;
import net.sf.jsqlparser.expression.operators.relational.GreaterThanEquals;
import net.sf.jsqlparser.expression.operators.relational.InExpression;
import net.sf.jsqlparser.expression.operators.relational.IsBooleanExpression;
import net.sf.jsqlparser.expression.operators.relational.IsDistinctExpression;
import net.sf.jsqlparser.expression.operators.relational.IsNullExpression;
import net.sf.jsqlparser.expression.operators.relational.ItemsListVisitor;
import net.sf.jsqlparser.expression.operators.relational.JsonOperator;
import net.sf.jsqlparser.expression.operators.relational.LikeExpression;
import net.sf.jsqlparser.expression.operators.relational.Matches;
import net.sf.jsqlparser.expression.operators.relational.MinorThan;
import net.sf.jsqlparser.expression.operators.relational.MinorThanEquals;
import net.sf.jsqlparser.expression.operators.relational.MultiExpressionList;
import net.sf.jsqlparser.expression.operators.relational.NamedExpressionList;
import net.sf.jsqlparser.expression.operators.relational.NotEqualsTo;
import net.sf.jsqlparser.expression.operators.relational.RegExpMatchOperator;
import net.sf.jsqlparser.expression.operators.relational.RegExpMySQLOperator;
import net.sf.jsqlparser.expression.operators.relational.SimilarToExpression;
import net.sf.jsqlparser.schema.Column;
import net.sf.jsqlparser.statement.select.AllColumns;
import net.sf.jsqlparser.statement.select.AllTableColumns;
import net.sf.jsqlparser.statement.select.OrderByElement;
import net.sf.jsqlparser.statement.select.SubSelect;
/**
* 判断表达是否为常量的分析器
*
* @author guyadong
*/
public class ConstAnalyzer implements ExpressionVisitor, ItemsListVisitor {
private static ThreadLocal<Boolean> constFlag = new ThreadLocal<Boolean>() {
@Override
protected Boolean initialValue() {
return true;
}
};
@Override
public void visit(NullValue value) {
}
@Override
public void visit(Function function) {
constFlag.set(false);
}
@Override
public void visit(SignedExpression expr) {
expr.getExpression().accept(this);
}
@Override
public void visit(JdbcParameter parameter) {
constFlag.set(false);
}
@Override
public void visit(JdbcNamedParameter parameter) {
constFlag.set(false);
}
@Override
public void visit(DoubleValue value) {
}
@Override
public void visit(LongValue value) {
}
@Override
public void visit(DateValue value) {
}
@Override
public void visit(TimeValue value) {
}
@Override
public void visit(TimestampValue value) {
}
@Override
public void visit(Parenthesis parenthesis) {
parenthesis.getExpression().accept(this);
}
@Override
public void visit(StringValue value) {
}
@Override
public void visit(Addition expr) {
visitBinaryExpression(expr);
}
@Override
public void visit(Division expr) {
visitBinaryExpression(expr);
}
@Override
public void visit(IntegerDivision expr) {
visitBinaryExpression(expr);
}
@Override
public void visit(Multiplication expr) {
visitBinaryExpression(expr);
}
@Override
public void visit(Subtraction expr) {
visitBinaryExpression(expr);
}
@Override
public void visit(AndExpression expr) {
visitBinaryExpression(expr);
}
@Override
public void visit(OrExpression expr) {
visitBinaryExpression(expr);
}
@Override
public void visit(XorExpression expr) {
visitBinaryExpression(expr);
}
@Override
public void visit(Between expr) {
expr.getLeftExpression().accept(this);
expr.getBetweenExpressionStart().accept(this);
expr.getBetweenExpressionEnd().accept(this);
}
@Override
public void visit(EqualsTo expr) {
visitBinaryExpression(expr);
}
@Override
public void visit(GreaterThan expr) {
visitBinaryExpression(expr);
}
@Override
public void visit(GreaterThanEquals expr) {
visitBinaryExpression(expr);
}
@Override
public void visit(InExpression expr) {
if (expr.getLeftExpression() != null) {
expr.getLeftExpression().accept(this);
}
}
@Override
public void visit(IsNullExpression expr) {
expr.getLeftExpression().accept(this);
}
@Override
public void visit(FullTextSearch expr) {
constFlag.set(false);
}
@Override
public void visit(IsBooleanExpression expr) {
expr.getLeftExpression().accept(this);
}
@Override
public void visit(LikeExpression expr) {
visitBinaryExpression(expr);
}
@Override
public void visit(MinorThan expr) {
visitBinaryExpression(expr);
}
@Override
public void visit(MinorThanEquals expr) {
visitBinaryExpression(expr);
}
@Override
public void visit(NotEqualsTo expr) {
visitBinaryExpression(expr);
}
@Override
public void visit(Column column) {
if (!ParserSupport.isBoolean(column)) {
constFlag.set(false);
}
}
@Override
public void visit(SubSelect subSelect) {
constFlag.set(false);
}
@Override
public void visit(CaseExpression expr) {
constFlag.set(false);
}
@Override
public void visit(WhenClause expr) {
constFlag.set(false);
}
@Override
public void visit(ExistsExpression expr) {
constFlag.set(false);
}
@Override
public void visit(AnyComparisonExpression expr) {
constFlag.set(false);
}
@Override
public void visit(Concat expr) {
visitBinaryExpression(expr);
}
@Override
public void visit(Matches expr) {
visitBinaryExpression(expr);
}
@Override
public void visit(BitwiseAnd expr) {
visitBinaryExpression(expr);
}
@Override
public void visit(BitwiseOr expr) {
visitBinaryExpression(expr);
}
@Override
public void visit(BitwiseXor expr) {
visitBinaryExpression(expr);
}
@Override
public void visit(CastExpression expr) {
expr.getLeftExpression().accept(this);
}
@Override
public void visit(TryCastExpression expr) {
constFlag.set(false);
}
@Override
public void visit(Modulo expr) {
visitBinaryExpression(expr);
}
@Override
public void visit(AnalyticExpression expr) {
constFlag.set(false);
}
@Override
public void visit(ExtractExpression expr) {
expr.getExpression().accept(this);
}
@Override
public void visit(IntervalExpression expr) {
constFlag.set(false);
}
@Override
public void visit(OracleHierarchicalExpression expr) {
constFlag.set(false);
}
@Override
public void visit(RegExpMatchOperator expr) {
visitBinaryExpression(expr);
}
@Override
public void visit(ExpressionList expressionList) {
for (Expression expr : expressionList.getExpressions()) {
expr.accept(this);
}
}
@Override
public void visit(NamedExpressionList namedExpressionList) {
for (Expression expr : namedExpressionList.getExpressions()) {
expr.accept(this);
}
}
@Override
public void visit(MultiExpressionList multiExprList) {
for (ExpressionList list : multiExprList.getExpressionLists()) {
visit(list);
}
}
@Override
public void visit(NotExpression notExpr) {
notExpr.getExpression().accept(this);
}
@Override
public void visit(BitwiseRightShift expr) {
visitBinaryExpression(expr);
}
@Override
public void visit(BitwiseLeftShift expr) {
visitBinaryExpression(expr);
}
protected void visitBinaryExpression(BinaryExpression expr) {
expr.getLeftExpression().accept(this);
expr.getRightExpression().accept(this);
}
@Override
public void visit(JsonExpression jsonExpr) {
jsonExpr.getExpression().accept(this);
}
@Override
public void visit(JsonOperator expr) {
visitBinaryExpression(expr);
}
@Override
public void visit(RegExpMySQLOperator expr) {
visitBinaryExpression(expr);
}
@Override
public void visit(UserVariable var) {
constFlag.set(false);
}
@Override
public void visit(NumericBind bind) {
constFlag.set(false);
}
@Override
public void visit(KeepExpression expr) {
for (OrderByElement element : expr.getOrderByElements()) {
element.getExpression().accept(this);
}
}
@Override
public void visit(MySQLGroupConcat groupConcat) {
constFlag.set(false);
}
@Override
public void visit(ValueListExpression valueListExpression) {
for (Expression expr : valueListExpression.getExpressionList().getExpressions()) {
expr.accept(this);
}
}
@Override
public void visit(AllColumns allColumns) {
}
@Override
public void visit(AllTableColumns allTableColumns) {
}
@Override
public void visit(AllValue allValue) {
}
@Override
public void visit(IsDistinctExpression isDistinctExpression) {
visitBinaryExpression(isDistinctExpression);
}
@Override
public void visit(RowGetExpression rowGetExpression) {
rowGetExpression.getExpression().accept(this);
}
@Override
public void visit(HexValue hexValue) {
}
@Override
public void visit(OracleHint hint) {
}
@Override
public void visit(TimeKeyExpression timeKeyExpression) {
}
@Override
public void visit(DateTimeLiteralExpression literal) {
}
@Override
public void visit(NextValExpression nextVal) {
constFlag.set(false);
}
@Override
public void visit(CollateExpression col) {
constFlag.set(false);
}
@Override
public void visit(SimilarToExpression expr) {
visitBinaryExpression(expr);
}
@Override
public void visit(ArrayExpression array) {
array.getObjExpression().accept(this);
if (array.getIndexExpression() != null) {
array.getIndexExpression().accept(this);
}
if (array.getStartIndexExpression() != null) {
array.getStartIndexExpression().accept(this);
}
if (array.getStopIndexExpression() != null) {
array.getStopIndexExpression().accept(this);
}
}
@Override
public void visit(ArrayConstructor aThis) {
for (Expression expression : aThis.getExpressions()) {
expression.accept(this);
}
}
@Override
public void visit(VariableAssignment var) {
constFlag.set(false);
}
@Override
public void visit(XMLSerializeExpr expr) {
constFlag.set(false);
}
@Override
public void visit(TimezoneExpression expr) {
expr.getLeftExpression().accept(this);
}
@Override
public void visit(JsonAggregateFunction expression) {
Expression expr = expression.getExpression();
if (expr != null) {
expr.accept(this);
}
expr = expression.getFilterExpression();
if (expr != null) {
expr.accept(this);
}
}
@Override
public void visit(JsonFunction expression) {
for (JsonFunctionExpression expr : expression.getExpressions()) {
expr.getExpression().accept(this);
}
}
@Override
public void visit(ConnectByRootOperator connectByRootOperator) {
constFlag.set(false);
}
@Override
public void visit(OracleNamedFunctionParameter oracleNamedFunctionParameter) {
constFlag.set(false);
}
@Override
public void visit(GeometryDistance geometryDistance) {
visitBinaryExpression(geometryDistance);
}
@Override
public void visit(RowConstructor rowConstructor) {
constFlag.set(false);
}
public boolean isConstExpression(Expression expression) {
if (null != expression) {
constFlag.set(true);
expression.accept(this);
return constFlag.get();
}
return false;
}
}

View File

@ -0,0 +1,177 @@
package org.jeecg.common.util.sqlInjection.parse;
import lombok.extern.slf4j.Slf4j;
import net.sf.jsqlparser.JSQLParserException;
import net.sf.jsqlparser.parser.*;
import net.sf.jsqlparser.schema.Column;
import net.sf.jsqlparser.statement.Statement;
import net.sf.jsqlparser.statement.select.PlainSelect;
import net.sf.jsqlparser.statement.select.Select;
import net.sf.jsqlparser.statement.select.SelectBody;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import java.lang.reflect.InvocationTargetException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import com.google.common.base.Throwables;
import org.jeecg.common.exception.JeecgBootException;
import org.jeecg.common.exception.JeecgSqlInjectionException;
/**
* 解析sql支持
*/
@Slf4j
public class ParserSupport {
/**
* 解析SELECT SQL语句,解析失败或非SELECT语句则抛出异常
*
* @param sql
* @return
*/
public static Select parseSelect(String sql) {
Statement stmt;
try {
stmt = CCJSqlParserUtil.parse(checkNotNull(sql, "sql is null"));
} catch (JSQLParserException e) {
throw new JeecgBootException(e);
}
checkArgument(stmt instanceof Select, "%s is not SELECT statment", sql);
Select select = (Select) stmt;
SelectBody selectBody = select.getSelectBody();
// 暂时只支持简单的SELECT xxxx FROM ....语句不支持复杂语句如WITH
checkArgument(selectBody instanceof PlainSelect, "ONLY SUPPORT plain select statement %s", sql);
return (Select) stmt;
}
/**
* 解析SELECT SQL语句,解析失败或非SELECT语句则
*
* @param sql
* @return
*/
public static Select parseSelectUnchecked(String sql) {
try {
return parseSelect(sql);
} catch (Exception e) {
return null;
}
}
/**
* 实现SQL语句解析,解析成功则返回解析后的{@link Statement}
* 并通过{@code visitor}参数提供基于AST(抽象语法树)的遍历所有节点的能力
*
* @param sql SQL语句
* @param visitor 遍历所有节点的{@link SimpleNodeVisitor}接口实例{@code null}忽略
* @param sqlSyntaxNormalizer SQL语句分析转换器{@code null}忽略
* @throws JSQLParserException 输入的SQL语句有语法错误
* @see #parse0(String, CCJSqlParserVisitor, SqlSyntaxNormalizer)
*/
public static Statement parse(String sql, CCJSqlParserVisitor visitor, SqlSyntaxNormalizer sqlSyntaxNormalizer) throws JSQLParserException {
return parse0(sql, visitor, sqlSyntaxNormalizer).statement;
}
/**
* 参照{@link CCJSqlParserUtil#parseAST(String)}{@link CCJSqlParserUtil#parse(String)}实现SQL语句解析,
* 解析成功则返回解析后的{@link SqlParserInfo}对象
* 并通过{@code visitor}参数提供基于AST(抽象语法树)的遍历所有节点的能力
*
* @param sql SQL语句
* @param visitor 遍历所有节点的{@link SimpleNodeVisitor}接口实例{@code null}忽略
* @param sqlSyntaxAnalyzer SQL语句分析转换器{@code null}忽略
* @throws JSQLParserException 输入的SQL语句有语法错误
* @see net.sf.jsqlparser.parser.Node#jjtAccept(SimpleNodeVisitor, Object)
*/
public static SqlParserInfo parse0(String sql, CCJSqlParserVisitor visitor, SqlSyntaxNormalizer sqlSyntaxAnalyzer) throws JeecgSqlInjectionException {
//检查是否非select开头暂不支持
if(!sql.toLowerCase().trim().startsWith("select ")) {
log.warn("传入sql 非select开头不支持非select开头的语句解析");
return null;
}
//检查是否存储过程暂不支持
if(sql.toLowerCase().trim().startsWith("call ")){
log.warn("传入call 开头存储过程,不支持存储过程解析!");
return null;
}
//检查特殊语义的特殊字符目前检查冒号$#三种特殊语义字符
String specialCharacters = "[:$#]";
Pattern pattern = Pattern.compile(specialCharacters);
Matcher matcher = pattern.matcher(sql);
if (matcher.find()) {
sql = sql.replaceAll("[:$#]", "@");
}
checkArgument(null != sql, "sql is null");
boolean allowComplexParsing = CCJSqlParserUtil.getNestingDepth(sql) <= CCJSqlParserUtil.ALLOWED_NESTING_DEPTH;
CCJSqlParser parser = CCJSqlParserUtil.newParser(sql).withAllowComplexParsing(allowComplexParsing);
Statement stmt;
try {
stmt = parser.Statement();
} catch (Exception ex) {
log.error("请注意SQL语法可能存在问题---> {}", ex.getMessage());
throw new JeecgSqlInjectionException("请注意SQL语法可能存在问题:"+sql);
}
if (null != visitor) {
parser.getASTRoot().jjtAccept(visitor, null);
}
if (null != sqlSyntaxAnalyzer) {
stmt.accept(sqlSyntaxAnalyzer.resetChanged());
}
return new SqlParserInfo(stmt.toString(), stmt, (SimpleNode) parser.getASTRoot());
}
/**
* 调用{@link CCJSqlParser}解析SQL语句部件返回解析生成的对象,{@code 'ORDER BY id DESC'}
*
* @param <T>
* @param input
* @param method 指定调用的{@link CCJSqlParser}解析方法
* @param targetType 返回的解析对象类型
* @return
* @since 3.18.3
*/
public static <T> T parseComponent(String input, String method, Class<T> targetType) {
try {
CCJSqlParser parser = new CCJSqlParser(new StringProvider(input));
try {
return checkNotNull(targetType, "targetType is null").cast(parser.getClass().getMethod(method).invoke(parser));
} catch (InvocationTargetException e) {
Throwables.throwIfUnchecked(e.getTargetException());
throw new RuntimeException(e.getTargetException());
}
} catch (IllegalAccessException | NoSuchMethodException | SecurityException e) {
Throwables.throwIfUnchecked(e);
throw new RuntimeException(e);
}
}
/**
* 如果{@link Column}没有定义table,且字段名为true/false(不区分大小写)则视为布尔常量
*
* @param column
*/
public static boolean isBoolean(Column column) {
return null != column && null == column.getTable() &&
Pattern.compile("(true|false)", Pattern.CASE_INSENSITIVE).matcher(column.getColumnName()).matches();
}
public static class SqlParserInfo {
public String nativeSql;
public Statement statement;
public SimpleNode simpleNode;
SqlParserInfo(String nativeSql, Statement statement, SimpleNode simpleNode) {
this.nativeSql = nativeSql;
this.statement = statement;
this.simpleNode = simpleNode;
}
}
}

View File

@ -0,0 +1,37 @@
package org.jeecg.common.util.sqlInjection.parse;
import net.sf.jsqlparser.util.TablesNamesFinder;
/**
* SQL语句分析转换器基类<br>
* 基于SQL语法对象实现对SQL的修改
* 暂时用不到
*
* @author guyadong
* @since 3.17.0
*/
public class SqlSyntaxNormalizer extends TablesNamesFinder {
protected static final ThreadLocal<Boolean> changed = new ThreadLocal<>();
public SqlSyntaxNormalizer() {
super();
init(true);
}
/**
* 语句改变返回{@code true},否则返回{@code false}
*/
public boolean changed() {
return Boolean.TRUE.equals(changed.get());
}
/**
* 复位线程局部变量{@link #changed}状态
*/
public SqlSyntaxNormalizer resetChanged() {
changed.remove();
return this;
}
}

View File

@ -0,0 +1,255 @@
package org.jeecg.common.util.sqlparse;
import lombok.extern.slf4j.Slf4j;
import net.sf.jsqlparser.JSQLParserException;
import net.sf.jsqlparser.expression.*;
import net.sf.jsqlparser.parser.CCJSqlParserManager;
import net.sf.jsqlparser.schema.Column;
import net.sf.jsqlparser.schema.Table;
import net.sf.jsqlparser.statement.Statement;
import net.sf.jsqlparser.statement.select.*;
import org.jeecg.common.exception.JeecgBootException;
import org.jeecg.common.util.sqlparse.vo.SelectSqlInfo;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 解析所有表名和字段的类
*/
@Slf4j
public class JSqlParserAllTableManager {
private final String sql;
private final Map<String, SelectSqlInfo> allTableMap = new HashMap<>();
/**
* 别名对应实际表名
*/
private final Map<String, String> tableAliasMap = new HashMap<>();
/**
* 解析后的sql
*/
private String parsedSql = null;
JSqlParserAllTableManager(String selectSql) {
this.sql = selectSql;
}
/**
* 开始解析
*
* @return
* @throws JSQLParserException
*/
public Map<String, SelectSqlInfo> parse() throws JSQLParserException {
// 1. 创建解析器
CCJSqlParserManager mgr = new CCJSqlParserManager();
// 2. 使用解析器解析sql生成具有层次结构的java类
Statement stmt = mgr.parse(new StringReader(this.sql));
if (stmt instanceof Select) {
Select selectStatement = (Select) stmt;
SelectBody selectBody = selectStatement.getSelectBody();
this.parsedSql = selectBody.toString();
// 3. 解析select查询sql的信息
if (selectBody instanceof PlainSelect) {
PlainSelect plainSelect = (PlainSelect) selectBody;
// 4. 合并 fromItems
List<FromItem> fromItems = new ArrayList<>();
fromItems.add(plainSelect.getFromItem());
// 4.1 处理join的表
List<Join> joins = plainSelect.getJoins();
if (joins != null) {
joins.forEach(join -> fromItems.add(join.getRightItem()));
}
// 5. 处理 fromItems
for (FromItem fromItem : fromItems) {
// 5.1 通过表名的方式from
if (fromItem instanceof Table) {
this.addSqlInfoByTable((Table) fromItem);
}
// 5.2 通过子查询的方式from
else if (fromItem instanceof SubSelect) {
this.handleSubSelect((SubSelect) fromItem);
}
}
// 6. 解析 selectFields
List<SelectItem> selectItems = plainSelect.getSelectItems();
for (SelectItem selectItem : selectItems) {
// 6.1 查询的是全部字段
if (selectItem instanceof AllColumns) {
// selectItem AllColumns fromItem 必定为 Table
String tableName = plainSelect.getFromItem(Table.class).getName();
// 此处必定不为空因为在解析 fromItem 已经将表名添加到 allTableMap
SelectSqlInfo sqlInfo = this.allTableMap.get(tableName);
assert sqlInfo != null;
// 设置为查询全部字段
sqlInfo.setSelectAll(true);
sqlInfo.setSelectFields(null);
sqlInfo.setRealSelectFields(null);
}
// 6.2 查询的是带表别名 u.* )的全部字段
else if (selectItem instanceof AllTableColumns) {
AllTableColumns allTableColumns = (AllTableColumns) selectItem;
String aliasName = allTableColumns.getTable().getName();
// 通过别名获取表名
String tableName = this.tableAliasMap.get(aliasName);
if (tableName == null) {
tableName = aliasName;
}
SelectSqlInfo sqlInfo = this.allTableMap.get(tableName);
// 如果此处为空则说明该字段是通过子查询获取的所以可以不处理只有实际表才需要处理
if (sqlInfo != null) {
// 设置为查询全部字段
sqlInfo.setSelectAll(true);
sqlInfo.setSelectFields(null);
sqlInfo.setRealSelectFields(null);
}
}
// 6.3 各种字段表达式处理
else if (selectItem instanceof SelectExpressionItem) {
SelectExpressionItem selectExpressionItem = (SelectExpressionItem) selectItem;
Expression expression = selectExpressionItem.getExpression();
Alias alias = selectExpressionItem.getAlias();
this.handleExpression(expression, alias, plainSelect.getFromItem());
}
}
} else {
log.warn("暂时尚未处理该类型的 SelectBody: {}", selectBody.getClass().getName());
throw new JeecgBootException("暂时尚未处理该类型的 SelectBody");
}
} else {
// select 查询sql不做处理
throw new JeecgBootException("非 select 查询sql不做处理");
}
return this.allTableMap;
}
/**
* 处理子查询
*
* @param subSelect
*/
private void handleSubSelect(SubSelect subSelect) {
try {
String subSelectSql = subSelect.getSelectBody().toString();
// 递归调用解析
Map<String, SelectSqlInfo> map = JSqlParserUtils.parseAllSelectTable(subSelectSql);
if (map != null) {
this.assignMap(map);
}
} catch (Exception e) {
log.error("解析子查询出错", e);
}
}
/**
* 处理查询字段表达式
*
* @param expression
*/
private void handleExpression(Expression expression, Alias alias, FromItem fromItem) {
// 处理函数式字段 CONCAT(name,'(',age,')')
if (expression instanceof Function) {
Function functionExp = (Function) expression;
List<Expression> expressions = functionExp.getParameters().getExpressions();
for (Expression expItem : expressions) {
this.handleExpression(expItem, null, fromItem);
}
return;
}
// 处理字段上的子查询
if (expression instanceof SubSelect) {
this.handleSubSelect((SubSelect) expression);
return;
}
// 不处理字面量
if (expression instanceof StringValue ||
expression instanceof NullValue ||
expression instanceof LongValue ||
expression instanceof DoubleValue ||
expression instanceof HexValue ||
expression instanceof DateValue ||
expression instanceof TimestampValue ||
expression instanceof TimeValue
) {
return;
}
// 处理字段
if (expression instanceof Column) {
Column column = (Column) expression;
// 查询字段名
String fieldName = column.getColumnName();
String aliasName = fieldName;
if (alias != null) {
aliasName = alias.getName();
}
String tableName;
if (column.getTable() != null) {
// 通过列的表名获取 sqlInfo
// 例如 user.name这里的 tableName 就是 user
tableName = column.getTable().getName();
// 有可能是别名需要转换为真实表名
if (this.tableAliasMap.get(tableName) != null) {
tableName = this.tableAliasMap.get(tableName);
}
} else {
// 当column的table为空时说明是 fromItem 中的字段
tableName = ((Table) fromItem).getName();
}
SelectSqlInfo $sqlInfo = this.allTableMap.get(tableName);
if ($sqlInfo != null) {
$sqlInfo.addSelectField(aliasName, fieldName);
} else {
log.warn("发生意外情况,未找到表名为 {} 的 SelectSqlInfo", tableName);
}
}
}
/**
* 根据表名添加sqlInfo
*
* @param table
*/
private void addSqlInfoByTable(Table table) {
String tableName = table.getName();
// 解析 aliasName
if (table.getAlias() != null) {
this.tableAliasMap.put(table.getAlias().getName(), tableName);
}
SelectSqlInfo sqlInfo = new SelectSqlInfo(this.parsedSql);
sqlInfo.setFromTableName(table.getName());
this.allTableMap.put(sqlInfo.getFromTableName(), sqlInfo);
}
/**
* 合并map
*
* @param source
*/
private void assignMap(Map<String, SelectSqlInfo> source) {
for (Map.Entry<String, SelectSqlInfo> entry : source.entrySet()) {
SelectSqlInfo sqlInfo = this.allTableMap.get(entry.getKey());
if (sqlInfo == null) {
this.allTableMap.put(entry.getKey(), entry.getValue());
} else {
// 合并
if (sqlInfo.getSelectFields() == null) {
sqlInfo.setSelectFields(entry.getValue().getSelectFields());
} else {
sqlInfo.getSelectFields().addAll(entry.getValue().getSelectFields());
}
if (sqlInfo.getRealSelectFields() == null) {
sqlInfo.setRealSelectFields(entry.getValue().getRealSelectFields());
} else {
sqlInfo.getRealSelectFields().addAll(entry.getValue().getRealSelectFields());
}
}
}
}
}

View File

@ -0,0 +1,184 @@
package org.jeecg.common.util.sqlparse;
import lombok.extern.slf4j.Slf4j;
import net.sf.jsqlparser.JSQLParserException;
import net.sf.jsqlparser.expression.*;
import net.sf.jsqlparser.parser.CCJSqlParserManager;
import net.sf.jsqlparser.schema.Column;
import net.sf.jsqlparser.schema.Table;
import net.sf.jsqlparser.statement.Statement;
import net.sf.jsqlparser.statement.select.*;
import org.jeecg.common.exception.JeecgBootException;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.common.util.sqlparse.vo.SelectSqlInfo;
import java.io.StringReader;
import java.util.List;
import java.util.Map;
@Slf4j
public class JSqlParserUtils {
/**
* 解析 查询selectsql的信息
* 此方法会展开所有子查询到一个map里
* key只存真实的表名如果查询的没有真实的表名则会被忽略
* value只存真实的字段名如果查询的没有真实的字段名则会被忽略
* <p>
* 例如SELECT a.*,d.age,(SELECT count(1) FROM sys_depart) AS count FROM (SELECT username AS foo, realname FROM sys_user) a, demo d
* 解析后的结果为{sys_user=[username, realname], demo=[age], sys_depart=[]}
*
* @param selectSql
* @return
*/
public static Map<String, SelectSqlInfo> parseAllSelectTable(String selectSql) throws JSQLParserException {
if (oConvertUtils.isEmpty(selectSql)) {
return null;
}
// log.info("解析查询Sql{}", selectSql);
JSqlParserAllTableManager allTableManager = new JSqlParserAllTableManager(selectSql);
return allTableManager.parse();
}
/**
* 解析 查询selectsql的信息子查询嵌套
*
* @param selectSql
* @return
*/
public static SelectSqlInfo parseSelectSqlInfo(String selectSql) throws JSQLParserException {
if (oConvertUtils.isEmpty(selectSql)) {
return null;
}
// log.info("解析查询Sql{}", selectSql);
// 使用 JSqlParer 解析sql
// 1创建解析器
CCJSqlParserManager mgr = new CCJSqlParserManager();
// 2使用解析器解析sql生成具有层次结构的java类
Statement stmt = mgr.parse(new StringReader(selectSql));
if (stmt instanceof Select) {
Select selectStatement = (Select) stmt;
// 3解析select查询sql的信息
return JSqlParserUtils.parseBySelectBody(selectStatement.getSelectBody());
} else {
// select 查询sql不做处理
throw new JeecgBootException("非 select 查询sql不做处理");
}
}
/**
* 解析 select 查询sql的信息
*
* @param selectBody
* @return
*/
private static SelectSqlInfo parseBySelectBody(SelectBody selectBody) {
// 简单的select查询
if (selectBody instanceof PlainSelect) {
SelectSqlInfo sqlInfo = new SelectSqlInfo(selectBody);
PlainSelect plainSelect = (PlainSelect) selectBody;
FromItem fromItem = plainSelect.getFromItem();
// 解析 aliasName
if (fromItem.getAlias() != null) {
sqlInfo.setFromTableAliasName(fromItem.getAlias().getName());
}
// 解析 表名
if (fromItem instanceof Table) {
// 通过表名的方式from
Table fromTable = (Table) fromItem;
sqlInfo.setFromTableName(fromTable.getName());
} else if (fromItem instanceof SubSelect) {
// 通过子查询的方式from
SubSelect fromSubSelect = (SubSelect) fromItem;
SelectSqlInfo subSqlInfo = JSqlParserUtils.parseBySelectBody(fromSubSelect.getSelectBody());
sqlInfo.setFromSubSelect(subSqlInfo);
}
// 解析 selectFields
List<SelectItem> selectItems = plainSelect.getSelectItems();
for (SelectItem selectItem : selectItems) {
if (selectItem instanceof AllColumns || selectItem instanceof AllTableColumns) {
// 全部字段
sqlInfo.setSelectAll(true);
sqlInfo.setSelectFields(null);
sqlInfo.setRealSelectFields(null);
break;
} else if (selectItem instanceof SelectExpressionItem) {
// 获取单个查询字段名
SelectExpressionItem selectExpressionItem = (SelectExpressionItem) selectItem;
Expression expression = selectExpressionItem.getExpression();
Alias alias = selectExpressionItem.getAlias();
JSqlParserUtils.handleExpression(sqlInfo, expression, alias);
}
}
return sqlInfo;
} else {
log.warn("暂时尚未处理该类型的 SelectBody: {}", selectBody.getClass().getName());
throw new JeecgBootException("暂时尚未处理该类型的 SelectBody");
}
}
/**
* 处理查询字段表达式
*
* @param sqlInfo
* @param expression
* @param alias 是否有别名无传null
*/
private static void handleExpression(SelectSqlInfo sqlInfo, Expression expression, Alias alias) {
// 处理函数式字段 CONCAT(name,'(',age,')')
if (expression instanceof Function) {
JSqlParserUtils.handleFunctionExpression((Function) expression, sqlInfo);
return;
}
// 处理字段上的子查询
if (expression instanceof SubSelect) {
SubSelect subSelect = (SubSelect) expression;
SelectSqlInfo subSqlInfo = JSqlParserUtils.parseBySelectBody(subSelect.getSelectBody());
// 字段上的子查询必须只查询一个字段否则会报错所以可以放心合并
sqlInfo.getSelectFields().addAll(subSqlInfo.getSelectFields());
sqlInfo.getRealSelectFields().addAll(subSqlInfo.getAllRealSelectFields());
return;
}
// 不处理字面量
if (expression instanceof StringValue ||
expression instanceof NullValue ||
expression instanceof LongValue ||
expression instanceof DoubleValue ||
expression instanceof HexValue ||
expression instanceof DateValue ||
expression instanceof TimestampValue ||
expression instanceof TimeValue
) {
return;
}
// 查询字段名
String selectField = expression.toString();
// 实际查询字段名
String realSelectField = selectField;
// 判断是否有别名
if (alias != null) {
selectField = alias.getName();
}
// 获取真实字段名
if (expression instanceof Column) {
Column column = (Column) expression;
realSelectField = column.getColumnName();
}
sqlInfo.addSelectField(selectField, realSelectField);
}
/**
* 处理函数式字段
*
* @param functionExp
* @param sqlInfo
*/
private static void handleFunctionExpression(Function functionExp, SelectSqlInfo sqlInfo) {
List<Expression> expressions = functionExp.getParameters().getExpressions();
for (Expression expression : expressions) {
JSqlParserUtils.handleExpression(sqlInfo, expression, null);
}
}
}

View File

@ -0,0 +1,101 @@
package org.jeecg.common.util.sqlparse.vo;
import lombok.Data;
import net.sf.jsqlparser.statement.select.SelectBody;
import java.util.HashSet;
import java.util.Set;
/**
* select 查询 sql 的信息
*/
@Data
public class SelectSqlInfo {
/**
* 查询的表名如果是子查询则此处为null
*/
private String fromTableName;
/**
* 表别名
*/
private String fromTableAliasName;
/**
* 通过子查询获取的表信息例如select name from (select * from user) u
* 如果不是子查询则为null
*/
private SelectSqlInfo fromSubSelect;
/**
* 查询的字段集合如果是 * 则为null如果设了别名则为别名
*/
private Set<String> selectFields;
/**
* 真实的查询字段集合如果是 * 则为null如果设了别名则为原始字段名
*/
private Set<String> realSelectFields;
/**
* 是否是查询所有字段
*/
private boolean selectAll;
/**
* 解析之后的 SQL 关键字都是大写
*/
private final String parsedSql;
public SelectSqlInfo(String parsedSql) {
this.parsedSql = parsedSql;
}
public SelectSqlInfo(SelectBody selectBody) {
this.parsedSql = selectBody.toString();
}
public void addSelectField(String selectField, String realSelectField) {
if (this.selectFields == null) {
this.selectFields = new HashSet<>();
}
if (this.realSelectFields == null) {
this.realSelectFields = new HashSet<>();
}
this.selectFields.add(selectField);
this.realSelectFields.add(realSelectField);
}
/**
* 获取所有字段包括子查询里的
*
* @return
*/
public Set<String> getAllRealSelectFields() {
Set<String> fields = new HashSet<>();
// 递归获取所有字段起个直观的方法名为
this.recursiveGetAllFields(this, fields);
return fields;
}
/**
* 递归获取所有字段
*/
private void recursiveGetAllFields(SelectSqlInfo sqlInfo, Set<String> fields) {
if (!sqlInfo.isSelectAll() && sqlInfo.getRealSelectFields() != null) {
fields.addAll(sqlInfo.getRealSelectFields());
}
if (sqlInfo.getFromSubSelect() != null) {
recursiveGetAllFields(sqlInfo.getFromSubSelect(), fields);
}
}
@Override
public String toString() {
return "SelectSqlInfo{" +
"fromTableName='" + fromTableName + '\'' +
", fromSubSelect=" + fromSubSelect +
", aliasName='" + fromTableAliasName + '\'' +
", selectFields=" + selectFields +
", realSelectFields=" + realSelectFields +
", selectAll=" + selectAll +
"}";
}
}

View File

@ -59,7 +59,9 @@ public class AutoPoiDictConfig implements AutoPoiDictServiceI {
for (DictModel t : dictList) {
if(t!=null){
//update-begin---author:liusq Date:20230517 for[issues/4917]excel 导出异常---
if(t!=null && t.getText()!=null && t.getValue()!=null){
//update-end---author:liusq Date:20230517 for[issues/4917]excel 导出异常---
//update-begin---author:scott Date:20211220 for[issues/I4MBB3]@Excel dicText字段的值有下划线时导入功能不能正确解析---
if(t.getValue().contains(EXCEL_SPLIT_TAG)){
String val = t.getValue().replace(EXCEL_SPLIT_TAG,TEMP_EXCEL_SPLIT_TAG);

View File

@ -1,9 +1,6 @@
package org.jeecg.config;
import org.jeecg.config.vo.DomainUrl;
import org.jeecg.config.vo.Elasticsearch;
import org.jeecg.config.vo.Path;
import org.jeecg.config.vo.Shiro;
import org.jeecg.config.vo.*;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@ -29,10 +26,12 @@ public class JeecgBaseConfig {
* 本地local\Miniominio\阿里云alioss
*/
private String uploadType;
/**
* 是否启用安全模式
* 平台安全模式配置
*/
private Boolean safeMode = false;
private Firewall firewall;
/**
* shiro拦截排除
*/
@ -58,6 +57,13 @@ public class JeecgBaseConfig {
*/
private Elasticsearch elasticsearch;
/**
* 微信支付
* @return
*/
private WeiXinPay weiXinPay;
public Elasticsearch getElasticsearch() {
return elasticsearch;
}
@ -66,12 +72,12 @@ public class JeecgBaseConfig {
this.elasticsearch = elasticsearch;
}
public Boolean getSafeMode() {
return safeMode;
public Firewall getFirewall() {
return firewall;
}
public void setSafeMode(Boolean safeMode) {
this.safeMode = safeMode;
public void setFirewall(Firewall firewall) {
this.firewall = firewall;
}
public String getSignatureSecret() {
@ -129,4 +135,13 @@ public class JeecgBaseConfig {
public void setUploadType(String uploadType) {
this.uploadType = uploadType;
}
public WeiXinPay getWeiXinPay() {
return weiXinPay;
}
public void setWeiXinPay(WeiXinPay weiXinPay) {
this.weiXinPay = weiXinPay;
}
}

View File

@ -116,7 +116,7 @@ public class Swagger2Config implements WebMvcConfigurer {
// 描述
.description("后台API接口")
// 作者
.contact(new Contact("北京敲敲云科技有限公司","www.jeccg.com","jeecgos@163.com"))
.contact(new Contact("北京国炬信息技术有限公司","www.jeccg.com","jeecgos@163.com"))
.license("The Apache License, Version 2.0")
.licenseUrl("http://www.apache.org/licenses/LICENSE-2.0.html")
.build();

View File

@ -31,7 +31,7 @@ public class WebSocketConfig {
FilterRegistrationBean bean = new FilterRegistrationBean();
bean.setFilter(websocketFilter());
//TODO 临时注释掉测试下线上socket总断的问题
bean.addUrlPatterns("/websocket/*","/eoaSocket/*","/eoaNewChatSocket/*", "/newsWebsocket/*", "/vxeSocket/*");
bean.addUrlPatterns("/taskCountSocket/*", "/websocket/*","/eoaSocket/*","/eoaNewChatSocket/*", "/newsWebsocket/*", "/vxeSocket/*");
return bean;
}

View File

@ -0,0 +1,38 @@
package org.jeecg.config.firewall.SqlInjection;
/**
* 字典表查询 :: 白名单配置
*
* @Author taoYan
* @Date 2022/3/17 11:21
**/
public interface IDictTableWhiteListHandler {
/**
* 校验表名字段是否合法允许查询允许则返回 true
*
* @param sql
* @return
*/
boolean isPassBySql(String sql);
/**
* 校验字典是否通过
*
* @param dictCodeString 字典表配置
* @return
*/
boolean isPassByDict(String dictCodeString);
boolean isPassByDict(String tableName, String... fields);
/**
* 清空缓存使更改生效
*
* @return
*/
boolean clear();
String getErrorMsg();
}

View File

@ -0,0 +1,102 @@
package org.jeecg.config.firewall.SqlInjection;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.util.oConvertUtils;
import java.util.HashSet;
import java.util.Set;
/**
* 查询的表的信息
*/
@Slf4j
public class SysDictTableWhite {
//表名
private String name;
//表的别名
private String alias;
// 字段名集合
private Set<String> fields;
// 是否查询所有字段
private boolean all;
public SysDictTableWhite() {
}
public SysDictTableWhite(String name, String alias) {
this.name = name;
this.alias = alias;
this.all = false;
this.fields = new HashSet<>();
}
public void addField(String field) {
this.fields.add(field);
}
public String getName() {
return name;
}
public Set<String> getFields() {
return new HashSet<>(fields);
}
public void setName(String name) {
this.name = name;
}
public void setFields(Set<String> fields) {
this.fields = fields;
}
public String getAlias() {
return alias;
}
public void setAlias(String alias) {
this.alias = alias;
}
public boolean isAll() {
return all;
}
public void setAll(boolean all) {
this.all = all;
}
/**
* 判断是否有相同字段
*
* @param fieldControlString
* @return
*/
public boolean isAllFieldsValid(String fieldControlString) {
//如果白名单中没有配置字段则返回false
String[] controlFields = fieldControlString.split(",");
if (oConvertUtils.isEmpty(fieldControlString)) {
return false;
}
for (String queryField : fields) {
if (oConvertUtils.isIn(queryField, controlFields)) {
log.warn("字典表白名单校验,表【" + name + "】中字段【" + queryField + "】无权限查询");
return false;
}
}
return true;
}
@Override
public String toString() {
return "QueryTable{" +
"name='" + name + '\'' +
", alias='" + alias + '\'' +
", fields=" + fields +
", all=" + all +
'}';
}
}

View File

@ -0,0 +1,19 @@
package org.jeecg.config.firewall.interceptor;
import org.jeecg.config.firewall.interceptor.enums.LowCodeUrlsEnum;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class LowCodeModeConfiguration implements WebMvcConfigurer {
public LowCodeModeInterceptor payInterceptor() {
return new LowCodeModeInterceptor();
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(payInterceptor()).addPathPatterns(LowCodeUrlsEnum.getLowCodeInterceptUrls());
}
}

View File

@ -0,0 +1,113 @@
package org.jeecg.config.firewall.interceptor;
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.SecurityUtils;
import org.jeecg.common.api.CommonAPI;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.constant.CommonConstant;
import org.jeecg.common.exception.JeecgBootException;
import org.jeecg.common.system.util.JwtUtil;
import org.jeecg.common.system.vo.LoginUser;
import org.jeecg.common.util.CommonUtils;
import org.jeecg.common.util.SpringContextUtils;
import org.jeecg.config.JeecgBaseConfig;
import org.jeecg.config.firewall.interceptor.enums.LowCodeUrlsEnum;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Set;
/**
* 低代码模式dev:开发模式prod:发布模式关闭所有在线开发配置能力
* <p>
* prod开启后会关闭以下功能只保留功能测试拥有admin角色账号可以使用配置能力
* 1.online表单的所有配置功能代码生成和导入表功能
* 2.online报表的所有配置功能和sql解析
* 3.online图表的所有配置功能和sql解析
* 4.仪表盘的在线配置功能和sql解析
* 5.大屏的在线配置功能和sql解析
*
* 积木的逻辑单独处理
* 1.积木报表的在线配置功能和sql解析
*
* @author qinfeng
* @date 20230904
*/
@Slf4j
public class LowCodeModeInterceptor implements HandlerInterceptor {
/**
* 低代码开发模式
*/
public static final String LOW_CODE_MODE_DEV = "dev";
public static final String LOW_CODE_MODE_PROD = "prod";
@Resource
private JeecgBaseConfig jeecgBaseConfig;
@Autowired
private CommonAPI commonAPI;
/**
* 在请求处理之前进行调用
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
//1验证是否开启低代码开发模式控制
if (jeecgBaseConfig == null) {
jeecgBaseConfig = SpringContextUtils.getBean(JeecgBaseConfig.class);
}
if (jeecgBaseConfig.getFirewall()!=null && LowCodeModeInterceptor.LOW_CODE_MODE_PROD.equals(jeecgBaseConfig.getFirewall().getLowCodeMode())) {
String requestURI = request.getRequestURI().substring(request.getContextPath().length());
log.info("低代码模式,拦截请求路径:" + requestURI);
LoginUser loginUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();
Set<String> hasRoles = null;
if (loginUser == null) {
loginUser = commonAPI.getUserByName(JwtUtil.getUserNameByToken(SpringContextUtils.getHttpServletRequest()));
//当前登录人拥有的角色
hasRoles = commonAPI.queryUserRoles(loginUser.getUsername());
}
log.info("get loginUser info: {}", loginUser);
log.info("get loginRoles info: {}", hasRoles != null ? hasRoles.toArray() : "");
//拥有的角色 允许开发角色存在交集
boolean hasIntersection = CommonUtils.hasIntersection(hasRoles, CommonConstant.allowDevRoles);
//如果是超级管理员 或者 允许开发的角色则不做限制
if (loginUser!=null && ("admin".equals(loginUser.getUsername()) || hasIntersection)) {
return true;
}
this.returnErrorMessage(response);
return false;
}
return true;
}
/**
* 返回结果
*
* @param response
*/
private void returnErrorMessage(HttpServletResponse response) {
//校验失败返回前端
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
PrintWriter out = null;
try {
out = response.getWriter();
Result<?> result = Result.error("低代码开发模式为发布模式,不允许使用在线配置!!");
out.print(JSON.toJSON(result));
} catch (IOException e) {
e.printStackTrace();
}
}
}

View File

@ -0,0 +1,104 @@
package org.jeecg.config.firewall.interceptor.enums;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
/**
*
* @author: qinfeng
* @date: 2023/09/04 11:44
*/
public enum LowCodeUrlsEnum {
/**
* online表单配置请求 TODO 增改删
*/
NEW_LOW_APP_ADD_URL("/online/cgform/api/addAll", "添加online表单"),
NEW_LOW_APP_EDIT_URL("/online/cgform/api/editAll", "编辑online表单"),
ONLINE_DB_SYNC("/online/cgform/api/doDbSynch/**/**", "online表单同步数据库"),
ONLINE_DEL_BATCH("/online/cgform/head/deleteBatch", "online表单批量删除"),
ONLINE_DELETE("/online/cgform/head/delete", "online表单删除"),
ONLINE_REMOVE("/online/cgform/head/removeRecord", "online表单移除"),
ONLINE_COPY("/online/cgform/head/copyOnline", "online表单生成视图"),
ONLINE_TABLE("/online/cgform/head/copyOnlineTable", "online表单复制表"),
ONLINE_BUTTON_AI_TEST("/online/cgform/button/aitest", "online表单自定义按钮生成数据"),
ONLINE_BUTTON_ADD("/online/cgform/button/add", "online表单自定义按钮新增"),
ONLINE_BUTTON_EDIT("/online/cgform/button/edit", "online表单自定义按钮编辑"),
ONLINE_BUTTON_DEL("/online/cgform/button/deleteBatch", "online表单自定义按钮删除"),
ONLINE_ENHANCE_JS("/online/cgform/head/enhanceJs/**", "online表单JS增强"),
ONLINE_ENHANCE_JAVA("/online/cgform/head/enhanceJava/**", "online表单JAVA增强"),
/**
* online报表配置请求
*/
ONLINE_CG_REPORT_ADD("/online/cgreport/head/add", "online报表新增"),
ONLINE_CG_REPORT_EDIT("/online/cgreport/head/editAll", "online报表编辑"),
ONLINE_CG_REPORT_DEL("/online/cgreport/head/delete", "online报表删除"),
ONLINE_CG_REPORT_PARSE_SQL("/online/cgreport/head/parseSql", "online报表SQL解析"),
/**
* online图表配置请求
*/
ONLINE_GRAPH_REPORT_ADD("/online/graphreport/head/add", "online图表新增"),
ONLINE_GRAPH_REPORT_EDIT("/online/graphreport/head/edit", "online图表编辑"),
ONLINE_GRAPH_REPORT_DEL("/online/graphreport/head/deleteBatch", "online图表删除"),
ONLINE_GRAPH_REPORT_PARSE_SQL("/online/cgreport/head/parseSql", "online图表解析SQL"),
/**
* 大屏配置请求
*/
BIG_SCREEN_DB_ADD("/bigscreen/bigScreenDb/add", "大屏数据源新增"),
BIG_SCREEN_DB_EDIT("/bigscreen/bigScreenDb/edit", "大屏数据源编辑"),
BIG_SCREEN_DB_DEL("/bigscreen/bigScreenDb/delete", "大屏数据源删除"),
BIG_SCREEN_DB_TEST_CONNECTION("/bigscreen/bigScreenDb/testConnection", "大屏数据源连接测试"),
// BIG_SCREEN_SAVE("/bigscreen/visual/save", "大屏新增"),
// BIG_SCREEN_EDIT("/bigscreen/visual/update", "大屏编辑"),
// BIG_SCREEN_COPY("/bigscreen/visual/copy", "大屏复制"),
// BIG_SCREEN_REMOVE("/bigscreen/visual/remove", "大屏移除"),
// BIG_SCREEN_DEL("/bigscreen/visual/deleteById", "大屏删除"),
/**
* 仪表盘配置请求
*/
DRAG_DB_ADD("/drag/onlDragDataSource/add", "仪表盘数据源新增"),
DRAG_DB_TEST_CONNECTION("/drag/onlDragDataSource/testConnection", "仪表盘数据源连接测试"),
DRAG_PARSE_SQL("/drag/onlDragDatasetHead/queryFieldBySql", "仪表盘数据集SQL解析"),
DRAG_DATASET_ADD("/drag/onlDragDatasetHead/add", "仪表盘数据集新增");
/**
* 其他配置请求
*/
private String url;
private String title;
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
LowCodeUrlsEnum(String url, String title) {
this.url = url;
this.title = title;
}
/**
* 根据code获取可用的数量
*
* @return
*/
public static List<String> getLowCodeInterceptUrls() {
return Arrays.stream(LowCodeUrlsEnum.values()).map(LowCodeUrlsEnum::getUrl).collect(Collectors.toList());
}
}

View File

@ -9,10 +9,7 @@ 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.web.mgt.DefaultWebSecurityManager;
import org.crazycake.shiro.IRedisManager;
import org.crazycake.shiro.RedisCacheManager;
import org.crazycake.shiro.RedisClusterManager;
import org.crazycake.shiro.RedisManager;
import org.crazycake.shiro.*;
import org.jeecg.common.constant.CommonConstant;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.config.JeecgBaseConfig;
@ -20,16 +17,21 @@ 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.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import org.springframework.core.env.Environment;
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.filter.DelegatingFilterProxy;
import redis.clients.jedis.HostAndPort;
import redis.clients.jedis.JedisCluster;
import javax.annotation.Resource;
import javax.servlet.DispatcherType;
import javax.servlet.Filter;
import java.util.*;
@ -49,6 +51,8 @@ public class ShiroConfig {
private Environment env;
@Resource
private JeecgBaseConfig jeecgBaseConfig;
@Autowired(required = false)
private RedisProperties redisProperties;
/**
* Filter Chain定义说明
@ -74,7 +78,6 @@ public class ShiroConfig {
}
}
}
filterChainDefinitionMap.put("/mini/user/phoneLogin","anon");//小程序手机号登录
filterChainDefinitionMap.put("/mini/user/login","anon");//小程序登录
filterChainDefinitionMap.put("/mini/article/**","anon");//小程序-文章
@ -104,8 +107,6 @@ public class ShiroConfig {
filterChainDefinitionMap.put("/sys/randomImage/**", "anon"); //登录验证码接口排除
filterChainDefinitionMap.put("/sys/checkCaptcha", "anon"); //登录验证码接口排除
filterChainDefinitionMap.put("/sys/login", "anon"); //登录接口排除
filterChainDefinitionMap.put("/sys/register", "anon"); //注册接口排除
filterChainDefinitionMap.put("/sys/findPassWord", "anon"); //找回密码接口排除
filterChainDefinitionMap.put("/sys/mLogin", "anon"); //登录接口排除
filterChainDefinitionMap.put("/sys/logout", "anon"); //登出接口排除
filterChainDefinitionMap.put("/sys/thirdLogin/**", "anon"); //第三方登录
@ -119,6 +120,9 @@ public class ShiroConfig {
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"); //登录二维码
@ -126,6 +130,7 @@ public class ShiroConfig {
filterChainDefinitionMap.put("/sys/checkAuth", "anon"); //授权接口排除
//update-begin--Author:scott Date:20221116 for排除静态资源后缀
filterChainDefinitionMap.put("/", "anon");
filterChainDefinitionMap.put("/doc.html", "anon");
filterChainDefinitionMap.put("/**/*.js", "anon");
@ -140,6 +145,7 @@ public class ShiroConfig {
filterChainDefinitionMap.put("/**/*.ttf", "anon");
filterChainDefinitionMap.put("/**/*.woff", "anon");
filterChainDefinitionMap.put("/**/*.woff2", "anon");
//update-end--Author:scott Date:20221116 for排除静态资源后缀
filterChainDefinitionMap.put("/druid/**", "anon");
filterChainDefinitionMap.put("/swagger-ui.html", "anon");
@ -147,13 +153,21 @@ public class ShiroConfig {
filterChainDefinitionMap.put("/webjars/**", "anon");
filterChainDefinitionMap.put("/v2/**", "anon");
// update-begin--Author:sunjianlei Date:20210510 for排除消息通告查看详情页面用于第三方APP
filterChainDefinitionMap.put("/sys/annountCement/show/**", "anon");
// update-end--Author:sunjianlei Date:20210510 for排除消息通告查看详情页面用于第三方APP
//积木报表排除
filterChainDefinitionMap.put("/jmreport/**", "anon");
filterChainDefinitionMap.put("/**/*.js.map", "anon");
filterChainDefinitionMap.put("/**/*.css.map", "anon");
//拖拽仪表盘设计器排除
filterChainDefinitionMap.put("/drag/view", "anon");
filterChainDefinitionMap.put("/drag/page/queryById", "anon");
filterChainDefinitionMap.put("/drag/onlDragDatasetHead/getAllChartData", "anon");
filterChainDefinitionMap.put("/drag/onlDragDatasetHead/getTotalData", "anon");
filterChainDefinitionMap.put("/drag/mock/json/**", "anon");
//大屏模板例子
filterChainDefinitionMap.put("/test/bigScreen/**", "anon");
filterChainDefinitionMap.put("/bigscreen/template1/**", "anon");
@ -172,10 +186,10 @@ public class ShiroConfig {
//测试模块排除
filterChainDefinitionMap.put("/test/seata/**", "anon");
// update-begin--author:liusq Date:20230522 for[issues/4829]访问不存在的url时会提示Token失效请重新登录呢
//错误路径排除
filterChainDefinitionMap.put("/error", "anon");
// update-end--author:liusq Date:20230522 for[issues/4829]访问不存在的url时会提示Token失效请重新登录呢
// 企业微信证书排除
filterChainDefinitionMap.put("/WW_verify*", "anon");
// 添加自己的过滤器并且取名为jwt
Map<String, Filter> filterMap = new HashMap<String, Filter>(1);
@ -193,6 +207,20 @@ public class ShiroConfig {
return shiroFilterFactoryBean;
}
//update-begin---author:chenrui ---date:20240126 forQQYUN-7932AI助手------------
@Bean
public FilterRegistrationBean shiroFilterRegistration() {
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(new DelegatingFilterProxy("shiroFilterFactoryBean"));
registration.setEnabled(true);
registration.addUrlPatterns("/*");
//支持异步
registration.setAsyncSupported(true);
registration.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ASYNC);
return registration;
}
//update-end---author:chenrui ---date:20240126 forQQYUN-7932AI助手------------
@Bean("securityManager")
public DefaultWebSecurityManager securityManager(ShiroRealm myRealm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
@ -270,11 +298,24 @@ public class ShiroConfig {
public IRedisManager redisManager() {
log.info("===============(2)创建RedisManager,连接Redis..");
IRedisManager manager;
// sentinel cluster redisissues/5569shiro集成 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.getSentinel().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());
redisManager.setPort(lettuceConnectionFactory.getPort());
redisManager.setHost(lettuceConnectionFactory.getHostName() + ":" + lettuceConnectionFactory.getPort());
//(lettuceConnectionFactory.getPort());
redisManager.setDatabase(lettuceConnectionFactory.getDatabase());
redisManager.setTimeout(0);
if (!StringUtils.isEmpty(lettuceConnectionFactory.getPassword())) {

View File

@ -62,9 +62,11 @@ public class ShiroRealm extends AuthorizingRealm {
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();
@ -74,7 +76,7 @@ public class ShiroRealm extends AuthorizingRealm {
info.setRoles(roleSet);
// 设置用户拥有的权限集合比如sys:role:add,sys:user:add
Set<String> permissionSet = commonApi.queryUserAuths(username);
Set<String> permissionSet = commonApi.queryUserAuths(userId);
info.addStringPermissions(permissionSet);
//System.out.println(permissionSet);
log.info("===============Shiro权限认证成功==============");

View File

@ -42,7 +42,7 @@ public class SignAuthInterceptor implements HandlerInterceptor {
String xTimestamp = request.getHeader(CommonConstant.X_TIMESTAMP);
if(oConvertUtils.isEmpty(xTimestamp)){
Result<?> result = Result.error("Sign签名校验失败");
Result<?> result = Result.error("Sign签名校验失败,时间戳为空");
log.error("Sign 签名校验失败Header xTimestamp 为空");
//校验失败返回前端
response.setCharacterEncoding("UTF-8");
@ -79,6 +79,7 @@ public class SignAuthInterceptor implements HandlerInterceptor {
log.debug("Sign 签名通过Header Sign : {}",headerSign);
return true;
} else {
log.info("sign allParams: {}", allParams);
log.error("request URI = " + request.getRequestURI());
log.error("Sign 签名校验失败Header Sign : {}",headerSign);
//校验失败返回前端

View File

@ -1,12 +1,5 @@
package org.jeecg.config.sign.util;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.constant.SymbolConstant;
import org.jeecg.common.util.oConvertUtils;
import org.springframework.http.HttpMethod;
import javax.servlet.http.HttpServletRequest;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
@ -17,6 +10,15 @@ import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;
import javax.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.constant.SymbolConstant;
import org.jeecg.common.util.oConvertUtils;
import org.springframework.http.HttpMethod;
import com.alibaba.fastjson.JSONObject;
/**
* http 工具类 获取请求中的参数
*
@ -44,8 +46,12 @@ public class HttpUtils {
//https://www.52dianzi.com/category/article/37/565371.html
if(deString.contains("%")){
deString = URLDecoder.decode(deString, "UTF-8");
log.info("存在%情况下,执行两次解码 — pathVariable decode: {}",deString);
try {
deString = URLDecoder.decode(deString, "UTF-8");
log.info("存在%情况下,执行两次解码 — pathVariable decode: {}",deString);
} catch (Exception e) {
//e.printStackTrace();
}
}
log.info(" pathVariable decode: {}",deString);
result.put(SignUtil.X_PATH_VARIABLE, deString);
@ -166,7 +172,11 @@ public class HttpUtils {
String[] params = param.split("&");
for (String s : params) {
int index = s.indexOf("=");
result.put(s.substring(0, index), s.substring(index + 1));
//update-begin---author:chenrui ---date:20240222 for[issues/5879]数据查询传ds=造成的异常------------
if (index != -1) {
result.put(s.substring(0, index), s.substring(index + 1));
}
//update-end---author:chenrui ---date:20240222 for[issues/5879]数据查询传ds=造成的异常------------
}
return result;
}
@ -190,7 +200,11 @@ public class HttpUtils {
String[] params = param.split("&");
for (String s : params) {
int index = s.indexOf("=");
result.put(s.substring(0, index), s.substring(index + 1));
//update-begin---author:chenrui ---date:20240222 for[issues/5879]数据查询传ds=造成的异常------------
if (index != -1) {
result.put(s.substring(0, index), s.substring(index + 1));
}
//update-end---author:chenrui ---date:20240222 for[issues/5879]数据查询传ds=造成的异常------------
}
return result;
}

View File

@ -1,80 +0,0 @@
package org.jeecg.config.thirdapp;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
/**
* 第三方App对接配置
* @author: jeecg-boot
*/
@Configuration
public class ThirdAppConfig {
/**
* 钉钉
*/
public final static String DINGTALK = "DINGTALK";
/**
* 企业微信
*/
public final static String WECHAT_ENTERPRISE = "WECHAT_ENTERPRISE";
/**
* 是否启用 第三方App对接
*/
@Value("${third-app.enabled:false}")
private boolean enabled;
/**
* 系统类型目前支持WECHAT_ENTERPRISE企业微信DINGTALK 钉钉
*/
@Autowired
private ThirdAppTypeConfig type;
public boolean isEnabled() {
return enabled;
}
public ThirdAppConfig setEnabled(boolean enabled) {
this.enabled = enabled;
return this;
}
/**
* 获取企业微信配置
*/
public ThirdAppTypeItemVo getWechatEnterprise() {
return this.type.getWECHAT_ENTERPRISE();
}
/**
* 获取钉钉配置
*/
public ThirdAppTypeItemVo getDingtalk() {
return this.type.getDINGTALK();
}
/**
* 获取企业微信是否启用
*/
public boolean isWechatEnterpriseEnabled() {
try {
return this.enabled && this.getWechatEnterprise().isEnabled();
} catch (Exception e) {
return false;
}
}
/**
* 获取钉钉是否启用
*/
public boolean isDingtalkEnabled() {
try {
return this.enabled && this.getDingtalk().isEnabled();
} catch (Exception e) {
return false;
}
}
}

View File

@ -1,26 +0,0 @@
package org.jeecg.config.thirdapp;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
/**
* 第三方APP配置
*
* @author sunjianlei
*/
@Data
@Configuration
@ConfigurationProperties(prefix = "third-app.type")
public class ThirdAppTypeConfig {
/**
* 对应企业微信配置
*/
private ThirdAppTypeItemVo WECHAT_ENTERPRISE;
/**
* 对应钉钉配置
*/
private ThirdAppTypeItemVo DINGTALK;
}

View File

@ -1,37 +0,0 @@
package org.jeecg.config.thirdapp;
import lombok.Data;
/**
* 第三方App对接
* @author: jeecg-boot
*/
@Data
public class ThirdAppTypeItemVo {
/**
* 是否启用
*/
private boolean enabled;
/**
* 应用Key
*/
private String clientId;
/**
* 应用Secret
*/
private String clientSecret;
/**
* 应用ID
*/
private String agentId;
/**
* 目前仅企业微信用到自建应用Secret
*/
private String agentAppSecret;
public int getAgentIdInt() {
return Integer.parseInt(agentId);
}
}

View File

@ -0,0 +1,39 @@
package org.jeecg.config.vo;
/**
* 平台安全配置
*
* @author: scott
* @date: 2023年09月05日 9:25
*/
public class Firewall {
/**
* 数据源安全 (开启后Online报表和图表的数据源为必填)
*/
private Boolean dataSourceSafe = false;
/**
* 低代码模式dev:开发模式prod:发布模式关闭所有在线开发配置能力
*/
private String lowCodeMode;
// /**
// * 表字典安全模式white:白名单配置了白名单的表才能通过表字典方式访问black:黑名单配置了黑名单的表不允许表字典方式访问
// */
// private String tableDictMode;
public Boolean getDataSourceSafe() {
return dataSourceSafe;
}
public void setDataSourceSafe(Boolean dataSourceSafe) {
this.dataSourceSafe = dataSourceSafe;
}
public String getLowCodeMode() {
return lowCodeMode;
}
public void setLowCodeMode(String lowCodeMode) {
this.lowCodeMode = lowCodeMode;
}
}

View File

@ -0,0 +1,31 @@
package org.jeecg.config.vo;
import lombok.Data;
@Data
public class WeiXinPay {
/**
* 微信公众号id
*/
private String appId;
/**
* 商户号id
*/
private String mchId;
/**
* 商户号秘钥
*/
private String apiKey;
/**
* 回调地址
*/
private String notifyUrl;
/**
* 是否开启会员认证
*/
private Boolean openVipLimit;
/**
* 证书路径
*/
private String certPath;
}

View File

@ -0,0 +1,104 @@
<html>
<head>
<meta http-equiv="content-type" content="text/html;charset=utf-8">
</head>
<body>
<div class="box-content">
<div class="info-top">
<img src="https://www.jeecg.com/images/logo.png" style="float: left; margin: 0 10px 0 0; width: 32px;height:32px" /><div style="color:#fff"><strong>【重要】流程办理的通知</strong></div>
</div>
<div class="info-wrap">
<div class="tips" style="padding:15px;">
<p style="margin: 10px 0;">
您好,您有一个新的流程任务亟待处理,任务内容如下::
</p>
<table style="width: 400px; border-spacing: 0px; border-collapse: collapse; border: none; margin-top: 20px;"><tbody>
<tr style="height: 45px;">
<td style="width: 150px; height: 40px; background: #F6F6F6;border: 1px solid #DBDBDB; font-size: 14px; font-weight: normal; text-align: left; padding-left: 14px;">
流程名称
</td>
<td style="width: 250px;height: 40px; border: 1px solid #DBDBDB; font-size: 14px; font-weight: normal; text-align: left; padding-left: 14px;">
${bpm_name}<a style="color: #006eff;" href="${url}" target="_blank" rel="noopener">[立刻办理]</a>
</td>
</tr>
<tr style="height: 45px;">
<td style="width: 150px;height: 40px; background: #F6F6F6;border: 1px solid #DBDBDB; font-size: 14px; font-weight: normal; text-align: left; padding-left: 14px;">
催办任务
</td>
<td style="width: 250px;height: 40px; border: 1px solid #DBDBDB; font-size: 14px; font-weight: normal; text-align: left; padding-left: 14px;">
${bpm_task}
</td>
</tr>
<tr style="height: 45px;">
<td style="width: 150px; height: 40px; background: #F6F6F6;border: 1px solid #DBDBDB; font-size: 14px; font-weight: normal; text-align: left; padding-left: 14px;">
催办时间
</td>
<td style="width: 250px;height: 40px; border: 1px solid #DBDBDB; font-size: 14px; font-weight: normal; text-align: left; padding-left: 14px;">
${datetime}
</td>
</tr>
<tr style="height: 45px;">
<td style="width: 150px; height: 40px; background: #F6F6F6;border: 1px solid #DBDBDB; font-size: 14px; font-weight: normal; text-align: left; padding-left: 14px;">
催办内容
</td>
<td style="width: 250px;height: 40px; border: 1px solid #DBDBDB; font-size: 14px; font-weight: normal; text-align: left; padding-left: 14px;">
${remark}
</td>
</tr>
</tbody>
</table>
</div>
<div class="footer">北京国炬平台</div>
</div>
<div style="margin-top: 60px;margin-bottom: 10px;">
<span style="font-size: 13px; font-weight: bold; color: #666;">温馨提醒</span>
<div style="line-height: 24px; margin-top: 10px;">
<div style="font-size: 13px; color: #666;">使用过程中如有任何问题,请联系系统管理员。</div>
</div>
</div>
<div style="width: 600px; margin: 0 auto; margin-top: 50px; font-size: 12px; -webkit-font-smoothing: subpixel-antialiased; text-size-adjust: 100%;">
<p style="text-align: center; line-height: 20.4px; text-size-adjust: 100%; font-family: 'Microsoft YaHei'!important; padding: 0px !important; margin: 0px !important; color: #7e8890 !important;">
<span class="appleLinks">Copyright © 2023-2024 北京国炬信息技术有限公司. 保留所有权利。</span>
</p>
<p style="text-align: center;line-height: 20.4px; text-size-adjust: 100%; font-family: 'Microsoft YaHei'!important; padding: 0px !important; margin: 0px; color: #7e8890 !important; margin-top: 10px;">
<span class="appleLinks">邮件由系统自动发送,请勿直接回复本邮件!</span>
</p>
</div>
</div>
</body>
<style>
.box-content{
width: 80%;
margin: 20px auto;
max-width: 800px;
min-width: 600px;
}
.info-top{
padding: 15px 25px;
border-top-left-radius: 10px;
border-top-right-radius: 10px;
background: #4ea3f2;
color: #fff;
overflow: hidden;
line-height: 32px;
}
.info-wrap{
border-bottom-left-radius: 10px;
border-bottom-right-radius: 10px;
border:1px solid #ddd;
overflow: hidden;
padding: 15px 15px 20px;
}
.footer{
text-align: right;
color: #999;
padding: 0 15px 15px;
}
</style>
</html>

View File

@ -0,0 +1,101 @@
<html>
<head>
<meta http-equiv="content-type" content="text/html;charset=utf-8">
</head>
<body>
<div class="box-content">
<div class="info-top">
<img src="https://jeecgdev.oss-cn-beijing.aliyuncs.com/temp/logo(1)_1697180761742.png"
style="float: left; margin: 0 10px 0 0; width: 32px;height:32px"/>
<div style="color:#fff"><strong>【重要】流程办理的通知</strong></div>
</div>
<div class="info-wrap">
<div class="tips" style="padding:15px;">
<p style="margin: 10px 0;">
您好, ${REALNAME}<br>您有一个新的流程任务需要处理,任务内容如下:
</p>
<table style="width: 400px; border-spacing: 0px; border-collapse: collapse; border: none; margin-top: 20px;">
<tbody>
<tr style="height: 45px;">
<td style="width: 150px;height: 40px; background: #F6F6F6;border: 1px solid #DBDBDB; font-size: 14px; font-weight: normal; text-align: left; padding-left: 14px;">
业务标题
</td>
<td style="width: 250px;height: 40px; border: 1px solid #DBDBDB; font-size: 14px; font-weight: normal; text-align: left; padding-left: 14px;">
${title}
</td>
</tr>
<tr style="height: 45px;">
<td style="width: 150px; height: 40px; background: #F6F6F6;border: 1px solid #DBDBDB; font-size: 14px; font-weight: normal; text-align: left; padding-left: 14px;">
流程名称
</td>
<td style="width: 250px;height: 40px; border: 1px solid #DBDBDB; font-size: 14px; font-weight: normal; text-align: left; padding-left: 14px;">
${name}
<a style="color: #006eff;" href="${url}" target="_blank" rel="noopener">[立刻办理]</a>
</td>
</tr>
<tr style="height: 45px;">
<td style="width: 150px; height: 40px; background: #F6F6F6;border: 1px solid #DBDBDB; font-size: 14px; font-weight: normal; text-align: left; padding-left: 14px;">
任务节点
</td>
<td style="width: 250px;height: 40px; border: 1px solid #DBDBDB; font-size: 14px; font-weight: normal; text-align: left; padding-left: 14px;">
${task}
</td>
</tr>
</tbody>
</table>
</div>
<div class="footer">北京国炬平台</div>
</div>
<div style="margin-top: 60px;margin-bottom: 10px;">
<span style="font-size: 13px; font-weight: bold; color: #666;">温馨提醒</span>
<div style="line-height: 24px; margin-top: 10px;">
<div style="font-size: 13px; color: #666;">使用过程中如有任何问题,请联系系统管理员。</div>
</div>
</div>
<div style="width: 600px; margin: 0 auto; margin-top: 50px; font-size: 12px; -webkit-font-smoothing: subpixel-antialiased; text-size-adjust: 100%;">
<p style="text-align: center; line-height: 20.4px; text-size-adjust: 100%; font-family: 'Microsoft YaHei'!important; padding: 0px !important; margin: 0px !important; color: #7e8890 !important;">
<span class="appleLinks">Copyright © 2023-2024 北京国炬信息技术有限公司. 保留所有权利。</span>
</p>
<p style="text-align: center;line-height: 20.4px; text-size-adjust: 100%; font-family: 'Microsoft YaHei'!important; padding: 0px !important; margin: 0px; color: #7e8890 !important; margin-top: 10px;">
<span class="appleLinks">邮件由系统自动发送,请勿直接回复本邮件!</span>
</p>
</div>
</div>
</body>
<style>
.box-content {
width: 80%;
margin: 20px auto;
max-width: 800px;
min-width: 600px;
}
.info-top {
padding: 15px 25px;
border-top-left-radius: 10px;
border-top-right-radius: 10px;
background: #4ea3f2;
color: #fff;
overflow: hidden;
line-height: 32px;
}
.info-wrap {
border-bottom-left-radius: 10px;
border-bottom-right-radius: 10px;
border: 1px solid #ddd;
overflow: hidden;
padding: 15px 15px 20px;
}
.footer {
text-align: right;
color: #999;
padding: 0 15px 15px;
}
</style>
</html>

View File

@ -0,0 +1,78 @@
<html>
<head>
<meta http-equiv="content-type" content="text/html;charset=utf-8">
</head>
<body>
<div class="box-content">
<div class="info-top">
<img src="https://qiaoqiaoyun.oss-cn-beijing.aliyuncs.com/site/qqyunemaillogo.png" style="width: 35px;height:35px; background: #5e8ee5; border-radius: 5px;" />
<div style="color:#fff;">
<strong>【重要】新数据提醒</strong>
</div>
</div>
<div class="info-wrap">
<div class="tips" style="padding:15px;">
<p style="margin: 10px 0;">
尊敬的 ${userName} 用户,您好:
</p>
你的表单 <a style="color: #006eff;" href="${formLink}" target="_blank" rel="noopener">【${formName}】</a>
在 ${createTime} 新增了1条数据。
${dataMarkdown}
<p>
如需查看更多请点击
<a style="color: #006eff;" href="${moreLink}" target="_blank" rel="noopener">[查看所有数据]</a>
</p>
</div>
<div class="footer">敲敲云平台</div>
<div class="footer" id="currentTime"></div>
</div>
<div style="width: 600px; margin: 0 auto; margin-top: 50px; font-size: 12px; -webkit-font-smoothing: subpixel-antialiased; text-size-adjust: 100%;">
<p style="text-align: center; line-height: 20.4px; text-size-adjust: 100%; font-family: 'Microsoft YaHei'!important; padding: 0px !important; margin: 0px !important; color: #7e8890 !important;">
<span class="appleLinks">Copyright © 2023-2024 北京敲敲云科技有限公司. 保留所有权利。</span>
</p>
<p style="text-align: center;line-height: 20.4px; text-size-adjust: 100%; font-family: 'Microsoft YaHei'!important; padding: 0px !important; margin: 0px; color: #7e8890 !important; margin-top: 10px;">
<span class="appleLinks">邮件由系统自动发送,请勿直接回复本邮件!</span>
</p>
</div>
</div>
</body>
<style>
.box-content{
width: 80%;
margin: 20px auto;
max-width: 800px;
min-width: 600px;
}
.info-top{
display: flex;
align-items: center;
padding: 15px 25px;
border-top-left-radius: 10px;
border-top-right-radius: 10px;
background: #4ea3f2;
color: #fff;
overflow: hidden;
line-height: 32px;
}
.info-wrap{
border-bottom-left-radius: 10px;
border-bottom-right-radius: 10px;
border:1px solid #ddd;
overflow: hidden;
padding: 15px 15px 20px;
}
.footer{
text-align: right;
color: #999;
padding: 0 15px 15px;
}
</style>
</html>

View File

@ -0,0 +1,75 @@
package org.jeecg.test.sqlinjection;
import lombok.extern.slf4j.Slf4j;
import net.sf.jsqlparser.JSQLParserException;
import org.jeecg.common.util.SqlInjectionUtil;
import org.junit.Test;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
/**
* SQL注入攻击检查测试
* @author: liusq
* @date: 2023年09月08日
*/
@Slf4j
public class TestInjectWithSqlParser {
/**
* 注入测试
*
* @param sql
* @return
*/
private boolean isExistSqlInject(String sql) {
try {
SqlInjectionUtil.specialFilterContentForOnlineReport(sql);
return false;
} catch (Exception e) {
log.info("===================================================");
return true;
}
}
@Test
public void test() throws JSQLParserException {
//不存在sql注入
assertFalse(isExistSqlInject("select * from fm_time where dept_id=:sqlparamsmap.id and time=:sqlparamsmap.time"));
assertFalse(isExistSqlInject("select * from test"));
assertFalse(isExistSqlInject("select load_file(\"C:\\\\benben.txt\")"));
assertFalse(isExistSqlInject("WITH SUB1 AS (SELECT user FROM t1) SELECT * FROM T2 WHERE id > 123 "));
//存在sql注入
assertTrue(isExistSqlInject("or 1= 1 --"));
assertTrue(isExistSqlInject("select * from test where sleep(%23)"));
assertTrue(isExistSqlInject("select * from test where id=1 and multipoint((select * from(select * from(select user())a)b));"));
assertTrue(isExistSqlInject("select * from users;show databases;"));
assertTrue(isExistSqlInject("select * from dc_device where id=1 and length((select group_concat(table_name) from information_schema.tables where table_schema=database()))>13"));
assertTrue(isExistSqlInject("update user set name = '123'"));
assertTrue(isExistSqlInject("SELECT * FROM users WHERE username = 'admin' AND password = '123456' OR 1=1;--"));
assertTrue(isExistSqlInject("select * from users where id=1 and (select count(*) from information_schema.tables where table_schema='数据库名')>4 %23"));
assertTrue(isExistSqlInject("select * from dc_device where sleep(5) %23"));
assertTrue(isExistSqlInject("select * from dc_device where id in (select id from other)"));
assertTrue(isExistSqlInject("select * from dc_device where id in (select id from other)"));
assertTrue(isExistSqlInject("select * from dc_device where 2=2.0 or 2 != 4"));
assertTrue(isExistSqlInject("select * from dc_device where 1!=2.0"));
assertTrue(isExistSqlInject("select * from dc_device where id=floor(2.0)"));
assertTrue(isExistSqlInject("select * from dc_device where not true"));
assertTrue(isExistSqlInject("select * from dc_device where 1 or id > 0"));
assertTrue(isExistSqlInject("select * from dc_device where 'tom' or id > 0"));
assertTrue(isExistSqlInject("select * from dc_device where '-2.3' "));
assertTrue(isExistSqlInject("select * from dc_device where 2 "));
assertTrue(isExistSqlInject("select * from dc_device where (3+2) "));
assertTrue(isExistSqlInject("select * from dc_device where -1 IS TRUE"));
assertTrue(isExistSqlInject("select * from dc_device where 'hello' is null "));
assertTrue(isExistSqlInject("select * from dc_device where '2022-10-31' and id > 0"));
assertTrue(isExistSqlInject("select * from dc_device where id > 0 or 1!=2.0 "));
assertTrue(isExistSqlInject("select * from dc_device where id > 0 or 1 in (1,3,4) "));
assertTrue(isExistSqlInject("select * from dc_device UNION select name from other"));
assertTrue(isExistSqlInject("(SELECT 6240 FROM (SELECT(SLEEP(5))and 1=2)vidl)"));
}
}

View File

@ -0,0 +1,50 @@
package org.jeecg.test.sqlinjection;
import lombok.extern.slf4j.Slf4j;
import net.sf.jsqlparser.JSQLParserException;
import org.jeecg.common.util.SqlInjectionUtil;
import org.junit.Test;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
/**
* SQL注入攻击检查测试
* @author: liusq
* @date: 2023年09月08日
*/
@Slf4j
public class TestSqlInjectForDict {
/**
* 注入测试
*
* @param sql
* @return
*/
private boolean isExistSqlInject(String sql) {
try {
SqlInjectionUtil.specialFilterContentForDictSql(sql);
return false;
} catch (Exception e) {
log.info("===================================================");
return true;
}
}
@Test
public void test() throws JSQLParserException {
//不存在sql注入
assertFalse(isExistSqlInject("sys_user,realname,id"));
assertFalse(isExistSqlInject("oa_officialdoc_organcode,organ_name,id"));
assertFalse(isExistSqlInject("onl_cgform_head where table_type!=3 and copy_type=0,table_txt,table_name"));
assertFalse(isExistSqlInject("onl_cgform_head where copy_type = 0,table_txt,table_name"));
//存在sql注入
assertTrue(isExistSqlInject("or 1= 1 --"));
assertTrue(isExistSqlInject("select * from test where sleep(%23)"));
}
}

View File

@ -0,0 +1,60 @@
package org.jeecg.test.sqlinjection;
import lombok.extern.slf4j.Slf4j;
import net.sf.jsqlparser.JSQLParserException;
import org.jeecg.common.util.SqlInjectionUtil;
import org.junit.Test;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
/**
* SQL注入攻击检查测试
* @author: liusq
* @date: 2023年09月08日
*/
@Slf4j
public class TestSqlInjectForOnlineReport {
/**
* 注入测试
*
* @param sql
* @return
*/
private boolean isExistSqlInject(String sql) {
try {
SqlInjectionUtil.specialFilterContentForOnlineReport(sql);
return false;
} catch (Exception e) {
log.info("===================================================");
return true;
}
}
@Test
public void test() throws JSQLParserException {
//不存在sql注入
assertFalse(isExistSqlInject("select * from fm_time where dept_id=:sqlparamsmap.id and time=:sqlparamsmap.time"));
assertFalse(isExistSqlInject("select * from test"));
assertFalse(isExistSqlInject("select load_file(\"C:\\\\benben.txt\")"));
assertFalse(isExistSqlInject("select * from dc_device where id in (select id from other)"));
assertFalse(isExistSqlInject("select * from dc_device UNION select name from other"));
//存在sql注入
assertTrue(isExistSqlInject("(SELECT 6240 FROM (SELECT(SLEEP(5))and 1=2)vidl)"));
assertTrue(isExistSqlInject("or 1= 1 --"));
assertTrue(isExistSqlInject("select * from test where sleep(%23)"));
assertTrue(isExistSqlInject("select * from test where SLEEP(3)"));
assertTrue(isExistSqlInject("select * from test where id=1 and multipoint((select * from(select * from(select user())a)b));"));
assertTrue(isExistSqlInject("select * from users;show databases;"));
assertTrue(isExistSqlInject("select * from dc_device where id=1 and length((select group_concat(table_name) from information_schema.tables where table_schema=database()))>13"));
assertTrue(isExistSqlInject("update user set name = '123'"));
assertTrue(isExistSqlInject("SELECT * FROM users WHERE username = 'admin' AND password = '123456' OR 1=1;--"));
assertTrue(isExistSqlInject("select * from users where id=1 and (select count(*) from information_schema.tables where table_schema='数据库名')>4 %23"));
assertTrue(isExistSqlInject("select * from dc_device where sleep(5) %23"));
}
}

View File

@ -0,0 +1,103 @@
package org.jeecg.test.sqlinjection;
import com.baomidou.mybatisplus.core.toolkit.sql.SqlInjectionUtils;
import org.jeecg.common.util.SqlInjectionUtil;
import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;
/**
* @Description: SQL注入测试类
* @author: scott
* @date: 2023年08月14日 9:55
*/
public class TestSqlInjection {
/**
* 表名带别名同时有html编码字符
*/
@Test
public void testSpecialSQL() {
String tableName = "sys_user t";
//解决使用参数tableName=sys_user t&复测漏洞仍然存在
if (tableName.contains(" ")) {
tableName = tableName.substring(0, tableName.indexOf(" "));
}
//issues/4393 sys_user , (sys_user), sys_user%20, %60sys_user%60
String reg = "\\s+|\\(|\\)|`";
tableName = tableName.replaceAll(reg, "");
System.out.println(tableName);
}
/**
* 测试sql是否含sql注入风险
* <p>
* mybatis plus的方法
*/
@Test
public void sqlInjectionCheck() {
String sql = "select * from sys_user";
System.out.println(SqlInjectionUtils.check(sql));
}
/**
* 测试sql是否有SLEEP风险
* <p>
* mybatisPlus的方法
*/
@Test
public void sqlSleepCheck() {
SqlInjectionUtil.checkSqlAnnotation("(SELECT 6240 FROM (SELECT(SLEEP(5))and 1=2)vidl)");
}
/**
* 测试sql是否含sql注入风险
* <p>
* 自定义方法
*/
@Test
public void sqlInjectionCheck2() {
String sql = "select * from sys_user";
SqlInjectionUtil.specialFilterContentForOnlineReport(sql);
}
/**
* 字段定义只能是是字母 数字 下划线的组合不允许有空格转义字符串等
* <p>
* 判断字段名是否符合规范
*/
@Test
public void testFieldSpecification() {
List<String> list = new ArrayList();
list.add("Hello World!");
list.add("Hello%20World!");
list.add("HelloWorld!");
list.add("Hello World");
list.add("age");
list.add("user_name");
list.add("user_name%20");
list.add("user_name%20 ");
for (String input : list) {
boolean containsSpecialChars = isValidString(input);
System.out.println("input:" + input + " ,包含空格和特殊字符: " + containsSpecialChars);
}
}
/**
* 字段定义只能是是字母 数字 下划线的组合不允许有空格转义字符串等
*
* @param input
* @return
*/
private static boolean isValidString(String input) {
Pattern pattern = Pattern.compile("^[a-zA-Z0-9_]+$");
return pattern.matcher(input).matches();
}
}

View File

@ -0,0 +1,109 @@
package org.jeecg.test.sqlparse;
import net.sf.jsqlparser.JSQLParserException;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.common.util.sqlparse.JSqlParserUtils;
import org.jeecg.common.util.sqlparse.vo.SelectSqlInfo;
import org.junit.Test;
import java.util.Map;
/**
* 针对 JSqlParserUtils 的单元测试
*/
public class JSqlParserUtilsTest {
private static final String[] sqlList = new String[]{
"select * from sys_user",
"select u.* from sys_user u",
"select u.*, c.name from sys_user u, demo c",
"select u.age, c.name from sys_user u, demo c",
"select sex, age, c.name from sys_user, demo c",
// 别名测试
"select username as realname from sys_user",
"select username as realname, u.realname as aaa, u.id bbb from sys_user u",
// 不存在真实地查询字段
"select count(1) from sys_user",
// 函数式字段
"select max(sex), id from sys_user",
// 复杂嵌套函数式字段
"select CONCAT(CONCAT(' _ ', sex), ' - ' , birthday) as info, id from sys_user",
// 更复杂的嵌套函数式字段
"select CONCAT(CONCAT(101,'_',NULL, DATE(create_time),'_',sex),' - ',birthday) as info, id from sys_user",
// 子查询SQL
"select u.name1 as name2 from (select username as name1 from sys_user) u",
// 多层嵌套子查询SQL
"select u2.name2 as name3 from (select u1.name1 as name2 from (select username as name1 from sys_user) u1) u2",
// 字段子查询SQL
"select id, (select username as name1 from sys_user u2 where u1.id = u2.id) as name2 from sys_user u1",
// 带条件的SQL不解析where条件里的字段但不影响解析查询字段
"select username as name1 from sys_user where realname LIKE '%张%'",
// 多重复杂关联表查询解析包含的表为sys_user, sys_depart, sys_dict_item, demo
"" +
"SELECT " +
" u.*, d.age, sd.item_text AS sex, (SELECT count(sd.id) FROM sys_depart sd) AS count " +
"FROM " +
" (SELECT sd.username AS foo, sd.realname FROM sys_user sd) u, " +
" demo d " +
"LEFT JOIN sys_dict_item AS sd ON d.sex = sd.item_value " +
"WHERE sd.dict_id = '3d9a351be3436fbefb1307d4cfb49bf2'",
};
@Test
public void testParseSelectSql() {
System.out.println("-----------------------------------------");
for (String sql : sqlList) {
System.out.println("待测试的sql" + sql);
try {
// 解析所有的表名key=表名value=解析后的sql信息
Map<String, SelectSqlInfo> parsedMap = JSqlParserUtils.parseAllSelectTable(sql);
assert parsedMap != null;
for (Map.Entry<String, SelectSqlInfo> entry : parsedMap.entrySet()) {
System.out.println("表名:" + entry.getKey());
this.printSqlInfo(entry.getValue(), 1);
}
} catch (JSQLParserException e) {
System.out.println("SQL解析出现异常" + e.getMessage());
}
System.out.println("-----------------------------------------");
}
}
private void printSqlInfo(SelectSqlInfo sqlInfo, int level) {
String beforeStr = this.getBeforeStr(level);
if (sqlInfo.getFromTableName() == null) {
// 子查询
System.out.println(beforeStr + "子查询:" + sqlInfo.getFromSubSelect().getParsedSql());
this.printSqlInfo(sqlInfo.getFromSubSelect(), level + 1);
} else {
// 非子查询
System.out.println(beforeStr + "查询的表名:" + sqlInfo.getFromTableName());
}
if (oConvertUtils.isNotEmpty(sqlInfo.getFromTableAliasName())) {
System.out.println(beforeStr + "查询的表别名:" + sqlInfo.getFromTableAliasName());
}
if (sqlInfo.isSelectAll()) {
System.out.println(beforeStr + "查询的字段:*");
} else {
System.out.println(beforeStr + "查询的字段:" + sqlInfo.getSelectFields());
System.out.println(beforeStr + "真实的字段:" + sqlInfo.getRealSelectFields());
if (sqlInfo.getFromTableName() == null) {
System.out.println(beforeStr + "所有的字段(包括子查询):" + sqlInfo.getAllRealSelectFields());
}
}
}
// 打印前缀根据层级来打印
private String getBeforeStr(int level) {
if (level == 0) {
return "";
}
StringBuilder beforeStr = new StringBuilder();
for (int i = 0; i < level; i++) {
beforeStr.append(" ");
}
beforeStr.append("- ");
return beforeStr.toString();
}
}

27
jeecg-module-demo/pom.xml Normal file
View File

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>jeecg-boot-parent</artifactId>
<groupId>org.jeecgframework.boot</groupId>
<version>3.6.3</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>jeecg-module-demo</artifactId>
<dependencies>
<dependency>
<groupId>org.jeecgframework.boot</groupId>
<artifactId>jeecg-boot-base-core</artifactId>
</dependency>
<!-- chatgpt -->
<dependency>
<groupId>org.jeecgframework.boot</groupId>
<artifactId>jeecg-boot-starter-chatgpt</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,68 @@
//package org.jeecg.modules.demo.cloud.controller;
//
//import com.alibaba.csp.sentinel.annotation.SentinelResource;
//import io.swagger.annotations.Api;
//import io.swagger.annotations.ApiOperation;
//import lombok.extern.slf4j.Slf4j;
//import org.jeecg.common.api.vo.Result;
//import org.jeecg.common.system.api.ISysBaseAPI;
//import org.jeecg.common.system.vo.DictModel;
//import org.springframework.beans.factory.annotation.Autowired;
//import org.springframework.web.bind.annotation.GetMapping;
//import org.springframework.web.bind.annotation.RequestMapping;
//import org.springframework.web.bind.annotation.RestController;
//import javax.annotation.Resource;
//import java.util.List;
//
///**
// *
// */
//@Slf4j
//@Api(tags = "【微服务】单元测试")
//@RestController
//@RequestMapping("/test")
//public class JcloudDemoFeignController {
// @Resource
// private ISysBaseAPI sysBaseApi;
//// @Autowired
//// private ErpHelloApi erpHelloApi;
//
// /**
// * 测试
// *
// * @return
// */
// @GetMapping("/callSystem")
// //@SentinelResource(value = "remoteDict",fallback = "getDefaultHandler")
// @ApiOperation(value = "通过feign调用system服务", notes = "测试jeecg-demo服务是否通过fegin调用system服务接口")
// public Result getRemoteDict() {
// List<DictModel> list = sysBaseApi.queryAllDict();
// return Result.OK(list);
// }
//
//
//// /**
//// * 测试调用 erp 微服务接口
//// * 如何测试通过archetype生成微服务模块快速集成测试
//// * https://help.jeecg.com/java/springcloud/archetype.html
//// * @return
//// */
//// @GetMapping("/callErp")
//// @ApiOperation(value = "测试feign erp", notes = "测试feign erp")
//// public Result callErp() {
//// log.info("call erp 服务");
//// String res = erpHelloApi.callHello();
//// return Result.OK(res);
//// }
//
// /**
// * 熔断默认回调函数
// *
// * @return
// */
// public Result<Object> getDefaultHandler() {
// log.info("测试JcloudDemoController-remoteDict 熔断降级");
// return Result.error("测试JcloudDemoController-remoteDict 熔断降级");
// }
//
//}

View File

@ -0,0 +1,33 @@
package org.jeecg.modules.demo.cloud.controller;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.api.vo.Result;
import org.jeecg.modules.demo.cloud.service.JcloudDemoService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
/**
* 服务端提供方feign接口
* 提供给system-start调用测试看feign是否畅通
* @author: jeecg-boot
*/
@Slf4j
@RestController
@RequestMapping("/test")
public class JcloudDemoProviderController {
@Resource
private JcloudDemoService jcloudDemoService;
@GetMapping("/getMessage")
public String getMessage(@RequestParam(name = "name") String name) {
String msg = jcloudDemoService.getMessage(name);
log.info(" 微服务被调用:{} ",msg);
return msg;
}
}

View File

@ -0,0 +1,17 @@
package org.jeecg.modules.demo.cloud.service;
import org.jeecg.common.api.vo.Result;
/**
* @Description: JcloudDemoService接口
* @author: jeecg-boot
*/
public interface JcloudDemoService {
/**
* 获取信息测试
* @param name 姓名
* @return "Hello" + name
*/
String getMessage(String name);
}

View File

@ -0,0 +1,18 @@
package org.jeecg.modules.demo.cloud.service.impl;
import org.jeecg.common.api.vo.Result;
import org.jeecg.modules.demo.cloud.service.JcloudDemoService;
import org.springframework.stereotype.Service;
/**
* @Description: JcloudDemoServiceImpl实现类
* @author: jeecg-boot
*/
@Service
public class JcloudDemoServiceImpl implements JcloudDemoService {
@Override
public String getMessage(String name) {
String resMsg = "Hello我是jeecg-demo服务节点收到你的消息"+ name +"";
return resMsg;
}
}

View File

@ -0,0 +1,74 @@
//
//package org.jeecg.modules.demo.cloud.xxljob;
//
//import com.xxl.job.core.biz.model.ReturnT;
//import com.xxl.job.core.handler.annotation.XxlJob;
//import lombok.extern.slf4j.Slf4j;
//import org.jeecg.common.config.mqtoken.UserTokenContext;
//import org.jeecg.common.constant.CommonConstant;
//import org.jeecg.common.system.api.ISysBaseAPI;
//import org.jeecg.common.system.util.JwtUtil;
//import org.jeecg.common.util.RedisUtil;
//import org.jeecg.common.util.SpringContextUtils;
//import org.springframework.beans.factory.annotation.Autowired;
//import org.springframework.stereotype.Component;
//
//
///**
// * xxl-job定时任务测试
// */
//@Slf4j
//@Component
//public class TestJobHandler {
// @Autowired
// ISysBaseAPI sysBaseApi;
//
// /**
// * 简单任务
// *
// * 测试无token调用feign接口
// *
// * @param params
// * @return
// */
//
// @XxlJob(value = "testJob")
// public ReturnT<String> demoJobHandler(String params) {
// //1.生成临时令牌Token到线程中
// UserTokenContext.setToken(getTemporaryToken());
//
// log.info("我是 jeecg-demo 服务里的定时任务 testJob , 我执行了...............................");
// log.info("我调用 jeecg-system 服务的字典接口:{}",sysBaseApi.queryAllDict());
// //此处可以写多个feign接口调用
//
// //2.使用完删除临时令牌Token
// UserTokenContext.remove();
// return ReturnT.SUCCESS;
// }
//
// public void init() {
// log.info("init");
// }
//
// public void destroy() {
// log.info("destory");
// }
//
// /**
// * 获取临时令牌
// *
// * 模拟登陆接口获取模拟 Token
// * @return
// */
// public static String getTemporaryToken() {
// RedisUtil redisUtil = SpringContextUtils.getBean(RedisUtil.class);
// // 模拟登录生成Token
// String token = JwtUtil.sign("??", "??");
// // 设置Token缓存有效时间为 5 分钟
// redisUtil.set(CommonConstant.PREFIX_USER_TOKEN + token, token);
// redisUtil.expire(CommonConstant.PREFIX_USER_TOKEN + token, 5 * 60 * 1000);
// return token;
// }
//
//}
//

View File

@ -0,0 +1,34 @@
package org.jeecg.modules.demo.gpt.cache;
import cn.hutool.cache.CacheUtil;
import cn.hutool.cache.impl.TimedCache;
import cn.hutool.core.date.DateUnit;
//update-begin---author:chenrui ---date:20240126 forQQYUN-7932AI助手------------
/**
* 聊天记录本地缓存
* @author chenrui
* @date 2024/1/26 20:06
*/
public class LocalCache {
/**
* 缓存时长
*/
public static final long TIMEOUT = 5 * DateUnit.MINUTE.getMillis();
/**
* 清理间隔
*/
private static final long CLEAN_TIMEOUT = 5 * DateUnit.MINUTE.getMillis();
/**
* 缓存对象
*/
public static final TimedCache<String, Object> CACHE = CacheUtil.newTimedCache(TIMEOUT);
static {
//启动定时任务
CACHE.schedulePrune(CLEAN_TIMEOUT);
}
}
//update-end---author:chenrui ---date:20240126 forQQYUN-7932AI助手------------

View File

@ -0,0 +1,74 @@
package org.jeecg.modules.demo.gpt.controller;
import org.jeecg.common.api.vo.Result;
import org.jeecg.modules.demo.gpt.service.ChatService;
import org.jeecg.modules.demo.gpt.vo.ChatHistoryVO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
//update-begin---author:chenrui ---date:20240126 forQQYUN-7932AI助手------------
/**
* @Description: chatGpt-聊天接口
* @Author: chenrui
* @Date: 2024/1/9 16:30
*/
@Controller
@RequestMapping("/ai/chat")
public class ChatController {
@Autowired
ChatService chatService;
/**
* 创建sse连接
*
* @return
*/
@GetMapping(value = "/send")
public SseEmitter createConnect(@RequestParam(name = "topicId", required = false) String topicId, @RequestParam(name = "message", required = true) String message) {
SseEmitter sse = chatService.createChat();
chatService.sendMessage(topicId, message);
return sse;
}
//update-begin---author:chenrui ---date:20240223 for[QQYUN-8225]聊天记录保存------------
/**
* 保存聊天记录
* @param chatHistoryVO
* @return
* @author chenrui
* @date 2024/2/22 13:54
*/
@PostMapping(value = "/history/save")
@ResponseBody
public Result<?> saveHistory(@RequestBody ChatHistoryVO chatHistoryVO) {
return chatService.saveHistory(chatHistoryVO);
}
/**
* 查询聊天记录
* @return
* @author chenrui
* @date 2024/2/22 14:03
*/
@GetMapping(value = "/history/get")
@ResponseBody
public Result<ChatHistoryVO> getHistoryByTopic() {
return chatService.getHistoryByTopic();
}
//update-end---author:chenrui ---date:20240223 for[QQYUN-8225]聊天记录保存------------
/**
* 关闭连接
*/
@GetMapping(value = "/close")
public void closeConnect() {
chatService.closeChat();
}
}
//update-end---author:chenrui ---date:20240126 forQQYUN-7932AI助手------------

View File

@ -0,0 +1,136 @@
package org.jeecg.modules.demo.gpt.listeners;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.unfbx.chatgpt.entity.chat.ChatCompletionResponse;
import com.unfbx.chatgpt.entity.chat.Message;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import okhttp3.Response;
import okhttp3.ResponseBody;
import okhttp3.sse.EventSource;
import okhttp3.sse.EventSourceListener;
import org.apache.commons.lang3.StringUtils;
import org.jetbrains.annotations.NotNull;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.util.Objects;
//update-begin---author:chenrui ---date:20240126 forQQYUN-7932AI助手------------
/**
* OpenAI的SSE监听
* @author chenrui
* @date 2024/1/26 20:06
*/
@Slf4j
public class OpenAISSEEventSourceListener extends EventSourceListener {
private long tokens;
private SseEmitter sseEmitter;
private String topicId;
public OpenAISSEEventSourceListener(SseEmitter sseEmitter) {
this.sseEmitter = sseEmitter;
}
public OpenAISSEEventSourceListener(String topicId,SseEmitter sseEmitter){
this.topicId = topicId;
this.sseEmitter = sseEmitter;
}
/**
* {@inheritDoc}
*/
@Override
public void onOpen(@NotNull EventSource eventSource, @NotNull Response response) {
log.info("OpenAI建立sse连接...");
}
/**
* {@inheritDoc}
*/
@SneakyThrows
@Override
public void onEvent(@NotNull EventSource eventSource, String id, String type, @NotNull String data) {
log.debug("OpenAI返回数据{}", data);
tokens += 1;
if (data.equals("[DONE]")) {
log.info("OpenAI返回数据结束了");
sseEmitter.send(SseEmitter.event()
.id("[TOKENS]")
.data("<br/><br/>tokens" + tokens())
.reconnectTime(3000));
sseEmitter.send(SseEmitter.event()
.id("[DONE]")
.data("[DONE]")
.reconnectTime(3000));
// 传输完成后自动关闭sse
sseEmitter.complete();
return;
}
ObjectMapper mapper = new ObjectMapper();
ChatCompletionResponse completionResponse = mapper.readValue(data, ChatCompletionResponse.class); // 读取Json
try {
sseEmitter.send(SseEmitter.event()
.id(this.topicId)
.data(completionResponse.getChoices().get(0).getDelta())
.reconnectTime(3000));
} catch (Exception e) {
log.error(e.getMessage(),e);
eventSource.cancel();
}
}
@Override
public void onClosed(@NotNull EventSource eventSource) {
log.info("流式输出返回值总共{}tokens", tokens() - 2);
log.info("OpenAI关闭sse连接...");
}
@SneakyThrows
@Override
public void onFailure(@NotNull EventSource eventSource, Throwable t, Response response) {
String errMsg = "";
ResponseBody body = null == response ? null:response.body();
if (Objects.nonNull(body)) {
log.error("OpenAI sse连接异常data{},异常:{}", body.string(), t.getMessage());
errMsg = body.string();
} else {
log.error("OpenAI sse连接异常data{},异常:{}", response, t.getMessage());
errMsg = t.getMessage();
}
eventSource.cancel();
sseEmitter.send(SseEmitter.event()
.id("[ERR]")
.data(Message.builder().content(explainErr(errMsg)).build())
.reconnectTime(3000));
sseEmitter.send(SseEmitter.event()
.id("[DONE]")
.data("[DONE]")
.reconnectTime(3000));
sseEmitter.complete();
}
private String explainErr(String errMsg){
if(StringUtils.isEmpty(errMsg)){
return "";
}
if(errMsg.contains("Rate limit")){
return "请求频率太快了,请等待20秒再试.";
}
return errMsg;
}
/**
* tokens
* @return
*/
public long tokens() {
return tokens;
}
}
//update-end---author:chenrui ---date:20240126 forQQYUN-7932AI助手------------

View File

@ -0,0 +1,56 @@
package org.jeecg.modules.demo.gpt.service;
import org.jeecg.common.api.vo.Result;
import org.jeecg.modules.demo.gpt.vo.ChatHistoryVO;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
//update-begin---author:chenrui ---date:20240126 forQQYUN-7932AI助手------------
/**
* AI助手聊天Service
* @author chenrui
* @date 2024/1/26 20:08
*/
public interface ChatService {
/**
* 创建SSE
* @return
*/
SseEmitter createChat();
/**
* 关闭SSE
*/
void closeChat();
/**
* 客户端发送消息到服务端
*
* @param topicId
* @param message
* @author chenrui
* @date 2024/1/26 20:01
*/
void sendMessage(String topicId, String message);
//update-begin---author:chenrui ---date:20240223 for[QQYUN-8225]聊天记录保存------------
/**
* 保存聊天记录
* @param chatHistoryVO
* @return
* @author chenrui
* @date 2024/2/22 13:37
*/
Result<?> saveHistory(ChatHistoryVO chatHistoryVO);
/**
* 查询聊天记录
* @return
* @author chenrui
* @date 2024/2/22 13:59
*/
Result<ChatHistoryVO> getHistoryByTopic();
//update-end---author:chenrui ---date:20240223 for[QQYUN-8225]聊天记录保存------------
}
//update-end---author:chenrui ---date:20240126 forQQYUN-7932AI助手------------

View File

@ -0,0 +1,199 @@
package org.jeecg.modules.demo.gpt.service.impl;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.alibaba.fastjson.JSONArray;
import com.unfbx.chatgpt.OpenAiStreamClient;
import com.unfbx.chatgpt.entity.chat.ChatCompletion;
import com.unfbx.chatgpt.entity.chat.Message;
import com.unfbx.chatgpt.exception.BaseException;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.SecurityUtils;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.exception.JeecgBootException;
import org.jeecg.common.system.vo.LoginUser;
import org.jeecg.common.util.SpringContextUtils;
import org.jeecg.common.util.UUIDGenerator;
import org.jeecg.modules.demo.gpt.cache.LocalCache;
import org.jeecg.modules.demo.gpt.listeners.OpenAISSEEventSourceListener;
import org.jeecg.modules.demo.gpt.service.ChatService;
import org.jeecg.modules.demo.gpt.vo.ChatHistoryVO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
//update-begin---author:chenrui ---date:20240126 forQQYUN-7932AI助手------------
/**
* AI助手聊天Service
* @author chenrui
* @date 2024/1/26 20:07
*/
@Service
@Slf4j
public class ChatServiceImpl implements ChatService {
//update-begin---author:chenrui ---date:20240223 for[QQYUN-8225]聊天记录保存------------
private static final String CACHE_KEY_PREFIX = "ai:chart:";
/**
*
*/
private static final String CACHE_KEY_MSG_CONTEXT = "msg_content";
/**
*
*/
private static final String CACHE_KEY_MSG_HISTORY = "msg_history";
@Autowired
RedisTemplate redisTemplate;
//update-end---author:chenrui ---date:20240223 for[QQYUN-8225]聊天记录保存------------
private OpenAiStreamClient openAiStreamClient = null;
//update-begin---author:chenrui ---date:20240131 for[QQYUN-8212]fix 没有配置启动报错------------
public ChatServiceImpl() {
try {
this.openAiStreamClient = SpringContextUtils.getBean(OpenAiStreamClient.class);
} catch (Exception ignored) {
}
}
/**
* 防止client不能成功注入
* @return
* @author chenrui
* @date 2024/2/3 23:08
*/
private OpenAiStreamClient ensureClient(){
if(null == this.openAiStreamClient){
this.openAiStreamClient = SpringContextUtils.getBean(OpenAiStreamClient.class);
}
return this.openAiStreamClient;
}
//update-end---author:chenrui ---date:20240131 for[QQYUN-8212]fix 没有配置启动报错------------
private String getUserId() {
LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();
return sysUser.getId();
}
@Override
public SseEmitter createChat() {
String uid = getUserId();
//默认30秒超时,设置为0L则永不超时
SseEmitter sseEmitter = new SseEmitter(-0L);
//完成后回调
sseEmitter.onCompletion(() -> {
log.info("[{}]结束连接...................",uid);
LocalCache.CACHE.remove(uid);
});
//超时回调
sseEmitter.onTimeout(() -> {
log.info("[{}]连接超时...................", uid);
});
//异常回调
sseEmitter.onError(
throwable -> {
try {
log.info("[{}]连接异常,{}", uid, throwable.toString());
sseEmitter.send(SseEmitter.event()
.id(uid)
.name("发生异常!")
.data(Message.builder().content("发生异常请重试!").build())
.reconnectTime(3000));
LocalCache.CACHE.put(uid, sseEmitter);
} catch (IOException e) {
log.error(e.getMessage(),e);
}
}
);
try {
sseEmitter.send(SseEmitter.event().reconnectTime(5000));
} catch (IOException e) {
log.error(e.getMessage(),e);
}
LocalCache.CACHE.put(uid, sseEmitter);
log.info("[{}]创建sse连接成功", uid);
return sseEmitter;
}
@Override
public void closeChat() {
String uid = getUserId();
SseEmitter sse = (SseEmitter) LocalCache.CACHE.get(uid);
if (sse != null) {
sse.complete();
//移除
LocalCache.CACHE.remove(uid);
}
}
@Override
public void sendMessage(String topicId, String message) {
String uid = getUserId();
if (StrUtil.isBlank(message)) {
log.info("参数异常message为null");
throw new BaseException("参数异常message不能为空~");
}
if (StrUtil.isBlank(topicId)) {
topicId = UUIDGenerator.generate();
}
//update-begin---author:chenrui ---date:20240223 for[QQYUN-8225]聊天记录保存------------
log.info("话题id:{}", topicId);
String cacheKey = CACHE_KEY_PREFIX + uid + "_" + topicId;
String messageContext = (String) redisTemplate.opsForHash().get(cacheKey, CACHE_KEY_MSG_CONTEXT);
List<Message> msgHistory = new ArrayList<>();
if (StrUtil.isNotBlank(messageContext)) {
List<Message> messages = JSONArray.parseArray(messageContext, Message.class);
msgHistory = messages == null ? new ArrayList<>() : messages;
}
Message currentMessage = Message.builder().content(message).role(Message.Role.USER).build();
msgHistory.add(currentMessage);
SseEmitter sseEmitter = (SseEmitter) LocalCache.CACHE.get(uid);
if (sseEmitter == null) {
log.info("聊天消息推送失败uid:[{}],没有创建连接,请重试。", uid);
throw new JeecgBootException("聊天消息推送失败uid:[{}],没有创建连接,请重试。~");
}
OpenAISSEEventSourceListener openAIEventSourceListener = new OpenAISSEEventSourceListener(topicId, sseEmitter);
ChatCompletion completion = ChatCompletion
.builder()
.messages(msgHistory)
.model(ChatCompletion.Model.GPT_3_5_TURBO.getName())
.build();
ensureClient().streamChatCompletion(completion, openAIEventSourceListener);
redisTemplate.opsForHash().put(cacheKey, CACHE_KEY_MSG_CONTEXT, JSONUtil.toJsonStr(msgHistory));
//update-end---author:chenrui ---date:20240223 for[QQYUN-8225]聊天记录保存------------
Result.ok(completion.tokens());
}
//update-begin---author:chenrui ---date:20240223 for[QQYUN-8225]聊天记录保存------------
@Override
public Result<?> saveHistory(ChatHistoryVO chatHistoryVO) {
String uid = getUserId();
String cacheKey = CACHE_KEY_PREFIX + CACHE_KEY_MSG_HISTORY + ":" + uid;
redisTemplate.opsForValue().set(cacheKey, chatHistoryVO.getContent());
return Result.OK("保存成功");
}
@Override
public Result<ChatHistoryVO> getHistoryByTopic() {
String uid = getUserId();
String cacheKey = CACHE_KEY_PREFIX + CACHE_KEY_MSG_HISTORY + ":" + uid;
String historyContent = (String) redisTemplate.opsForValue().get(cacheKey);
ChatHistoryVO chatHistoryVO = new ChatHistoryVO();
chatHistoryVO.setContent(historyContent);
return Result.OK(chatHistoryVO);
}
//update-end---author:chenrui ---date:20240223 for[QQYUN-8225]聊天记录保存------------
}
//update-end---author:chenrui ---date:20240126 forQQYUN-7932AI助手------------

View File

@ -0,0 +1,25 @@
package org.jeecg.modules.demo.gpt.vo;
import lombok.Data;
import java.io.Serializable;
/**
* @Description: 聊天记录
* @Author: chenrui
* @Date: 2024/2/22 13:36
*/
@Data
public class ChatHistoryVO implements Serializable {
private static final long serialVersionUID = 3238429500037511283L;
/**
* 话题id
*/
String topicId;
/**
* 聊天记录内容
*/
String content;
}

View File

@ -0,0 +1,220 @@
package org.jeecg.modules.demo.mock;
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils;
import org.jeecg.common.api.vo.Result;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.swing.filechooser.FileSystemView;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @Description: MockController
* @author: jeecg-boot
*/
@RestController
@RequestMapping("/mock/api")
@Slf4j
public class MockController {
private final String JSON_PATH = "classpath:org/jeecg/modules/demo/mock/json";
/**
* 通用json访问接口
* 格式 http://localhost:8080/jeecg-boot/api/json/{filename}
* @param filename
* @return
*/
@RequestMapping(value = "/json/{filename}", method = RequestMethod.GET)
public String getJsonData(@PathVariable("filename") String filename) {
String jsonpath = "classpath:org/jeecg/modules/demo/mock/json/"+filename+".json";
return readJson(jsonpath);
}
@GetMapping(value = "/asynTreeList")
public Result asynTreeList(String id) {
String json = readJson(JSON_PATH + "/asyn_tree_list_" + id + ".json");
return Result.OK(JSON.parseArray(json));
}
@GetMapping(value = "/user")
public String user() {
return readJson("classpath:org/jeecg/modules/demo/mock/json/user.json");
}
/**
* 老的登录获取用户信息接口
* @return
*/
@GetMapping(value = "/user/info")
public String userInfo() {
return readJson("classpath:org/jeecg/modules/demo/mock/json/user_info.json");
}
@GetMapping(value = "/role")
public String role() {
return readJson("classpath:org/jeecg/modules/demo/mock/json/role.json");
}
@GetMapping(value = "/service")
public String service() {
return readJson("classpath:org/jeecg/modules/demo/mock/json/service.json");
}
@GetMapping(value = "/permission")
public String permission() {
return readJson("classpath:org/jeecg/modules/demo/mock/json/permission.json");
}
@GetMapping(value = "/permission/no-pager")
public String permissionNoPage() {
return readJson("classpath:org/jeecg/modules/demo/mock/json/permission_no_page.json");
}
/**
* 省市县
*/
@GetMapping(value = "/area")
public String area() {
return readJson("classpath:org/jeecg/modules/demo/mock/json/area.json");
}
/**
* 测试报表数据
*/
@GetMapping(value = "/report/getYearCountInfo")
public String getYearCountInfo() {
return readJson("classpath:org/jeecg/modules/demo/mock/json/getCntrNoCountInfo.json");
}
@GetMapping(value = "/report/getMonthCountInfo")
public String getMonthCountInfo() {
return readJson("classpath:org/jeecg/modules/demo/mock/json/getCntrNoCountInfo.json");
}
@GetMapping(value = "/report/getCntrNoCountInfo")
public String getCntrNoCountInfo() {
return readJson("classpath:org/jeecg/modules/demo/mock/json/getCntrNoCountInfo.json");
}
@GetMapping(value = "/report/getCabinetCountInfo")
public String getCabinetCountInfo() {
return readJson("classpath:org/jeecg/modules/demo/mock/json/getCntrNoCountInfo.json");
}
@GetMapping(value = "/report/getTubiao")
public String getTubiao() {
return readJson("classpath:org/jeecg/modules/demo/mock/json/getTubiao.json");
}
/**
* 实时磁盘监控
* @param request
* @param response
* @return
*/
@GetMapping("/queryDiskInfo")
public Result<List<Map<String,Object>>> queryDiskInfo(HttpServletRequest request, HttpServletResponse response){
Result<List<Map<String,Object>>> res = new Result<>();
try {
// 当前文件系统类
FileSystemView fsv = FileSystemView.getFileSystemView();
// 列出所有windows 磁盘
File[] fs = File.listRoots();
log.info("查询磁盘信息:"+fs.length+"");
List<Map<String,Object>> list = new ArrayList<>();
for (int i = 0; i < fs.length; i++) {
if(fs[i].getTotalSpace()==0) {
continue;
}
Map<String,Object> map = new HashMap<>(5);
map.put("name", fsv.getSystemDisplayName(fs[i]));
map.put("max", fs[i].getTotalSpace());
map.put("rest", fs[i].getFreeSpace());
map.put("restPPT", fs[i].getFreeSpace()*100/fs[i].getTotalSpace());
list.add(map);
log.info(map.toString());
}
res.setResult(list);
res.success("查询成功");
} catch (Exception e) {
res.error500("查询失败"+e.getMessage());
}
return res;
}
//-------------------------------------------------------------------------------------------
/**
* 工作台首页的数据
* @return
*/
@GetMapping(value = "/list/search/projects")
public String projects() {
return readJson("classpath:org/jeecg/modules/demo/mock/json/workplace_projects.json");
}
@GetMapping(value = "/workplace/activity")
public String activity() {
return readJson("classpath:org/jeecg/modules/demo/mock/json/workplace_activity.json");
}
@GetMapping(value = "/workplace/teams")
public String teams() {
return readJson("classpath:org/jeecg/modules/demo/mock/json/workplace_teams.json");
}
@GetMapping(value = "/workplace/radar")
public String radar() {
return readJson("classpath:org/jeecg/modules/demo/mock/json/workplace_radar.json");
}
@GetMapping(value = "/task/process")
public String taskProcess() {
return readJson("classpath:org/jeecg/modules/demo/mock/json/task_process.json");
}
//-------------------------------------------------------------------------------------------
//author:lvdandan-----date20190315---for:添加数据日志json----
/**
* 数据日志
*/
public String sysDataLogJson() {
return readJson("classpath:org/jeecg/modules/demo/mock/json/sysdatalog.json");
}
//author:lvdandan-----date20190315---for:添加数据日志json----
//--update-begin--author:wangshuai-----date20201023---for:返回用户信息json数据----
/**
* 用户信息
*/
@GetMapping(value = "/getUserInfo")
public String getUserInfo(){
return readJson("classpath:org/jeecg/modules/demo/mock/json/userinfo.json");
}
//--update-end--author:wangshuai-----date20201023---for:返回用户信息json数据----
/**
* 读取json格式文件
* @param jsonSrc
* @return
*/
private String readJson(String jsonSrc) {
String json = "";
try {
//File jsonFile = ResourceUtils.getFile(jsonSrc);
//json = FileUtils.re.readFileToString(jsonFile);
//换个写法解决springboot读取jar包中文件的问题
InputStream stream = getClass().getClassLoader().getResourceAsStream(jsonSrc.replace("classpath:", ""));
json = IOUtils.toString(stream,"UTF-8");
} catch (IOException e) {
log.error(e.getMessage(),e);
}
return json;
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,36 @@
[
{"value": "110000", "label": "北京市"},
{"value": "120000", "label": "天津市"},
{"value": "130000", "label": "河北省"},
{"value": "140000", "label": "山西省"},
{"value": "150000", "label": "内蒙古自治区"},
{"value": "210000", "label": "辽宁省"},
{"value": "220000", "label": "吉林省"},
{"value": "230000", "label": "黑龙江省"},
{"value": "310000", "label": "上海市"},
{"value": "320000", "label": "江苏省"},
{"value": "330000", "label": "浙江省"},
{"value": "340000", "label": "安徽省"},
{"value": "350000", "label": "福建省"},
{"value": "360000", "label": "江西省"},
{"value": "370000", "label": "山东省"},
{"value": "410000", "label": "河南省"},
{"value": "420000", "label": "湖北省"},
{"value": "430000", "label": "湖南省"},
{"value": "440000", "label": "广东省"},
{"value": "450000", "label": "广西壮族自治区"},
{"value": "460000", "label": "海南省"},
{"value": "500000", "label": "重庆市"},
{"value": "510000", "label": "四川省"},
{"value": "520000", "label": "贵州省"},
{"value": "530000", "label": "云南省"},
{"value": "540000", "label": "西藏自治区"},
{"value": "610000", "label": "陕西省"},
{"value": "620000", "label": "甘肃省"},
{"value": "630000", "label": "青海省"},
{"value": "640000", "label": "宁夏回族自治区"},
{"value": "650000", "label": "新疆维吾尔自治区"},
{"value": "710000", "label": "台湾省"},
{"value": "810000", "label": "香港特别行政区"},
{"value": "820000", "label": "澳门特别行政区"}
]

View File

@ -0,0 +1,23 @@
[
{
"id": 1,
"name": "首页",
"component": "dashboard/Analysis",
"orderNum": 1,
"hasChildren": false
},
{
"id": 2,
"name": "常见案例",
"component": "layouts/RouteView",
"orderNum": 2,
"hasChildren": true
},
{
"id": 3,
"name": "系统监控",
"component": "layouts/RouteView",
"orderNum": 3,
"hasChildren": true
}
]

View File

@ -0,0 +1,23 @@
[
{
"id": 11,
"name": "首页",
"component": "dashboard/Analysis",
"orderNum": 1,
"hasChildren": false
},
{
"id": 12,
"name": "系统管理",
"component": "layouts/RouteView",
"orderNum": 2,
"hasChildren": true
},
{
"id": 13,
"name": "常见案例",
"component": "layouts/RouteView",
"orderNum": 3,
"hasChildren": true
}
]

Some files were not shown because too many files have changed in this diff Show More