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、端口和参数名稍作调整即可。

相关推荐
乘云数字DATABUFF3 天前
5分钟部署开源APM Databuff:OpenTelemetry全链路追踪入门实战
运维·后端
荣--5 天前
一键部署不是为了省时间 —— 它是把"买来的 PaaS"变成"自己的平台"的拐点
运维·zabbix·工程化·一键部署·平台化·边界设计
江华森5 天前
动手实战学 Docker — 从零到集群编排完全指南
运维
Avan_菜菜6 天前
FRP 内网穿透完整实战:从 HTTP 映射到 HTTPS 自签代理
运维·nginx·https
SelectDB7 天前
Litefuse 开源并推出单进程轻量模式,25 秒就能跑起来的 Agent 可观测与评估平台
运维·后端·自动化运维
XIAOHEZIcode8 天前
Linux系统鼠标偏移常见原因以及修复方案
linux·运维·游戏
用户0328472220709 天前
如何搭建本地yum源(上)
运维
ping某10 天前
为什么 Nginx 明明监听了 80,转发后端时却用了 4xxxx 端口?
后端·nginx
大树8812 天前
金刚石散热越强,管路越先见顶
大数据·运维·服务器·人工智能·ai
摇滚侠12 天前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql