fix: 调用API时返回了 HTML 登录页

This commit is contained in:
叁玖领域 2026-06-09 12:08:22 +08:00
parent f44c3eeae6
commit 23ec8a8803
2 changed files with 88 additions and 28 deletions

View File

@ -9,6 +9,9 @@ import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.HandlerInterceptor;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
@Component @Component
public class ApiKeyInterceptor implements HandlerInterceptor { public class ApiKeyInterceptor implements HandlerInterceptor {
@ -17,8 +20,11 @@ public class ApiKeyInterceptor implements HandlerInterceptor {
@Override @Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
// 1. 优先检查 API KeyHeader: X-API-Key Query: apiKey // 1. 优先检查 API KeyHeader: X-API-Key / X-API-TOKEN Query: apiKey
String reqApiKey = request.getHeader("X-API-Key"); String reqApiKey = request.getHeader("X-API-Key");
if (!StringUtils.hasText(reqApiKey)) {
reqApiKey = request.getHeader("X-API-TOKEN");
}
if (!StringUtils.hasText(reqApiKey)) { if (!StringUtils.hasText(reqApiKey)) {
reqApiKey = request.getParameter("apiKey"); reqApiKey = request.getParameter("apiKey");
} }
@ -35,6 +41,32 @@ public class ApiKeyInterceptor implements HandlerInterceptor {
return true; return true;
} }
// 3. 鉴权失败 API 请求返回 JSON浏览器请求抛异常跳转登录页
if (isApiRequest(request)) {
writeJsonResponse(response, 403, "{\"code\":403,\"msg\":\"无权限API Key 无效或未提供\",\"data\":null}");
return false;
}
throw new UnauthorizedException("redirect:/admin/login?error=please login first"); throw new UnauthorizedException("redirect:/admin/login?error=please login first");
} }
private boolean isApiRequest(HttpServletRequest request) {
String header = request.getHeader("X-API-Key");
if (!StringUtils.hasText(header)) {
header = request.getHeader("X-API-TOKEN");
}
return StringUtils.hasText(header)
|| StringUtils.hasText(request.getParameter("apiKey"))
|| request.getRequestURI().startsWith("/api/");
}
private void writeJsonResponse(HttpServletResponse response, int status, String body) {
response.setStatus(status);
response.setContentType("application/json");
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
try {
response.getWriter().write(body);
response.getWriter().flush();
} catch (IOException ignored) {
}
}
} }

View File

