Nginx 輕鬆搞定跨域問題

當你遇到跨域問題,不要立刻就選擇複製去嘗試。請詳細看完這篇文章再處理 。我相信它能幫到你。

分析前準備:

首先保證服務端是沒有處理跨域的,其次,先用 postman 測試服務端接口是正常的

當網站 8080 去訪問服務端接口時,就產生了跨域問題,那麼如何解決?接下來我把跨域遇到的各種情況都列舉出來並通過 nginx 代理的方式解決(後臺也是一樣的,只要你理解的原理)。

跨域主要涉及 4 個響應頭:

網上很多文章都是告訴你直接 Nginx 添加這幾個響應頭信息就能解決跨域,當然大部分情況是能解決,但是我相信還是有很多情況,明明配置上了,也同樣會報跨域問題。

什麼是預檢請求?:當發生跨域條件時候,覽器先詢問服務器,當前網頁所在的域名是否在服務器的許可名單之中,以及可以使用哪些 HTTP 動詞和頭信息字段。只有得到肯定答覆,瀏覽器纔會發出正式的XMLHttpRequest請求,否則就報錯。如下圖

開始動手模擬:

Nginx 代理端口:22222,配置如下

server {
        listen       22222;
        server_name  localhost;
        location  / {
            proxy_pass  http://localhost:59200;
        }
}

測試代理是否成功,通過 Nginx 代理端口 2222 再次訪問接口,可以看到如下圖通過代理後接口也是能正常訪問

接下來開始用網站 8080 訪問 Nginx 代理後的接口地址,報錯情況如下↓↓↓

情況 1:

Access to XMLHttpRequest at 'http://localhost:22222/api/Login/TestGet' from origin 'http://localhost:8080' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.

通過錯誤信息可以很清晰的定位到錯誤(注意看標紅部分)priflight 說明是個預請求,CORS 機制跨域會首先進行 preflight(一個 OPTIONS 請求), 該請求成功後纔會發送真正的請求。這一設計旨在確保服務器對 CORS 標準知情,以保護不支持 CORS 的舊服務器

通過錯誤信息,我們可以得到是預檢請求的請求響應頭缺少了 Access-Control-Allow-Origin,錯哪裏,我們改哪裏就好了。修改 Nginx 配置信息如下(紅色部分爲添加部分),缺什麼就補什麼,很簡單明瞭

server {
        listen       22222;
        server_name  localhost;
        location  / {
           add_header Access-Control-Allow-Origin 'http://localhost:8080';
           proxy_pass  http://localhost:59200; 
        }
    }

哈哈,當滿懷歡喜的以爲能解決後,發現還是報了同樣的問題

不過我們的配置沒什麼問題,問題在 Nginx

add_header 指令用於添加返回頭字段,當且僅當狀態碼爲圖中列出的那些時有效。如果想要每次響應信息都攜帶頭字段信息,需要在最後添加 always(經我測試,只有Access-Control-Allow-Origin這個頭信息需要加 always,其他的不加 always 也會攜帶回來),那我們加上試試

server {
        listen       22222;
        server_name  localhost;
        location  / {
           add_header Access-Control-Allow-Origin 'http://localhost:8080' always;
           proxy_pass  http://localhost:59200; 
        }
    }

修改了配置後,發現生效了,當然不是跨域就解決了,是上面這個問題已經解決了,因爲報錯內容已經變了

情況 2:

Access to XMLHttpRequest at 'http://localhost:22222/api/Login/TestGet' from origin 'http://localhost:8080' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: It does not have HTTP ok status.

通過報錯信息提示可以得知,是跨域瀏覽器默認行爲的預請求(option 請求)沒有收到 ok 狀態碼,此時再修改配置文件,當請求爲 option 請求時候,給瀏覽器返回一個狀態碼(一般是 204)

server {
        listen       22222;
        server_name  localhost;
        location  / {
           add_header Access-Control-Allow-Origin 'http://localhost:8080' always;
           if ($request_method = 'OPTIONS') {
                return 204;
           }
           proxy_pass  http://localhost:59200; 
        }
    }

當配置完後,發現報錯信息變了

情況 3:

