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