Files
happy-life-star/backend-single/src/main/java/com/emotion/controller/ApiTestProxyController.java
T
peanut 89fbc6437a feat: 添加接口管理功能(后端OpenAPI解析入库+前端列表/详情/测试)
- 新增 ApiEndpoint/ApiParam 实体和 Mapper
- 新增 DTO 层(分页查询请求、列表项、详情项、参数项、代理测试请求/响应)
- 新增 ApiEndpointService 含 OpenAPI JSON 解析、\ 展开(最大10层)、分页查询
- 新增 ApiEndpointSyncRunner 启动时异步同步
- 新增 ApiEndpointController 分页/详情/手动同步接口
- 新增 ApiTestProxyController 代理测试接口(SSRF 防护)
- 前端新增接口列表页、详情弹窗(含测试面板、Token 来源选择)
- 前端新增菜单和路由
2026-05-23 18:21:07 +08:00

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("<", "&lt;").replace(">", "&gt;");
}
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("<", "&lt;").replace(">", "&gt;"));
return Result.success(response);
} catch (Exception e) {
return Result.error("代理请求失败: " + e.getMessage());
}
}
}