fix(admin): 补齐 /api/admin 方法级鉴权并加兜底过滤器

ShopKey/SkipPriceAsin/QueryAsin/ShopManage/ProductCategory 五个 controller
此前无任何 require* 鉴权,SecurityConfig 全局 permitAll 且 nginx 无 ACL,
公网 18080 上可匿名读写店铺密钥等敏感数据。统一补 requireAdminOrInternal;
其中 ShopManage/SkipPrice/QueryAsin 原先信任调用方传入 operatorId/superAdmin,
改为从已认证 principal 推导,消除传 superAdmin=true 越权扩大数据范围的问题。

新增 AdminApiGuardFilter 对 /api/admin/** 兜底要求有效身份(JWT 或内部令牌),
防止后续新增端点遗漏鉴权再裸奔;支持 admin-guard-enabled/exempt-prefixes 配置。
This commit is contained in:
2026-09-04 10:21:12 +08:00
parent 1474f69c73
commit e5909c9977
7 changed files with 436 additions and 116 deletions
@@ -0,0 +1,125 @@
package com.nanri.aiimage.config;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.nanri.aiimage.common.exception.BusinessException;
import com.nanri.aiimage.modules.admin.support.AdminAuthSupport;
import com.nanri.aiimage.modules.permission.model.entity.AdminUserEntity;
import org.junit.jupiter.api.Test;
import org.springframework.mock.web.MockFilterChain;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.test.util.ReflectionTestUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* /api/admin 兜底鉴权过滤器单测:非 admin 路径放行、OPTIONS/豁免前缀放行、
* 匿名拒绝返回与全局一致的 401 体、紧急开关可整体关闭。
*/
class AdminApiGuardFilterTest {
private final ObjectMapper objectMapper = new ObjectMapper();
private AdminApiGuardFilter newFilter(AdminAuthSupport adminAuthSupport, boolean enabled, String exemptPrefixes) {
AdminApiGuardFilter filter = new AdminApiGuardFilter(adminAuthSupport, objectMapper);
ReflectionTestUtils.setField(filter, "enabled", enabled);
ReflectionTestUtils.setField(filter, "exemptPrefixes", exemptPrefixes);
return filter;
}
@Test
void nonAdminApiPathIsNotGuarded() throws Exception {
AdminAuthSupport authSupport = mock(AdminAuthSupport.class);
AdminApiGuardFilter filter = newFilter(authSupport, true, "");
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/admin.html");
MockHttpServletResponse response = new MockHttpServletResponse();
MockFilterChain chain = new MockFilterChain();
filter.doFilter(request, response, chain);
verify(authSupport, never()).requireUserOrInternal(any());
assertThat(chain.getRequest()).isNotNull();
}
@Test
void validIdentityPassesThrough() throws Exception {
AdminAuthSupport authSupport = mock(AdminAuthSupport.class);
when(authSupport.requireUserOrInternal(any())).thenReturn(new AdminUserEntity());
AdminApiGuardFilter filter = newFilter(authSupport, true, "");
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/api/admin/users");
MockHttpServletResponse response = new MockHttpServletResponse();
MockFilterChain chain = new MockFilterChain();
filter.doFilter(request, response, chain);
assertThat(chain.getRequest()).isNotNull();
assertThat(response.getContentAsString()).isEmpty();
}
@Test
void anonymousAdminApiRequestRejectedWith401Body() throws Exception {
AdminAuthSupport authSupport = mock(AdminAuthSupport.class);
when(authSupport.requireUserOrInternal(any()))
.thenThrow(new BusinessException(401, "未登录"));
AdminApiGuardFilter filter = newFilter(authSupport, true, "");
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/api/admin/shop-keys");
MockHttpServletResponse response = new MockHttpServletResponse();
MockFilterChain chain = new MockFilterChain();
filter.doFilter(request, response, chain);
JsonNode body = objectMapper.readTree(response.getContentAsString());
assertThat(body.path("success").asBoolean()).isFalse();
assertThat(body.path("code").asInt()).isEqualTo(401);
assertThat(chain.getRequest()).isNull();
}
@Test
void optionsPreflightIsExempt() throws Exception {
AdminAuthSupport authSupport = mock(AdminAuthSupport.class);
AdminApiGuardFilter filter = newFilter(authSupport, true, "");
MockHttpServletRequest request = new MockHttpServletRequest("OPTIONS", "/api/admin/shop-keys");
MockHttpServletResponse response = new MockHttpServletResponse();
MockFilterChain chain = new MockFilterChain();
filter.doFilter(request, response, chain);
verify(authSupport, never()).requireUserOrInternal(any());
assertThat(chain.getRequest()).isNotNull();
}
@Test
void exemptPrefixPassesThrough() throws Exception {
AdminAuthSupport authSupport = mock(AdminAuthSupport.class);
AdminApiGuardFilter filter = newFilter(authSupport, true, "/api/admin/shop-credential-checks");
MockHttpServletRequest request =
new MockHttpServletRequest("GET", "/api/admin/shop-credential-checks/poll");
MockHttpServletResponse response = new MockHttpServletResponse();
MockFilterChain chain = new MockFilterChain();
filter.doFilter(request, response, chain);
verify(authSupport, never()).requireUserOrInternal(any());
assertThat(chain.getRequest()).isNotNull();
}
@Test
void disabledGuardPassesEverything() throws Exception {
AdminAuthSupport authSupport = mock(AdminAuthSupport.class);
AdminApiGuardFilter filter = newFilter(authSupport, false, "");
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/api/admin/shop-keys");
MockHttpServletResponse response = new MockHttpServletResponse();
MockFilterChain chain = new MockFilterChain();
filter.doFilter(request, response, chain);
verify(authSupport, never()).requireUserOrInternal(any());
assertThat(chain.getRequest()).isNotNull();
}
}