89fbc6437a
- 新增 ApiEndpoint/ApiParam 实体和 Mapper - 新增 DTO 层(分页查询请求、列表项、详情项、参数项、代理测试请求/响应) - 新增 ApiEndpointService 含 OpenAPI JSON 解析、\ 展开(最大10层)、分页查询 - 新增 ApiEndpointSyncRunner 启动时异步同步 - 新增 ApiEndpointController 分页/详情/手动同步接口 - 新增 ApiTestProxyController 代理测试接口(SSRF 防护) - 前端新增接口列表页、详情弹窗(含测试面板、Token 来源选择) - 前端新增菜单和路由
142 lines
5.8 KiB
Java
142 lines
5.8 KiB
Java
package com.emotion.controller;
|
|
|
|
import com.emotion.common.Result;
|
|
import com.emotion.dto.request.ApiTestProxyRequest;
|
|
import com.emotion.dto.response.ApiTestProxyResponse;
|
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
import io.swagger.v3.oas.annotations.Operation;
|
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
|
import org.slf4j.Logger;
|
|
import org.slf4j.LoggerFactory;
|
|
import org.springframework.beans.factory.annotation.Autowired;
|
|
import org.springframework.beans.factory.annotation.Value;
|
|
import org.springframework.http.*;
|
|
import org.springframework.web.bind.annotation.*;
|
|
import org.springframework.web.client.HttpClientErrorException;
|
|
import org.springframework.web.client.HttpServerErrorException;
|
|
import org.springframework.web.client.ResourceAccessException;
|
|
import org.springframework.web.client.RestTemplate;
|
|
|
|
import javax.validation.Valid;
|
|
import java.net.SocketTimeoutException;
|
|
import java.util.HashMap;
|
|
import java.util.Map;
|
|
|
|
/**
|
|
* 接口代理测试控制器
|
|
* 仅允许转发到 /api/* 路径,避免 SSRF
|
|
*
|
|
* @author Peanut
|
|
* @date 2026-05-23
|
|
*/
|
|
@RestController
|
|
@RequestMapping("/admin/endpoint")
|
|
@Tag(name = "接口管理", description = "接口测试代理")
|
|
public class ApiTestProxyController {
|
|
|
|
private static final Logger log = LoggerFactory.getLogger(ApiTestProxyController.class);
|
|
private static final int DEFAULT_TIMEOUT = 30;
|
|
private static final int MAX_RAW_BODY_LENGTH = 2000;
|
|
|
|
@Value("${server.port:19089}")
|
|
private int serverPort;
|
|
|
|
@Autowired
|
|
private RestTemplate restTemplate;
|
|
|
|
@Autowired
|
|
private ObjectMapper objectMapper;
|
|
|
|
@Operation(summary = "代理测试请求", description = "转发请求到本地后端并返回响应")
|
|
@PostMapping("/test")
|
|
public Result<ApiTestProxyResponse> test(@Valid @RequestBody ApiTestProxyRequest request) {
|
|
if (!request.getPath().startsWith("/api/")) {
|
|
return Result.error("仅允许代理 /api/* 路径的请求");
|
|
}
|
|
|
|
String url = "http://127.0.0.1:" + serverPort + request.getPath();
|
|
int timeout = request.getTimeoutSeconds() != null ? request.getTimeoutSeconds() : DEFAULT_TIMEOUT;
|
|
|
|
HttpHeaders headers = new HttpHeaders();
|
|
headers.setContentType(MediaType.APPLICATION_JSON);
|
|
if (request.getHeaders() != null) {
|
|
for (Map.Entry<String, String> entry : request.getHeaders().entrySet()) {
|
|
headers.set(entry.getKey(), entry.getValue());
|
|
}
|
|
}
|
|
|
|
// Append query params to URL if present
|
|
if (request.getParams() != null && !request.getParams().isEmpty()) {
|
|
StringBuilder sb = new StringBuilder(url);
|
|
sb.append("?");
|
|
for (Map.Entry<String, String> entry : request.getParams().entrySet()) {
|
|
sb.append(entry.getKey()).append("=").append(entry.getValue()).append("&");
|
|
}
|
|
url = sb.substring(0, sb.length() - 1);
|
|
}
|
|
|
|
Object body = null;
|
|
if (request.getBody() != null && !request.getBody().isBlank()) {
|
|
try {
|
|
body = objectMapper.readValue(request.getBody(), Object.class);
|
|
} catch (Exception e) {
|
|
return Result.error("请求体 JSON 格式错误: " + e.getMessage());
|
|
}
|
|
}
|
|
|
|
HttpMethod httpMethod = HttpMethod.valueOf(request.getMethod().toUpperCase());
|
|
HttpEntity<Object> entity = new HttpEntity<>(body, headers);
|
|
|
|
long startTime = System.currentTimeMillis();
|
|
ApiTestProxyResponse response = new ApiTestProxyResponse();
|
|
|
|
try {
|
|
ResponseEntity<String> rawResponse = restTemplate.exchange(url, httpMethod, entity, String.class);
|
|
long duration = System.currentTimeMillis() - startTime;
|
|
|
|
response.setStatus(rawResponse.getStatusCodeValue());
|
|
response.setDuration(duration);
|
|
|
|
try {
|
|
response.setBody(objectMapper.readValue(rawResponse.getBody(), Object.class));
|
|
} catch (Exception e) {
|
|
String rawBody = rawResponse.getBody();
|
|
if (rawBody != null && rawBody.length() > MAX_RAW_BODY_LENGTH) {
|
|
rawBody = rawBody.substring(0, MAX_RAW_BODY_LENGTH) + "\n... (已截断)";
|
|
}
|
|
if (rawBody != null) {
|
|
rawBody = rawBody.replace("<", "<").replace(">", ">");
|
|
}
|
|
response.setRawBody(rawBody);
|
|
}
|
|
|
|
Map<String, String> respHeaders = new HashMap<>();
|
|
for (String key : rawResponse.getHeaders().keySet()) {
|
|
respHeaders.put(key, rawResponse.getHeaders().getFirst(key));
|
|
}
|
|
response.setHeaders(respHeaders);
|
|
|
|
return Result.success(response);
|
|
|
|
} catch (ResourceAccessException e) {
|
|
long duration = System.currentTimeMillis() - startTime;
|
|
if (e.getCause() instanceof SocketTimeoutException) {
|
|
return Result.error("代理请求超时(" + timeout + "s),目标接口可能响应过慢或不可达");
|
|
}
|
|
return Result.error("代理请求失败: " + e.getMessage());
|
|
} catch (HttpClientErrorException | HttpServerErrorException e) {
|
|
long duration = System.currentTimeMillis() - startTime;
|
|
response.setStatus(e.getStatusCode().value());
|
|
response.setDuration(duration);
|
|
String rawBody = e.getResponseBodyAsString();
|
|
if (rawBody.length() > MAX_RAW_BODY_LENGTH) {
|
|
rawBody = rawBody.substring(0, MAX_RAW_BODY_LENGTH) + "\n... (已截断)";
|
|
}
|
|
response.setRawBody(rawBody.replace("<", "<").replace(">", ">"));
|
|
return Result.success(response);
|
|
} catch (Exception e) {
|
|
return Result.error("代理请求失败: " + e.getMessage());
|
|
}
|
|
}
|
|
}
|