增强待办路由安全和查询索引

This commit is contained in:
jsowell
2026-07-17 18:12:53 +08:00
parent fd678c9ab3
commit 1bffb95b18
6 changed files with 208 additions and 3 deletions

View File

@@ -1,5 +1,7 @@
package com.jsowell.system.service.impl;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.jsowell.common.exception.ServiceException;
import com.jsowell.common.util.StringUtils;
import com.jsowell.system.constant.TodoTaskConstants;
@@ -9,11 +11,17 @@ import com.jsowell.system.domain.dto.TodoTaskQuery;
import com.jsowell.system.mapper.SysTodoTaskMapper;
import com.jsowell.system.service.TodoTaskService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* 统一待办任务服务实现。
@@ -23,6 +31,12 @@ import java.util.List;
@Service
public class TodoTaskServiceImpl implements TodoTaskService {
private static final String SYSTEM_OPERATOR = "system";
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
private static final Set<String> DEFAULT_ROUTE_WHITELIST = new HashSet<>(
Arrays.asList("invoiceDetail", "financeDetail"));
@Value("${todo.route-whitelist:invoiceDetail,financeDetail}")
private String routeWhitelist;
@Autowired
private SysTodoTaskMapper todoTaskMapper;
@@ -223,8 +237,10 @@ public class TodoTaskServiceImpl implements TodoTaskService {
todoTask.setContent(normalizeOptional(command.getContent(), "待办内容", 20000));
todoTask.setBusinessType(normalizeRequired(command.getBusinessType(), "业务类型", 64));
todoTask.setBusinessId(normalizeRequired(command.getBusinessId(), "业务主键", 64));
todoTask.setRouteName(normalizeRequired(command.getRouteName(), "业务路由", 100));
todoTask.setRouteParams(normalizeOptional(command.getRouteParams(), "路由参数", 1000));
String routeName = normalizeRequired(command.getRouteName(), "业务路由", 100);
validateRouteName(routeName);
todoTask.setRouteName(routeName);
todoTask.setRouteParams(normalizeRouteParams(command.getRouteParams()));
todoTask.setAssigneeUserId(command.getAssigneeUserId());
todoTask.setAssigneeMerchantId(normalizeOptional(command.getAssigneeMerchantId(), "运营商 ID", 64));
todoTask.setPriority(priority);
@@ -288,6 +304,83 @@ public class TodoTaskServiceImpl implements TodoTaskService {
return safeValue;
}
private void validateRouteName(String routeName) {
Set<String> allowedRoutes = DEFAULT_ROUTE_WHITELIST;
if (StringUtils.isNotBlank(routeWhitelist)) {
allowedRoutes = new HashSet<>();
for (String configuredRoute : routeWhitelist.split(",")) {
if (StringUtils.isNotBlank(configuredRoute)) {
allowedRoutes.add(configuredRoute.trim());
}
}
}
if (!allowedRoutes.contains(routeName)) {
throw new ServiceException("待办业务路由未加入白名单");
}
}
private String normalizeRouteParams(String routeParams) {
String safeRouteParams = normalizeOptional(routeParams, "路由参数", 1000);
if (safeRouteParams == null) {
return null;
}
try {
JsonNode root = OBJECT_MAPPER.readTree(safeRouteParams);
if (root == null || !root.isObject()) {
throw new ServiceException("待办路由参数必须是 JSON 对象");
}
boolean structured = root.has("params") || root.has("query");
Iterator<Map.Entry<String, JsonNode>> fields = root.fields();
while (fields.hasNext()) {
Map.Entry<String, JsonNode> field = fields.next();
validateRouteParamKey(field.getKey());
if (structured) {
if (!"params".equals(field.getKey()) && !"query".equals(field.getKey())) {
throw new ServiceException("待办路由参数仅允许 params 和 query");
}
validateRouteParamObject(field.getValue());
} else {
validateRouteParamValue(field.getValue());
}
}
return safeRouteParams;
} catch (ServiceException exception) {
throw exception;
} catch (Exception exception) {
throw new ServiceException("待办路由参数不是合法 JSON");
}
}
private void validateRouteParamObject(JsonNode node) {
if (node == null || !node.isObject()) {
throw new ServiceException("待办路由 params 和 query 必须是 JSON 对象");
}
Iterator<Map.Entry<String, JsonNode>> fields = node.fields();
while (fields.hasNext()) {
Map.Entry<String, JsonNode> field = fields.next();
validateRouteParamKey(field.getKey());
validateRouteParamValue(field.getValue());
}
}
private void validateRouteParamKey(String key) {
if (StringUtils.isBlank(key) || key.length() > 64) {
throw new ServiceException("待办路由参数名称不合法");
}
}
private void validateRouteParamValue(JsonNode value) {
if (value == null || value.isNull()) {
return;
}
if (!value.isTextual() && !value.isNumber() && !value.isBoolean()) {
throw new ServiceException("待办路由参数值只能是字符串、数字或布尔值");
}
if (value.isTextual() && value.asText().length() > 500) {
throw new ServiceException("待办路由参数值不能超过500个字符");
}
}
private String normalizeOperator(String operatorName, Long operatorId) {
String safeOperator = StringUtils.isNotEmpty(operatorName)
? operatorName.trim()