fix: 修复反向代理跨域误判与中文附件名落盘失败

生产经 nginx/frp 反向代理时后端未识别 X-Forwarded-Proto,把 HTTPS 同源请求按 http 判定为跨域,而 CORS 白名单仅含内网 http,导致带 Origin 的请求(登录、加密握手等)返回 403 Invalid CORS request;同时部分运行环境未设置 UTF-8 locale,含中文的邮箱附件名落盘时抛 InvalidPathException 报「处理失败」——本次一并修复。

变更内容:
- WebConfig.addCorsMappings 放开全部来源(allowedOriginPatterns("*"),allowCredentials(true),后者不允许 allowedOrigins("*"))。
- application.yml 新增 server.forward-headers-strategy: framework,信任 X-Forwarded-Proto/Host,使 HTTPS 同源请求被正确识别。
- EmailBillStorage.sanitize 不再使用 Paths.get 解析文件名(改为字符串截取末段),并对非 ASCII 文件名做百分号编码,保证落盘名为纯 ASCII;附件原始名仍存 attachment_name,展示不受影响。

说明:
- 接口认证基于 JWT(Authorization 头存于 localStorage,跨站脚本无法自动携带),放开跨域风险可控;容器侧仍建议设置 LANG/LC_ALL=C.UTF-8。
This commit is contained in:
2026-10-13 21:45:49 +08:00
parent 3b7b59ff6f
commit f0956f6587
3 changed files with 25 additions and 14 deletions
@@ -62,21 +62,16 @@ public class WebConfig implements WebMvcConfigurer {
/** /**
* 配置跨域策略。 * 配置跨域策略。
* <p>仅放开本机与局域网来源,避免任意站点携带凭据发起跨域请求;支持常用 HTTP 方法并允许携带凭据。 * <p>放开全部来源:允许任意站点携带凭据发起跨域请求。因 {@code allowCredentials(true)}
* 生产部署前后端同源,无需跨域;开发场景为 5173/5174,局域网访问为 192.168.*、10.*、172.* 网段。</p> * 不支持 {@code allowedOrigins("*")},须使用 {@code allowedOriginPatterns("*")}(回显请求 Origin)。
* 接口认证依赖 JWT(Authorization 头,存于 localStorage,跨站脚本无法自动携带),风险可控。</p>
* *
* @param registry 跨域配置注册器 * @param registry 跨域配置注册器
*/ */
@Override @Override
public void addCorsMappings(CorsRegistry registry) { public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**") registry.addMapping("/**")
.allowedOriginPatterns( .allowedOriginPatterns("*")
"http://localhost:*",
"http://127.0.0.1:*",
"http://[::1]:*",
"http://192.168.*.*:*",
"http://10.*.*.*:*",
"http://172.*.*.*:*")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*") .allowedHeaders("*")
.allowCredentials(true) .allowCredentials(true)
@@ -1,9 +1,10 @@
package cn.rosercode.ledgerly.email; package cn.rosercode.ledgerly.email;
import java.io.IOException; import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@@ -97,7 +98,12 @@ public class EmailBillStorage {
} }
/** /**
* 清洗附件文件名,去除路径分隔符与非法字符。 * 清洗附件文件名,去除路径分隔符与非法字符,并保证结果可安全落盘。
*
* <p>仅用字符串截取末段而非 {@code Paths.get},避免在文件系统编码非 UTF-8
* (如 locale 未设置、{@code sun.jnu.encoding=ANSI_X3.4-1968})的运行环境下,
* 对含中文的文件名抛出 {@code InvalidPathException}(Malformed input or input
* contains unmappable characters)。非 ASCII 名称统一转为百分号编码,确保纯 ASCII。</p>
* *
* @param fileName 原始文件名 * @param fileName 原始文件名
* @return 安全的文件名 * @return 安全的文件名
@@ -106,8 +112,16 @@ public class EmailBillStorage {
if (fileName == null || fileName.isBlank()) { if (fileName == null || fileName.isBlank()) {
return "attachment"; return "attachment";
} }
Path namePath = Paths.get(fileName).getFileName(); String name = fileName.replace('\\', '/');
String name = namePath == null ? fileName : namePath.toString(); int slash = name.lastIndexOf('/');
return name.replaceAll("[\\\\/:*?\"<>|]", "_"); if (slash >= 0) {
name = name.substring(slash + 1);
}
name = name.replaceAll("[\\\\/:*?\"<>|]", "_");
// 含非 ASCII 字符时转为百分号编码,避免文件系统编码不支持中文导致保存失败
if (!StandardCharsets.US_ASCII.newEncoder().canEncode(name)) {
name = URLEncoder.encode(name, StandardCharsets.UTF_8).replace("+", "%20");
}
return name;
} }
} }
+2
View File
@@ -48,3 +48,5 @@ knife4j:
server: server:
port: 8080 port: 8080
# 信任前置 nginx/frp 透传的 X-Forwarded-*,使 HTTPS 同源请求被正确识别、避免被误判为跨域
forward-headers-strategy: framework