在網頁開發的過程中,我們經常需要從不同來源請求資源。 然而,由於同源政策 (Same Origin Policy) 的限制,這些跨網域的請求常常會被瀏覽器攔截。

同源政策是瀏覽器的一項基本安全功能,用來防止惡意網站讀取或操作其他網站的敏感資料。 這項政策對網絡安全至關重要,但在需要訪問第三方 API 或服務時,這常常造成諸多不便。 本文介紹幾種常見的方法可以解決或繞過 CORS (跨來源資源共享) 問題。

如何判斷是否同源?

Mozilla 網站上有一個列表,可以幫助您理解兩個網址是否同源,假設 A 網址為 http://store.company.com/dir/page.html,下表列出與這個網址是否同源。

URL 是否同源 原因
http://store.company.com/dir2/other.html
http://store.company.com/dir/inner/another.html
https://store.company.com/secure.html scheme 不同
http://store.company.com:81/dir/etc.html port 不同
http://news.company.com/dir/other.html domain 不同

* scheme、domain、port 都要一樣才會被視為同源,否則為不同源。

方案一: 使用 Access-Control-Allow-Origin

解決 CORS 問題最直接的方法就是在伺服器端的 HTTP header 設置 Access-Control-Allow-Origin,此設置指示瀏覽器允許哪些來源的請求訪問資源。 例如,設置為 * 可以允許所有來源的請求,但出於安全考慮,建議僅允許特定的來源。 以下以 NGINX 為例:

            
                # 開放特定來源
                add_header 'Access-Control-Allow-Origin' 'http://www.example.com' always;

                # 允許的請求方法
                add_header 'Access-Control-Allow-Methods' 'GET, POST, PATCH, DELETE, PUT, OPTIONS' always;

                # 允許的請求 Header
                add_header 'Access-Control-Allow-Headers' '*' always;

                # 是否允許發送 Cookie,如果為允許發送 cookie,Access-Control-Allow-Origin 就不能設定為 *,必須明確指定要開放的網址清單。
                add_header 'Access-Control-Allow-Credentials' false;

                # Preflight Request 的快取時間,以秒為單位
                add_header 'Access-Control-Max-Age' 1728000;
            
        

方案二: 使用代理伺服器

由於同源政策只作用在瀏覽器上,因此只要不要透過瀏覽器 (前端 JavaScript) 取得跨域資料就好了。 而要達成這個目的,您可以自己在後端撰寫代理程式 (例如:PHP + CURL),也可以直接使用代理伺服器就可以了。 以下以 NGINX 為例:

            
                # 用於 WebSocket
                map $http_upgrade $connection_upgrade {
                    default upgrade;
                    ''      close;
                }
                server {
                    listen 8000;
                    location / {
                        # 假設您的網頁程式是 443 Port
                        # 而代理伺服器是 8000 Port
                        # 這樣也算跨源,所以也需要設定以下 Header
                        add_header 'Access-Control-Allow-Origin' '*' always;
                        add_header 'Access-Control-Allow-Methods' 'GET, POST, PATCH, DELETE, PUT, OPTIONS' always;
                        add_header 'Access-Control-Allow-Headers' '*' always;
                        add_header 'Access-Control-Allow-Credentials' false;
                        add_header 'Access-Control-Max-Age' 1728000;

                        # 實際的遠端 API 伺服器
                        proxy_pass https://api.example.com;
                        proxy_http_version 1.1;
                        proxy_set_header Host $http_host;
                        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
                        proxy_set_header X-Real-IP $remote_addr;
                        proxy_set_header X-Forwarded-Proto $scheme;
                        proxy_set_header Upgrade $http_upgrade;                 # 用於 WebSocket
                        proxy_set_header Connection $connection_upgrade;        # 用於 WebSocket
                        proxy_redirect http://$http_host/ https://$http_host/;
                        proxy_connect_timeout 1800s;
                        proxy_send_timeout 1800s;
                        proxy_read_timeout 1800s;
                    }
                }
            
        

以上使用後端 8000 Port 代理遠端 https://api.example.com API 服務,當然您也可以用 URL 子目錄的方式進行代理。 後續前端呼叫 API 時,將直接與代理伺服器溝通,而不是原始的 https://api.example.com 網址。

方案三: 使用 JSONP

JSONP (JSON with Padding) 是一種跨網域請求資料的方法,用於繞過 CORS 限制。 它通過動態建立 script 標籤並指定跨域 URL 作為 src 屬性來工作。 由於 script 標籤不受同源政策限制,因此可以透過此方法從不同來源獲取資料。 不過,這種方法只適用於 GET 請求,且需要伺服器支援 JSONP。 由於安全性問題,不建議在專案中使用此方法,以下範例看看就好。

前端程式

            
                // JSONP 與後端請求完,會執行這個 Function
                function jsonpCallback(data) {
                    console.log('獲取的資料:', data);
                }
                var script = document.createElement('script');
                script.src = 'http://example.com/data?callback=jsonpCallback';
                document.body.appendChild(script);
            
        

後端程式

            
                <?php
                // 從請求 URL 中取得 callback 參數
                $callback = filter_input(INPUT_GET, 'callback');

                // 將資料轉換成 JSON 格式
                $jsonData = json_encode(['name' => 'Test', 'age' => 30]);

                // 輸出封裝了 JSON 資料的 CallBack 函數調用
                header('Content-Type: application/javascript');
                echo $callback . '(' . $jsonData . ');';
            
        

參考資料