This commit is contained in:
Laysan
2024-08-06 16:07:49 +08:00
parent b87af56991
commit 4d7032017b
1330 changed files with 567757 additions and 0 deletions

View File

@@ -0,0 +1,279 @@
package org.jeecg.config.firewall.SqlInjection.impl;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.constant.SymbolConstant;
import org.jeecg.common.exception.JeecgSqlInjectionException;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.common.util.sqlparse.JSqlParserUtils;
import org.jeecg.common.util.sqlparse.vo.SelectSqlInfo;
import org.jeecg.config.JeecgBaseConfig;
import org.jeecg.config.firewall.SqlInjection.IDictTableWhiteListHandler;
import org.jeecg.config.firewall.interceptor.LowCodeModeInterceptor;
import org.jeecg.modules.system.entity.SysTableWhiteList;
import org.jeecg.modules.system.security.DictQueryBlackListHandler;
import org.jeecg.modules.system.service.ISysTableWhiteListService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.net.URLDecoder;
import java.util.*;
/**
* 通用情况的白名单处理,若有无法处理的情况,可以单独写实现类
*/
@Slf4j
@Component("dictTableWhiteListHandlerImpl")
public class DictTableWhiteListHandlerImpl implements IDictTableWhiteListHandler {
/**
* key-表名
* value-字段名,多个逗号隔开
* 两种配置方式-- 全部配置成小写
* whiteTablesRuleMap.put("sys_user", "*") sys_user所有的字段都支持查询
* whiteTablesRuleMap.put("sys_user", "username,password") sys_user中的username和password支持查询
*/
private static final Map<String, String> whiteTablesRuleMap = new HashMap<>();
/**
* LowCode 是否为 dev 模式
*/
private static Boolean LOW_CODE_IS_DEV = null;
@Autowired
private ISysTableWhiteListService sysTableWhiteListService;
@Autowired
private JeecgBaseConfig jeecgBaseConfig;
/**
* 初始化 whiteTablesRuleMap 方法
*/
private void init() {
// 如果当前为dev模式则每次都查询数据库防止缓存
if (this.isDev()) {
DictTableWhiteListHandlerImpl.whiteTablesRuleMap.clear();
}
// 如果map为空则从数据库中查询
if (DictTableWhiteListHandlerImpl.whiteTablesRuleMap.isEmpty()) {
Map<String, String> ruleMap = sysTableWhiteListService.getAllConfigMap();
log.info("表字典白名单初始化完成:{}", ruleMap);
DictTableWhiteListHandlerImpl.whiteTablesRuleMap.putAll(ruleMap);
}
}
@Override
public boolean isPassBySql(String sql) {
Map<String, SelectSqlInfo> parsedMap = null;
try {
parsedMap = JSqlParserUtils.parseAllSelectTable(sql);
} catch (Exception e) {
log.warn("校验sql语句解析报错{}", e.getMessage());
}
// 如果sql有问题则肯定执行不了所以直接返回true
if (parsedMap == null) {
return true;
}
log.info("获取select sql信息 {} ", parsedMap);
// 遍历当前sql中的所有表名如果有其中一个表或表的字段不在白名单中则不通过
for (Map.Entry<String, SelectSqlInfo> entry : parsedMap.entrySet()) {
SelectSqlInfo sqlInfo = entry.getValue();
if (sqlInfo.isSelectAll()) {
log.warn("查询语句中包含 * 字段,暂时先通过");
continue;
}
Set<String> queryFields = sqlInfo.getAllRealSelectFields();
// 校验表名和字段是否允许查询
String tableName = entry.getKey();
if (!this.checkWhiteList(tableName, queryFields)) {
return false;
}
}
return true;
}
@Override
public boolean isPassByDict(String dictCodeString) {
if (oConvertUtils.isEmpty(dictCodeString)) {
return true;
}
try {
// 针对转义字符进行解码
dictCodeString = URLDecoder.decode(dictCodeString, "UTF-8");
} catch (Exception e) {
log.warn(e.getMessage());
//this.throwException("字典code解码失败可能是使用了非法字符请检查");
}
dictCodeString = dictCodeString.trim();
String[] arr = dictCodeString.split(SymbolConstant.COMMA);
// 获取表名
String tableName = this.getTableName(arr[0]);
// 获取查询字段
arr = Arrays.copyOfRange(arr, 1, arr.length);
// distinct的作用是去重相当于 Set<String>
String[] fields = Arrays.stream(arr).map(String::trim).distinct().toArray(String[]::new);
// 校验表名和字段是否允许查询
return this.isPassByDict(tableName, fields);
}
@Override
public boolean isPassByDict(String tableName, String... fields) {
if (oConvertUtils.isEmpty(tableName)) {
return true;
}
if (fields == null || fields.length == 0) {
fields = new String[]{"*"};
}
String sql = "select " + String.join(",", fields) + " from " + tableName;
log.info("字典拼接的查询SQL{}", sql);
try {
// 进行SQL解析
JSqlParserUtils.parseSelectSqlInfo(sql);
} catch (Exception e) {
// 如果SQL解析失败则通过字段名和表名进行校验
return checkWhiteList(tableName, new HashSet<>(Arrays.asList(fields)));
}
// 通过SQL解析进行校验可防止SQL注入
return this.isPassBySql(sql);
}
/**
* 校验表名和字段是否在白名单内
*
* @param tableName
* @param queryFields
* @return
*/
public boolean checkWhiteList(String tableName, Set<String> queryFields) {
this.init();
// 1、判断“表名”是否通过校验如果为空则未通过校验
if (oConvertUtils.isEmpty(tableName)) {
log.error("白名单校验:表名为空");
this.throwException();
}
// 统一转成小写
tableName = tableName.toLowerCase();
String allowFieldStr = DictTableWhiteListHandlerImpl.whiteTablesRuleMap.get(tableName);
log.info("checkWhiteList tableName: {}", tableName);
if (oConvertUtils.isEmpty(allowFieldStr)) {
// 如果是dev模式自动向数据库里添加数据
if (this.isDev()) {
this.autoAddWhiteList(tableName, String.join(",", queryFields));
allowFieldStr = DictTableWhiteListHandlerImpl.whiteTablesRuleMap.get(tableName);
} else {
// prod模式下直接抛出异常
log.error("白名单校验:表\"{}\"未通过校验", tableName);
this.throwException();
}
}
// 2、判断“字段名”是否通过校验
// 统一转成小写
allowFieldStr = allowFieldStr.toLowerCase();
Set<String> allowFields = new HashSet<>(Arrays.asList(allowFieldStr.split(",")));
// 需要合并的字段
Set<String> waitMergerFields = new HashSet<>();
for (String field : queryFields) {
if(oConvertUtils.isEmpty(field)){
continue;
}
// 统一转成小写
field = field.toLowerCase();
// 如果允许的字段里不包含查询的字段,则直接抛出异常
if (!allowFields.contains(field)) {
// 如果是dev模式记录需要合并的字段
if (this.isDev()) {
waitMergerFields.add(field);
} else {
log.error("白名单校验:字段 {} 不在 {} 范围内,拒绝访问!", field, allowFields);
this.throwException();
}
}
}
// 自动向数据库中合并未通过的字段
if (!waitMergerFields.isEmpty()) {
this.autoAddWhiteList(tableName, String.join(",", waitMergerFields));
}
log.info("白名单校验:查询表\"{}\",查询字段 {} 通过校验", tableName, queryFields);
return true;
}
/**
* 自动添加白名单,如果数据库已有,则字段会自动合并
*
* @param tableName
* @param allowFieldStr
*/
private void autoAddWhiteList(String tableName, String allowFieldStr) {
try {
SysTableWhiteList entity = sysTableWhiteListService.autoAdd(tableName, allowFieldStr);
DictTableWhiteListHandlerImpl.whiteTablesRuleMap.put(tableName, entity.getFieldName());
log.warn("\"{}\"未通过校验,且当前为 dev 模式,已自动向数据库中增加白名单数据。查询字段:{}", tableName, allowFieldStr);
} catch (Exception e) {
log.error("\"{}\"未通过校验,且当前为 dev 模式,但自动向数据库中增加白名单数据失败,请排查后重试。错误原因:{}", tableName, e.getMessage(), e);
this.throwException();
}
}
/**
* 判断当前 LowCode 是否为 dev 模式
*/
private boolean isDev() {
if (DictTableWhiteListHandlerImpl.LOW_CODE_IS_DEV == null) {
if (this.jeecgBaseConfig.getFirewall() != null) {
String lowCodeMode = this.jeecgBaseConfig.getFirewall().getLowCodeMode();
DictTableWhiteListHandlerImpl.LOW_CODE_IS_DEV = LowCodeModeInterceptor.LOW_CODE_MODE_DEV.equals(lowCodeMode);
} else {
// 如果没有 firewall 配置,则默认为 false
DictTableWhiteListHandlerImpl.LOW_CODE_IS_DEV = false;
}
}
return DictTableWhiteListHandlerImpl.LOW_CODE_IS_DEV;
}
@Override
public boolean clear() {
DictTableWhiteListHandlerImpl.whiteTablesRuleMap.clear();
return true;
}
/**
* 取where前面的为table name
*
* @param str
* @see DictQueryBlackListHandler#getTableName(String)
*/
@SuppressWarnings("JavadocReference")
private String getTableName(String str) {
String[] arr = str.split("\\s+(?i)where\\s+");
String tableName = arr[0].trim();
//【20230814】解决使用参数tableName=sys_user t&复测,漏洞仍然存在
if (tableName.contains(".")) {
tableName = tableName.substring(tableName.indexOf(".") + 1, tableName.length()).trim();
}
if (tableName.contains(" ")) {
tableName = tableName.substring(0, tableName.indexOf(" ")).trim();
}
//【issues/4393】 sys_user , (sys_user), sys_user%20, %60sys_user%60
String reg = "\\s+|\\(|\\)|`";
return tableName.replaceAll(reg, "");
}
private void throwException() throws JeecgSqlInjectionException {
this.throwException(this.getErrorMsg());
}
private void throwException(String message) throws JeecgSqlInjectionException {
if (oConvertUtils.isEmpty(message)) {
message = this.getErrorMsg();
}
log.error(message);
throw new JeecgSqlInjectionException(message);
}
@Override
public String getErrorMsg() {
return "白名单校验未通过!";
}
}

