Nginx 实战:如何通过代理转发下载中文文件并保留原文件名

本文基于一个实际的项目需求,做下记录并分享。

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 需要:
    1. 根据请求参数 changeprotocol 解析出真实文件 URL;
    2. 把请求转发到目标服务器(IP 不是固定的,只能从 URL 中截取);
    3. 返回给浏览器时,触发下载 ,并且 下载文件名就是 URL 中的中文文件名 (例如:中国.png)。

1.2 遇到的典型问题

在实现过程中,遇到了不少坑:

  1. 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 当成本机路径去找了,而不是作为代理转发。
  2. 中文文件名被编码/双编码

    • 日志里出现:/%25E4%25B8%25AD%25E5%259B%BD.png 这样的路径。
    • %25 代表原来的 % 又被 URL 编码了一次,导致找不到真实文件。
  3. 下载文件名错误

    • 通过代理访问时,浏览器下载下来的文件名变成了:
      • changeprotocol
      • 或者一个乱码的 %E4%B8%AD%E5%9B%BD.png
    • 说明 Content-Disposition 处理不正确。

二、整体设计思路

要实现:

通过 代理转发 下载中文文件,并且 保留原始中文文件名

可以分解为几步:

  1. 源站(9301)负责文件存储与基础下载

    • 使用 root 指向静态目录,
    • 为模拟源站有header的情况,在源站上可以统一加上 Content-Disposition: attachment;
  2. 代理站(9302)负责 URL 解析与转发

    • /changeprotocol 作为统一入口;
    • 从查询参数 changeprotocol 里取出真实文件 URL,例如:
      • http://10.86.37.169:9301/中国.png
    • 用正则从 URL 中截取文件名部分中国.png
    • 对上游响应头里的 Content-Disposition 做策略:
      • 如果上游已经提供了合适的 Content-Disposition,则直接沿用;
      • 如果上游没有提供,则由代理站(9302)自动补上带中文文件名的 Content-Disposition
  3. 使用 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;
}

逐行解释:

  1. location = /changeprotocol

    • 精确匹配 /changeprotocol 路径,避免和其他路径混淆;
    • 所有代理下载请求统一走这个入口。
  2. resolver 10.86.37.1;

    • 配置 Nginx 在做 proxy_pass 时的 DNS 解析器;
    • 适用于 changeprotocol 参数中的目标服务器可能使用域名,或 IP 会变化的情况。
  3. set $stream_url $arg_changeprotocol;

    • $arg_changeprotocol 是 Nginx 内置变量,表示查询参数 changeprotocol 的值;
    • 例如:http://10.86.37.169:9301/中国.png
    • 通过 set 赋值给自定义变量 $stream_url 方便后续使用。
  4. 从 URL 中提取文件名:

    nginx 复制代码
    set $filename "";
    
    if ($stream_url ~* ".*/([^/?]+)$") {
        set $filename $1;
    }
    • 使用正则:".*/([^/?]+)$" 匹配 URL 中最后一个 / 后、? 前的部分;
    • 例如:
      • http://10.86.37.169:9301/中国.png中国.png
      • http://host/path/a/b/c.txt?token=xxxc.txt
    • 匹配结果通过 $1 赋值给 $filename
  5. 清理上游不需要的头部:

    nginx 复制代码
    proxy_set_header Cookie "";
    proxy_hide_header Content-Disposition;
    • proxy_set_header Cookie "";
      • 不向上游透传 Cookie,减少状态依赖;
    • proxy_hide_header Content-Disposition;
      • 即使上游返回了 Content-Disposition,在代理响应里也先隐藏掉;
      • 结合前面的 map,我们可以统一由代理端决定是否再次添加。
  6. 条件性添加下载头,保持中文文件名:

    nginx 复制代码
    if ($need_cd) {
        add_header Content-Disposition "attachment; filename*=UTF-8''$filename";
    }
    • $need_cd1 时,说明上游没有 Content-Disposition,需要代理补充;
    • 使用:filename*=UTF-8''$filename 格式:
      • 明确声明文件名使用 UTF-8 编码;
      • 更有利于浏览器正确显示中文名称;
    • $filename 即我们正则提取到的 中国.png
  7. 关键的反向代理:

    nginx 复制代码
    proxy_pass $stream_url;
    • 把当前请求直接代理到 $stream_url 指向的真实地址;
    • $stream_url 可以是 IP、域名等,也支持中文路径(Nginx 会帮你做 URL 编码)。

