跨域是什麼?
跨域問題是瀏覽器的安全機制,即同源策略(Same-origin policy)
限制不同源之間的交互,從而保證資源的安全
同源策略限制內容
- Cookie、LocalStorage、IndexedDB 等存儲性內容只有同源才能訪問
- AJAX 請求發送後,響應內容被瀏覽器攔截了
- DOM
允許跨域加載的資源
- img src=XXX
- link href=XXX
- script src=XXX
為什麼需要「同源策略」?
其實從上面的表現形式就能看出來了——我們不希望將我們資源被惡意網站獲取
所以在瀏覽器加以限制,阻止向未經授權的跨站數據訪問和跨站請求。一定程度上避免 XSS、CSRF 攻擊
動態請求就會有跨域的問題?
跨域只存在於瀏覽器,不存在於 node.js/python/java 等其它環境
跨域請求時,請求是否被髮送出去了?
表單的方式可以發起跨域請求可以正常發送請求,因為它不需要 JavaScript 來直接訪問響應內容
AJAX 的請求將會被正常發送,但響應的結果被瀏覽器攔截了。因為響應內容往往涉及 JavaScript 的讀寫,瀏覽器認為不安全,所以攔截了響應,不將數據傳遞我們使用
以上也説明了跨域並不能完全阻止 CSRF,畢竟請求是發出去了的
同源
只有當協議、域名、端口三者完全一致,才認為同源
當不同源時,就會出現上面所説的「跨域問題」
示例
http://www.a.com/a.js與http://www.a.com/b.js同源http://www.a.com與https://www.a.com不同源,因為它們分別為 http 和 https,協議不同。同時,端口也不同,http 默認端口為 80,https 默認端口為 443http://www.a.com與https://www.b.com不同源,因為域名不同http://www.a.com:8888與http://www.a.com:7777不同源,因為端口不同
解決跨域的方式
一般我們要解決的是「AJAX 請求」的跨域問題
因為這種跨域問題的存在,使得我們正常的請求響應也被瀏覽器攔截了
所以,問題的核心在於——只允許我們期望的跨域請求響應接收,除此之外的跨域請求響應都應該被阻止
CORS
CORS(跨域資源共享,Cross-Origin Resource Sharing)是一種跨域請求機制
允許服務器聲明哪些外部域名可以訪問其資源,瀏覽器通過響應頭判斷是否被允許跨域請求
CORS 機制會在實際的請求之前,對於「非簡單請求」,會先發出一個預檢請求(OPTIONS 請求),來詢問服務器是否接受跨域請求。而 OPTION 請求不受瀏覽器的「同源策略」限制
通過 HTTP 頭部中的 Access-Control-Allow-Origin 等字段,服務器可以明確指定允許哪些源的請求訪問資源
涉及到的 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 個條件:
- web 請求設置 withCredentials。默認情況下在跨域請求,瀏覽器是不帶 cookie 的。但是我們可以通過設置 withCredentials 來進行傳遞 cookie
- HTTP 響應頭 Access-Control-Allow-Credentials 為 true
- 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