@ -28,7 +28,7 @@ class ApiKeyInterceptorTest {
response = new MockHttpServletResponse(); response = new MockHttpServletResponse();
} }
// ==================== API Key Header ==================== // ==================== X-API-Key Header ====================
@Test @Test
void validApiKeyInHeader_shouldPass() { void validApiKeyInHeader_shouldPass() {
@ -40,20 +40,35 @@ class ApiKeyInterceptorTest {
} }
@Test @Test
void invalidApiKeyInHeader_shouldThrow() { void invalidApiKeyInHeader_shouldReturnJson403() {
request.addHeader("X-API-Key", "wrong-key"); request.addHeader("X-API-Key", "wrong-key");
assertThatThrownBy(() -> interceptor.preHandle(request, response, null)) boolean result = interceptor.preHandle(request, response, null);
.isInstanceOf(UnauthorizedException.class);
assertThat(result).isFalse();
assertThat(response.getStatus()).isEqualTo(403);
assertThat(response.getContentType()).startsWith("application/json");
}
// ==================== X-API-TOKEN Header ====================
@Test
void validApiTokenHeader_shouldPass() {
request.addHeader("X-API-TOKEN", VALID_API_KEY);
boolean result = interceptor.preHandle(request, response, null);
assertThat(result).isTrue();
} }
@Test @Test
void emptyApiKeyInHeader_shouldFallbackToSession() { void invalidApiTokenHeader_shouldReturnJson403() {
request.addHeader("X-API-Key", ""); request.addHeader("X-API-TOKEN", "wrong-token");
// no session, should throw boolean result = interceptor.preHandle(request, response, null);
assertThatThrownBy(() -> interceptor.preHandle(request, response, null))
.isInstanceOf(UnauthorizedException.class); assertThat(result).isFalse();
assertThat(response.getStatus()).isEqualTo(403);
} }
// ==================== API Key Query Param ==================== // ==================== API Key Query Param ====================
@ -68,11 +83,25 @@ class ApiKeyInterceptorTest {
} }
@Test @Test
void invalidApiKeyInQueryParam_shouldThrow() { void invalidApiKeyInQueryParam_shouldReturnJson403() {
request.setParameter("apiKey", "wrong-key"); request.setParameter("apiKey", "wrong-key");
assertThatThrownBy(() -> interceptor.preHandle(request, response, null)) boolean result = interceptor.preHandle(request, response, null);
.isInstanceOf(UnauthorizedException.class);
assertThat(result).isFalse();
assertThat(response.getStatus()).isEqualTo(403);
}
// ==================== Priority ====================
@Test
void apiKeyHeaderTakesPriorityOverTokenHeader() {
request.addHeader("X-API-Key", VALID_API_KEY);
request.addHeader("X-API-TOKEN", "wrong-token");
boolean result = interceptor.preHandle(request, response, null);
assertThat(result).isTrue();
} }
@Test @Test
@ -80,13 +109,25 @@ class ApiKeyInterceptorTest {
request.addHeader("X-API-Key", VALID_API_KEY); request.addHeader("X-API-Key", VALID_API_KEY);
request.setParameter("apiKey", "wrong-key"); request.setParameter("apiKey", "wrong-key");
// header should be used and pass
boolean result = interceptor.preHandle(request, response, null); boolean result = interceptor.preHandle(request, response, null);
assertThat(result).isTrue(); assertThat(result).isTrue();
} }
// ==================== Session Fallback ==================== // ==================== API URI path ====================
@Test
void requestToApiPath_withoutAuth_shouldReturnJson403() {
request.setRequestURI("/api/whitelist/list");
boolean result = interceptor.preHandle(request, response, null);
assertThat(result).isFalse();
assertThat(response.getStatus()).isEqualTo(403);
assertThat(response.getContentType()).startsWith("application/json");
}
// ==================== Session Fallback (browser) ====================
@Test @Test
void validAdminSession_shouldPass() { void validAdminSession_shouldPass() {
@ -113,7 +154,6 @@ class ApiKeyInterceptorTest {
void sessionNoAdminAccount_shouldThrow() { void sessionNoAdminAccount_shouldThrow() {
HttpSession session = request.getSession(true); HttpSession session = request.getSession(true);
session.setAttribute("isLoggedIn", true); session.setAttribute("isLoggedIn", true);
// no adminAccount set
assertThatThrownBy(() -> interceptor.preHandle(request, response, null)) assertThatThrownBy(() -> interceptor.preHandle(request, response, null))
.isInstanceOf(UnauthorizedException.class); .isInstanceOf(UnauthorizedException.class);
@ -121,18 +161,6 @@ class ApiKeyInterceptorTest {
@Test @Test
void noSession_noApiKey_shouldThrow() { void noSession_noApiKey_shouldThrow() {
// no session created, no API key set
assertThatThrownBy(() -> interceptor.preHandle(request, response, null))
.isInstanceOf(UnauthorizedException.class);
}
@Test
void nullSession_noApiKey_shouldThrow() {
// explicitly test null session behavior
// MockHttpServletRequest.getSession() returns non-null by default,
// but we test invalid session attributes above which covers this path
assertThatThrownBy(() -> interceptor.preHandle(request, response, null)) assertThatThrownBy(() -> interceptor.preHandle(request, response, null))
.isInstanceOf(UnauthorizedException.class); .isInstanceOf(UnauthorizedException.class);
} }