View File

@@ -0,0 +1,54 @@
package org.jeecg.config.init;
import com.alibaba.druid.filter.config.ConfigTools;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.jeecgframework.codegenerate.database.CodegenDatasourceConfig;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @Description: 代码生成器,自定义DB配置
* 【加了此类则online模式DB连接使用平台的配置jeecg_database.properties配置无效;
* 但是使用GUI模式代码生成还是走jeecg_database.properties配置】
* 提醒: 达梦数据库需要修改下面的参数${spring.datasource.dynamic.datasource.master.url:}配置
* @author: scott
* @date: 2021年02月18日 16:30
*
* 重要说明:此类改路径或者名称,需要同步修改
* org/jeecg/interceptor/OnlineRepairCodeGenerateDbConfig.java里面的注解
* @ConditionalOnMissingClass("org.jeecg.config.init.CodeGenerateDbConfig")
*/
@Slf4j
@Configuration
public class CodeGenerateDbConfig {
@Value("${spring.datasource.dynamic.datasource.master.url:}")
private String url;
@Value("${spring.datasource.dynamic.datasource.master.username:}")
private String username;
@Value("${spring.datasource.dynamic.datasource.master.password:}")
private String password;
@Value("${spring.datasource.dynamic.datasource.master.driver-class-name:}")
private String driverClassName;
@Value("${spring.datasource.dynamic.datasource.master.druid.public-key:}")
private String publicKey;
@Bean
public CodeGenerateDbConfig initCodeGenerateDbConfig() {
if(StringUtils.isNotBlank(url)){
if(StringUtils.isNotBlank(publicKey)){
try {
password = ConfigTools.decrypt(publicKey, password);
} catch (Exception e) {
e.printStackTrace();
log.error(" 代码生成器数据库连接,数据库密码解密失败!");
}
}
CodegenDatasourceConfig.initDbConfig(driverClassName,url, username, password);
log.info(" Init CodeGenerate Config [ Get Db Config From application.yml ] ");
}
return null;
}
}

