Stories

Detail Return Return

計算機網絡——常見的跨域方案 - Stories Detail

跨域是什麼?

跨域問題是瀏覽器的安全機制,即同源策略(Same-origin policy)

限制不同源之間的交互,從而保證資源的安全

同源策略限制內容
  • Cookie、LocalStorage、IndexedDB 等存儲性內容只有同源才能訪問
  • AJAX 請求發送後,響應內容被瀏覽器攔截了
  • DOM

跨域請求圖片

跨域 AJAX 請求

允許跨域加載的資源
  • img src=XXX
  • link href=XXX
  • script src=XXX
為什麼需要「同源策略」?

其實從上面的表現形式就能看出來了——我們不希望將我們資源被惡意網站獲取

所以在瀏覽器加以限制,阻止向未經授權的跨站數據訪問和跨站請求。一定程度上避免 XSS、CSRF 攻擊

動態請求就會有跨域的問題?

跨域只存在於瀏覽器,不存在於 node.js/python/java 等其它環境

跨域請求時,請求是否被髮送出去了?

表單的方式可以發起跨域請求可以正常發送請求,因為它不需要 JavaScript 來直接訪問響應內容

AJAX 的請求將會被正常發送,但響應的結果被瀏覽器攔截了。因為響應內容往往涉及 JavaScript 的讀寫,瀏覽器認為不安全,所以攔截了響應,不將數據傳遞我們使用

以上也説明了跨域並不能完全阻止 CSRF,畢竟請求是發出去了的

同源

圖源:https://segmentfault.com/a/1190000022398875

只有當協議、域名、端口三者完全一致,才認為同源

當不同源時,就會出現上面所説的「跨域問題」

示例
  • http://www.a.com/a.jshttp://www.a.com/b.js 同源
  • http://www.a.comhttps://www.a.com 不同源,因為它們分別為 http 和 https,協議不同。同時,端口也不同,http 默認端口為 80,https 默認端口為 443
  • http://www.a.comhttps://www.b.com 不同源,因為域名不同
  • http://www.a.com:8888http://www.a.com:7777 不同源,因為端口不同

解決跨域的方式

一般我們要解決的是「AJAX 請求」的跨域問題

因為這種跨域問題的存在,使得我們正常的請求響應也被瀏覽器攔截了

所以,問題的核心在於——只允許我們期望的跨域請求響應接收,除此之外的跨域請求響應都應該被阻止

CORS

CORS(跨域資源共享,Cross-Origin Resource Sharing)是一種跨域請求機制

允許服務器聲明哪些外部域名可以訪問其資源,瀏覽器通過響應頭判斷是否被允許跨域請求

CORS 機制會在實際的請求之前,對於「非簡單請求」,會先發出一個預檢請求(OPTIONS 請求),來詢問服務器是否接受跨域請求。而 OPTION 請求不受瀏覽器的「同源策略」限制

通過 HTTP 頭部中的 Access-Control-Allow-Origin 等字段,服務器可以明確指定允許哪些源的請求訪問資源

圖源:https://mp.weixin.qq.com/s/nTapgae7PHl2w7Y4ngpO2w

圖源:https://mp.weixin.qq.com/s/nTapgae7PHl2w7Y4ngpO2w

涉及到的 HTTP 請求頭部
  • Access-Control-Request-Method: 表示實際請求的 HTTP 方法(例如 POST、PUT 等)
  • Access-Control-Request-Headers: 表示實際請求中自定義的請求頭部
涉及到的 HTTP 響應頭部
  • Access-Control-Allow-Origin: 服務器響應頭部,指定哪些域名可以訪問資源。可以是單一域名或 *(表示允許所有域名訪問)
  • Access-Control-Allow-Methods: 允許的方法,如 GET, POST, PUT, DELETE
  • Access-Control-Allow-Headers: 指定允許的請求頭部
  • Access-Control-Allow-Credentials: 是否允許攜帶憑證。如 true 表示可以攜帶 cookie
CORS 的 cookie 問題

