fix(security): 文件内联预览默认需登录,公开目录改为显式白名单

- 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
This commit is contained in:
2026-11-03 23:14:13 +08:00
parent 43f0119b25
commit 6bfdae5787
3 changed files with 53 additions and 101 deletions
@@ -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;
* <ul>
* <li>下载 {@code /api/file/download/**}:一律需要登录——"取走原件"不再是匿名动作;</li>
* <li>内联预览 {@code /api/file/{dir}/{name}} 与缩略图 {@code /api/file/thumbnail/**}:
* 默认公开({@code file.public-preview=true}),否则现有公开页面的配图会整片变成需要 token;</li>
* <li>其余文件接口:默认需要登录;命中 {@code file.public-dirs} 的目录额外公开。</li>
* <b>默认也需要登录</b>({@code file.public-preview} 默认 false)。上传一律落在
* {@code /yyyyMMdd/} 日期目录,日期目录正好是两段路径,所以默认公开等于把所有上传文件公开;</li>
* <li>公开页面引用的文件必须把所在目录列进 {@code file.public-dirs} 白名单,
* 否则匿名访问会被拒(表现为 401/403 的空图)。白名单是显式声明,默认空。</li>
* </ul>
*
* <p>规则以字符串模式产出,交给 {@link SecurityConfig} 注册;类本身不碰请求,
* 因此可以在单元测试里直接验证。
* 因此可以在单元测试里直接验证模式的<b>内容与顺序</b>——顺序恰恰是这条改造的关键:
* 下载必须在公开模式之前注册,公开模式必须在 {@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<String> publicDirs;
/**
* 内联资源(预览 + 缩略图)是否默认公开
* 内联资源(预览 + 缩略图)是否默认公开;默认 false,只有显式打开才整体放行
*/
private final boolean publicPreview;
public FileAccessRules(@Value("${file.public-dirs:}") String publicDirs,
@Value("${file.public-preview:true}") boolean publicPreview) {
@Value("${file.public-preview:false}") boolean publicPreview) {
this.publicDirs = normalize(publicDirs);
this.publicPreview = publicPreview;
}
@@ -71,7 +71,8 @@ public class FileAccessRules {
/**
* 匿名放行的文件路径模式。
*
* <p>内联公开时是预览 + 缩略图;否则是 {@code file.public-dirs} 里的目录(原图与缩略图)。
* <p>默认({@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<String> publicDirPatterns() {
if (publicDirs.isEmpty()) {
return Collections.emptyList();