feat(A1/A3): 三个客户端零调用前缀立即无条件收紧
实测桌面端 Python 侧调用面:/api/image-video、/api/task-file-jobs、/api/ziniao 为 0 次调用 (只有带 JWT 的网页端在用),因此从 user-tool-guard-enabled 开关名单移入无条件守卫名单, 不必等客户端铺开即完成收紧;其余 14 个前缀的客户端调用面与令牌携带情况已逐一实测, 仍在开关后面(老客户端不带身份,提前打开会 401)。 新增 2 个守卫测试:开关关闭时这三组前缀对匿名同样 401、带用户令牌仍放行。
This commit is contained in:
@@ -65,10 +65,10 @@ public class AdminApiGuardFilter extends OncePerRequestFilter {
|
|||||||
"/api/price-track",
|
"/api/price-track",
|
||||||
// 2026-09 全维度审查补:以下前缀此前完全在守卫范围之外。
|
// 2026-09 全维度审查补:以下前缀此前完全在守卫范围之外。
|
||||||
// /api/files 匿名可上传(2GB/次,可耗尽临时盘);/api/digital-human 匿名可发布/删除版本;
|
// /api/files 匿名可上传(2GB/次,可耗尽临时盘);/api/digital-human 匿名可发布/删除版本;
|
||||||
// /api/image-video 的 secrets 接口 userId 取自请求体;/api/brand 的 fileUrl 曾可直接请求任意地址。
|
// /api/brand 的 fileUrl 曾可直接请求任意地址(/api/image-video 与 /api/task-file-jobs
|
||||||
|
// 客户端零调用,已移入下方无条件名单)。
|
||||||
"/api/files",
|
"/api/files",
|
||||||
"/api/digital-human",
|
"/api/digital-human",
|
||||||
"/api/image-video",
|
|
||||||
"/api/brand",
|
"/api/brand",
|
||||||
"/api/appearance-patent",
|
"/api/appearance-patent",
|
||||||
"/api/similar-asin",
|
"/api/similar-asin",
|
||||||
@@ -78,10 +78,6 @@ public class AdminApiGuardFilter extends OncePerRequestFilter {
|
|||||||
"/api/shop-match",
|
"/api/shop-match",
|
||||||
"/api/shop-data-crawl",
|
"/api/shop-data-crawl",
|
||||||
"/api/withdraw",
|
"/api/withdraw",
|
||||||
"/api/task-file-jobs",
|
|
||||||
// 2026-09 复核补:该前缀全部接口已在 controller 内 requireAdmin(含可换取员工店铺登录
|
|
||||||
// 令牌的 /shops/open),此处纳入名单是"开关打开后的第二层";开关关闭时行为不变
|
|
||||||
"/api/ziniao",
|
|
||||||
// /api/tasks/{taskId}/interrupted 仅凭 taskId 即可把 RUNNING 任务置为 FAILED,
|
// /api/tasks/{taskId}/interrupted 仅凭 taskId 即可把 RUNNING 任务置为 FAILED,
|
||||||
// 匿名遍历 taskId 就能批量打断线上任务
|
// 匿名遍历 taskId 就能批量打断线上任务
|
||||||
"/api/tasks",
|
"/api/tasks",
|
||||||
@@ -94,6 +90,13 @@ public class AdminApiGuardFilter extends OncePerRequestFilter {
|
|||||||
private static final String[] SELF_SERVICE_PREFIXES = {
|
private static final String[] SELF_SERVICE_PREFIXES = {
|
||||||
"/api/user-secrets",
|
"/api/user-secrets",
|
||||||
"/api/notifications",
|
"/api/notifications",
|
||||||
|
// 2026-09-14:这三组前缀**桌面端 Python 侧零调用**(实测),只有带 JWT 的网页端在用,
|
||||||
|
// 因此不必等客户端铺开即可无条件收紧(其余用户态前缀仍在 user-tool-guard-enabled 开关后面)
|
||||||
|
"/api/image-video",
|
||||||
|
"/api/task-file-jobs",
|
||||||
|
// 其全部接口已在 controller 内 requireAdmin(含可换取员工店铺登录令牌的 /shops/open),
|
||||||
|
// 纳入守卫是"鉴权失败返回统一 401 体"的第二层
|
||||||
|
"/api/ziniao",
|
||||||
// 2026-09 全维度审查补:内部端点此前仅靠 controller 自校验令牌,纳入守卫后
|
// 2026-09 全维度审查补:内部端点此前仅靠 controller 自校验令牌,纳入守卫后
|
||||||
// 不带令牌的请求直接 401(带可信令牌的仍由 doFilterInternal 放行)
|
// 不带令牌的请求直接 401(带可信令牌的仍由 doFilterInternal 放行)
|
||||||
"/api/internal",
|
"/api/internal",
|
||||||
|
|||||||
@@ -321,6 +321,39 @@ class AdminApiGuardFilterTest {
|
|||||||
assertThat(chain.getRequest()).isNotNull();
|
assertThat(chain.getRequest()).isNotNull();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void webOnlyPrefixesAreGuardedEvenWhenSwitchOff() throws Exception {
|
||||||
|
// 2026-09-14:image-video / task-file-jobs / ziniao 在桌面端 Python 侧零调用(实测),
|
||||||
|
// 因此不挂开关、无条件收紧——开关关闭时匿名也必须 401
|
||||||
|
for (String uri : new String[]{"/api/image-video/secrets", "/api/task-file-jobs/list", "/api/ziniao/staff"}) {
|
||||||
|
AdminAuthSupport authSupport = mock(AdminAuthSupport.class);
|
||||||
|
when(authSupport.requireUserOrInternal(any())).thenThrow(new BusinessException(401, "未登录"));
|
||||||
|
AdminApiGuardFilter filter = newFilter(authSupport, true, "", false);
|
||||||
|
MockHttpServletRequest request = new MockHttpServletRequest("GET", uri);
|
||||||
|
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||||
|
MockFilterChain chain = new MockFilterChain();
|
||||||
|
|
||||||
|
filter.doFilter(request, response, chain);
|
||||||
|
|
||||||
|
assertThat(chain.getRequest()).as(uri + " 应被无条件守卫拦截").isNull();
|
||||||
|
assertThat(response.getContentAsString()).contains("401");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void webOnlyPrefixesStillLetWebUserTokenThrough() throws Exception {
|
||||||
|
AdminAuthSupport authSupport = mock(AdminAuthSupport.class);
|
||||||
|
when(authSupport.requireUserOrInternal(any())).thenReturn(new AdminUserEntity());
|
||||||
|
AdminApiGuardFilter filter = newFilter(authSupport, true, "", false);
|
||||||
|
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/api/image-video/secrets");
|
||||||
|
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||||
|
MockFilterChain chain = new MockFilterChain();
|
||||||
|
|
||||||
|
filter.doFilter(request, response, chain);
|
||||||
|
|
||||||
|
assertThat(chain.getRequest()).isNotNull();
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void enabledUserToolGuardCoversZiniaoPrefixAsSecondLayer() throws Exception {
|
void enabledUserToolGuardCoversZiniaoPrefixAsSecondLayer() throws Exception {
|
||||||
AdminAuthSupport authSupport = mock(AdminAuthSupport.class);
|
AdminAuthSupport authSupport = mock(AdminAuthSupport.class);
|
||||||
|
|||||||
Reference in New Issue
Block a user