想要請求可以傳遞 cookie,需要同時滿足以下 3 個條件:

  1. web 請求設置 withCredentials。默認情況下在跨域請求,瀏覽器是不帶 cookie 的。但是我們可以通過設置 withCredentials 來進行傳遞 cookie
  2. HTTP 響應頭 Access-Control-Allow-Credentials 為 true
  3. HTTP 響應頭 Access-Control-Allow-Origin 為非 *

只要不滿足以上其一條件,瀏覽器會報錯,獲取不到返回值

示例

假設前端應用在 http://example.com,後端 API 在 http://api.example.com

發現是跨域請求,且為「非簡單請求」,瀏覽器會向後端發送 OPTION 請求:

OPTIONS /data HTTP/1.1
Host: api.example.com
Origin: http://example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type, Authorization

後端 API 需要在響應中加入以下頭部來支持跨域請求:

Access-Control-Allow-Origin: http://example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization

瀏覽器通過當前網頁的 URL 和請求的方法,與 CORS 響應頭比較,決定是否允許跨域訪問

假設此時我們訪問的是http://foo.com,此時向後端發送 OPTION 請求,獲得被允許的域為http://example.com。瀏覽器發現當前網頁的 URL 和被允許的域不一致,瀏覽器將禁止該網頁向該後端服務器跨域

SpringBoot 解決跨域示例
@Configuration
public class CorsConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOrigins("*")
                .allowedMethods("GET", "POST", "PUT", "DELETE")
                .allowedHeaders("*");
    }

}

以上配置就是通過 CORS 來解決跨域的。不過需要注意的是:

  • 假設使用了allowCredentials(true),即允許跨域請求攜帶憑證(例如 Cookies 或 Authorization 頭),那麼 allowedOrigins("*")不允許的,因為這會帶來潛在的安全風險——允許任意來源攜帶憑證,可能導致 跨站請求偽造(CSRF) 攻擊
  • 為了提高靈活性,Spring 5.x 引入了 allowedOriginPatterns 配置項,它允許使用 通配符(例如 *)來匹配多個域名,而不會引起上面提到的限制問題。即 allowedOriginPatterns("*")allowCredentials(true) 可以同時使用
  • 上面代碼的配置將允許所有請求訪問,這將帶來安全隱患。瀏所以,在生產環境中,應該指定特定的域名、請求方法、請求頭
  • 一般後端服務還會設置全局的「攔截器」,用於攔截所有請求,判斷是否登錄。所以,全局「攔截器」需要把所有 OPTION 請求放行,否則將無法觸發上面配置的 CORS 代碼,導致 OPTION 請求無法送達,進一步導致瀏覽器無法發送跨域請求
在後端服務中使用 CORS 解決跨域問題的缺點

由服務端來配置允許哪些請求的訪問,實現簡單

但是,如果有多個不同服務要部署,此時要修改跨域的配置的話,不僅需要去修改代碼,還要將服務重新編譯打包上線。這將帶來非常大的工作量。主要問題在於跨域處理和業務代碼耦合了

所以後端服務指定允許跨域請求的方案,不適合在大型服務中使用,只適合簡單的測試環境

Nginx 反向代理

Nginx 是 Web 網關,可以用於靜態資源映射、URL 重寫、動態修改請求頭、反向代理等功能

Nginx 也常常被用來解決跨域問題

方案一:讓前端和後端“同源”

Nginx 是中間層,前端實際上只與 Nginx 交互,至於後端是誰來服務並不關心,即 Nginx 充當反向代理的作用

瀏覽器訪問網頁,前端頁面是 Nginx 通過靜態資源映射獲取的。而前端向後端請求也是由 Nginx 轉發的。所以,在 Nginx 的協商下,前端和後端可以看做“同源”

適用場景:前端靜態映射和後端都使用同一個 Nginx

假設有一個前端應用和一個後端 API 上:

  • 前端應用/Nginx 地址: http://localhost
  • 後端 API: http://localhost:8888

前端和運維溝通好:

  • 當路徑為 /api,則轉發到後端 http://localhost:8888
  • 當路徑為 /,則為靜態資源映射,訪問本地的靜態資源
