资源代理
This commit is contained in:
@@ -3,6 +3,7 @@ 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.framework.tape.modules.utils.DigestUtil;
|
||||
import com.iqudoo.platform.application.setting.SystemGlobalHttpProxyConfig;
|
||||
import org.springframework.core.io.ByteArrayResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
@@ -21,7 +22,6 @@ 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;
|
||||
@@ -60,7 +60,7 @@ public class HttpProxyController {
|
||||
/**
|
||||
* 通用代理接口(基于目录路由)
|
||||
*/
|
||||
@RequestMapping(value = {"/domain-proxy/", "/domain-proxy/{proxyDir:.*}/**"}, method = {
|
||||
@RequestMapping(value = {"/http-proxy/", "/http-proxy/{proxyDir:.*}/**"}, method = {
|
||||
RequestMethod.GET,
|
||||
RequestMethod.POST,
|
||||
RequestMethod.PUT,
|
||||
@@ -76,33 +76,26 @@ public class HttpProxyController {
|
||||
.getSetting(SystemGlobalHttpProxyConfig.class);
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
String requestUri = servletRequest.getRequestURI();
|
||||
String proxyPath = requestUri.replaceFirst("^/domain-proxy(/[^/]+)?", "");
|
||||
String proxyPath = requestUri.replaceFirst("^/http-proxy(/[^/]+)?", "");
|
||||
if (proxyPath.startsWith("//")) {
|
||||
proxyPath = proxyPath.substring(1);
|
||||
}
|
||||
// 匹配路由配置
|
||||
String proxyConfig = "";
|
||||
String proxyTargetUrl = null;
|
||||
String proxyDomainUrl = null;
|
||||
List<String> replaceList = new ArrayList<>();
|
||||
Map<String, String> replaceMap = new HashMap<>();
|
||||
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) {
|
||||
ProxyRouteConfig routeConfig = ProxyRouteConfig.parse(domainProxy);
|
||||
// 检查是否匹配当前请求
|
||||
if (routeConfig.getRoute().equals(proxyDir)) {
|
||||
proxyConfig = domainProxy;
|
||||
proxyDomainUrl = routeConfig.getDomainUrl();
|
||||
proxyTargetUrl = routeConfig.getFullTargetUrl(proxyPath);
|
||||
replaceMap.putAll(routeConfig.getReplaceMap());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -119,6 +112,8 @@ public class HttpProxyController {
|
||||
}
|
||||
// 构建请求头
|
||||
copyRequestHeaders(servletRequest, headers);
|
||||
// 根据代理配置判断是否禁用本次缓存
|
||||
validateETagWithCacheVersion(servletRequest, headers, proxyConfig);
|
||||
// 处理请求方法和请求体
|
||||
HttpMethod method = HttpMethod.valueOf(servletRequest.getMethod());
|
||||
BodyInserter<?, ? super ClientHttpRequest> bodyInserter = buildBodyInserter(body, servletRequest);
|
||||
@@ -221,7 +216,7 @@ public class HttpProxyController {
|
||||
public Mono<ResponseEntity<?>> apply(List<DataBuffer> dataBuffers) {
|
||||
try {
|
||||
byte[] responseBytes = mergeDataBuffers(dataBuffers);
|
||||
return handleResponseContent(responseBytes, clientResponse, finalProxyDomainUrl, proxyDir, finalProxyTargetUrl, replaceList);
|
||||
return handleResponseContent(responseBytes, clientResponse, finalProxyDomainUrl, proxyDir, finalProxyTargetUrl, replaceMap);
|
||||
} catch (Exception e) {
|
||||
HttpHeaders errorHeaders = new HttpHeaders();
|
||||
errorHeaders.setContentType(new MediaType(MediaType.APPLICATION_JSON, StandardCharsets.UTF_8));
|
||||
@@ -269,172 +264,12 @@ public class HttpProxyController {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用代理接口(直接指定目标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) {
|
||||
String domainUrl, String proxyDir, String proxyTargetUrl,
|
||||
Map<String, String> replaceMap) {
|
||||
HttpHeaders responseHeaders = new HttpHeaders();
|
||||
HttpHeaders clientHeaders = response.headers().asHttpHeaders();
|
||||
HttpStatus statusCode = response.statusCode();
|
||||
@@ -528,7 +363,7 @@ public class HttpProxyController {
|
||||
// 仅对文本类资源进行替换
|
||||
|
||||
if (needReplace) {
|
||||
processedContent = replaceDomainPrecisely(processedContent, domainUrl, proxyDir, replaceList);
|
||||
processedContent = replaceDomainPrecisely(processedContent, domainUrl, proxyDir, replaceMap);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -583,16 +418,16 @@ public class HttpProxyController {
|
||||
/**
|
||||
* 精准域名替换
|
||||
*/
|
||||
private String replaceDomainPrecisely(String content, String originalDomain, String proxyDir, List<String> replaceList) {
|
||||
private String replaceDomainPrecisely(String content, String originalDomain,
|
||||
String proxyDir, Map<String, String> replaceMap) {
|
||||
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);
|
||||
for (Map.Entry<String, String> stringEntry : replaceMap.entrySet()) {
|
||||
content = content.replaceAll(stringEntry.getKey(), stringEntry.getValue());
|
||||
}
|
||||
return content.replaceAll(escapedDomain, proxyPath);
|
||||
return content.replaceAll(escapedDomain, "/http-proxy/" + proxyDir);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -729,6 +564,24 @@ public class HttpProxyController {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验ETag是否匹配缓存版本号
|
||||
*/
|
||||
private void validateETagWithCacheVersion(HttpServletRequest servletRequest, HttpHeaders requestHeaders, String proxyConfig) {
|
||||
String eTag = servletRequest.getHeader("If-None-Match");
|
||||
if (eTag != null) {
|
||||
String[] eTagArr = eTag.split(";cache-version=");
|
||||
String cacheHash = DigestUtil.md5DigestAsHex(proxyConfig);
|
||||
if (eTagArr.length == 2 && Objects.equals(eTagArr[1], "\"" + cacheHash + "\"")) {
|
||||
requestHeaders.put("If-None-Match", Collections.singletonList(eTagArr[0]));
|
||||
} else {
|
||||
requestHeaders.remove("If-None-Match");
|
||||
requestHeaders.remove("If-Modified-Since");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 构建请求体
|
||||
*/
|
||||
@@ -757,25 +610,6 @@ public class HttpProxyController {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析路由配置的键值对
|
||||
*/
|
||||
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<>();
|
||||
|
||||
@@ -880,87 +714,95 @@ public class HttpProxyController {
|
||||
}
|
||||
|
||||
/**
|
||||
* 对URL中的QueryString参数值进行URL编码(UTF-8)
|
||||
*
|
||||
* @param originalUrl 原始URL(包含中文等特殊字符的QueryString)
|
||||
* @return 编码后的完整URL
|
||||
* 代理路由配置类
|
||||
* 格式:[key]http://domain.com{/api,/http-proxy/api}{value1,key1}
|
||||
* 说明:{path1,path2} 中的每对都是替换映射,即 path1 会被替换成 path2
|
||||
* {value1,key1} 也是替换映射,即 value1 会被替换成 key1
|
||||
*/
|
||||
public static String encodeUrlQueryParams(String originalUrl) {
|
||||
try {
|
||||
// 空值校验
|
||||
if (originalUrl == null || originalUrl.isEmpty()) {
|
||||
return originalUrl;
|
||||
@SuppressWarnings({"JavadocLinkAsPlainText", "LombokGetterMayBeUsed", "RegExpRedundantEscape"})
|
||||
public static class ProxyRouteConfig {
|
||||
private String route; // 路由键,如 pet
|
||||
private String proxyUrl; // 基础URL,如 http://pet.iqudoo.com
|
||||
private final Map<String, String> replaceMap = new HashMap<>(); // 替换映射表
|
||||
|
||||
/**
|
||||
* 解析路由配置
|
||||
*
|
||||
* @param input 配置行,如 [pet]http://pet.iqudoo.com{/api,/http-proxy/api}{value1,key1}
|
||||
* @return 解析后的路由配置对象
|
||||
*/
|
||||
public static ProxyRouteConfig parse(String input) {
|
||||
ProxyRouteConfig config = new ProxyRouteConfig();
|
||||
if (StringUtils.isBlank(input)) {
|
||||
return config;
|
||||
}
|
||||
|
||||
// 解析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("&");
|
||||
// 正则匹配:[key]http://domain.com{/api,/http-proxy/api}{value1,key1}
|
||||
Pattern pattern = Pattern.compile("\\[(.*?)]\\s*([^{]+)\\s*(?:\\{([^}]*)\\})?\\s*(?:\\{([^}]*)\\})?");
|
||||
Matcher matcher = pattern.matcher(input.trim());
|
||||
if (matcher.find()) {
|
||||
config.route = matcher.group(1).trim();
|
||||
config.proxyUrl = matcher.group(2).trim();
|
||||
// 去除baseUrl末尾的斜杠
|
||||
if (config.proxyUrl.endsWith("/")) {
|
||||
config.proxyUrl = config.proxyUrl.substring(0, config.proxyUrl.length() - 1);
|
||||
}
|
||||
// 解析第一个花括号 {} 中的内容(路径映射对)
|
||||
String firstPairStr = matcher.group(3);
|
||||
if (StringUtils.isNotBlank(firstPairStr)) {
|
||||
parseAndPutToMap(firstPairStr, config.replaceMap);
|
||||
}
|
||||
// 解析第二个花括号 {} 中的内容(替换映射对)
|
||||
String secondPairStr = matcher.group(4);
|
||||
if (StringUtils.isNotBlank(secondPairStr)) {
|
||||
parseAndPutToMap(secondPairStr, config.replaceMap);
|
||||
}
|
||||
encodedQuery.append(entry.getKey())
|
||||
.append("=")
|
||||
.append(entry.getValue());
|
||||
}
|
||||
return encodedQuery.toString();
|
||||
} catch (Throwable throwable) {
|
||||
return queryString;
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析键值对并放入Map
|
||||
* 格式:key1,value1,key2,value2 或 /api,/http-proxy/api
|
||||
* 注意:必须是成对出现
|
||||
*/
|
||||
private static void parseAndPutToMap(String pairStr, Map<String, String> map) {
|
||||
String[] pairs = pairStr.split(",");
|
||||
// 必须是偶数个元素才能组成键值对
|
||||
for (int i = 0; i < pairs.length - 1; i += 2) {
|
||||
String key = pairs[i].trim();
|
||||
String value = pairs[i + 1].trim();
|
||||
if (StringUtils.isNotBlank(key) && StringUtils.isNotBlank(value)) {
|
||||
map.put(key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public String getRoute() {
|
||||
return route;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取完整的代理目标URL
|
||||
*
|
||||
* @param proxyPath 代理路径
|
||||
* @return 完整的URL
|
||||
*/
|
||||
public String getFullTargetUrl(String proxyPath) {
|
||||
return getDomainUrl() + (proxyPath.isEmpty() ? "" : (proxyPath.startsWith("/") ? proxyPath : "/" + proxyPath));
|
||||
}
|
||||
|
||||
public String getDomainUrl() {
|
||||
String url = proxyUrl;
|
||||
if (url.endsWith("/")) {
|
||||
url = url.substring(0, url.length() - 1);
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
public Map<String, String> getReplaceMap() {
|
||||
return replaceMap;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -14,8 +14,8 @@ import lombok.Data;
|
||||
sort = 999998)
|
||||
public class SystemGlobalHttpProxyConfig {
|
||||
|
||||
@SettingField(label = "域名代理路由",
|
||||
tips = "域名代理配置,多域名时换行,格式如下:[访问路径]代理域名,对应{代理域名}{资源PATH}的请求路径为{serverUrl}/domain-proxy/{访问路径}{资源PATH}",
|
||||
@SettingField(label = "代理路由配置",
|
||||
tips = "多条配置时换行分隔,每行的格式如下:[路由键]基础URL{替换规则1,替换内容1}{替换规则2,替换内容2}",
|
||||
type = IModuleSetting.SettingFieldType.TYPE_TEXTAREA,
|
||||
rows = 6)
|
||||
private String domainProxyRouterMap = "";
|
||||
|
||||
Reference in New Issue
Block a user