五、关于中文文件名与编码的一些坑

  1. 双重 URL 编码问题

    • 日志中看到 /%25E4%25B8%25AD%25E5%9B%BD.png,说明 % 又被编码成了 %25
    • 这种情况通常与:
      • 浏览器、后端或 Nginx 对 URL 做了多次编码有关;
    • 建议:
      • 保证前端传入的 changeprotocol 已经是一次正确的 URL 编码;
      • 在 Nginx 端不要再对同一段字符串进行手工编码处理。
  2. 文件系统路径与 URL 不要混用

    • 报错 CreateFile() "D:/nginx-1.17.1/www/%E4%B8%AD%E5%9B%BD.png" failed 提示 Nginx 尝试从本地磁盘读取文件;
    • 这是静态文件模式下的行为,和我们希望的"反向代理模式"不同;
    • 若希望所有请求走代理,需要确保:
      • 对应的 location 使用 proxy_pass
      • 不要再在该 location 中使用 root/alias 去做本地文件查找。
  3. 浏览器对中文文件名的兼容性

    • filename= 对中文支持不统一,经常会出现乱码;

    • filename*= 搭配 UTF-8,在现代浏览器(Chrome/Edge/Firefox)中表现更好;

    • 示例:

      http 复制代码
      Content-Disposition: attachment; filename*=UTF-8''%E4%B8%AD%E5%9B%BD.png
    • 实际上,Nginx 在处理 $filename中国.png 时,会根据配置自动进行正确的编码。


六、总结

这次实践的核心难点在于:

在 Nginx 代理转发场景下,既要从请求中动态解析目标服务器地址与文件名,又要让浏览器在下载时完整保留中文文件名。

关键做法可以概括为三点:

  1. 使用 /changeprotocol + 查询参数的方式,把真实下载地址封装在 URL 里;
  2. 通过 Nginx 的 map、正则与变量组合:
    • 从参数中解析出完整 URL;
    • 从 URL 中提取文件名;
    • 根据上游响应头决定是否由代理补充 Content-Disposition
  3. 在代理响应里,用 Content-Disposition: attachment; filename*=UTF-8''$filename 强制浏览器以附件下载方式,并保留 UTF-8 中文文件名。

如果你也需要在复杂网络/代理架构下,通过 Nginx 实现"下载中文文件并保留原名",可以直接参考上面的 nginx.conf 片段,根据自己的 IP、端口和参数名稍作调整即可。

相关推荐
OliverH-yishuihan2 小时前
在 Windows 上安装 Linux
linux·运维·windows
zclinux_2 小时前
【Linux】虚拟化的内存气泡
linux·运维·服务器
松涛和鸣3 小时前
DAY33 Linux Thread Synchronization and Mutual Exclusion
linux·运维·服务器·前端·数据结构·哈希算法
CCI3443 小时前
Remote ssh无法连接?
运维·ssh
技术小李...3 小时前
docker下mysql更改密码后WordPress提示无法连接数据库问题
运维·docker·容器
Focussend智能化营销3 小时前
2026破局:以营销自动化成熟度Macom模型为鞍,驰骋增长新赛道!
运维·自动化
wanhengidc4 小时前
什么是裸金属服务器
运维·服务器·科技·智能手机·云计算
刘某的Cloud4 小时前
shell脚本-read-输入
linux·运维·bash·shell·read
莫问前程_满城风雨4 小时前
verilog 可变范围的bit选择
运维·服务器·verilog