Access to XMLHttpRequest at 'http://localhost:22222/api/Login/TestGet' from origin 'http://localhost:8080' has been blocked by CORS policy: Request header field authorization is not allowed by Access-Control-Allow-Headers in preflight response.

意思就是預請求響應頭Access-Control-Allow-Headers中缺少頭信息 authorization(各種情況會不一樣,在發生跨域後,在自定義添加的頭信息是不允許的,需要添加到請求響應頭Access-Control-Allow-Headers中,以便瀏覽器知道此頭信息的攜帶是服務器承認合法的,我這裏攜帶的是 authorization,其他的可能是 token 之類的,缺什麼加什麼),知道了問題所在,然後修改配置文件,添加對應缺少的部分,再試試

server {
        listen       22222;
        server_name  localhost;
        location  / {
           add_header Access-Control-Allow-Origin 'http://localhost:8080' always;
           if ($request_method = 'OPTIONS') {               add_header Access-Control-Allow-Headers 'authorization'; #爲什麼寫在if裏面而不是接着Access-Control-Allow-Origin往下寫?因爲這裏只有預檢請求才會檢查               return 204;           }         proxy_pass http://localhost:59200;     }
}

此時發現報錯問題又回到了情況 1

經測試驗證,只要if ($request_method = 'OPTIONS') 裏面寫了 add_header ,當爲預檢請求時外部配置的都會失效,爲什麼?↓↓。

官方文檔是這樣說的:

There could be several add_header directives. These directives are inherited from the previous level if and only if there are no add_header directives defined on the current level.

意思就是當前層級無 add_header 指令時,則繼承上一層級的add_header。相反的若當前層級有了add_header,就應該無法繼承上一層的add_header

配置修改如下:

server {
        listen       22222;
        server_name  localhost;
        location  / {
            add_header Access-Control-Allow-Origin 'http://localhost:8080' always;
            if ($request_method = 'OPTIONS') {
                add_header Access-Control-Allow-Origin 'http://localhost:8080';
                add_header Access-Control-Allow-Headers 'authorization';
                return 204;
            }
            proxy_pass  http://localhost:59200; 
        }
    }

此時改完發現跨域問題已經解決了,

不過以上雖然解決了跨域問題,但是考慮後期可能 Nginx 版本更新, 不知道這個規則會不會被修改,考慮到這樣的寫法可能會攜帶上兩個 Access-Control-Allow-Origin ,這種情況也是不允許的,下面會說到。所以配置適當修改如下:

server {
        listen       22222;
        server_name  localhost;
        location  / {
            if ($request_method = 'OPTIONS') {
                add_header Access-Control-Allow-Origin 'http://localhost:8080';
                add_header Access-Control-Allow-Headers 'authorization';
                return 204;
            }
            if ($request_method != 'OPTIONS') {
                add_header Access-Control-Allow-Origin 'http://localhost:8080' always;
            }
            proxy_pass  http://localhost:59200; 
        }
    }

還沒完,繼續聊 ↓↓

情況 4:

比較早期的 API 可能只用到了 POST 和 GET 請求,而Access-Control-Allow-Methods這個請求響應頭跨域默認只支持 POST 和 GET,當出現其他請求類型時候,同樣會出現跨域異常。

比如,我這裏將請求的 API 接口請求方式從原來的 GET 改成 PUT,在發起一次試試。在控制檯上會拋出錯誤:

Access to XMLHttpRequest at 'http://localhost:22222/api/Login/TestGet' from origin 'http://localhost:8080' has been blocked by CORS policy: Method PUT is not allowed by Access-Control-Allow-Methods in preflight response.

報錯內容也講的很清楚,在這個預請求中,PUT 方法是不允許在跨域中使用的,我們需要改下Access-Control-Allow-Methods的配置 (缺什麼加上麼,這裏我只加了 PUT,可以自己加全一點),讓瀏覽器知道服務端是允許的

server {
    listen 22222;
    server_name localhost;
    location / {
        if ($request_method = 'OPTIONS') {
            add_header Access-Control-Allow-Origin 'http://localhost:8080';
            add_header Access-Control-Allow-Headers 'content-type,authorization';
            add_header Access-Control-Allow-Methods 'PUT';#爲這麼只加在這個if中,不再下面的if也加上?因爲這裏只有預檢請求會校驗,當然你加上也沒事。
            return 204;
        }
        if ($request_method != 'OPTIONS') {
            add_header Access-Control-Allow-Origin 'http://localhost:8080' always;
        }
        proxy_pass http://localhost:59200;
    }
}

這裏注意一下,改成 PUT 類型後,Access-Control-Allow-Headers請求響應頭又會自動校驗content-type這個請求頭,和情況 3 是一樣的,缺啥補啥就行了。如果不加上content-type,則會報如下錯誤。

想簡單的話,Access-Control-Allow-HeadersAccess-Control-Allow-Methods可以設置爲 * , 表示全都匹配。但是Access-Control-Allow-Origin就不建議設置成 * 了,爲了安全考慮,限制域名是很有必要的。

都加上後,問題就解決了,這裏報 405 是我服務端這個接口只開放了 GET,沒有開放 PUT,而此刻我將此接口用 PUT 方法去請求,所以接口會返回這個狀態碼。

情況 5:

最後再說一種情況,就是後端處理了跨域,就不需要自己在處理了(這裏吐槽下,某些後端工程師自己改服務端代碼解決跨域,但是又不理解其中原理,網上隨便找段代碼黏貼,導致響應信息可能處理不完全,如 method 沒添加全,headers 沒加到點上,自己用的那個可能複製過來的並不包含實際項目所用到的,沒有添加 options 請求返回狀態碼等,導致 Nginx 再用通用的配置就會可能報以下異常)

Access to XMLHttpRequest at 'http://localhost:22222/api/Login/TestGet' from origin 'http://localhost:8080' has been blocked by CORS policy: The 'Access-Control-Allow-Origin' header contains multiple values '*, http://localhost:8080', but only one is allowed.

意思就是此刻Access-Control-Allow-Origin請求響應頭返回了多個,而只允許有一個,這種情況當然修改配置去掉Access-Control-Allow-Origin這個配置就可以了,不過遇到這種情況,建議 Nginx 配置和服務端自己解決跨域只選其一。

這裏注意如果按我上面的寫法,if $request_method = 'OPTIONS' 這個裏面的Access-Control-Allow-Origin可不能刪除,刪除!='OPTIONS'裏面的就好了,因爲這裏如果是預檢請求直接就 ruturn 了,請求不會再轉發到 59200 服務,如果也刪除了,就會報和情況 1 一樣的錯誤。所以爲什麼說要不服務端代碼層面解決跨域,要不就 Nginx 代理解決,不要混着搞,不然不明白原理的人,網上找一段代碼貼就很可能解決不了問題

再貼一份完整配置(*號根據自己‘喜好’填寫):

server {
        listen       22222;
        server_name  localhost;
        location  / {
            if ($request_method = 'OPTIONS') {
                add_header Access-Control-Allow-Origin 'http://localhost:8080';
                add_header Access-Control-Allow-Headers '*';
                add_header Access-Control-Allow-Methods '*';
                add_header Access-Control-Allow-Credentials 'true';
                return 204;
            }
            if ($request_method != 'OPTIONS') {
                add_header Access-Control-Allow-Origin 'http://localhost:8080' always;
                add_header Access-Control-Allow-Credentials 'true';
            }
            proxy_pass  http://localhost:59200; 
        }
    }

或者:

server {
        listen       22222;
        server_name  localhost;
        location  / {
            add_header Access-Control-Allow-Origin 'http://localhost:8080' always;
            add_header Access-Control-Allow-Headers '*';
            add_header Access-Control-Allow-Methods '*';
            add_header Access-Control-Allow-Credentials 'true';
            if ($request_method = 'OPTIONS') {
                return 204;
            }
            proxy_pass  http://localhost:59200; 
        }
    }

最後,這是一篇解決跨域遇到問題解決問題的過程,如果認真看完了,我相信應該都能很容易的理解,並且在實際使用中自己解決該問題,希望能幫助到大家,以上內容都是自己理解自己測試碼出來的,如有理解不對的地方,望大家指正。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/7hUUYY_BsLQLTcJtoDFAmA