http proxy
This commit is contained in:
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
// 返回 Resource,Spring 会自动处理二进制数据
|
||||
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 Office(doc/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 XML(docx/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 原始QueryString(如:text=你好,世界&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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -20,7 +20,7 @@ import javax.annotation.Resource;
|
||||
)
|
||||
@ApiDocActionConfiguration(
|
||||
response = {
|
||||
SystemAsyncTaskInfoDetailResponse.class
|
||||
Void.class
|
||||
}
|
||||
)
|
||||
public class ApiSystemAsyncTaskInfoCancelAction extends ProtoActionHandler<SystemAsyncTaskInfoCancelParam> {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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> {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 {
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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 = "";
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user