feat: 新增 /api/admin/httpProxy 环回代理端点,供 B 端接口测试直连调用(自动附加 Basic/X-Admin-Token,路径白名单限 /order 与 /api/admin,读超时 120s)
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,8 @@ package com.hospital.front.api.admin;
|
|||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.lang.management.ManagementFactory;
|
import java.lang.management.ManagementFactory;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.util.Base64;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
@@ -280,7 +282,123 @@ public class AdminApi {
|
|||||||
return Result.success();
|
return Result.success();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ================= 5. 接口清单(供 B 端接口测试工具加载) =================
|
// ================= 5. 接口代理(供 B 端接口测试工具直连调用) =================
|
||||||
|
|
||||||
|
/** httpProxy 请求体(Solon 自动绑定 JSON 请求体) */
|
||||||
|
public static class HttpProxyReq {
|
||||||
|
/** HTTP 方法:GET/POST/PUT/DELETE */
|
||||||
|
public String method = "GET";
|
||||||
|
/** 接口路径,如 /order/getItemList */
|
||||||
|
public String path;
|
||||||
|
/** query 参数(拼到 URL 上) */
|
||||||
|
public Map<String, String> query;
|
||||||
|
/** 请求体字符串(POST/PUT 时使用) */
|
||||||
|
public String body;
|
||||||
|
/** 额外请求头 */
|
||||||
|
public Map<String, String> headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 代理专用 HttpClient:环回调用本机接口,读超时 120s 适配大接口 */
|
||||||
|
private final java.net.http.HttpClient proxyHttp = java.net.http.HttpClient.newBuilder()
|
||||||
|
.connectTimeout(java.time.Duration.ofSeconds(5))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP 代理:在 main 进程内环回调用本机接口,原样透传响应。
|
||||||
|
*
|
||||||
|
* B 端接口测试工具经此代理访问业务接口,免去 Agent WS 通道倒手
|
||||||
|
* (WS 通道有 2MB 消息上限与 60s 等待超时,大接口会超时/掉线)。
|
||||||
|
* 自动按 path 附加鉴权头:/order/** → Basic(order.basic 配置),
|
||||||
|
* /api/admin/** → X-Admin-Token。
|
||||||
|
*/
|
||||||
|
@Mapping(value = "/httpProxy", method = {MethodType.POST})
|
||||||
|
public Result<Map<String, Object>> httpProxy(Context ctx, HttpProxyReq req) {
|
||||||
|
checkToken(ctx);
|
||||||
|
// 参数校验与路径白名单:只允许代理本机 /order/** 与 /api/admin/**
|
||||||
|
if (req == null || req.path == null || req.path.isEmpty()) {
|
||||||
|
throw BusinessException.paramMissing("path 不能为空");
|
||||||
|
}
|
||||||
|
String path = req.path.trim();
|
||||||
|
if (!path.startsWith("/order/") && !path.startsWith("/api/admin/")) {
|
||||||
|
throw BusinessException.businessFailed("仅允许代理 /order/** 与 /api/admin/** 路径:" + path);
|
||||||
|
}
|
||||||
|
// 拼接环回 URL:server.port + path + query
|
||||||
|
String port = Solon.cfg().get("server.port", "8080");
|
||||||
|
StringBuilder url = new StringBuilder("http://127.0.0.1:").append(port).append(path);
|
||||||
|
if (req.query != null && !req.query.isEmpty()) {
|
||||||
|
StringBuilder qs = new StringBuilder();
|
||||||
|
for (Map.Entry<String, String> e : req.query.entrySet()) {
|
||||||
|
if (e.getKey() == null || e.getValue() == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (qs.length() > 0) {
|
||||||
|
qs.append('&');
|
||||||
|
}
|
||||||
|
qs.append(java.net.URLEncoder.encode(e.getKey(), StandardCharsets.UTF_8))
|
||||||
|
.append('=')
|
||||||
|
.append(java.net.URLEncoder.encode(e.getValue(), StandardCharsets.UTF_8));
|
||||||
|
}
|
||||||
|
if (qs.length() > 0) {
|
||||||
|
url.append('?').append(qs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 构建请求:按 path 附加鉴权头
|
||||||
|
java.net.http.HttpRequest.Builder rb = java.net.http.HttpRequest.newBuilder(URI_COMPAT.apply(url.toString()));
|
||||||
|
String m = req.method == null ? "GET" : req.method.toUpperCase();
|
||||||
|
java.net.http.HttpRequest.BodyPublisher pub = req.body == null || req.body.isEmpty()
|
||||||
|
? java.net.http.HttpRequest.BodyPublishers.noBody()
|
||||||
|
: java.net.http.HttpRequest.BodyPublishers.ofString(req.body, StandardCharsets.UTF_8);
|
||||||
|
switch (m) {
|
||||||
|
case "POST" -> rb.POST(pub);
|
||||||
|
case "PUT" -> rb.PUT(pub);
|
||||||
|
case "DELETE" -> rb.DELETE();
|
||||||
|
default -> rb.GET();
|
||||||
|
}
|
||||||
|
if (path.startsWith("/order/")) {
|
||||||
|
String basic = Solon.cfg().get("order.basic", "");
|
||||||
|
boolean authFlag = !"false".equals(Solon.cfg().get("order.authFlag", "true"));
|
||||||
|
if (authFlag && basic != null && !basic.isEmpty()) {
|
||||||
|
// 对齐 OrderApi checkBasic 的校验格式:Basic + Base64(user:password)
|
||||||
|
rb.header("Authorization", "Basic "
|
||||||
|
+ Base64.getEncoder().encodeToString(basic.getBytes(StandardCharsets.UTF_8)));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
rb.header("X-Admin-Token", Solon.cfg().get("admin.token", ""));
|
||||||
|
}
|
||||||
|
if (req.headers != null) {
|
||||||
|
for (Map.Entry<String, String> e : req.headers.entrySet()) {
|
||||||
|
if (e.getKey() != null && e.getValue() != null) {
|
||||||
|
rb.header(e.getKey(), e.getValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rb.timeout(java.time.Duration.ofSeconds(120));
|
||||||
|
// 执行并透传
|
||||||
|
try {
|
||||||
|
java.net.http.HttpResponse<String> resp = proxyHttp.send(rb.build(),
|
||||||
|
java.net.http.HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
|
||||||
|
Map<String, Object> out = new LinkedHashMap<>();
|
||||||
|
out.put("status", resp.statusCode());
|
||||||
|
out.put("body", resp.body());
|
||||||
|
return Result.success(out);
|
||||||
|
} catch (java.io.IOException e) {
|
||||||
|
throw BusinessException.businessFailed("环回调用失败(本机 " + path + "):" + e.getMessage());
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
throw BusinessException.businessFailed("环回调用被中断:" + path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** URI 构建容错(URL 已校验,异常时给出可读错误) */
|
||||||
|
private static final java.util.function.Function<String, URI> URI_COMPAT = s -> {
|
||||||
|
try {
|
||||||
|
return URI.create(s);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw BusinessException.paramMissing("非法 URL:" + s);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ================= 6. 接口清单(供 B 端接口测试工具加载) =================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 返回 /order 业务接口元数据清单。
|
* 返回 /order 业务接口元数据清单。
|
||||||
|
|||||||
Reference in New Issue
Block a user