server {
    listen 80;
    server_name localhost; # 替換為你的域名或 IP 地址

    # 處理 /api 路徑的請求,代理到本地的 8888 端口
    location /api/ {
        proxy_pass http://localhost:8888/;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    # 對於所有其他請求,映射到靜態資源
    location / {
        root /path/to/your/static/files; # 替換為你的靜態文件路徑
        index index.html index.htm;
        try_files $uri $uri/ =404;
    }
}
方案二:Nginx 添加 CORS 頭部

如果前端服務和後端反向代理的 Nginx 並不在同一個服務器,那麼,前端頁面向反向代理的後端 Nginx 發送請求,肯定會遇到跨域問題(瀏覽器和 Nginx 之間) 。所以需要在 Nginx 中添加 CORS 頭部,解決瀏覽器和 Nginx 的跨域問題。而後端服務與 Nginx 之間,是不需要解決跨域問題的,因為它們並沒有「同源策略」的機制

適用場景:Nginx 和前端頁面並不同源

假設有一個前端應用和一個後端 API:

  • 前端應用: http://frontend.com
  • 後端 API: http://backend.com

設置 Nginx 反向代理並設置 CORS 響應頭

server {
    listen 80;
    server_name frontend.com;  # 前端域名

    location /api/ {  # 假設 API 路徑以 /api/ 開頭
        proxy_pass http://backend.com/;  # 轉發請求到後端 API

        # 設置 CORS 頭,允許前端跨域訪問後端資源
        add_header 'Access-Control-Allow-Origin' '*' always;  # 允許特定來源訪問
        add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
        add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization' always;
        # add_header 'Access-Control-Allow-Credentials' 'true' always;

        # 處理預檢請求(OPTIONS 請求)
        if ($request_method = 'OPTIONS') {
            add_header 'Access-Control-Allow-Origin' 'http://frontend.com' always;
            add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
            add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization' always;
            add_header 'Access-Control-Max-Age' 3600;  # 緩存預檢請求的時間,單位為秒
            return 204;  # 預檢請求的響應狀態碼
        }

        # 反向代理設置
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

JSONP

JSON with Padding

通過動態添加 script 標籤的方式繞過瀏覽器的同源策略,因為 script 標籤本身不受同源策略的限制

存在問題:僅支持 GET 方法

function handleResponse(data) {
  console.log(data);
}

var script = document.createElement('script');
script.src = 'http://example.com/api?callback=handleResponse';
document.body.appendChild(script);

關閉瀏覽器跨域

跨域是瀏覽器自身實現的安全機制。在其他服務中,一般是沒有實現跨域機制的。比如,通過 RPC 在兩個不同端口的 Java 服務互相調用時,是不受跨域限制的

既然跨域是瀏覽器開啓的安全機制,那自然是可以關閉的

不過不推薦關閉瀏覽器的跨域機制,弊遠大於利

總結

跨域是瀏覽器限制與非同源交互,所實現的安全機制

實際上還有其他解決跨域的方案:WebSocket、document.domain + Iframe、window.postMessage 等等。不過這些方案都只是在特定的場景中才能使用

對於實際的項目部署,可以採用以下更通用的方案:

  • 如果只是簡單的開發測試環境,可以選擇服務端配置 CORS
  • 如果是實際的生產環境,推薦 Nginx + CORS

公眾號【牛肉燒烤屋】

B 站【愛烤豬蹄的喬治】

參考資料

https://juejin.cn/post/6844903767226351623

https://juejin.cn/post/6844903553069219853

https://segmentfault.com/a/1190000022398875

https://mp.weixin.qq.com/s/nTapgae7PHl2w7Y4ngpO2w

user avatar u_16297326 Avatar pulsgarney Avatar blbl-blog Avatar u_13529088 Avatar u_11365552 Avatar jiangyi Avatar shumile_5f6954c414184 Avatar daqianduan Avatar lvlaotou Avatar tssc Avatar aitibao_shichangyingxiao Avatar aipaobudezuoyeben Avatar
Favorites 58 users favorite the story!
Favorites

Add a new Comments

Some HTML is okay.