导出文件名需过滤非法字符、兼容中文编码、校验长度与来源:替换/\等9个非法字符为_或-;用filename*支持UTF-8中文,同时提供ASCII备份;模板渲染后白名单过滤(仅保留字母数字及_.-());扩展名由后端硬编码;超120字符主动截断并避让中文断字。
导出文件名含斜杠或反斜杠导致报错
浏览器会把 / 和 \ 当作路径分隔符,直接用在 content-disposition 的 filename 字段里,轻则截断文件名,重则触发下载失败或 400 错误。
- 服务端生成文件名时,必须提前替换掉所有非法字符,不能只靠前端拼接
-
/、\、:、*、?、"、、<code>>、|这 9 个字符在 Windows 下全都不合法;macOS 和 Linux 虽然允许部分(如:),但为跨平台兼容,建议统一过滤 - 推荐用下划线
_或短横线-替代,避免空格(某些旧版 IE 会把空格转成+)
中文文件名在 Chrome/Firefox 中显示乱码
HTTP 响应头里的 Content-Disposition 不支持 UTF-8 直接编码,得用 RFC 5987 标准的 filename*= 字段,否则中文会变成一堆问号或方块。
- 不要只写
filename="订单_202405.xlsx",要同时提供 ASCII 备份和 UTF-8 编码版本 - Python Flask 示例:
response.headers["Content-Disposition"] = 'attachment; filename="order_202405.xlsx"; filename*=UTF-8\'\'%E8%AE%A2%E5%8D%95_202405.xlsx'
- Node.js Express 可用
encodeURI()处理中文,但注意它不编码'和/,需手动补 escape
模板变量渲染后未做文件名安全校验
用户输入的字段(比如 user.name 或 report.title)如果没清洗就塞进文件名,可能带入恶意字符甚至路径遍历片段(如 ../etc/passwd)。
- 模板渲染完成后的最终字符串,必须经过一次白名单过滤:只保留字母、数字、下划线、短横线、点号、括号
- 正则推荐:
/[^a-zA-Z0-9_\-\.\(\)]/g,替换为空字符串 - 特别注意点号
.:只允许出现在扩展名前,且整个文件名中最多一个(避免name..xlsx被误判) - 扩展名必须由后端硬编码决定,绝不能从模板变量里取
不同浏览器对长文件名截断策略不一致
Chrome 限制约 170 字符,Safari 约 100,IE 更严。超长文件名不会报错,但会被静默截断,用户看到的可能是 非常非常长的报...,丢失关键信息。
- 服务端应在拼接完模板后检查总长度,超过 120 字符就主动截断(留出扩展名空间)
- 截断位置优先选在空格或标点后,避免切开中文字符(用
.slice(0, 120)可能截半字,改用substring(0, 120)更稳妥) - 如果业务强依赖完整语义(如带时间戳+ID),建议把核心标识放前面,描述性文字放后面并允许被裁掉
真正麻烦的不是怎么替换字符,而是模板变量来源不可控时,你永远不知道下一个 user.input 里藏着什么——所以校验必须落在最终拼接结果上,而不是某个中间环节。










