Nginx-替换response header中的Content-Disposition值


我们有个需求要在打开合同PDF的时候,要将response的header里的Content-Disposition从

attachment;filename*="utf-8\' \'文件名"

改为

inline;filename*="utf-8\' \'文件名"

这样文件就可以直接在浏览器里预览打开,而不是直接下载。
理论上最好的方式自然是从应用端解决。但我们提供文件的内容管理服务器不提供这个配置选项。虽然是开源软件,但我也不想为了这个修改源代码。除此之外,为了避免影响其他和文件相关的功能,减少回归测试量,我们也不想把全局修改这个header值。
那么剩下的办法就只有从Nginx反向代理层找解决方案了。理想的解决方案是对xxx.domain.com域名(内容管理服务器的域名),所有URL中带PDF关键字和“?inline=1”参数的请求,修改header中Content-Disposition的值。(我们可以在前端请求的时候加?inline=1这个path variable)
我模糊记得Nginx可以带if条件,所以原本估计就是个小case。事实证明我估计错得离谱【捂脸】。。。如果要直接看结论的请跳转到最后一节。

教训1:Nginx“基本”不支持if里多个条件

我先找到了一段匹配文件后缀的正则表达式:

.*\.(后缀1|后缀2)$

后缀替换成pdf后,就尝试写了如下的代码:

            if ($request_filename ~* ".*\.(pdf)" && $request_uri ~ "(.*)inline=1") {
                # 修改header值
            }

然而很快我就发现,Nginx不支持if(condition1 && condition2)的语法【捂脸】。。。
其实也有一些奇淫技巧可以实现AND和OR,比如这一篇,通过拼字符串的方式:

    location = /test_and/ {
        default_type text/html;
        set $a 0;
        set $b 0;
        if ( $remote_addr != '' ){
            set $a 1;
        }
        if ( $http_x_forwarded_for != '' ){
            set $a 1$a;
        }
        if ( $a = 11 ){
            set $b 1;
        }
        echo $b;
    }

根据Nginx企业官网的一篇文章:If Is Evil,平时应该尽量谨慎用if。
除此以外,Nginx中要实现if...else...的语法也需要费一番周折。这里就不详细展开了。

教训2:location不包含参数

接下来尝试用正则表达式表现url中同时包含.pdf(不区分大小写)和“inline=1”参数。
考虑到问号可能需要转义,就用.来替代。于是写了类似如下的正则表达式:

location ~* ".*\.(pdf).(inline=1)"

但结果发现死活匹配不到inline=1的那段。反复尝试了多种正则表达式后,才想起来location不包含URI参数。。。
最终决定通过location匹配后缀,在location内用if匹配URI参数(inline=1):

        location ~* ".*\.(pdf)$" {
            # 省略其他
            if ($args ~ inline=) {
                # 替换header值逻辑
            }
            # proxy_pass逻辑
        }

教训3:当location为正则表达式时,proxy_pass不能包含URI部分

在写proxy_pass的时候,参考了“location /”的那段逻辑,写成了:

proxy_pass  http://docsvr/;

nginx -s reload的时候报错:

[root@nginx-internal proxy]# nginx -s reload
nginx: [emerg] "proxy_pass" cannot have URI part in location given by regular expression, or inside named location, or inside "if" statement, or inside "limit_except" block in /etc/nginx/conf.d/proxy/doc.conf:56

查了之后才得知当location为正则表达式时,proxy_pass不能包含URI部分。在此处“/”也是URI部分。所以去除了http://docsvr/ 最后的斜杠,调整为:

        location ~* ".*\.(pdf)$" {
            # 省略其他
            if ($args ~ inline=) {
                # 替换header值逻辑
            }
            proxy_pass  http://docsvr;
        }

在location后使用~*是为了让后缀忽略大小写。

教训4:proxy_set_header不能包含在if语句中

接下来就是要替换Content-Disposition值了。
我们先尝试将该值替换成其他任意值:

            if ($args ~ inline=) {
                proxy_set_header  'Content-Disposition' 'bbb';
            }

然后就在nginx -s reload的时候收到了报错:

nginx: [emerg] "proxy_set_header" directive is not allowed here in /etc/nginx/conf.d/proxy/doc.conf:32

从这篇How nginx "location if" works,我们可以知道Nginx实现if是通过一个嵌入的location。而不允许proxy_set_header很可能是因为嵌套的location不支持。
顺带提一句,除了proxy_set_header外,proxy_hide_header也不能包含在if语句中。

看上去我们只能靠变量了。逻辑大概如下:

            set $is_inline_pdf 0
            set $content_disposition 'attachment;filename*="utf-8\' \'attachement.pdf"';
            if ($args ~ inline=) {
                set $is_inline_pdf 1;
                set $content_disposition 'inline;filename*="utf-8\' \'inline.pdf"';
            }
            proxy_set_header 'Content-Disposition' $content_disposition;

教训5:proxy_set_header只能用来设置自定义header

上面那段配置测试后发现无效。事实上,不管proxy_set_header给Content-Disposition设置什么值都无效。
查询之后发现proxy_set_header可能只对自定义的header有效,但不能改非自定义的header。

改用add_header替换proxy_set_header,会因为出现两个Content-Disposition而无法正常展现。在Chrome下会显示ERR_RESPONSE_HEADERS_MULTIPLE_CONTENT_DISPOSITION的报错。

所以需要用proxy_hide_header + add_header,先隐藏后添加了。即:

proxy_hide_header 'Content-Disposition';
add_header 'Content-Disposition' $content_disposition;

教训6:if语句内外的add_header不会同时生效

附带发现了一个很神奇的现象:当在命中if条件时,只有if条件内的add_header语句会执行。例如在下面的这个例子中:

            add_header  'testa' 'aaa';
            if ($args ~ inline=) {
                add_header  'testb' 'bbb';
            }
            add_header  'testc' 'ccc';

按照我们其他语言中对if的理解,当符合条件($args ~ inline=)这个条件时,应该是testa/testb/testc三个header都会显示。
但实际上,当符合($args ~ inline=)这个条件时,只有testb这个header会显示;而如果不符合if条件时,testa和testc这两个header会显示。
原因应该也和How nginx "location if" works这篇中介绍的原理有关。

最终成果

最终语法如下:

            set $is_inline_pdf 0;
            if ($args ~ inline=) {
                set $is_inline_pdf 1;
            }

            proxy_hide_header 'Content-Disposition';
            if ($is_inline_pdf = 1) {
                add_header 'Content-Disposition' 'inline;filename*="utf-8\' \'inline.pdf"';
                proxy_pass  http://docsvr;
            }
            add_header 'Content-Disposition' 'attachment;filename*="utf-8\' \'attachement.pdf"';

            proxy_pass  http://docsvr;

理论上要做的更好的话,可以用$request_filename或$request_uri中的文件名来替换Content-Disposition中的文件名。但实际发现Content-Disposition中的文件名不影响浏览器中显示,也不影响下载的文件名。而且要截取$request_filename中的filename所需要写的正则表达式有点变态,于是这个问题就先搁置不做优化了。

最终的感想:Nginx对if的支持太有限了。。。应该是Nginx为了解析速度和性能所必要的代价吧。

扩展阅读

在查资料的时候顺带查到一篇挺有意思的文章和一个挺有用的网站:

通过正则表达式来DDOS还挺有创意。。。
一个由正则表达式引发的血案(解决版)

看到知乎上尤雨溪推荐的JS正则可视化的工具,对理解复杂正则挺有帮助。
Regexper


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM