From 6bfdae5787b30d5301f35cf1b0936f3d8c5f261b Mon Sep 17 00:00:00 2001 From: weicw <594098497@qq.com> Date: Tue, 3 Nov 2026 23:14:13 +0800 Subject: [PATCH] =?UTF-8?q?fix(security):=20=E6=96=87=E4=BB=B6=E5=86=85?= =?UTF-8?q?=E8=81=94=E9=A2=84=E8=A7=88=E9=BB=98=E8=AE=A4=E9=9C=80=E7=99=BB?= =?UTF-8?q?=E5=BD=95=EF=BC=8C=E5=85=AC=E5=BC=80=E7=9B=AE=E5=BD=95=E6=94=B9?= =?UTF-8?q?=E4=B8=BA=E6=98=BE=E5=BC=8F=E7=99=BD=E5=90=8D=E5=8D=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - file.public-preview 默认值由 true 改为 false(application.yml 与 @Value 同步), 上传一律落在 /yyyyMMdd/ 日期目录,默认公开等于把所有上传文件(含盖章证明材料) 变成匿名可读;公开页面引用的文件必须把所在目录列进 file.public-dirs - 注释写清「公开页面引用的文件必须列进白名单,否则匿名访问会被拒」,同步更新 FileAccessRules 类注释 - 删掉只被测试调用的 FileAccessRules.isPublicFileRequest(测试钉住的是没上线的 平行判定);测试改为断言 downloadPatterns / permitAllPatterns / authenticatedPatterns 的内容与顺序 - 顺带把 gxmu.module-registry.strict 改成 ${GXMU_MODULE_REGISTRY_STRICT:true}, 与相邻配置一致 refs #58 --- .../common/core/security/FileAccessRules.java | 44 ++------- src/main/resources/application.yml | 18 ++-- .../core/security/FileAccessRulesTest.java | 92 +++++++------------ 3 files changed, 53 insertions(+), 101 deletions(-) diff --git a/src/main/java/com/gxwebsoft/common/core/security/FileAccessRules.java b/src/main/java/com/gxwebsoft/common/core/security/FileAccessRules.java index bdb0cfb..b8fc6bf 100644 --- a/src/main/java/com/gxwebsoft/common/core/security/FileAccessRules.java +++ b/src/main/java/com/gxwebsoft/common/core/security/FileAccessRules.java @@ -3,7 +3,6 @@ package com.gxwebsoft.common.core.security; import cn.hutool.core.util.StrUtil; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; -import org.springframework.util.AntPathMatcher; import java.util.ArrayList; import java.util.Collections; @@ -17,12 +16,15 @@ import java.util.List; *
规则以字符串模式产出,交给 {@link SecurityConfig} 注册;类本身不碰请求,
- * 因此可以在单元测试里直接验证。
+ * 因此可以在单元测试里直接验证模式的内容与顺序——顺序恰恰是这条改造的关键:
+ * 下载必须在公开模式之前注册,公开模式必须在 {@code GET /**} 兜底之前注册。
*
* @author Codex
* @since 2026-11-03
@@ -50,20 +52,18 @@ public class FileAccessRules {
*/
private static final String THUMBNAIL_PATTERN = FILE_PATH_PREFIX + "/thumbnail/**";
- private final AntPathMatcher pathMatcher = new AntPathMatcher();
-
/**
* 允许匿名读取的目录(相对 {@code /api/file}),已去空白与首尾斜杠
*/
private final List 内联公开时是预览 + 缩略图;否则是 {@code file.public-dirs} 里的目录(原图与缩略图)。
+ * 默认({@code public-preview=false})只有 {@code file.public-dirs} 白名单里的目录;
+ * 显式打开内联公开时才额外放行预览与缩略图,且排在白名单之前。
*
* @return antMatchers 模式,没有任何公开项时返回空列表
*/
@@ -104,31 +105,6 @@ public class FileAccessRules {
return Collections.singletonList(DOWNLOAD_PATTERN);
}
- /**
- * 这条文件请求是否按公开资源处理(匿名可读)。
- *
- * @param path 请求路径,可带查询串
- * @return true 表示命中公开规则;下载、未命中目录、非文件路径一律 false
- */
- public boolean isPublicFileRequest(String path) {
- if (StrUtil.isBlank(path)) {
- return false;
- }
- String clean = StrUtil.subBefore(path, "?", false);
- if (!clean.startsWith(FILE_PATH_PREFIX + "/")) {
- return false;
- }
- if (pathMatcher.match(DOWNLOAD_PATTERN, clean)) {
- return false;
- }
- for (String pattern : permitAllPatterns()) {
- if (pathMatcher.match(pattern, clean)) {
- return true;
- }
- }
- return false;
- }
-
private List 改造前 {@code GET /api/file/**} 是 {@code permitAll},盖章证明材料可以被
- * 未登录者直接按 URL 取走。spec 的要求只落在"下载接口"上:下载改为需登录;
- * 内联预览与缩略图必须继续匿名可读,否则公开页面的配图会整片裂掉。
- * 测试钉住三件事:下载无论怎么配都要登录、内联默认公开、显式关闭时内联也要登录。
+ * 改造前 {@code GET /api/file/**} 是 {@code permitAll},盖章证明材料可以被未登录者按 URL
+ * 直接取走。现在默认「全部需登录」,只有显式声明的东西才匿名可读。
+ *
+ * 测试钉的是 {@link SecurityConfig} 真正注册的那三个方法返回的模式内容与顺序——
+ * 顺序本身就是这条改造的关键(下载在公开之前、公开在兜底之前)。这里不再另造一套
+ * 「这条请求算不算公开」的平行判定,避免测试全绿而线上照旧放行。
*
* @author Codex
* @since 2026-11-03
*/
class FileAccessRulesTest {
- /** 下载接口永远要登录,白名单为空、内联公开也一样——它是"取走原件"的动作。 */
+ /** 下载接口单独一条模式,且只放这一个:它是"取走原件"的动作,永远要登录。 */
@Test
- void downloadAlwaysRequiresLogin() {
+ void downloadPatternIsRegisteredAlone() {
FileAccessRules rules = new FileAccessRules("", true);
- assertFalse(rules.isPublicFileRequest("/api/file/download/20260708/材料.pdf"));
- assertFalse(rules.isPublicFileRequest("/api/file/download/cms-article/20260708/材料.pdf"));
+ assertEquals(Collections.singletonList("/api/file/download/**"), rules.downloadPatterns());
}
- /** 内联预览与缩略图默认公开:公开页面上的配图不能因为这次收敛而变成需要 token。 */
+ /** 安全默认:不配白名单、不打开内联公开时,没有任何匿名可读的文件路径。 */
@Test
- void previewAndThumbnailArePublicByDefault() {
- FileAccessRules rules = new FileAccessRules("", true);
-
- assertTrue(rules.isPublicFileRequest("/api/file/20260708/封面.png"));
- assertTrue(rules.isPublicFileRequest("/api/file/thumbnail/20260708/封面.png"));
- }
-
- /** 显式关闭内联公开后,预览与缩略图都要登录——白名单开关确实生效。 */
- @Test
- void previewRequiresLoginWhenPublicPreviewDisabled() {
- FileAccessRules rules = new FileAccessRules("", false);
-
- assertFalse(rules.isPublicFileRequest("/api/file/20260708/封面.png"));
- assertFalse(rules.isPublicFileRequest("/api/file/thumbnail/20260708/封面.png"));
- assertFalse(rules.permitAllPatterns().contains("/api/file/*/*"));
- }
-
- /** 额外公开目录即使在内联关闭时也生效(原图与缩略图)。 */
- @Test
- void whitelistedDirectoryIsPublic() {
- FileAccessRules rules = new FileAccessRules("cms-article", false);
-
- assertTrue(rules.isPublicFileRequest("/api/file/cms-article/20260708/封面.png"));
- assertTrue(rules.isPublicFileRequest("/api/file/thumbnail/cms-article/20260708/封面.png"));
- assertFalse(rules.isPublicFileRequest("/api/file/20260708/盖章证明.jpg"));
- }
-
- /** 没有公开项时不产生任何放行模式,安全默认 = 全部需登录。 */
- @Test
- void noPublicItemYieldsNoPublicPatterns() {
+ void defaultIsClosedSoNoFilePatternIsAnonymous() {
assertTrue(new FileAccessRules("", false).permitAllPatterns().isEmpty());
assertTrue(new FileAccessRules(null, false).permitAllPatterns().isEmpty());
- assertFalse(new FileAccessRules(null, false).isPublicFileRequest("/api/file/20260708/a.jpg"));
}
- /** 配置里允许空格、斜杠与空项,但产出的模式必须干净。 */
+ /** 白名单目录产出「原图 → 缩略图」两条模式,按声明顺序展开。 */
@Test
- void patternsAreNormalized() {
+ void whitelistedDirsProduceOriginThenThumbnailPatternsInOrder() {
FileAccessRules rules = new FileAccessRules(" cms-article/ , ,/public/ ", false);
- List