本文基于一个实际的项目需求,做下记录并分享。
Nginx 通过代理转发下载中文文件的完整实践
难题:访问地址类似
http://10.86.37.169:9302/changeprotocol?changeprotocol=http://10.86.37.169:9301/中国.png通过二级 Nginx 代理转发下载文件,同时在浏览器中保留原始中文文件名。
本文记录我在 Windows 环境下,用 Nginx 实现"代理转发下载中文文件并保留中文文件名"的完整踩坑过程和最终解决方案。重点放在:如何在反向代理场景中正确处理 Content-Disposition 和中文文件名编码。
一、问题背景
1.1 业务场景
- 文件真实存放在 9301 端口 对应的 Nginx 静态服务器上:
- 例如:
http://10.86.37.169:9301/中国.png
- 例如:
- 客户端实际访问的是 9302 端口 的代理服务器:
- 访问地址示例:
http://10.86.37.169:9302/changeprotocol?changeprotocol=http://10.86.37.169:9301/中国.png
- 访问地址示例:
- 9302 上的 Nginx 需要:
- 根据请求参数
changeprotocol解析出真实文件 URL; - 把请求转发到目标服务器(IP 不是固定的,只能从 URL 中截取);
- 返回给浏览器时,触发下载 ,并且 下载文件名就是 URL 中的中文文件名 (例如:
中国.png)。
- 根据请求参数
1.2 遇到的典型问题
在实现过程中,遇到了不少坑:
-
Nginx 当成本地静态文件去找
- 报错类似:
CreateFile() "D:/nginx-1.17.1/www/%E4%B8%AD%E5%9B%BD.png" failed (2: The system cannot find the file specified)
- 说明 Nginx 把 URL 当成本机路径去找了,而不是作为代理转发。
- 报错类似:
-
中文文件名被编码/双编码
- 日志里出现:
/%25E4%25B8%25AD%25E5%259B%BD.png这样的路径。 %25代表原来的%又被 URL 编码了一次,导致找不到真实文件。
- 日志里出现:
-
下载文件名错误
- 通过代理访问时,浏览器下载下来的文件名变成了:
changeprotocol- 或者一个乱码的
%E4%B8%AD%E5%9B%BD.png
- 说明
Content-Disposition处理不正确。
- 通过代理访问时,浏览器下载下来的文件名变成了:
二、整体设计思路
要实现:
通过 代理转发 下载中文文件,并且 保留原始中文文件名。
可以分解为几步:
-
源站(9301)负责文件存储与基础下载
- 使用
root指向静态目录, - 为模拟源站有header的情况,在源站上可以统一加上
Content-Disposition: attachment;。
- 使用
-
代理站(9302)负责 URL 解析与转发
- 用
/changeprotocol作为统一入口; - 从查询参数
changeprotocol里取出真实文件 URL,例如:http://10.86.37.169:9301/中国.png
- 用正则从 URL 中截取文件名部分 :
中国.png; - 对上游响应头里的
Content-Disposition做策略:- 如果上游已经提供了合适的
Content-Disposition,则直接沿用; - 如果上游没有提供,则由代理站(9302)自动补上带中文文件名的 Content-Disposition。
- 如果上游已经提供了合适的
- 用
-
使用
filename*=搭配 UTF-8 编码- 为了浏览器更好地识别中文文件名,代理端设置:
Content-Disposition: attachment; filename*=UTF-8''中国.png
- 使用
filename*=+ UTF-8,在现代浏览器中兼容性较好。
- 为了浏览器更好地识别中文文件名,代理端设置:
三、最终 Nginx 配置
下面是完整的 nginx.conf 核心配置片段(已在 Windows + Nginx 1.17.1 环境测试):
nginx
worker_processes 1;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
# 1. 通过上游响应头判断是否需要由代理补充 Content-Disposition
map $upstream_http_content_disposition $need_cd {
default 0; # 上游返回了 Content-Disposition,则不再处理
"" 1; # 上游没返回(为空),则需要由代理补充
}
# 2. 源站:真正存放文件的静态服务器(示例端口 9301)
server {
listen 9301;
server_name 10.86.37.169;
charset utf-8;
location / {
root D:/nginx-1.17.1/www;
index index.html index.htm;
# 源站上可以选择性加上 attachment
add_header Content-Disposition "attachment;";
}
}
# 3. 代理站:通过 /changeprotocol 代理转发下载文件(示例端口 9302)
server {
listen 9302;
server_name localhost;
location = /changeprotocol {
# DNS 解析器,支持目标服务器 IP 可变
resolver 10.86.37.1;
# 从查询参数中获取真实 URL
# 例如:changeprotocol=http://10.86.37.169:9301/中国.png
set $stream_url $arg_changeprotocol;
set $filename "";
# 用正则从 URL 中截取文件名部分:最后一个 / 后、? 前
if ($stream_url ~* ".*/([^/?]+)$") {
set $filename $1;
}
# 不向上游透传 Cookie,避免带入不必要的状态
proxy_set_header Cookie "";
# 隐藏上游返回的 Content-Disposition 头
# 后续根据 need_cd 决定是否添加自己的
proxy_hide_header Content-Disposition;
# 如果上游没有返回 Content-Disposition,则由代理补充
if ($need_cd) {
add_header Content-Disposition "attachment; filename*=UTF-8''$filename";
}
# 最关键:把请求代理到真实文件 URL
proxy_pass $stream_url;
}
}
}
3.1 访问示例
- 源站直连(测试用):
http://10.86.37.169:9301/中国.png
- 代理站访问(实际给前端的地址):
http://10.86.37.169:9302/changeprotocol?changeprotocol=http://10.86.37.169:9301/中国.png
效果:
- 浏览器会直接触发下载对话框;
- 下载的文件名为:
中国.png; - 即使目标服务器 IP 改变,只要 URL 中的
changeprotocol参数跟着变,代理依然可以正常工作。
四、关键配置逐项解析
4.1 通过 map 判断是否需要补充 Content-Disposition
nginx
map $upstream_http_content_disposition $need_cd {
default 0;
"" 1;
}
含义:
$upstream_http_content_disposition是 上游返回的Content-Disposition响应头;map指令会根据上游头的值,生成一个新的变量$need_cd:- 默认情况下(不为空)设为
0,表示不需要代理再添加头; - 如果上游没有返回(为空字符串
""),则设为1,表示需要代理自己添加。
- 默认情况下(不为空)设为
这样做的好处:
- 如果上游应用已经对
Content-Disposition做了精细控制,代理不会多此一举; - 如果上游没处理,代理可以作为兜底逻辑补上下载头。
4.2 静态文件源站配置
nginx
server {
listen 9301;
server_name 10.86.37.169;
charset utf-8;
location / {
root D:/nginx-1.17.1/www;
index index.html index.htm;
add_header Content-Disposition "attachment;";
}
}
说明:
root指定了真实文件所在目录,例如D:/nginx-1.17.1/www;charset utf-8;:在处理文本类响应时使用 UTF-8;add_header Content-Disposition "attachment;";:- 默认把所有响应当成附件下载,而不是在浏览器中直接打开;
- 若你只希望代理端控制下载行为,也可以去掉这一行,仅保留代理段逻辑。
4.3 代理入口 /changeprotocol
nginx
location = /changeprotocol {
resolver 10.86.37.1;
set $stream_url $arg_changeprotocol;
set $filename "";
if ($stream_url ~* ".*/([^/?]+)$") {
set $filename $1;
}
proxy_set_header Cookie "";
proxy_hide_header Content-Disposition;
if ($need_cd) {
add_header Content-Disposition "attachment; filename*=UTF-8''$filename";
}
proxy_pass $stream_url;
}
逐行解释:
-
location = /changeprotocol:- 精确匹配
/changeprotocol路径,避免和其他路径混淆; - 所有代理下载请求统一走这个入口。
- 精确匹配
-
resolver 10.86.37.1;:- 配置 Nginx 在做
proxy_pass时的 DNS 解析器; - 适用于
changeprotocol参数中的目标服务器可能使用域名,或 IP 会变化的情况。
- 配置 Nginx 在做
-
set $stream_url $arg_changeprotocol;:$arg_changeprotocol是 Nginx 内置变量,表示查询参数changeprotocol的值;- 例如:
http://10.86.37.169:9301/中国.png; - 通过
set赋值给自定义变量$stream_url方便后续使用。
-
从 URL 中提取文件名:
nginxset $filename ""; if ($stream_url ~* ".*/([^/?]+)$") { set $filename $1; }- 使用正则:
".*/([^/?]+)$"匹配 URL 中最后一个/后、?前的部分; - 例如:
http://10.86.37.169:9301/中国.png→中国.png;http://host/path/a/b/c.txt?token=xxx→c.txt;
- 匹配结果通过
$1赋值给$filename。
- 使用正则:
-
清理上游不需要的头部:
nginxproxy_set_header Cookie ""; proxy_hide_header Content-Disposition;proxy_set_header Cookie "";:- 不向上游透传 Cookie,减少状态依赖;
proxy_hide_header Content-Disposition;:- 即使上游返回了
Content-Disposition,在代理响应里也先隐藏掉; - 结合前面的
map,我们可以统一由代理端决定是否再次添加。
- 即使上游返回了
-
条件性添加下载头,保持中文文件名:
nginxif ($need_cd) { add_header Content-Disposition "attachment; filename*=UTF-8''$filename"; }- 当
$need_cd为1时,说明上游没有Content-Disposition,需要代理补充; - 使用:
filename*=UTF-8''$filename格式:- 明确声明文件名使用 UTF-8 编码;
- 更有利于浏览器正确显示中文名称;
$filename即我们正则提取到的中国.png。
- 当
-
关键的反向代理:
nginxproxy_pass $stream_url;- 把当前请求直接代理到
$stream_url指向的真实地址; $stream_url可以是 IP、域名等,也支持中文路径(Nginx 会帮你做 URL 编码)。
- 把当前请求直接代理到
五、关于中文文件名与编码的一些坑
-
双重 URL 编码问题
- 日志中看到
/%25E4%25B8%25AD%25E5%9B%BD.png,说明%又被编码成了%25; - 这种情况通常与:
- 浏览器、后端或 Nginx 对 URL 做了多次编码有关;
- 建议:
- 保证前端传入的
changeprotocol已经是一次正确的 URL 编码; - 在 Nginx 端不要再对同一段字符串进行手工编码处理。
- 保证前端传入的
- 日志中看到
-
文件系统路径与 URL 不要混用
- 报错
CreateFile() "D:/nginx-1.17.1/www/%E4%B8%AD%E5%9B%BD.png" failed提示 Nginx 尝试从本地磁盘读取文件; - 这是静态文件模式下的行为,和我们希望的"反向代理模式"不同;
- 若希望所有请求走代理,需要确保:
- 对应的
location使用proxy_pass; - 不要再在该
location中使用root/alias去做本地文件查找。
- 对应的
- 报错
-
浏览器对中文文件名的兼容性
-
filename=对中文支持不统一,经常会出现乱码; -
filename*=搭配 UTF-8,在现代浏览器(Chrome/Edge/Firefox)中表现更好; -
示例:
httpContent-Disposition: attachment; filename*=UTF-8''%E4%B8%AD%E5%9B%BD.png -
实际上,Nginx 在处理
$filename为中国.png时,会根据配置自动进行正确的编码。
-
六、总结
这次实践的核心难点在于:
在 Nginx 代理转发场景下,既要从请求中动态解析目标服务器地址与文件名,又要让浏览器在下载时完整保留中文文件名。
关键做法可以概括为三点:
- 使用
/changeprotocol+ 查询参数的方式,把真实下载地址封装在 URL 里; - 通过 Nginx 的
map、正则与变量组合:- 从参数中解析出完整 URL;
- 从 URL 中提取文件名;
- 根据上游响应头决定是否由代理补充
Content-Disposition;
- 在代理响应里,用
Content-Disposition: attachment; filename*=UTF-8''$filename强制浏览器以附件下载方式,并保留 UTF-8 中文文件名。
如果你也需要在复杂网络/代理架构下,通过 Nginx 实现"下载中文文件并保留原名",可以直接参考上面的 nginx.conf 片段,根据自己的 IP、端口和参数名稍作调整即可。