View File

@@ -0,0 +1,66 @@
package org.jeecg.config.init;
import cn.hutool.core.io.FileUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.stereotype.Component;
import java.io.File;
import java.net.URL;
import java.nio.charset.StandardCharsets;
/**
* 自动初始化代码生成器模板
* <p>
* 解决JAR发布需要手工配置代码生成器模板问题
* @author zhang
*/
@Slf4j
@Component
public class CodeTemplateInitListener implements ApplicationListener<ApplicationReadyEvent> {
@Override
public void onApplicationEvent(ApplicationReadyEvent event) {
try {
log.info(" Init Code Generate Template [ 检测如果是JAR启动环境Copy模板到config目录 ] ");
this.initJarConfigCodeGeneratorTemplate();
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* ::Jar包启动模式下::
* 初始化代码生成器模板文件
*/
private void initJarConfigCodeGeneratorTemplate() throws Exception {
//1.获取jar同级下的config路径
String configPath = System.getProperty("user.dir") + File.separator + "config" + File.separator;
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
Resource[] resources = resolver.getResources("classpath*:jeecg/code-template-online/**/*");
for (Resource re : resources) {
URL url = re.getURL();
String filepath = url.getPath();
//System.out.println("native url= " + filepath);
filepath = java.net.URLDecoder.decode(filepath, "utf-8");
//System.out.println("decode url= " + filepath);
//2.在config下创建jeecg/code-template-online/*模板
String createFilePath = configPath + filepath.substring(filepath.indexOf("jeecg/code-template-online"));
// 非jar模式不生成模板
// 不生成目录,只生成具体模板文件
if (!filepath.contains(".jar!/BOOT-INF/lib/") || !createFilePath.contains(".")) {
continue;
}
if (!FileUtil.exist(createFilePath)) {
log.info("create file codeTemplate = " + createFilePath);
FileUtil.writeString(IOUtils.toString(url, StandardCharsets.UTF_8), createFilePath, "UTF-8");
}
}
}
}

View File

@@ -0,0 +1,42 @@
package org.jeecg.config.init;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.constant.CacheConstant;
import org.jeecg.config.JeecgCloudCondition;
import org.jeecg.modules.system.service.ISysGatewayRouteService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.context.annotation.Conditional;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Component;
/**
* @desc: 启动程序,初始化路由配置
* @author: flyme
*/
@Slf4j
@Component
@Conditional(JeecgCloudCondition.class)
public class SystemInitListener implements ApplicationListener<ApplicationReadyEvent>, Ordered {
@Autowired
private ISysGatewayRouteService sysGatewayRouteService;
@Override
public void onApplicationEvent(ApplicationReadyEvent applicationReadyEvent) {
log.info(" 服务已启动,初始化路由配置 ###################");
String context = "AnnotationConfigServletWebServerApplicationContext";
if (applicationReadyEvent.getApplicationContext().getDisplayName().indexOf(context) > -1) {
sysGatewayRouteService.addRoute2Redis(CacheConstant.GATEWAY_ROUTES);
}
}
@Override
public int getOrder() {
return 1;
}
}

View File

@@ -0,0 +1,33 @@
//package org.jeecg.config.init;
//
//import org.apache.catalina.Context;
//import org.apache.tomcat.util.scan.StandardJarScanner;
//import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
//import org.springframework.context.annotation.Bean;
//import org.springframework.context.annotation.Configuration;
//
///**
// * @Description: TomcatFactoryConfig
// * @author: scott
// * @date: 2021年01月25日 11:40
// */
//@Configuration
//public class TomcatFactoryConfig {
// /**
// * tomcat-embed-jasper引用后提示jar找不到的问题
// */
// @Bean
// public TomcatServletWebServerFactory tomcatFactory() {
// TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory() {
// @Override
// protected void postProcessContext(Context context) {
// ((StandardJarScanner) context.getJarScanner()).setScanManifest(false);
// }
// };
// factory.addConnectorCustomizers(connector -> {
// connector.setProperty("relaxedPathChars", "[]{}");
// connector.setProperty("relaxedQueryChars", "[]{}");
// });
// return factory;
// }
//}

View File

@@ -0,0 +1,102 @@
package org.jeecg.config.jimureport;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.api.dto.LogDTO;
import org.jeecg.common.system.api.ISysBaseAPI;
import org.jeecg.common.system.vo.DictModel;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.base.service.BaseCommonService;
import org.jeecg.modules.drag.service.IOnlDragExternalService;
import org.jeecg.modules.drag.vo.DragDictModel;
import org.jeecg.modules.drag.vo.DragLogDTO;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @Description: 字典处理
* @Author: lsq
* @Date:2023-01-09
* @Version:V1.0
*/
@Slf4j
@Service("onlDragExternalServiceImpl")
public class JimuDragExternalServiceImpl implements IOnlDragExternalService {
@Autowired
@Lazy
private BaseCommonService baseCommonService;
@Autowired
@Lazy
private ISysBaseAPI sysBaseApi;
/**
* 根据多个字典code查询多个字典项
* @param codeList
* @return key = dictCode value=对应的字典项
*/
@Override
public Map<String, List<DragDictModel>> getManyDictItems(List<String> codeList) {
Map<String, List<DragDictModel>> manyDragDictItems = new HashMap<>();
Map<String, List<DictModel>> dictItemsMap = sysBaseApi.getManyDictItems(codeList);
dictItemsMap.forEach((k,v)->{
List<DragDictModel> dictItems = new ArrayList<>();
v.forEach(dictItem->{
DragDictModel dictModel = new DragDictModel();
BeanUtils.copyProperties(dictItem,dictModel);
dictItems.add(dictModel);
});
manyDragDictItems.put(k,dictItems);
});
return manyDragDictItems;
}
/**
*
* @param dictCode
* @return
*/
@Override
public List<DragDictModel> getDictItems(String dictCode) {
List<DragDictModel> dictItems = new ArrayList<>();
if(oConvertUtils.isNotEmpty(dictCode)){
List<DictModel> dictItemsList = sysBaseApi.getDictItems(dictCode);
dictItemsList.forEach(dictItem->{
DragDictModel dictModel = new DragDictModel();
BeanUtils.copyProperties(dictItem,dictModel);
dictItems.add(dictModel);
});
}
return dictItems;
}
/**
* 添加日志
* @param dragLogDTO
*/
@Override
public void addLog(DragLogDTO dragLogDTO) {
if(oConvertUtils.isNotEmpty(dragLogDTO)){
LogDTO dto = new LogDTO();
BeanUtils.copyProperties(dragLogDTO,dto);
baseCommonService.addLog(dto);
}
}
/**
* 保存日志
* @param logMsg
* @param logType
* @param operateType
*/
@Override
public void addLog(String logMsg, int logType, int operateType) {
baseCommonService.addLog(logMsg,logType,operateType);
}
}

View File

@@ -0,0 +1,81 @@
package org.jeecg.config.jimureport;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.system.util.JwtUtil;
import org.jeecg.common.system.vo.SysUserCacheInfo;
import org.jeecg.common.util.RedisUtil;
import org.jeecg.common.util.TokenUtils;
import org.jeecg.modules.jmreport.api.JmReportTokenServiceI;
import org.jeecg.modules.system.service.impl.SysBaseApiImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import jakarta.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
/**
* 自定义积木报表鉴权(如果不进行自定义,则所有请求不做权限控制)
* * 1.自定义获取登录token
* * 2.自定义获取登录用户
* @author: jeecg-boot
*/
@Slf4j
@Component
public class JimuReportTokenService implements JmReportTokenServiceI {
@Autowired
private SysBaseApiImpl sysBaseApi;
@Autowired
@Lazy
private RedisUtil redisUtil;
@Override
public String getToken(HttpServletRequest request) {
return TokenUtils.getTokenByRequest(request);
}
@Override
public String getUsername(String token) {
return JwtUtil.getUsername(token);
}
@Override
public String[] getRoles(String token) {
String username = JwtUtil.getUsername(token);
Set roles = sysBaseApi.getUserRoleSet(username);
if(CollectionUtils.isEmpty(roles)){
return null;
}
return (String[]) roles.toArray(new String[roles.size()]);
}
@Override
public Boolean verifyToken(String token) {
return TokenUtils.verifyToken(token, sysBaseApi, redisUtil);
}
@Override
public Map<String, Object> getUserInfo(String token) {
Map<String, Object> map = new HashMap(5);
String username = JwtUtil.getUsername(token);
//此处通过token只能拿到一个信息 用户账号 后面的就是根据账号获取其他信息 查询数据或是走redis 用户根据自身业务可自定义
SysUserCacheInfo userInfo = null;
try {
userInfo = sysBaseApi.getCacheUser(username);
} catch (Exception e) {
log.error("获取用户信息异常:"+ e.getMessage());
return map;
}
//设置账号名
map.put(SYS_USER_CODE, userInfo.getSysUserCode());
//设置部门编码
map.put(SYS_ORG_CODE, userInfo.getSysOrgCode());
// 将所有信息存放至map 解析sql/api会根据map的键值解析
return map;
}
}