diff --git a/src/main/java/com/linearpast/minecraftmanager/interceptor/ApiKeyInterceptor.java b/src/main/java/com/linearpast/minecraftmanager/interceptor/ApiKeyInterceptor.java index 11a272f..9545a45 100644 --- a/src/main/java/com/linearpast/minecraftmanager/interceptor/ApiKeyInterceptor.java +++ b/src/main/java/com/linearpast/minecraftmanager/interceptor/ApiKeyInterceptor.java @@ -9,6 +9,9 @@ import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; import org.springframework.web.servlet.HandlerInterceptor; +import java.io.IOException; +import java.nio.charset.StandardCharsets; + @Component public class ApiKeyInterceptor implements HandlerInterceptor { @@ -17,8 +20,11 @@ public class ApiKeyInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { - // 1. 优先检查 API Key(Header: X-API-Key 或 Query: apiKey) + // 1. 优先检查 API Key(Header: X-API-Key / X-API-TOKEN 或 Query: apiKey) String reqApiKey = request.getHeader("X-API-Key"); + if (!StringUtils.hasText(reqApiKey)) { + reqApiKey = request.getHeader("X-API-TOKEN"); + } if (!StringUtils.hasText(reqApiKey)) { reqApiKey = request.getParameter("apiKey"); } @@ -35,6 +41,32 @@ public class ApiKeyInterceptor implements HandlerInterceptor { 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"); } + + 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) { + } + } } diff --git a/src/test/java/com/linearpast/minecraftmanager/interceptor/ApiKeyInterceptorTest.java b/src/test/java/com/linearpast/minecraftmanager/interceptor/ApiKeyInterceptorTest.java index dc403d5..acfb157 100644 --- a/src/test/java/com/linearpast/minecraftmanager/interceptor/ApiKeyInterceptorTest.java +++ b/src/test/java/com/linearpast/minecraftmanager/interceptor/ApiKeyInterceptorTest.java @@ -28,7 +28,7 @@ class ApiKeyInterceptorTest { response = new MockHttpServletResponse(); } - // ==================== API Key Header ==================== + // ==================== X-API-Key Header ==================== @Test void validApiKeyInHeader_shouldPass() { @@ -40,20 +40,35 @@ class ApiKeyInterceptorTest { } @Test - void invalidApiKeyInHeader_shouldThrow() { + void invalidApiKeyInHeader_shouldReturnJson403() { request.addHeader("X-API-Key", "wrong-key"); - assertThatThrownBy(() -> interceptor.preHandle(request, response, null)) - .isInstanceOf(UnauthorizedException.class); + boolean result = interceptor.preHandle(request, response, null); + + 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 - void emptyApiKeyInHeader_shouldFallbackToSession() { - request.addHeader("X-API-Key", ""); + void invalidApiTokenHeader_shouldReturnJson403() { + request.addHeader("X-API-TOKEN", "wrong-token"); - // no session, should throw - assertThatThrownBy(() -> interceptor.preHandle(request, response, null)) - .isInstanceOf(UnauthorizedException.class); + boolean result = interceptor.preHandle(request, response, null); + + assertThat(result).isFalse(); + assertThat(response.getStatus()).isEqualTo(403); } // ==================== API Key Query Param ==================== @@ -68,11 +83,25 @@ class ApiKeyInterceptorTest { } @Test - void invalidApiKeyInQueryParam_shouldThrow() { + void invalidApiKeyInQueryParam_shouldReturnJson403() { request.setParameter("apiKey", "wrong-key"); - assertThatThrownBy(() -> interceptor.preHandle(request, response, null)) - .isInstanceOf(UnauthorizedException.class); + boolean result = interceptor.preHandle(request, response, null); + + 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 @@ -80,13 +109,25 @@ class ApiKeyInterceptorTest { request.addHeader("X-API-Key", VALID_API_KEY); request.setParameter("apiKey", "wrong-key"); - // header should be used and pass boolean result = interceptor.preHandle(request, response, null); 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 void validAdminSession_shouldPass() { @@ -113,7 +154,6 @@ class ApiKeyInterceptorTest { void sessionNoAdminAccount_shouldThrow() { HttpSession session = request.getSession(true); session.setAttribute("isLoggedIn", true); - // no adminAccount set assertThatThrownBy(() -> interceptor.preHandle(request, response, null)) .isInstanceOf(UnauthorizedException.class); @@ -121,18 +161,6 @@ class ApiKeyInterceptorTest { @Test 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)) .isInstanceOf(UnauthorizedException.class); }