我們有個需求要在打開合同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