我们有个需求要在打开合同PDF的时候,要将response的header里的Content-Disposition从
1 | attachment;filename*="utf-8\' \'文件名" |
改为
1 | 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 | .*\.(后缀1|后缀2)$ |
后缀替换成pdf后,就尝试写了如下的代码:
1 | if ($request_filename ~* ".*\.(pdf)" && $request_uri ~ "(.*)inline=1") { |
然而很快我就发现,Nginx不支持if(condition1 && condition2)的语法【捂脸】。。。
其实也有一些奇淫技巧可以实现AND和OR,比如这一篇,通过拼字符串的方式:
1 | location = /test_and/ { |
根据Nginx企业官网的一篇文章:If Is Evil,平时应该尽量谨慎用if。
除此以外,Nginx中要实现if…else…的语法也需要费一番周折。这里就不详细展开了。
教训2:location不包含参数
接下来尝试用正则表达式表现url中同时包含.pdf(不区分大小写)和“inline=1”参数。
考虑到问号可能需要转义,就用.来替代。于是写了类似如下的正则表达式:
1 | location ~* ".*\.(pdf).(inline=1)" |
但结果发现死活匹配不到inline=1的那段。反复尝试了多种正则表达式后,才想起来location不包含URI参数。。。
最终决定通过location匹配后缀,在location内用if匹配URI参数(inline=1):
1 | location ~* ".*\.(pdf)$" { |
教训3:当location为正则表达式时,proxy_pass不能包含URI部分
在写proxy_pass的时候,参考了“location /”的那段逻辑,写成了:
1 | proxy_pass http://docsvr/; |
nginx -s reload的时候报错:
1 | [root@nginx-internal proxy]# nginx -s reload |
查了之后才得知当location为正则表达式时,proxy_pass不能包含URI部分。在此处“/”也是URI部分。所以去除了http://docsvr/ 最后的斜杠,调整为:
1 | location ~* ".*\.(pdf)$" { |
在location后使用~*是为了让后缀忽略大小写。
教训4:proxy_set_header不能包含在if语句中
接下来就是要替换Content-Disposition值了。
我们先尝试将该值替换成其他任意值:
1 | if ($args ~ inline=) { |
然后就在nginx -s reload的时候收到了报错:
1 | 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语句中。
看上去我们只能靠变量了。逻辑大概如下:
1 | set $is_inline_pdf 0 |
教训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,先隐藏后添加了。即:
1 | proxy_hide_header 'Content-Disposition'; |
教训6:if语句内外的add_header不会同时生效
附带发现了一个很神奇的现象:当在命中if条件时,只有if条件内的add_header语句会执行。例如在下面的这个例子中:
1 | add_header 'testa' 'aaa'; |
按照我们其他语言中对if的理解,当符合条件($args ~ inline=)这个条件时,应该是testa/testb/testc三个header都会显示。
但实际上,当符合($args ~ inline=)这个条件时,只有testb这个header会显示;而如果不符合if条件时,testa和testc这两个header会显示。
原因应该也和How nginx “location if” works这篇中介绍的原理有关。
最终成果
最终语法如下:
1 | set $is_inline_pdf 0; |
理论上要做的更好的话,可以用$request_filename或$request_uri中的文件名来替换Content-Disposition中的文件名。但实际发现Content-Disposition中的文件名不影响浏览器中显示,也不影响下载的文件名。而且要截取$request_filename中的filename所需要写的正则表达式有点变态,于是这个问题就先搁置不做优化了。
最终的感想:Nginx对if的支持太有限了。。。应该是Nginx为了解析速度和性能所必要的代价吧。
扩展阅读
在查资料的时候顺带查到一篇挺有意思的文章和一个挺有用的网站:
通过正则表达式来DDOS还挺有创意。。。
一个由正则表达式引发的血案(解决版)
看到知乎上尤雨溪推荐的JS正则可视化的工具,对理解复杂正则挺有帮助。
Regexper