feat(品牌服务): 内部接口按 uid 供用户代理提取链接;代理检测改真实提取+转发探测

- 新增 /api/internal/user-proxy(X-Internal-Token 自校验,18080 公网可达必须校验),
  供主机 A 品牌检测服务(15126)按 uid 取用户代理提取链接,未配置返回空串
- 代理检测修复"假通过":旧实现把提取链接直接当静态代理交给 HttpClientPool,
  链接无显式端口解析失败静默回退直连,检测到的是自家站点直连响应(40 个用户
  3-39ms 全 301 假通过);现在先真实提取一次(余额不足/解析失败如实报),
  再经提取到的代理请求自家域名,兼容旧静态代理地址
This commit is contained in:
2026-09-13 21:21:48 +08:00
parent fe052879bd
commit 1066625078
4 changed files with 310 additions and 8 deletions
@@ -0,0 +1,76 @@
package com.nanri.aiimage.modules.usersecret.controller;
import com.nanri.aiimage.common.api.ApiResponse;
import com.nanri.aiimage.common.exception.BusinessException;
import com.nanri.aiimage.modules.admin.support.AdminAuthSupport;
import com.nanri.aiimage.modules.usersecret.service.UserApiSecretService;
import jakarta.servlet.http.HttpServletRequest;
import org.junit.jupiter.api.Test;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
class InternalUserProxyControllerTest {
private final UserApiSecretService userApiSecretService = mock(UserApiSecretService.class);
private final AdminAuthSupport adminAuthSupport = mock(AdminAuthSupport.class);
private InternalUserProxyController newController() {
return new InternalUserProxyController(userApiSecretService, adminAuthSupport);
}
private HttpServletRequest request() {
return mock(HttpServletRequest.class);
}
@Test
void rejectsRequestWithoutTrustedInternalTokenAndSkipsLookup() {
when(adminAuthSupport.isTrustedInternalToken(any())).thenReturn(false);
assertThatThrownBy(() -> newController().userProxy(request(), 1098L))
.isInstanceOf(BusinessException.class)
.hasMessageContaining("未授权");
verify(userApiSecretService, never()).findPlainValue(anyLong(), anyString());
}
@Test
void returnsProxyUrlForConfiguredUser() {
when(adminAuthSupport.isTrustedInternalToken(any())).thenReturn(true);
when(userApiSecretService.findPlainValue(1098L, "proxy")).thenReturn("http://p.example/x");
ApiResponse<Map<String, Object>> resp = newController().userProxy(request(), 1098L);
assertThat(resp.isSuccess()).isTrue();
assertThat(resp.getData()).containsEntry("userId", 1098L);
assertThat(resp.getData()).containsEntry("proxyUrl", "http://p.example/x");
}
@Test
void returnsEmptyUrlForUnconfiguredUser() {
when(adminAuthSupport.isTrustedInternalToken(any())).thenReturn(true);
when(userApiSecretService.findPlainValue(1098L, "proxy")).thenReturn("");
ApiResponse<Map<String, Object>> resp = newController().userProxy(request(), 1098L);
assertThat(resp.getData()).containsEntry("proxyUrl", "");
}
@Test
void skipsLookupForMissingOrInvalidUserId() {
when(adminAuthSupport.isTrustedInternalToken(any())).thenReturn(true);
InternalUserProxyController controller = newController();
assertThat(controller.userProxy(request(), null).getData()).containsEntry("proxyUrl", "");
assertThat(controller.userProxy(request(), 0L).getData()).containsEntry("proxyUrl", "");
verify(userApiSecretService, never()).findPlainValue(any(), anyString());
}
}
@@ -136,4 +136,56 @@ class UserApiSecretCheckServiceTest {
assertThat(outcome.status()).isEqualTo(UserApiSecretCheckService.STATUS_ERROR);
assertThat(outcome.code()).isEqualTo(UserApiSecretCheckService.CODE_SERVER_ERROR);
}
// ===== 代理配置探测(提取链接语义;2026-09-13 修复「直连自家站点假通过」)=====
@Test
void looksLikeStaticProxyOnlyForHostPortWithoutPathOrQuery() {
assertThat(UserApiSecretCheckService.looksLikeStaticProxy("http://1.2.3.4:8080")).isTrue();
assertThat(UserApiSecretCheckService.looksLikeStaticProxy("http://u:p@1.2.3.4:8080")).isTrue();
assertThat(UserApiSecretCheckService.looksLikeStaticProxy("https://1.2.3.4:8080/")).isTrue();
// 提取链接(带 path/query 或无显式端口)不能按静态代理直连探测
assertThat(UserApiSecretCheckService.looksLikeStaticProxy(
"https://api.jikip.com/ip-get?num=1&mode=2&key=xx")).isFalse();
assertThat(UserApiSecretCheckService.looksLikeStaticProxy("https://api.jikip.com")).isFalse();
assertThat(UserApiSecretCheckService.looksLikeStaticProxy("")).isFalse();
}
@Test
void extractProxyUrlPrefersJsonWithCredentials() {
String body = "{\"data\":{\"list\":[{\"ip\":\"218.95.39.19\",\"port\":\"15902\","
+ "\"account\":\"acc1\",\"password\":\"pw1\"}]},\"code\":0,\"message\":\"\"}";
assertThat(service.extractProxyUrlFromBody(body))
.isEqualTo("http://acc1:pw1@218.95.39.19:15902");
}
@Test
void extractProxyUrlWithoutCredentialsWhenWhitelistMode() {
String body = "{\"data\":{\"list\":[{\"ip\":\"1.2.3.4\",\"port\":8080}]}}";
assertThat(service.extractProxyUrlFromBody(body)).isEqualTo("http://1.2.3.4:8080");
}
@Test
void extractProxyUrlFallsBackToPlainText() {
assertThat(service.extractProxyUrlFromBody("1.2.3.4:8080")).isEqualTo("http://1.2.3.4:8080");
}
@Test
void extractProxyUrlReturnsNullWhenNoIpPresent() {
assertThat(service.extractProxyUrlFromBody("{\"code\":0,\"data\":null}")).isNull();
assertThat(service.extractProxyUrlFromBody("")).isNull();
assertThat(service.extractProxyUrlFromBody(null)).isNull();
}
@Test
void insufficientBalanceDetectedByMessageOrCode() {
assertThat(service.isInsufficientBalance(
"{\"code\":-1,\"data\":null,\"status\":200,\"message\":\"余额不足\"}")).isTrue();
assertThat(service.isInsufficientBalance("余额不足")).isTrue();
assertThat(service.isInsufficientBalance(
"{\"data\":{\"list\":[{\"ip\":\"1.2.3.4\",\"port\":8080}]},\"code\":0}")).isFalse();
assertThat(service.isInsufficientBalance("")).isFalse();
}
}