http proxy

This commit is contained in:
iqudoo
2026-06-07 19:16:04 +08:00
parent 83bc7d5856
commit 67d2198e2d
11 changed files with 1229 additions and 145 deletions

View File

@@ -0,0 +1,966 @@
package com.iqudoo.platform.application.controller;
import com.alibaba.excel.util.StringUtils;
import com.iqudoo.framework.tape.Tape;
import com.iqudoo.framework.tape.modules.IModuleSetting;
import com.iqudoo.platform.application.setting.SystemGlobalHttpProxyConfig;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.http.*;
import org.springframework.http.client.reactive.ClientHttpRequest;
import org.springframework.http.codec.ClientCodecConfigurer;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.reactive.function.BodyInserter;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.client.ClientResponse;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.reactive.function.client.WebClientResponseException;
import reactor.core.publisher.Mono;
import javax.servlet.http.HttpServletRequest;
import java.net.URI;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 通用请求代理控制器
*/
@SuppressWarnings({"DuplicatedCode", "Convert2Lambda", "deprecation"})
@RestController
public class HttpProxyController {
private final WebClient webClient;
// 静态资源后缀匹配正则包含JSON
private static final Pattern STATIC_RESOURCE_PATTERN = Pattern.compile("\\.(css|js|png|jpg|jpeg|gif|ico|woff|woff2|ttf|eot|svg|json)(\\?.*)?$", Pattern.CASE_INSENSITIVE);
// HTML内容识别正则强制识别忽略Content-Type
private static final Pattern HTML_CONTENT_PATTERN = Pattern.compile("^\\s*<!DOCTYPE\\s+html|^\\s*<html", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE);
// 文本类资源后缀(需要替换域名)
private static final Pattern TEXT_RESOURCE_PATTERN = Pattern.compile("\\.(css|js|json|html|htm|txt|xml)(\\?.*)?$", Pattern.CASE_INSENSITIVE);
// 构造注入
public HttpProxyController(WebClient.Builder webClientBuilder) {
this.webClient = webClientBuilder
.codecs(new Consumer<ClientCodecConfigurer>() {
@Override
public void accept(ClientCodecConfigurer clientCodecConfigurer) {
clientCodecConfigurer.defaultCodecs().maxInMemorySize(50 * 1024 * 1024); // 50MB
}
})
.build();
}
/**
* 通用代理接口(基于目录路由)
*/
@RequestMapping(value = {"/domain-proxy/", "/domain-proxy/{proxyDir:.*}/**"}, method = {
RequestMethod.GET,
RequestMethod.POST,
RequestMethod.PUT,
RequestMethod.DELETE,
RequestMethod.PATCH
})
public Mono<ResponseEntity<?>> domainProxy(
@PathVariable(required = false) String proxyDir,
@RequestBody(required = false) Object body,
HttpServletRequest servletRequest) {
try {
SystemGlobalHttpProxyConfig httpProxyConfig = Tape.getService(IModuleSetting.class)
.getSetting(SystemGlobalHttpProxyConfig.class);
HttpHeaders headers = new HttpHeaders();
String requestUri = servletRequest.getRequestURI();
String proxyPath = requestUri.replaceFirst("^/domain-proxy(/[^/]+)?", "");
if (proxyPath.startsWith("//")) {
proxyPath = proxyPath.substring(1);
}
// 匹配路由配置
String proxyTargetUrl = null;
String proxyDomainUrl = null;
List<String> replaceList = new ArrayList<>();
if (StringUtils.isNotBlank(httpProxyConfig.getDomainProxyRouterMap())) {
String[] domainProxyConfig = httpProxyConfig.getDomainProxyRouterMap().split("\n");
for (String domainProxy : domainProxyConfig) {
try {
Map<String, String> folderMap = parseKeyValue(domainProxy);
if (folderMap.containsKey("key") && Objects.equals(folderMap.get("key"), proxyDir)) {
String[] routers = folderMap.get("value").trim().split("#");
if (routers.length > 1) {
replaceList.addAll(Arrays.asList(routers).subList(1, routers.length));
}
proxyDomainUrl = routers[0];
if (proxyDomainUrl.endsWith("/")) {
proxyDomainUrl = proxyDomainUrl.substring(0, proxyDomainUrl.length() - 1);
}
proxyTargetUrl = proxyDomainUrl + (proxyPath.isEmpty() ? ""
: (proxyPath.startsWith("/") ? proxyPath : "/" + proxyPath));
break;
}
} catch (Throwable ignored) {
}
}
}
// 校验目标地址
if (StringUtils.isBlank(proxyTargetUrl)) {
headers.setContentType(new MediaType(MediaType.APPLICATION_JSON, StandardCharsets.UTF_8));
String errorMsg = "{\"code\":404,\"msg\": \"Not found " + proxyDir + " proxy router\"}";
return Mono.just(new ResponseEntity<>(errorMsg, headers, HttpStatus.NOT_FOUND));
}
// 拼接查询参数
String queryString = servletRequest.getQueryString();
if (StringUtils.isNotBlank(queryString)) {
proxyTargetUrl += (proxyTargetUrl.contains("?") ? "&" : "?") + queryString;
}
// 构建请求头
copyRequestHeaders(servletRequest, headers);
// 处理请求方法和请求体
HttpMethod method = HttpMethod.valueOf(servletRequest.getMethod());
BodyInserter<?, ? super ClientHttpRequest> bodyInserter = buildBodyInserter(body, servletRequest);
// 发送代理请求并处理响应
String finalProxyDomainUrl = proxyDomainUrl;
String finalProxyTargetUrl = proxyTargetUrl;
return webClient.method(method)
.uri(URI.create(proxyTargetUrl))
.headers(new Consumer<HttpHeaders>() {
@Override
public void accept(HttpHeaders httpHeaders) {
httpHeaders.addAll(headers);
}
})
.body(bodyInserter)
.exchange()
.flatMap(new Function<ClientResponse, Mono<ResponseEntity<?>>>() {
@Override
public Mono<ResponseEntity<?>> apply(ClientResponse clientResponse) {
try {
// 先判断是否为二进制请求,避免不必要的收集
String requestUriLower = requestUri.toLowerCase();
boolean isLikelyBinary = true;
try {
isLikelyBinary = isLikelyBinaryRequest(requestUriLower, clientResponse);
} catch (Exception e) {
// 判断失败时,默认按文本处理
}
if (isLikelyBinary) {
// 二进制请求:收集数据后返回 Resource
return clientResponse.bodyToFlux(DataBuffer.class)
.collectList()
.flatMap(new Function<List<DataBuffer>, Mono<ResponseEntity<?>>>() {
@Override
public Mono<ResponseEntity<?>> apply(List<DataBuffer> dataBuffers) {
try {
byte[] responseBytes = mergeDataBuffers(dataBuffers);
// 构建响应头
HttpHeaders responseHeaders = new HttpHeaders();
HttpHeaders clientHeaders = clientResponse.headers().asHttpHeaders();
// 先检查原始响应头的 Content-Type
MediaType originalContentType = clientHeaders.getContentType();
boolean hasBinaryContentType = isBinaryContentType(originalContentType);
// 复制响应头,但排除可能冲突的
clientHeaders.forEach(new HttpHeadersBiConsumer(responseHeaders));
// 处理 Content-Type既然进入了二进制分支说明应该是二进制内容
if (hasBinaryContentType) {
// 如果原始响应头已经是二进制类型,使用它
responseHeaders.setContentType(originalContentType);
} else {
// 如果原始响应头不是二进制类型(比如 text/html尝试从 URI 自动检测
MediaType contentType = getContentTypeFromUri(requestUriLower);
if (contentType != null) {
// 从 URI 检测到了类型,设置它
responseHeaders.setContentType(contentType);
}
// 如果检测不到,不设置 content-type已经在上面的复制时排除了
}
// 设置 Content-Length
responseHeaders.setContentLength(responseBytes.length);
// 使用 ByteArrayResource 包装
Resource resource = new ByteArrayResource(responseBytes) {
@Override
public String getFilename() {
String filename = requestUriLower;
int lastSlash = filename.lastIndexOf('/');
if (lastSlash >= 0 && lastSlash < filename.length() - 1) {
filename = filename.substring(lastSlash + 1);
}
int queryIndex = filename.indexOf('?');
if (queryIndex > 0) {
filename = filename.substring(0, queryIndex);
}
return filename.isEmpty() ? "binary" : filename;
}
};
return Mono.just(new ResponseEntity<>(resource, responseHeaders, clientResponse.statusCode()));
} catch (Exception e) {
HttpHeaders errorHeaders = new HttpHeaders();
errorHeaders.setContentType(new MediaType(MediaType.APPLICATION_JSON, StandardCharsets.UTF_8));
String errorMsg = "{\"code\":500,\"msg\": \"处理二进制响应失败: " + e.getMessage().replace("\"", "\\\"") + "\"}";
return Mono.just(new ResponseEntity<>(errorMsg, errorHeaders, HttpStatus.INTERNAL_SERVER_ERROR));
}
}
});
} else {
// 文本请求:收集后处理
return clientResponse.bodyToFlux(DataBuffer.class)
.collectList()
.flatMap(new Function<List<DataBuffer>, Mono<ResponseEntity<?>>>() {
@Override
public Mono<ResponseEntity<?>> apply(List<DataBuffer> dataBuffers) {
try {
byte[] responseBytes = mergeDataBuffers(dataBuffers);
return handleResponseContent(responseBytes, clientResponse, finalProxyDomainUrl, proxyDir, finalProxyTargetUrl, replaceList);
} catch (Exception e) {
HttpHeaders errorHeaders = new HttpHeaders();
errorHeaders.setContentType(new MediaType(MediaType.APPLICATION_JSON, StandardCharsets.UTF_8));
String errorMsg = "{\"code\":500,\"msg\": \"处理响应内容失败: " + e.getMessage().replace("\"", "\\\"") + "\"}";
return Mono.just(new ResponseEntity<>(errorMsg, errorHeaders, HttpStatus.INTERNAL_SERVER_ERROR));
}
}
});
}
} catch (Exception e) {
HttpHeaders errorHeaders = new HttpHeaders();
errorHeaders.setContentType(new MediaType(MediaType.APPLICATION_JSON, StandardCharsets.UTF_8));
String errorMsg = "{\"code\":500,\"msg\": \"处理响应失败: " + e.getMessage().replace("\"", "\\\"") + "\"}";
return Mono.just(new ResponseEntity<>(errorMsg, errorHeaders, HttpStatus.INTERNAL_SERVER_ERROR));
}
}
})
// 异常处理
.onErrorResume(WebClientResponseException.class, new Function<WebClientResponseException, Mono<ResponseEntity<?>>>() {
@Override
public Mono<ResponseEntity<?>> apply(WebClientResponseException e) {
HttpHeaders errorHeaders = new HttpHeaders();
errorHeaders.setContentType(new MediaType(MediaType.APPLICATION_JSON, StandardCharsets.UTF_8));
String errorMsg = String.format("{\"code\":%d,\"msg\": \"Proxy request failure: %s\"}",
e.getRawStatusCode(), e.getMessage());
return Mono.just(new ResponseEntity<>(errorMsg, errorHeaders, HttpStatus.valueOf(e.getRawStatusCode())));
}
})
.onErrorResume(new Function<Throwable, Mono<ResponseEntity<?>>>() {
@Override
public Mono<ResponseEntity<?>> apply(Throwable e) {
HttpHeaders errorHeaders = new HttpHeaders();
errorHeaders.setContentType(new MediaType(MediaType.APPLICATION_JSON, StandardCharsets.UTF_8));
String errorMsg = "{\"code\":500,\"msg\": \"Proxy request failure: " +
e.getMessage().replace("\"", "\\\"") + "\"}";
return Mono.just(new ResponseEntity<>(errorMsg, errorHeaders, HttpStatus.INTERNAL_SERVER_ERROR));
}
});
} catch (Throwable e) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(new MediaType(MediaType.APPLICATION_JSON, StandardCharsets.UTF_8));
String errorMsg = "{\"code\":500,\"msg\": \"Internal server error: " +
e.getMessage().replace("\"", "\\\"") + "\"}";
return Mono.just(new ResponseEntity<>(errorMsg, headers, HttpStatus.INTERNAL_SERVER_ERROR));
}
}
/**
* 通用代理接口直接指定目标URL
*/
@RequestMapping(value = "/http-proxy", method = {
RequestMethod.GET,
RequestMethod.POST,
RequestMethod.PUT,
RequestMethod.DELETE,
RequestMethod.PATCH
})
public Mono<ResponseEntity<?>> httpProxy(
@RequestParam(required = false) String targetUrl,
@RequestBody(required = false) Object body,
HttpServletRequest servletRequest) {
String proxyTargetUrl = encodeUrlQueryParams(targetUrl);
if (StringUtils.isBlank(proxyTargetUrl)) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(new MediaType(MediaType.APPLICATION_JSON, StandardCharsets.UTF_8));
String errorMsg = "{\"code\":400,\"msg\": \"目标地址不能为空\"}";
return Mono.just(new ResponseEntity<>(errorMsg, headers, HttpStatus.BAD_REQUEST));
}
HttpHeaders headers = new HttpHeaders();
copyRequestHeaders(servletRequest, headers);
HttpMethod method = HttpMethod.valueOf(servletRequest.getMethod());
BodyInserter<?, ? super ClientHttpRequest> bodyInserter = buildBodyInserter(body, servletRequest);
return webClient.method(method)
.uri(URI.create(proxyTargetUrl))
.headers(new Consumer<HttpHeaders>() {
@Override
public void accept(HttpHeaders httpHeaders) {
httpHeaders.addAll(headers);
}
})
.body(bodyInserter)
.exchange()
.flatMap(new Function<ClientResponse, Mono<ResponseEntity<?>>>() {
@Override
public Mono<ResponseEntity<?>> apply(ClientResponse clientResponse) {
try {
// 先判断是否为二进制请求,避免不必要的收集
String targetUrlLower = proxyTargetUrl.toLowerCase();
boolean isLikelyBinary = false;
try {
isLikelyBinary = isLikelyBinaryRequest(targetUrlLower, clientResponse);
} catch (Exception e) {
// 判断失败时,默认按文本处理
}
if (isLikelyBinary) {
// 二进制请求:收集数据后返回 Resource
return clientResponse.bodyToFlux(DataBuffer.class)
.collectList()
.flatMap(new Function<List<DataBuffer>, Mono<ResponseEntity<?>>>() {
@Override
public Mono<ResponseEntity<?>> apply(List<DataBuffer> dataBuffers) {
try {
byte[] responseBytes = mergeDataBuffers(dataBuffers);
// 构建响应头
HttpHeaders responseHeaders = new HttpHeaders();
HttpHeaders clientHeaders = clientResponse.headers().asHttpHeaders();
// 先检查原始响应头的 Content-Type
MediaType originalContentType = clientHeaders.getContentType();
boolean hasBinaryContentType = isBinaryContentType(originalContentType);
// 复制响应头,但排除可能冲突的
clientHeaders.forEach(new HttpHeadersBiConsumer(responseHeaders));
// 处理 Content-Type既然进入了二进制分支说明应该是二进制内容
if (hasBinaryContentType) {
// 如果原始响应头已经是二进制类型,使用它
responseHeaders.setContentType(originalContentType);
} else {
// 如果原始响应头不是二进制类型(比如 text/html尝试从 URI 自动检测
MediaType contentType = getContentTypeFromUri(targetUrlLower);
if (contentType != null) {
// 从 URI 检测到了类型,设置它
responseHeaders.setContentType(contentType);
}
// 如果检测不到,不设置 content-type已经在上面的复制时排除了
}
// 设置 Content-Length
responseHeaders.setContentLength(responseBytes.length);
// 使用 ByteArrayResource 包装
Resource resource = new ByteArrayResource(responseBytes) {
@Override
public String getFilename() {
String filename = targetUrlLower;
int lastSlash = filename.lastIndexOf('/');
if (lastSlash >= 0 && lastSlash < filename.length() - 1) {
filename = filename.substring(lastSlash + 1);
}
int queryIndex = filename.indexOf('?');
if (queryIndex > 0) {
filename = filename.substring(0, queryIndex);
}
return filename.isEmpty() ? "binary" : filename;
}
};
return Mono.just(new ResponseEntity<>(resource, responseHeaders, clientResponse.statusCode()));
} catch (Exception e) {
HttpHeaders errorHeaders = new HttpHeaders();
errorHeaders.setContentType(new MediaType(MediaType.APPLICATION_JSON, StandardCharsets.UTF_8));
String errorMsg = "{\"code\":500,\"msg\": \"处理二进制响应失败: " + e.getMessage().replace("\"", "\\\"") + "\"}";
return Mono.just(new ResponseEntity<>(errorMsg, errorHeaders, HttpStatus.INTERNAL_SERVER_ERROR));
}
}
});
} else {
// 文本请求:收集后处理
return clientResponse.bodyToFlux(DataBuffer.class)
.collectList()
.flatMap(new Function<List<DataBuffer>, Mono<ResponseEntity<?>>>() {
@Override
public Mono<ResponseEntity<?>> apply(List<DataBuffer> dataBuffers) {
try {
byte[] responseBytes = mergeDataBuffers(dataBuffers);
return handleResponseContent(responseBytes, clientResponse, null, null, proxyTargetUrl, new ArrayList<>());
} catch (Exception e) {
HttpHeaders errorHeaders = new HttpHeaders();
errorHeaders.setContentType(new MediaType(MediaType.APPLICATION_JSON, StandardCharsets.UTF_8));
String errorMsg = "{\"code\":500,\"msg\": \"处理响应内容失败: " + e.getMessage().replace("\"", "\\\"") + "\"}";
return Mono.just(new ResponseEntity<>(errorMsg, errorHeaders, HttpStatus.INTERNAL_SERVER_ERROR));
}
}
});
}
} catch (Exception e) {
HttpHeaders errorHeaders = new HttpHeaders();
errorHeaders.setContentType(new MediaType(MediaType.APPLICATION_JSON, StandardCharsets.UTF_8));
String errorMsg = "{\"code\":500,\"msg\": \"处理响应失败: " + e.getMessage().replace("\"", "\\\"") + "\"}";
return Mono.just(new ResponseEntity<>(errorMsg, errorHeaders, HttpStatus.INTERNAL_SERVER_ERROR));
}
}
})
.onErrorResume(WebClientResponseException.class, new Function<WebClientResponseException, Mono<ResponseEntity<?>>>() {
@Override
public Mono<ResponseEntity<?>> apply(WebClientResponseException e) {
HttpHeaders errorHeaders = new HttpHeaders();
errorHeaders.setContentType(new MediaType(MediaType.APPLICATION_JSON, StandardCharsets.UTF_8));
String errorMsg = String.format("{\"code\":%d,\"msg\": \"Proxy request failure: %s\"}",
e.getRawStatusCode(), e.getMessage());
return Mono.just(new ResponseEntity<>(errorMsg, errorHeaders, HttpStatus.valueOf(e.getRawStatusCode())));
}
})
.onErrorResume(new Function<Throwable, Mono<ResponseEntity<?>>>() {
@Override
public Mono<ResponseEntity<?>> apply(Throwable e) {
HttpHeaders errorHeaders = new HttpHeaders();
errorHeaders.setContentType(new MediaType(MediaType.APPLICATION_JSON, StandardCharsets.UTF_8));
String errorMsg = "{\"code\":500,\"msg\": \"Proxy request failure: " +
e.getMessage().replace("\"", "\\\"") + "\"}";
return Mono.just(new ResponseEntity<>(errorMsg, errorHeaders, HttpStatus.INTERNAL_SERVER_ERROR));
}
});
}
/**
* 核心修复:精准域名替换 + 保护HTML标签 + 统一响应格式
*/
private Mono<ResponseEntity<?>> handleResponseContent(byte[] responseBytes, ClientResponse response,
String domainUrl, String proxyDir, String proxyTargetUrl, List<String> replaceList) {
HttpHeaders responseHeaders = new HttpHeaders();
HttpHeaders clientHeaders = response.headers().asHttpHeaders();
HttpStatus statusCode = response.statusCode();
if (responseBytes == null || responseBytes.length == 0) {
return Mono.just(new ResponseEntity<>(responseHeaders, statusCode));
}
// 复制响应头,但排除可能冲突的
clientHeaders.forEach(new HttpHeadersBiConsumer(responseHeaders));
// ========== 首先判断是否为二进制内容 ==========
// 检查响应的 Content-Type
// 先检查原始响应头的 Content-Type
MediaType contentType = clientHeaders.getContentType();
if (contentType == null) {
contentType = detectFileMediaType(responseBytes);
}
boolean isBinaryByContentType = isBinaryContentType(contentType);
// 检查文件扩展名是否为二进制类型
boolean isBinaryByExtension = STATIC_RESOURCE_PATTERN.matcher(proxyTargetUrl).find() &&
!TEXT_RESOURCE_PATTERN.matcher(proxyTargetUrl).find();
// 如果是二进制内容,直接返回,不进行字符串转换
// 判断条件1. 有二进制 content-type 2. 有二进制扩展名且内容是二进制 3. 内容是二进制且没有 content-type避免误判
if (isBinaryByContentType || isBinaryByExtension || contentType == null) {
// 确保 Content-Type 正确设置
if (contentType == null || !isBinaryByContentType) {
// 根据文件扩展名设置 Content-Type
String lowerUri = proxyTargetUrl.toLowerCase();
if (lowerUri.endsWith(".png")) {
responseHeaders.setContentType(MediaType.IMAGE_PNG);
} else if (lowerUri.endsWith(".jpg") || lowerUri.endsWith(".jpeg")) {
responseHeaders.setContentType(MediaType.IMAGE_JPEG);
} else if (lowerUri.endsWith(".gif")) {
responseHeaders.setContentType(MediaType.IMAGE_GIF);
} else if (lowerUri.endsWith(".ico")) {
responseHeaders.setContentType(new MediaType("image", "x-icon"));
} else if (lowerUri.endsWith(".woff")) {
responseHeaders.setContentType(new MediaType("font", "woff"));
} else if (lowerUri.endsWith(".woff2")) {
responseHeaders.setContentType(new MediaType("font", "woff2"));
} else if (lowerUri.endsWith(".ttf")) {
responseHeaders.setContentType(new MediaType("font", "ttf"));
} else if (lowerUri.endsWith(".eot")) {
responseHeaders.setContentType(new MediaType("application", "vnd.ms-fontobject"));
} else if (lowerUri.endsWith(".svg")) {
responseHeaders.setContentType(new MediaType("image", "svg+xml"));
} else if (lowerUri.endsWith(".webp")) {
responseHeaders.setContentType(new MediaType("image", "webp"));
} else if (lowerUri.endsWith(".bmp")) {
responseHeaders.setContentType(new MediaType("image", "bmp"));
}
}
if (contentType != null) {
responseHeaders.setContentType(contentType);
}
// 设置 Content-Length
responseHeaders.setContentLength(responseBytes.length);
// 使用 ByteArrayResource 包装字节数组Spring 可以正确序列化
Resource resource = new ByteArrayResource(responseBytes) {
@Override
public String getFilename() {
// 从 URI 中提取文件名
String filename = proxyTargetUrl;
int lastSlash = filename.lastIndexOf('/');
if (lastSlash >= 0 && lastSlash < filename.length() - 1) {
filename = filename.substring(lastSlash + 1);
}
int queryIndex = filename.indexOf('?');
if (queryIndex > 0) {
filename = filename.substring(0, queryIndex);
}
return filename.isEmpty() ? "binary" : filename;
}
};
// 返回 ResourceSpring 会自动处理二进制数据
return Mono.just(new ResponseEntity<>(resource, responseHeaders, statusCode));
}
// ========== 文本内容处理 ==========
// 只有确定是文本内容时才转换为字符串
String responseStr = new String(responseBytes, StandardCharsets.UTF_8);
String processedContent = responseStr;
// ========== 精准域名替换(仅对文本内容) ==========
if (StringUtils.isNotBlank(domainUrl) && StringUtils.isNotBlank(proxyDir)) {
boolean needReplace = TEXT_RESOURCE_PATTERN.matcher(proxyTargetUrl).find() ||
HTML_CONTENT_PATTERN.matcher(responseStr).find() ||
responseStr.trim().startsWith("{") || responseStr.trim().startsWith("[");
// 仅对文本类资源进行替换
if (needReplace) {
processedContent = replaceDomainPrecisely(processedContent, domainUrl, proxyDir, replaceList);
}
}
// ========== 文本响应类型处理 ==========
// 1. HTML类型最高优先级确保标签完整
if (HTML_CONTENT_PATTERN.matcher(responseStr).find()) {
responseHeaders.setContentType(new MediaType(MediaType.TEXT_HTML, StandardCharsets.UTF_8));
responseHeaders.remove(HttpHeaders.CONTENT_LENGTH);
return Mono.just(new ResponseEntity<>(processedContent, responseHeaders, statusCode));
}
// 2. JSON类型
else if (responseStr.trim().startsWith("{") || responseStr.trim().startsWith("[")) {
responseHeaders.setContentType(new MediaType(MediaType.APPLICATION_JSON, StandardCharsets.UTF_8));
responseHeaders.remove(HttpHeaders.CONTENT_LENGTH);
return Mono.just(new ResponseEntity<>(processedContent, responseHeaders, statusCode));
}
// 3. 文本类型CSS/JS/TXT等
else {
// 设置文本类型的content-type
setTextContentTypeWithCharset(responseHeaders, proxyTargetUrl, contentType);
responseHeaders.remove(HttpHeaders.CONTENT_LENGTH);
return Mono.just(new ResponseEntity<>(processedContent, responseHeaders, statusCode));
}
}
@SuppressWarnings("ClassCanBeRecord")
private static class HttpHeadersBiConsumer implements BiConsumer<String, List<String>> {
private final HttpHeaders responseHeaders;
public HttpHeadersBiConsumer(HttpHeaders responseHeaders) {
this.responseHeaders = responseHeaders;
}
@Override
public void accept(String key, List<String> values) {
String lowerKey = key.toLowerCase();
// 过滤目标接口返回的 CORS 相关头(避免和全局配置重复)
if (lowerKey.startsWith("access-control-") || lowerKey.startsWith("vary")) {
return; // 跳过,不复制这些头
}
// 排除这些响应头
if (!"content-length".equals(lowerKey)
&& !"transfer-encoding".equals(lowerKey)
&& !"content-encoding".equals(lowerKey)
&& !"content-type".equals(lowerKey)) {
responseHeaders.put(key, values);
}
}
}
/**
* 精准域名替换
*/
private String replaceDomainPrecisely(String content, String originalDomain, String proxyDir, List<String> replaceList) {
if (StringUtils.isBlank(content) || StringUtils.isBlank(originalDomain) || StringUtils.isBlank(proxyDir)) {
return content;
}
String proxyPath = "/domain-proxy/" + proxyDir;
String escapedDomain = Pattern.quote(originalDomain);
for (String str : replaceList) {
content = content.replaceAll(str, proxyPath + str);
}
return content.replaceAll(escapedDomain, proxyPath);
}
/**
* 低版本兼容设置文本类型Content-Type带UTF-8编码
*/
private void setTextContentTypeWithCharset(HttpHeaders headers, String requestUri, MediaType contentType) {
MediaType mediaType;
if (requestUri.endsWith(".css")) {
mediaType = new MediaType(MediaType.valueOf("text/css"), StandardCharsets.UTF_8);
} else if (requestUri.endsWith(".js")) {
mediaType = new MediaType(MediaType.valueOf("application/javascript"), StandardCharsets.UTF_8);
} else if (requestUri.endsWith(".json")) {
mediaType = new MediaType(MediaType.APPLICATION_JSON, StandardCharsets.UTF_8);
} else if (requestUri.endsWith(".xml")) {
mediaType = new MediaType(MediaType.APPLICATION_XML, StandardCharsets.UTF_8);
} else {
mediaType = contentType;
}
headers.setContentType(mediaType);
}
/**
* 合并DataBuffer列表为字节数组
*/
private byte[] mergeDataBuffers(List<DataBuffer> dataBuffers) {
if (dataBuffers.isEmpty()) {
return new byte[0];
}
int totalLength = 0;
for (DataBuffer buffer : dataBuffers) {
totalLength += buffer.readableByteCount();
}
byte[] result = new byte[totalLength];
int offset = 0;
for (DataBuffer buffer : dataBuffers) {
int length = buffer.readableByteCount();
buffer.read(result, offset, length);
offset += length;
DataBufferUtils.release(buffer);
}
return result;
}
/**
* 快速判断是否为二进制请求(基于 URI 和响应头)
*/
private boolean isLikelyBinaryRequest(String requestUriLower, ClientResponse clientResponse) {
try {
// 检查文件扩展名
if (STATIC_RESOURCE_PATTERN.matcher(requestUriLower).find() &&
!TEXT_RESOURCE_PATTERN.matcher(requestUriLower).find()) {
return true;
}
// 检查响应的 Content-Type
HttpHeaders headers = clientResponse.headers().asHttpHeaders();
MediaType contentType = headers.getContentType();
if (isBinaryContentType(contentType)) {
return true;
}
} catch (Exception e) {
// 忽略异常,返回 false
}
return false;
}
/**
* 判断 Content-Type 是否为二进制类型
*/
private boolean isBinaryContentType(MediaType contentType) {
if (contentType == null) return false;
String type = contentType.getType().toLowerCase();
String subtype = contentType.getSubtype().toLowerCase();
return type.equals("image") || type.equals("font") ||
(type.equals("application") && (subtype.contains("octet-stream") ||
subtype.contains("pdf") || subtype.contains("zip") ||
subtype.contains("rar") || subtype.contains("x-")));
}
/**
* 根据 URI 获取 Content-Type
*/
private MediaType getContentTypeFromUri(String lowerUri) {
if (lowerUri.endsWith(".png")) {
return MediaType.IMAGE_PNG;
} else if (lowerUri.endsWith(".jpg") || lowerUri.endsWith(".jpeg")) {
return MediaType.IMAGE_JPEG;
} else if (lowerUri.endsWith(".gif")) {
return MediaType.IMAGE_GIF;
} else if (lowerUri.endsWith(".ico")) {
return new MediaType("image", "x-icon");
} else if (lowerUri.endsWith(".woff")) {
return new MediaType("font", "woff");
} else if (lowerUri.endsWith(".woff2")) {
return new MediaType("font", "woff2");
} else if (lowerUri.endsWith(".ttf")) {
return new MediaType("font", "ttf");
} else if (lowerUri.endsWith(".eot")) {
return new MediaType("application", "vnd.ms-fontobject");
} else if (lowerUri.endsWith(".svg")) {
return new MediaType("image", "svg+xml");
} else if (lowerUri.endsWith(".webp")) {
return new MediaType("image", "webp");
} else if (lowerUri.endsWith(".bmp")) {
return new MediaType("image", "bmp");
}
return null;
}
/**
* 复制请求头
*/
private void copyRequestHeaders(HttpServletRequest request, HttpHeaders targetHeaders) {
Enumeration<String> headerNames = request.getHeaderNames();
while (headerNames.hasMoreElements()) {
String headerName = headerNames.nextElement();
if (!"host".equalsIgnoreCase(headerName)
&& !"content-length".equalsIgnoreCase(headerName)
&& !"connection".equalsIgnoreCase(headerName)
&& !"transfer-encoding".equalsIgnoreCase(headerName)) {
Enumeration<String> headerValues = request.getHeaders(headerName);
while (headerValues.hasMoreElements()) {
targetHeaders.add(headerName, headerValues.nextElement());
}
}
}
// 强制添加 Origin 头(若前端未传,避免代理请求丢失)
String origin = request.getHeader("Origin");
if (StringUtils.isNotBlank(origin) && !targetHeaders.containsKey("Origin")) {
targetHeaders.add("Origin", origin);
}
}
/**
* 构建请求体
*/
private BodyInserter<?, ? super ClientHttpRequest> buildBodyInserter(Object body, HttpServletRequest request) {
if (body == null) {
return BodyInserters.empty();
}
String contentType = request.getContentType();
if (StringUtils.isBlank(contentType)) {
return BodyInserters.fromValue(body);
}
if (contentType.contains(MediaType.MULTIPART_FORM_DATA_VALUE)) {
return BodyInserters.fromValue(body);
} else if (contentType.contains(MediaType.APPLICATION_FORM_URLENCODED_VALUE)) {
return BodyInserters.fromValue(body);
} else if (contentType.contains(MediaType.APPLICATION_OCTET_STREAM_VALUE)) {
if (body instanceof byte[]) {
return BodyInserters.fromValue((byte[]) body);
} else {
return BodyInserters.fromValue(body.toString().getBytes(StandardCharsets.UTF_8));
}
} else {
return BodyInserters.fromValue(body);
}
}
/**
* 解析路由配置的键值对
*/
public static Map<String, String> parseKeyValue(String input) {
Map<String, String> result = new HashMap<>();
if (StringUtils.isBlank(input)) {
return result;
}
Pattern pattern = Pattern.compile("\\[(.*?)](.*)");
Matcher matcher = pattern.matcher(input.trim());
if (matcher.find()) {
result.put("key", matcher.group(1).trim());
result.put("value", matcher.group(2).trim());
}
return result;
}
// 魔数 -> MediaType 映射表覆盖常见文件格式的标准MIME类型
private static final Map<byte[], MediaType> MAGIC_NUMBER_MEDIA_TYPE_MAP = new HashMap<>();
static {
// ========== 图片格式 ==========
MAGIC_NUMBER_MEDIA_TYPE_MAP.put(new byte[]{(byte) 0xFF, (byte) 0xD8, (byte) 0xFF}, MediaType.IMAGE_JPEG);
MAGIC_NUMBER_MEDIA_TYPE_MAP.put(new byte[]{(byte) 0x89, (byte) 0x50, (byte) 0x4E, (byte) 0x47, (byte) 0x0D, (byte) 0x0A, (byte) 0x1A, (byte) 0x0A}, MediaType.IMAGE_PNG);
MAGIC_NUMBER_MEDIA_TYPE_MAP.put(new byte[]{(byte) 0x47, (byte) 0x49, (byte) 0x46, (byte) 0x38}, MediaType.IMAGE_GIF);
MAGIC_NUMBER_MEDIA_TYPE_MAP.put(new byte[]{(byte) 0x42, (byte) 0x4D}, MediaType.valueOf("image/bmp"));
MAGIC_NUMBER_MEDIA_TYPE_MAP.put(new byte[]{(byte) 0x52, (byte) 0x49, (byte) 0x46, (byte) 0x46, -1, -1, -1, -1, (byte) 0x57, (byte) 0x45, (byte) 0x42, (byte) 0x50}, MediaType.valueOf("image/webp"));
// ========== 文档格式 ==========
MAGIC_NUMBER_MEDIA_TYPE_MAP.put(new byte[]{(byte) 0x25, (byte) 0x50, (byte) 0x44, (byte) 0x46}, MediaType.APPLICATION_PDF);
// MS Officedoc/xls/ppt
MAGIC_NUMBER_MEDIA_TYPE_MAP.put(new byte[]{(byte) 0xD0, (byte) 0xCF, (byte) 0x11, (byte) 0xE0, (byte) 0xA1, (byte) 0xB1, (byte) 0x1A, (byte) 0xE1}, MediaType.valueOf("application/msword"));
// Office Open XMLdocx/xlsx/pptx
MAGIC_NUMBER_MEDIA_TYPE_MAP.put(new byte[]{(byte) 0x50, (byte) 0x4B, (byte) 0x03, (byte) 0x04}, MediaType.valueOf("application/vnd.openxmlformats-officedocument.wordprocessingml.document"));
// ========== 压缩包格式 ==========
MAGIC_NUMBER_MEDIA_TYPE_MAP.put(new byte[]{(byte) 0x1F, (byte) 0x8B}, MediaType.valueOf("application/gzip"));
MAGIC_NUMBER_MEDIA_TYPE_MAP.put(new byte[]{(byte) 0x52, (byte) 0x61, (byte) 0x72, (byte) 0x21, (byte) 0x1A, (byte) 0x07}, MediaType.valueOf("application/x-rar-compressed"));
// ========== 音频格式 ==========
MAGIC_NUMBER_MEDIA_TYPE_MAP.put(new byte[]{(byte) 0x49, (byte) 0x44, (byte) 0x33}, MediaType.valueOf("audio/mpeg"));
MAGIC_NUMBER_MEDIA_TYPE_MAP.put(new byte[]{(byte) 0xFF, (byte) 0xFB}, MediaType.valueOf("audio/mpeg"));
MAGIC_NUMBER_MEDIA_TYPE_MAP.put(new byte[]{(byte) 0x4F, (byte) 0x67, (byte) 0x67, (byte) 0x53}, MediaType.valueOf("audio/ogg"));
// ========== 视频格式 ==========
MAGIC_NUMBER_MEDIA_TYPE_MAP.put(new byte[]{(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x18, (byte) 0x66, (byte) 0x74, (byte) 0x79, (byte) 0x70}, MediaType.valueOf("video/mp4"));
MAGIC_NUMBER_MEDIA_TYPE_MAP.put(new byte[]{(byte) 0x41, (byte) 0x56, (byte) 0x49, (byte) 0x20}, MediaType.valueOf("video/x-msvideo")); // AVI
MAGIC_NUMBER_MEDIA_TYPE_MAP.put(new byte[]{(byte) 0x52, (byte) 0x49, (byte) 0x46, (byte) 0x46, -1, -1, -1, -1, (byte) 0x41, (byte) 0x56, (byte) 0x49, (byte) 0x20}, MediaType.valueOf("video/x-msvideo"));
// ========== 文本格式 ==========
// UTF-8 BOM
MAGIC_NUMBER_MEDIA_TYPE_MAP.put(new byte[]{(byte) 0xEF, (byte) 0xBB, (byte) 0xBF}, MediaType.TEXT_PLAIN);
}
/**
* 从二进制byte数组检测文件内容类型返回标准MediaType对象
*
* @param data 二进制字节数组建议至少包含前16字节
* @return 匹配的MediaType无法识别返回 MediaType.APPLICATION_OCTET_STREAM二进制流
*/
public static MediaType detectFileMediaType(byte[] data) {
// 空数据返回二进制流
if (data == null || data.length == 0) {
return MediaType.APPLICATION_OCTET_STREAM;
}
// 遍历魔数映射表匹配
for (Map.Entry<byte[], MediaType> entry : MAGIC_NUMBER_MEDIA_TYPE_MAP.entrySet()) {
byte[] magicBytes = entry.getKey();
MediaType mediaType = entry.getValue();
// 数据长度不足当前魔数长度,跳过
if (data.length < magicBytes.length) {
continue;
}
// 提取数据开头的对应长度字节
byte[] dataSlice = Arrays.copyOfRange(data, 0, magicBytes.length);
// 匹配魔数(处理-1表示任意字节的情况
boolean isMatch = true;
for (int i = 0; i < magicBytes.length; i++) {
byte magicByte = magicBytes[i];
byte dataByte = dataSlice[i];
if (magicByte != -1 && magicByte != dataByte) {
isMatch = false;
break;
}
}
if (isMatch) {
return mediaType;
}
}
// 额外检测纯文本(非二进制)
if (isPlainText(data)) {
return MediaType.TEXT_PLAIN;
}
// 未识别的二进制流
return MediaType.APPLICATION_OCTET_STREAM;
}
/**
* 辅助方法:判断是否为纯文本(简单检测)
*
* @param data 字节数组
* @return 是否为纯文本
*/
private static boolean isPlainText(byte[] data) {
int printableCount = 0;
// 最多检查前1024字节避免大文件性能问题
int checkLength = Math.min(data.length, 1024);
for (int i = 0; i < checkLength; i++) {
byte b = data[i];
// 可打印ASCII字符32-126、换行(10)、回车(13)、制表符(9)
if ((b >= 32 && b <= 126) || b == 10 || b == 13 || b == 9) {
printableCount++;
}
}
// 可打印字符占比超过90%判定为纯文本
return (double) printableCount / checkLength > 0.9;
}
/**
* 对URL中的QueryString参数值进行URL编码UTF-8
*
* @param originalUrl 原始URL包含中文等特殊字符的QueryString
* @return 编码后的完整URL
*/
public static String encodeUrlQueryParams(String originalUrl) {
try {
// 空值校验
if (originalUrl == null || originalUrl.isEmpty()) {
return originalUrl;
}
// 解析URL为URI对象方便拆分各部分
URI uri = new URI(originalUrl);
String scheme = uri.getScheme(); // http/https等
String host = uri.getHost(); // 域名
int port = uri.getPort(); // 端口
String path = uri.getPath(); // 路径
String query = uri.getQuery(); // QueryString部分
String fragment = uri.getFragment(); // 锚点部分(#后面的内容)
// 如果没有QueryString直接返回原URL
if (query == null || query.isEmpty()) {
return originalUrl;
}
// 解析并编码QueryString参数
String encodedQuery = encodeQueryParams(query);
// 重新构建URI
URI encodedUri = new URI(
scheme,
null, // userInfo
host,
port,
path,
encodedQuery,
fragment
);
return encodedUri.toString();
} catch (Throwable throwable) {
return originalUrl;
}
}
/**
* 解析并编码QueryString参数保留参数名仅编码参数值
*
* @param queryString 原始QueryStringtext=你好,世界&name=test
* @return 编码后的QueryString
*/
private static String encodeQueryParams(String queryString) {
try {
Map<String, String> paramMap = new LinkedHashMap<>();
String[] params = queryString.split("&");
// 拆分参数名和参数值
for (String param : params) {
String[] keyValue = param.split("=", 2); // 只拆分第一个=,避免值中包含=的情况
String key = keyValue[0];
String value = keyValue.length > 1 ? keyValue[1] : "";
// 对参数值进行URL编码RFC 3986标准替换空格为%20而非+
String encodedValue = URLEncoder.encode(value, StandardCharsets.UTF_8)
.replace("+", "%20") // 替换URLEncoder默认的+为%20
.replace("*", "%2A")
.replace("%7E", "~");
paramMap.put(key, encodedValue);
}
// 重新拼接编码后的参数
StringBuilder encodedQuery = new StringBuilder();
for (Map.Entry<String, String> entry : paramMap.entrySet()) {
if (!encodedQuery.isEmpty()) {
encodedQuery.append("&");
}
encodedQuery.append(entry.getKey())
.append("=")
.append(entry.getValue());
}
return encodedQuery.toString();
} catch (Throwable throwable) {
return queryString;
}
}
}

