最近在做前後端交互的時候遇到了特殊的跨域問題。其實自己平時做項目的時候也多多少少處理過跨域問題,但是這次情況特殊。正好借此機會將跨域問題的解決方法做一下總結,以備不時之需。
情景#
自己前段時間在學習前端,所以想趁著這個機會自己搞個小工具玩玩。所以就有了如下這個極其簡陋的彈幕網站。
為了實現彈幕的持久保存,自己嘗試前後端交互將彈幕存儲在數據庫中,然後前端每間隔一段時間就從後台拉取一次數據。
實現其實比較簡單,但是當我在本地開始測試向後台發送請求的時候,問題出現了:
很明顯這是一個跨域問題,所以接下來我們就要動用我們所學的知識去解決它。
首先第一步,分析問題原因:什麼是跨域?
什麼是跨域?#
跨域是指一個域下的文檔或腳本試圖去請求另一個域下的資源
跨域其實不是什么 bug,而是由於瀏覽器為了安全起見指定了一系列 “同源策略”,這些同源策略可以保證用戶信息的安全,防止被惡意的網站竊取數據。
該政策限制網頁的某些行為必須限制在與自己 “同源” 的網頁中才能進行,如果不是同源,就會導致該行為無法生效,也就是跨域失敗。
這裡出現了兩個概念,一個是同源,一個是跨域行為
同源的定義包含三個方面
- 協議相同
- 域名相同
- 端口相同
只有三個條件都滿足,才能認定兩個網頁是” 同源 “
跨域行為(自己定義的名詞,大概就是我們會被同源政策影響到的操作)隨著互聯網的發展,範圍變得越來越寬泛,一般我們常見的跨域行為包括
- 獲取 Cookie、LocalStorage 和 IndexDB
- 獲得 DOM 和 JS 對象
- AJAX 請求
我們可以看到在前後端交互中最常見的 AJAX 請求也赫然在列,在前後端交互中解決跨域問題是不可避免的。
那為什麼要定義同源策略呢?沒有跨域限制不是更好嗎?
如果沒有跨域限制,網頁將很容易受到 XSS、CSRF 等攻擊,因為沒有限制,惡意網站同樣可以自由地發起攻擊,這將大大提高網站的維護成本。因此,同源策略其實是一把雙刃劍,只是在保護網頁的同時,偶爾總會誤傷友軍。
知道了問題的根源,我們就可以對症下藥,尋找解決方案
解決方案#
我們可以看到,跨域問題的關鍵在於我們的 M請求處於限制範圍內,沒有做到同源,從而導致的。
重點已經標出來了,其實我們解決的方法也就是從這兩個思路著手
- 采用不在同源策略的行為操作
- 想辦法讓行為處於同源狀態
這裡我們指針對 AJAX 請求,對於其他諸如 cookie、iframe 等的跨域方案,其實參考相關的博客相信一定能得到答案
1.JSONP 跨域#
我們可以通過在 AJAX 請求中定義 JSONP 類型實現跨域,雖說如此,但 JSONP 本質上采用的是和 AJAX 完全不同的請求方式。
傳統的 AJAX 請求其實是xhr
的異步請求,而 JSONP 本質上是去構建一個<script>
標籤,利用script
標籤中的src
不受同源政策的限制,在src
中填寫後端URL
並添加回調函數,獲取到的數據就通過回調函數處理。
參考阮一峰的博客,實現思想大致如此:
function addScriptTag(src) {
var script = document.createElement('script');
script.setAttribute("type","text/javascript");
script.src = src;
document.body.appendChild(script);
}
window.onload = function () {
addScriptTag('http://example.com/ip?callback=foo');
}
function foo(data) {
console.log('response data: ' + JSON.stringify(data));
};
由於
<script>
元素請求的腳本,直接作為代碼運行。這時,只要瀏覽器定義了foo
函數,該函數就會立即調用。作為參數的 JSON 數據被視為 JavaScript 對象,而不是字符串,因此避免了使用JSON.parse
的步驟。
如何在 AJAX 實現:
$.ajax({
url: 'http://www.domain2.com:8080/login',
type: 'get',
dataType: 'jsonp', // 請求方式為jsonp
jsonpCallback: "handleCallback", // 自定義回調函數名
data: {}
});
如何在 vue 上實現:
this.$http.jsonp('http://www.domain2.com:8080/login', {
params: {},
jsonp: 'handleCallback'
}).then((res) => {
console.log(res);
})
從實現原理上可以看出 JSONP 還是存在弊端,那就是使用 JSONP 必須是 GET 請求,如果要 POST 請求實現跨域,還是需要使用其他方法
2.WebSocket#
websocket
本身就是一種通信協議,通過websocket
通信,實際上就可以跨過同源策略,實現某種意義上的 "同源"。
下面是websocket
請求的 HTTP 頭信息,重點關注Origin
字段,這是實現跨域的關鍵
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
Origin: http://example.com
Origin 字段表示該請求的請求源,只要 Origin 字段中的源域名和請求的目的域名是同一個,就可以通過同源策略中的域名一致,實現跨域。
如果允許通信,WebSocket 的響應頭如下
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
Sec-WebSocket-Protocol: chat
3.CORS#
CORS 即跨域資源共享 "(Cross-origin resource sharing),它允許瀏覽器向跨源服務器,發出 XMLHttpRequest 請求,從而克服了 AJAX 只能同源使用的限制。
這是解決跨域問題的常用方法。
實現原理#
其實現原理如圖
CORS 請求主要分成兩類:簡單請求和非簡單請求。
滿足以下條件的就是簡單請求,否則就是非簡單請求
(1) 請求方法是以下三種方法之一:
HEAD
GET
POST
(2)HTTP 的頭信息不超出以下幾種字段:
Accept
Accept-Language
Content-Language
Last-Event-ID
Content-Type
:只限於三個值application/x-www-form-urlencoded
、multipart/form-data
、text/plain
簡單 CORS 請求只是在請求的時候在 http 頭中加入 Origin 字段
非簡單 CORS 請求的話,瀏覽器會在正式通信後先發送預檢請求,先詢問服務器是否允許請求,得到響應,檢查相關字段後就可以做出回應,發起正式請求
假設現在發起一段 js 腳本
var url = 'http://api.alice.com/cors';
var xhr = new XMLHttpRequest();
xhr.open('PUT', url, true);
xhr.setRequestHeader('X-Custom-Header', 'value');
xhr.send();
其中預檢請求的請求方法是 OPTIONS,具體請求頭類似如下
OPTIONS /cors HTTP/1.1
Origin: http://api.bob.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: X-Custom-Header
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...
主要關注三個字段
-
Origin
表示請求來自哪個源
-
Access-Control-Request-Method
該字段是必須的,用來列出瀏覽器的 CORS 請求會用到哪些 HTTP 方法,上例是
PUT
。 -
Access-Control-Request-Headers
該字段是一個逗號分隔的字符串,指定瀏覽器 CORS 請求會額外發送的頭信息字段,上例是
X-Custom-Header
。
得到響應如下之後就能確認允許跨域請求
HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:15:39 GMT
Server: Apache/2.0.61 (Unix)
Access-Control-Allow-Origin: http://api.bob.com
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom-Header
Content-Type: text/html; charset=utf-8
Content-Encoding: gzip
Content-Length: 0
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Content-Type: text/plain
其中
//表示支持任意跨域請求
Access-Control-Allow-Origin: *
//表示支持跨域請求的方法
Access-Control-Allow-Methods: GET, POST, PUT
//當瀏覽器請求包含Access-Control-Request-Headers的時候必需,表示支持的頭信息字段
Access-Control-Allow-Headers: X-Custom-Header
//允許發送cookie和認證信息
Access-Control-Allow-Credentials: true
//指定本次預檢請求的有效期
Access-Control-Max-Age: 1728000
實現方式#
這裡主要是後端的操作,這裡用了java
springboot
的跨域方式做為樣例
@Configuration
public class CORSConfiguration {
@Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurer() {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowedHeaders("*")
.allowCredentials(true)
.allowedMethods("GET", "POST", "DELETE", "PUT","PATCH")
.maxAge(3600);
}
};
}
}
其實就是在服務端響應的時候添加請求頭,springboot
還支持在不同controller
上使用註解添加
4.nginx 代理跨域#
這個原理也簡單,其實就是讓前端和後端處於同源上,利用 nginx 的反向代理可以修改請求的域名、端口,也能添加 cookie 信息啥的實現跨域
#proxy服務器
server {
listen 81;
server_name www.domain1.com;
location / {
proxy_pass http://www.domain2.com:8080; #反向代理
proxy_cookie_domain www.domain2.com www.domain1.com; #修改cookie里域名
index index.html index.htm;
# 當用webpack-dev-server等中間件代理接口訪問nignx時,此時無瀏覽器參與,故沒有同源限制,下面的跨域配置可不啟用
add_header Access-Control-Allow-Origin http://www.domain1.com; #當前端只跨域不帶cookie時,可為*
add_header Access-Control-Allow-Credentials true;
}
}
另外使用一些中間件的代理方式其原理都是這回事,這裡就不加贅述
分析問題#
一通分析#
上面解決方案說了一大堆,但最終還是要回歸我們的問題,這次,我們開始對症下藥。首先再看一遍報錯日誌:
嗯?好像和想象中的不太一樣,常見的跨域問題應該如同:
這種,看起來其中有詐?
果不其然,通過後台添加跨域設置,我們的報錯信息依然沒有變化。
這時我們就需要對報錯信息好好分析(其實這應該是分析日誌的第一步,為了強行引入跨域解決方案,因此特地將分析放在了後面)
這句話引起了我的注意
Cross origin requests are only supported for protocol schemes: http, data, chrome, chrome-extension, https.
通過查閱資料發現,原來這裡我的前後端交互都是再本地實現,本地打開 html 使用的 file 協議,但是 file 協議的請求無法被瀏覽器認可,網上提供的方法如下
在谷歌瀏覽器下的快捷方式位置

在目標處添加:
"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe" -args --disable-web-security --user-data-dir --allow-file-access-from-files
大概就是這樣,但是我還是推薦使用這種方法,因為這樣的方式並不是特別優雅的解決方法
另外的嘗試#
另外的解決方法就是在本地部署 nginx,諸如上面提到過的解決方法,不通過 file 協議打開文件。
什麼是 file 協議的打開方式?
大概就是這種
要換成用 http 形式打開的方式,諸如這種
還有如果不嫌麻煩直接把網頁部署到服務器上也是一種解決方法
意外的結果#
但是!!最終問題還是沒有得到解決!這可把我難到了。。
事必有因,經過一個多小時的不懈努力,我終於找到了問題的根源
————
ajax 的請求 URL 必須以 http 的格式。。。。
啊啊啊啊果然還是我太菜了。。
因為剛學 ajax,對其原理不熟,導致最後出現了這種問題。