From 6a90adc765b2ae3cd0086950b6f0f6cc6af30138 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E8=87=AA=E8=BE=BE?= <980324341@qq.com> Date: Mon, 14 Sep 2026 10:46:05 +0800 Subject: [PATCH] =?UTF-8?q?feat(A1/A3):=20=E4=B8=89=E4=B8=AA=E5=AE=A2?= =?UTF-8?q?=E6=88=B7=E7=AB=AF=E9=9B=B6=E8=B0=83=E7=94=A8=E5=89=8D=E7=BC=80?= =?UTF-8?q?=E7=AB=8B=E5=8D=B3=E6=97=A0=E6=9D=A1=E4=BB=B6=E6=94=B6=E7=B4=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 实测桌面端 Python 侧调用面:/api/image-video、/api/task-file-jobs、/api/ziniao 为 0 次调用 (只有带 JWT 的网页端在用),因此从 user-tool-guard-enabled 开关名单移入无条件守卫名单, 不必等客户端铺开即完成收紧;其余 14 个前缀的客户端调用面与令牌携带情况已逐一实测, 仍在开关后面(老客户端不带身份,提前打开会 401)。 新增 2 个守卫测试:开关关闭时这三组前缀对匿名同样 401、带用户令牌仍放行。 --- .../aiimage/config/AdminApiGuardFilter.java | 15 +++++---- .../config/AdminApiGuardFilterTest.java | 33 +++++++++++++++++++ 2 files changed, 42 insertions(+), 6 deletions(-) diff --git a/backend-java/src/main/java/com/nanri/aiimage/config/AdminApiGuardFilter.java b/backend-java/src/main/java/com/nanri/aiimage/config/AdminApiGuardFilter.java index a20c0630..3bd3e271 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/config/AdminApiGuardFilter.java +++ b/backend-java/src/main/java/com/nanri/aiimage/config/AdminApiGuardFilter.java @@ -65,10 +65,10 @@ public class AdminApiGuardFilter extends OncePerRequestFilter { "/api/price-track", // 2026-09 全维度审查补:以下前缀此前完全在守卫范围之外。 // /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/digital-human", - "/api/image-video", "/api/brand", "/api/appearance-patent", "/api/similar-asin", @@ -78,10 +78,6 @@ public class AdminApiGuardFilter extends OncePerRequestFilter { "/api/shop-match", "/api/shop-data-crawl", "/api/withdraw", - "/api/task-file-jobs", - // 2026-09 复核补:该前缀全部接口已在 controller 内 requireAdmin(含可换取员工店铺登录 - // 令牌的 /shops/open),此处纳入名单是"开关打开后的第二层";开关关闭时行为不变 - "/api/ziniao", // /api/tasks/{taskId}/interrupted 仅凭 taskId 即可把 RUNNING 任务置为 FAILED, // 匿名遍历 taskId 就能批量打断线上任务 "/api/tasks", @@ -94,6 +90,13 @@ public class AdminApiGuardFilter extends OncePerRequestFilter { private static final String[] SELF_SERVICE_PREFIXES = { "/api/user-secrets", "/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 自校验令牌,纳入守卫后 // 不带令牌的请求直接 401(带可信令牌的仍由 doFilterInternal 放行) "/api/internal", diff --git a/backend-java/src/test/java/com/nanri/aiimage/config/AdminApiGuardFilterTest.java b/backend-java/src/test/java/com/nanri/aiimage/config/AdminApiGuardFilterTest.java index e2429574..f3682a8f 100644 --- a/backend-java/src/test/java/com/nanri/aiimage/config/AdminApiGuardFilterTest.java +++ b/backend-java/src/test/java/com/nanri/aiimage/config/AdminApiGuardFilterTest.java @@ -321,6 +321,39 @@ class AdminApiGuardFilterTest { 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 void enabledUserToolGuardCoversZiniaoPrefixAsSecondLayer() throws Exception { AdminAuthSupport authSupport = mock(AdminAuthSupport.class);