View File

@@ -20,7 +20,7 @@ import javax.annotation.Resource;
)
@ApiDocActionConfiguration(
response = {
SystemAsyncTaskInfoDetailResponse.class
Void.class
}
)
public class ApiSystemAsyncTaskInfoCancelAction extends ProtoActionHandler<SystemAsyncTaskInfoCancelParam> {

View File

@@ -0,0 +1,35 @@
package com.iqudoo.platform.application.domain.actions.apiSystemAsyncTaskInfo;
import com.iqudoo.framework.tape.base.action.annotation.ActionConfig;
import com.iqudoo.framework.tape.base.action.annotation.ActionMapping;
import com.iqudoo.framework.tape.base.action.handler.ProtoActionHandler;
import com.iqudoo.framework.tape.base.docs.annotation.ApiDocActionConfiguration;
import com.iqudoo.framework.tape.modules.proto.Proto;
import com.iqudoo.platform.application.constants.BizConstants;
import com.iqudoo.platform.application.facade.request.SystemAsyncTaskInfoClearParam;
import com.iqudoo.platform.application.facade.service.ISystemAsyncTaskInfoService;
import javax.annotation.Resource;
@ActionMapping(action = "api.system.async.task.info.clear")
@ActionConfig(
group = {
BizConstants.AUTH_GROUP_SYS_ADMIN
}
)
@ApiDocActionConfiguration(
response = {
Void.class
}
)
public class ApiSystemAsyncTaskInfoClearAction extends ProtoActionHandler<SystemAsyncTaskInfoClearParam> {
@Resource
private ISystemAsyncTaskInfoService systemAsyncTaskInfoService;
@Override
public Proto<?> onHandleRequest(SystemAsyncTaskInfoClearParam param) throws Throwable {
return systemAsyncTaskInfoService.doClear(param);
}
}

View File

@@ -7,7 +7,6 @@ import com.iqudoo.framework.tape.base.docs.annotation.ApiDocActionConfiguration;
import com.iqudoo.framework.tape.modules.proto.Proto;
import com.iqudoo.platform.application.constants.BizConstants;
import com.iqudoo.platform.application.facade.request.SystemAsyncTaskInfoDeleteParam;
import com.iqudoo.platform.application.facade.response.SystemAsyncTaskInfoDetailResponse;
import com.iqudoo.platform.application.facade.service.ISystemAsyncTaskInfoService;
import javax.annotation.Resource;
@@ -20,7 +19,7 @@ import javax.annotation.Resource;
)
@ApiDocActionConfiguration(
response = {
SystemAsyncTaskInfoDetailResponse.class
Void.class
}
)
public class ApiSystemAsyncTaskInfoDeleteAction extends ProtoActionHandler<SystemAsyncTaskInfoDeleteParam> {

View File

@@ -1,125 +1,31 @@
package com.iqudoo.platform.application.domain.service;
import com.iqudoo.framework.tape.Tape;
import com.iqudoo.framework.tape.base.action.bean.ActionAuthInfo;
import com.iqudoo.framework.tape.base.action.bean.ActionState;
import com.iqudoo.framework.tape.base.action.bean.ActionTokenInfo;
import com.iqudoo.framework.tape.modules.IModuleSession;
import com.iqudoo.framework.tape.modules.asserts.AssertUtil;
import com.iqudoo.framework.tape.modules.proto.Proto;
import com.iqudoo.framework.tape.modules.proto.ProtoLoader;
import com.iqudoo.framework.tape.modules.proto.ProtoUtil;
import com.iqudoo.framework.tape.modules.session.RefreshSessionParam;
import com.iqudoo.framework.tape.modules.utils.DigestUtil;
import com.iqudoo.framework.tape.modules.utils.RandomUtil;
import com.iqudoo.platform.application.constants.ErrorConstants;
import com.iqudoo.platform.application.database.model.SystemAdminInfo;
import com.iqudoo.platform.application.database.model.SystemAdminInfoExample;
import com.iqudoo.platform.application.facade.enums.AuthTokenGroupEnum;
import com.iqudoo.platform.application.facade.enums.SystemAdminInfoOptStatusEnum;
import com.iqudoo.platform.application.facade.enums.SystemAdminInfoOptSuperEnum;
import com.iqudoo.platform.application.facade.repository.ISystemAdminInfoRepository;
import com.iqudoo.platform.application.domain.transactionals.SystemAdminInfoAuthTransactional;
import com.iqudoo.platform.application.facade.request.SystemAdminInfoAuthEditPasswordParam;
import com.iqudoo.platform.application.facade.request.SystemAdminInfoAuthEditProfileParam;
import com.iqudoo.platform.application.facade.request.SystemAdminInfoAuthLoginParam;
import com.iqudoo.platform.application.facade.request.SystemAdminInfoCreateParam;
import com.iqudoo.platform.application.facade.response.SystemAdminInfoAuthLoginResponse;
import com.iqudoo.platform.application.facade.response.SystemAdminInfoDetailResponse;
import com.iqudoo.platform.application.facade.service.ISystemAdminInfoAuthService;
import com.iqudoo.platform.application.facade.service.ISystemAdminInfoService;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.Objects;
@Service
@SuppressWarnings("DuplicatedCode")
public class SystemAdminInfoAuthServiceImpl implements ISystemAdminInfoAuthService {
private final String initAdminNickname = "超级管理员";
private final String initAdminUsername = "admin";
private final String initAdminPassword = "admin";
@Resource
private ISystemAdminInfoRepository systemAdminInfoRepository;
@Resource
private ISystemAdminInfoService systemAdminInfoService;
private SystemAdminInfoAuthTransactional systemAdminInfoAuthTransactional;
@Override
public Proto<SystemAdminInfoAuthLoginResponse> doLogin(SystemAdminInfoAuthLoginParam param) throws Throwable {
return ProtoUtil.load(param, new ProtoLoader<SystemAdminInfoAuthLoginResponse, SystemAdminInfoAuthLoginParam>() {
@Override
public SystemAdminInfoAuthLoginResponse onLoad(SystemAdminInfoAuthLoginParam loadParam) throws Throwable {
AssertUtil.oneNotInvalid(loadParam.getUsername(), "账号不能为空");
AssertUtil.oneNotInvalid(loadParam.getPassword(), "密码不能为空");
SystemAdminInfoExample systemAdminInfoExample = new SystemAdminInfoExample();
systemAdminInfoExample.createCriteria()
.andOptSuperEqualTo(SystemAdminInfoOptSuperEnum.SUPER.getValue());
long superCount = systemAdminInfoRepository.count(systemAdminInfoExample);
if (superCount <= 0) {
if (Objects.equals(loadParam.getUsername(), initAdminUsername)
&& (Objects.equals(loadParam.getPassword(),
initAdminPassword) || Objects.equals(loadParam.getPassword(),
DigestUtil.md5DigestAsHex(initAdminPassword)))) {
SystemAdminInfoCreateParam createParam = new SystemAdminInfoCreateParam();
createParam.setNickname(initAdminNickname);
createParam.setUsername(initAdminUsername);
createParam.setPassword(initAdminPassword);
createParam.setOptStatus(SystemAdminInfoOptStatusEnum.ENABLE.getValue());
createParam.setOptSuper(SystemAdminInfoOptSuperEnum.SUPER.getValue());
Proto<SystemAdminInfoDetailResponse> systemAdminInfoDetailResponseProto
= systemAdminInfoService.doCreate(createParam);
if (!systemAdminInfoDetailResponseProto.isSuccess()) {
throw systemAdminInfoDetailResponseProto.getErr();
}
}
}
SystemAdminInfoExample whereDoExample = new SystemAdminInfoExample();
whereDoExample.createCriteria()
.andUsernameEqualTo(loadParam.getUsername());
SystemAdminInfo aDo = systemAdminInfoRepository
.findOne(whereDoExample);
if (aDo == null) {
throw ErrorConstants.AUTH_USER_NOT_EXIST;
}
// check password
String passwordHash = loadParam.getPassword();
String passwordEncode = DigestUtil.md5DigestAsHex(aDo.getPwdSlot()
+ passwordHash);
if (!aDo.getPwdMd5().equals(passwordEncode)) {
throw ErrorConstants.AUTH_USER_PASSWORD_ERROR;
}
// check status
if (aDo.getOptStatus() == SystemAdminInfoOptStatusEnum.DISABLE.getValue()) {
throw ErrorConstants.AUTH_USER_NOT_ENABLE;
}
String userGroup = AuthTokenGroupEnum.SYS_ADMIN.getValue();
String originPlatform = loadParam.getRequestPlatform();
String originDomain = loadParam.getClientDomain();
String originIp = loadParam.getClientIpAddress();
RefreshSessionParam refreshSessionParam = new RefreshSessionParam();
refreshSessionParam.setUserGuid(aDo.getGuid());
refreshSessionParam.setUserGroup(userGroup);
refreshSessionParam.setOriginPlatform(originPlatform);
refreshSessionParam.setOriginDomain(originDomain);
refreshSessionParam.setOriginIp(originIp);
refreshSessionParam.setRefresh(false);
String curSessionKey = Tape.getService(IModuleSession.class)
.refreshSession(refreshSessionParam) + aDo.getPwdSlot();
ActionAuthInfo actionAuthInfo = new ActionAuthInfo();
actionAuthInfo.setOriginPlatform(originPlatform);
actionAuthInfo.setUserGroup(userGroup);
actionAuthInfo.setUserGuid(aDo.getGuid());
actionAuthInfo.setSessionKey(curSessionKey);
// login success
ActionTokenInfo actionTokenInfo = actionAuthInfo.createToken();
SystemAdminInfoAuthLoginResponse systemAdminLoginResponse = new SystemAdminInfoAuthLoginResponse();
systemAdminLoginResponse.setToken(actionTokenInfo.getToken());
systemAdminLoginResponse.setRefreshToken(actionTokenInfo.getRefreshToken());
systemAdminLoginResponse.setExpiresIn(actionTokenInfo.getExpiresIn());
systemAdminLoginResponse.setRefreshExpiresIn(actionTokenInfo.getRefreshExpiresIn());
// set user info
loadParam.getActionRequest().setAuthInfo(actionAuthInfo);
return systemAdminLoginResponse;
return systemAdminInfoAuthTransactional.handleLogin(loadParam);
}
});
@@ -130,32 +36,7 @@ public class SystemAdminInfoAuthServiceImpl implements ISystemAdminInfoAuthServi
return ProtoUtil.load(param, new ProtoLoader<Void, SystemAdminInfoAuthEditPasswordParam>() {
@Override
public Void onLoad(SystemAdminInfoAuthEditPasswordParam loadParam) throws Throwable {
AssertUtil.oneNotInvalid(loadParam.getGuid(), "GUID不能为空");
AssertUtil.oneNotInvalid(loadParam.getPassword(), "密码不能为空");
AssertUtil.oneNotInvalid(loadParam.getNewPassword(), "新密码不能为空");
SystemAdminInfo systemAdminInfo = systemAdminInfoRepository.findValidById(loadParam.getGuid());
if (systemAdminInfo == null) {
throw ErrorConstants.AUTH_USER_NOT_EXIST;
}
// check old password
String passwordHash = loadParam.getPassword();
String newPasswordHash = loadParam.getNewPassword();
String passwordEncode = DigestUtil.md5DigestAsHex(systemAdminInfo.getPwdSlot()
+ passwordHash);
if (!systemAdminInfo.getPwdMd5().equals(passwordEncode)) {
throw ErrorConstants.AUTH_USER_PASSWORD_ERROR;
}
if (passwordHash.equals(newPasswordHash)) {
throw ErrorConstants.AUTH_USER_NEW_PASSWORD_ERROR;
}
String pwdSlot = RandomUtil.getRandomString(6);
systemAdminInfo.setPwdMd5(DigestUtil.md5DigestAsHex(pwdSlot
+ loadParam.getNewPassword()));
systemAdminInfo.setPwdSlot(pwdSlot);
int update = systemAdminInfoRepository.update(systemAdminInfo);
if (update == 0) {
throw ActionState.getRequestToFrequentError();
}
systemAdminInfoAuthTransactional.handleEditPassword(loadParam);
return null;
}
@@ -167,18 +48,7 @@ public class SystemAdminInfoAuthServiceImpl implements ISystemAdminInfoAuthServi
return ProtoUtil.load(param, new ProtoLoader<Void, SystemAdminInfoAuthEditProfileParam>() {
@Override
public Void onLoad(SystemAdminInfoAuthEditProfileParam loadParam) throws Throwable {
AssertUtil.oneNotInvalid(loadParam.getGuid(), "GUID不能为空");
SystemAdminInfo systemAdminInfo = systemAdminInfoRepository.findValidById(loadParam.getGuid());
if (systemAdminInfo == null) {
throw new ActionState("账号不存在");
}
if (loadParam.getNickname() != null && !loadParam.getNickname().isEmpty()) {
systemAdminInfo.setNickname(loadParam.getNickname());
}
int update = systemAdminInfoRepository.update(systemAdminInfo);
if (update == 0) {
throw ActionState.getRequestToFrequentError();
}
systemAdminInfoAuthTransactional.handleEditProfile(loadParam);
return null;
}

View File

@@ -0,0 +1,167 @@
package com.iqudoo.platform.application.domain.transactionals;
import com.iqudoo.framework.tape.Tape;
import com.iqudoo.framework.tape.base.action.bean.ActionAuthInfo;
import com.iqudoo.framework.tape.base.action.bean.ActionState;
import com.iqudoo.framework.tape.base.action.bean.ActionTokenInfo;
import com.iqudoo.framework.tape.modules.IModuleSession;
import com.iqudoo.framework.tape.modules.asserts.AssertUtil;
import com.iqudoo.framework.tape.modules.proto.Proto;
import com.iqudoo.framework.tape.modules.session.RefreshSessionParam;
import com.iqudoo.framework.tape.modules.utils.DigestUtil;
import com.iqudoo.framework.tape.modules.utils.RandomUtil;
import com.iqudoo.platform.application.constants.ErrorConstants;
import com.iqudoo.platform.application.database.model.SystemAdminInfo;
import com.iqudoo.platform.application.database.model.SystemAdminInfoExample;
import com.iqudoo.platform.application.facade.enums.AuthTokenGroupEnum;
import com.iqudoo.platform.application.facade.enums.SystemAdminInfoOptStatusEnum;
import com.iqudoo.platform.application.facade.enums.SystemAdminInfoOptSuperEnum;
import com.iqudoo.platform.application.facade.repository.ISystemAdminInfoRepository;
import com.iqudoo.platform.application.facade.request.SystemAdminInfoAuthEditPasswordParam;
import com.iqudoo.platform.application.facade.request.SystemAdminInfoAuthEditProfileParam;
import com.iqudoo.platform.application.facade.request.SystemAdminInfoAuthLoginParam;
import com.iqudoo.platform.application.facade.request.SystemAdminInfoCreateParam;
import com.iqudoo.platform.application.facade.response.SystemAdminInfoAuthLoginResponse;
import com.iqudoo.platform.application.facade.response.SystemAdminInfoDetailResponse;
import com.iqudoo.platform.application.facade.service.ISystemAdminInfoService;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.util.Objects;
@SuppressWarnings({"DuplicatedCode", "FieldCanBeLocal"})
@Component
public class SystemAdminInfoAuthTransactional {
private final String initAdminNickname = "超级管理员";
private final String initAdminUsername = "admin";
private final String initAdminPassword = "admin";
@Resource
private ISystemAdminInfoRepository systemAdminInfoRepository;
@Resource
private ISystemAdminInfoService systemAdminInfoService;
@Transactional(rollbackFor = Throwable.class)
public SystemAdminInfoAuthLoginResponse handleLogin(SystemAdminInfoAuthLoginParam loadParam) throws Throwable {
AssertUtil.oneNotInvalid(loadParam.getUsername(), "账号不能为空");
AssertUtil.oneNotInvalid(loadParam.getPassword(), "密码不能为空");
SystemAdminInfoExample systemAdminInfoExample = new SystemAdminInfoExample();
systemAdminInfoExample.createCriteria()
.andOptSuperEqualTo(SystemAdminInfoOptSuperEnum.SUPER.getValue());
long superCount = systemAdminInfoRepository.count(systemAdminInfoExample);
if (superCount <= 0) {
if (Objects.equals(loadParam.getUsername(), initAdminUsername)
&& (Objects.equals(loadParam.getPassword(),
initAdminPassword) || Objects.equals(loadParam.getPassword(),
DigestUtil.md5DigestAsHex(initAdminPassword)))) {
SystemAdminInfoCreateParam createParam = new SystemAdminInfoCreateParam();
createParam.setNickname(initAdminNickname);
createParam.setUsername(initAdminUsername);
createParam.setPassword(initAdminPassword);
createParam.setOptStatus(SystemAdminInfoOptStatusEnum.ENABLE.getValue());
createParam.setOptSuper(SystemAdminInfoOptSuperEnum.SUPER.getValue());
Proto<SystemAdminInfoDetailResponse> systemAdminInfoDetailResponseProto
= systemAdminInfoService.doCreate(createParam);
if (!systemAdminInfoDetailResponseProto.isSuccess()) {
throw systemAdminInfoDetailResponseProto.getErr();
}
}
}
SystemAdminInfoExample whereDoExample = new SystemAdminInfoExample();
whereDoExample.createCriteria()
.andUsernameEqualTo(loadParam.getUsername());
SystemAdminInfo aDo = systemAdminInfoRepository
.findOne(whereDoExample);
if (aDo == null) {
throw ErrorConstants.AUTH_USER_NOT_EXIST;
}
// check password
String passwordHash = loadParam.getPassword();
String passwordEncode = DigestUtil.md5DigestAsHex(aDo.getPwdSlot()
+ passwordHash);
if (!aDo.getPwdMd5().equals(passwordEncode)) {
throw ErrorConstants.AUTH_USER_PASSWORD_ERROR;
}
// check status
if (aDo.getOptStatus() == SystemAdminInfoOptStatusEnum.DISABLE.getValue()) {
throw ErrorConstants.AUTH_USER_NOT_ENABLE;
}
String userGroup = AuthTokenGroupEnum.SYS_ADMIN.getValue();
String originPlatform = loadParam.getRequestPlatform();
String originDomain = loadParam.getClientDomain();
String originIp = loadParam.getClientIpAddress();
RefreshSessionParam refreshSessionParam = new RefreshSessionParam();
refreshSessionParam.setUserGuid(aDo.getGuid());
refreshSessionParam.setUserGroup(userGroup);
refreshSessionParam.setOriginPlatform(originPlatform);
refreshSessionParam.setOriginDomain(originDomain);
refreshSessionParam.setOriginIp(originIp);
refreshSessionParam.setRefresh(false);
String curSessionKey = Tape.getService(IModuleSession.class)
.refreshSession(refreshSessionParam) + aDo.getPwdSlot();
ActionAuthInfo actionAuthInfo = new ActionAuthInfo();
actionAuthInfo.setOriginPlatform(originPlatform);
actionAuthInfo.setUserGroup(userGroup);
actionAuthInfo.setUserGuid(aDo.getGuid());
actionAuthInfo.setSessionKey(curSessionKey);
// login success
ActionTokenInfo actionTokenInfo = actionAuthInfo.createToken();
SystemAdminInfoAuthLoginResponse systemAdminLoginResponse = new SystemAdminInfoAuthLoginResponse();
systemAdminLoginResponse.setToken(actionTokenInfo.getToken());
systemAdminLoginResponse.setRefreshToken(actionTokenInfo.getRefreshToken());
systemAdminLoginResponse.setExpiresIn(actionTokenInfo.getExpiresIn());
systemAdminLoginResponse.setRefreshExpiresIn(actionTokenInfo.getRefreshExpiresIn());
// set user info
loadParam.getActionRequest().setAuthInfo(actionAuthInfo);
return systemAdminLoginResponse;
}
@Transactional(rollbackFor = Throwable.class)
public void handleEditProfile(SystemAdminInfoAuthEditProfileParam loadParam) throws Throwable {
AssertUtil.oneNotInvalid(loadParam.getGuid(), "GUID不能为空");
SystemAdminInfo systemAdminInfo = systemAdminInfoRepository.findValidById(loadParam.getGuid());
if (systemAdminInfo == null) {
throw new ActionState("账号不存在");
}
if (loadParam.getNickname() != null && !loadParam.getNickname().isEmpty()) {
systemAdminInfo.setNickname(loadParam.getNickname());
}
int update = systemAdminInfoRepository.update(systemAdminInfo);
if (update == 0) {
throw ActionState.getRequestToFrequentError();
}
}
@Transactional(rollbackFor = Throwable.class)
public void handleEditPassword(SystemAdminInfoAuthEditPasswordParam loadParam) throws Throwable {
AssertUtil.oneNotInvalid(loadParam.getGuid(), "GUID不能为空");
AssertUtil.oneNotInvalid(loadParam.getPassword(), "密码不能为空");
AssertUtil.oneNotInvalid(loadParam.getNewPassword(), "新密码不能为空");
SystemAdminInfo systemAdminInfo = systemAdminInfoRepository.findValidById(loadParam.getGuid());
if (systemAdminInfo == null) {
throw ErrorConstants.AUTH_USER_NOT_EXIST;
}
// check old password
String passwordHash = loadParam.getPassword();
String newPasswordHash = loadParam.getNewPassword();
String passwordEncode = DigestUtil.md5DigestAsHex(systemAdminInfo.getPwdSlot()
+ passwordHash);
if (!systemAdminInfo.getPwdMd5().equals(passwordEncode)) {
throw ErrorConstants.AUTH_USER_PASSWORD_ERROR;
}
if (passwordHash.equals(newPasswordHash)) {
throw ErrorConstants.AUTH_USER_NEW_PASSWORD_ERROR;
}
String pwdSlot = RandomUtil.getRandomString(6);
systemAdminInfo.setPwdMd5(DigestUtil.md5DigestAsHex(pwdSlot
+ loadParam.getNewPassword()));
systemAdminInfo.setPwdSlot(pwdSlot);
int update = systemAdminInfoRepository.update(systemAdminInfo);
if (update == 0) {
throw ActionState.getRequestToFrequentError();
}
}
}

View File

@@ -176,6 +176,10 @@ public class SystemAsyncTaskInfoTransactional {
&& !Objects.equals(loadParam.getBizId(), systemAsyncTaskInfo.getBizId())) {
throw new ActionState("异步任务业务编号不匹配");
}
int update = systemAsyncTaskInfoRepository.deleteById(systemAsyncTaskInfo.getGuid(), true);
if (update == 0) {
throw ActionState.getRequestToFrequentError();
}
SystemAsyncTaskInfoCreateParam systemAsyncTaskInfoCreateParam
= new SystemAsyncTaskInfoCreateParam();
systemAsyncTaskInfoCreateParam.setTransId(systemAsyncTaskInfo.getTransId());
@@ -213,10 +217,18 @@ public class SystemAsyncTaskInfoTransactional {
// 大于24小时的已完成的异步任务定时清除记录
SystemAsyncTaskInfoExample clearTaskExample
= new SystemAsyncTaskInfoExample();
clearTaskExample.createCriteria()
.andCreateTimeLessThanOrEqualTo(new Date(new Date().getTime()
- 24 * 60 * 60 * 1000L))
.andOptStatusEqualTo(SystemAsyncTaskInfoOptStatusEnum.FULFILLED.getValue());
SystemAsyncTaskInfoExample.Criteria criteria
= clearTaskExample.createCriteria();
if (loadParam.getBizType() != null && !loadParam.getBizType().isEmpty()) {
criteria.andBizTypeEqualTo(loadParam.getBizType());
}
if (loadParam.getBizId() != null && !loadParam.getBizId().isEmpty()) {
criteria.andBizIdEqualTo(loadParam.getBizId());
}
if (loadParam.getClearTime() > 0) {
criteria.andCreateTimeLessThanOrEqualTo(new Date(loadParam.getClearTime()));
}
criteria.andOptStatusEqualTo(SystemAsyncTaskInfoOptStatusEnum.FULFILLED.getValue());
long count = systemAsyncTaskInfoRepository.count(clearTaskExample);
if (count > 0) {
systemAsyncTaskInfoRepository.deleteAll(clearTaskExample, true);
@@ -395,4 +407,4 @@ public class SystemAsyncTaskInfoTransactional {
});
}
}
}

View File

@@ -1,5 +1,6 @@
package com.iqudoo.platform.application.facade.request;
import com.iqudoo.framework.tape.base.docs.annotation.ApiDocFieldConfiguration;
import com.iqudoo.platform.application.facade.base.BaseParam;
import lombok.Data;
import lombok.EqualsAndHashCode;
@@ -8,4 +9,13 @@ import lombok.EqualsAndHashCode;
@EqualsAndHashCode(callSuper = false)
public class SystemAsyncTaskInfoClearParam extends BaseParam {
@ApiDocFieldConfiguration(desc = "截止时间")
private long clearTime;
@ApiDocFieldConfiguration(desc = "业务类型")
private String bizType;
@ApiDocFieldConfiguration(desc = "业务编号")
private String bizId;
}

View File

@@ -22,10 +22,10 @@ public interface ISystemAsyncTaskInfoService {
Proto<Void> doDelete(SystemAsyncTaskInfoDeleteParam param) throws Throwable;
Proto<Void> doReset(SystemAsyncTaskInfoResetParam param) throws Throwable;
Proto<Void> doTick(SystemAsyncTaskInfoTickParam param) throws Throwable;
Proto<Void> doReset(SystemAsyncTaskInfoResetParam param) throws Throwable;
Proto<Void> doSchedule(SystemAsyncTaskInfoScheduleParam param) throws Throwable;
Proto<Void> doClear(SystemAsyncTaskInfoClearParam param) throws Throwable;

View File

@@ -9,6 +9,7 @@ import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.Date;
@Component
public class LoopScheduling {
@@ -52,6 +53,7 @@ public class LoopScheduling {
}
SystemAsyncTaskInfoClearParam systemAsyncTaskInfoClearParam = new SystemAsyncTaskInfoClearParam();
systemAsyncTaskInfoClearParam.setThrowThrowable(true);
systemAsyncTaskInfoClearParam.setClearTime(new Date().getTime() - 24 * 60 * 60 * 1000L);
Proto<Void> clearProto = systemAsyncTaskInfoService.doClear(systemAsyncTaskInfoClearParam);
if (!clearProto.isSuccess()) {
LOGGER.error("检查清理异步任务失败", clearProto.getErr());

View File

@@ -0,0 +1,23 @@
package com.iqudoo.platform.application.setting;
import com.iqudoo.framework.tape.modules.IModuleSetting;
import com.iqudoo.framework.tape.modules.annotation.SettingConfiguration;
import com.iqudoo.framework.tape.modules.annotation.SettingField;
import com.iqudoo.platform.application.constants.BizConstants;
import lombok.Data;
@Data
@SettingConfiguration(group = BizConstants.SETTING_GROUP_SYSTEM,
key = "system_global_http_proxy_config",
title = "网络代理配置",
desc = "资源代理,适用于非同源资源代理为同源资源,解决跨域问题",
sort = 999998)
public class SystemGlobalHttpProxyConfig {
@SettingField(label = "域名代理路由",
tips = "域名代理配置,多域名时换行,格式如下:[访问路径]代理域名,对应{代理域名}{资源PATH}的请求路径为{serverUrl}/domain-proxy/{访问路径}{资源PATH}",
type = IModuleSetting.SettingFieldType.TYPE_TEXTAREA,
rows = 6)
private String domainProxyRouterMap = "";
}