發表日期:2019年8月15日
跨域警告的發生
如果你做了一些前后端分離的項目,由於此時前端所在的服務地址與后端所在的服務地址不一樣,你可能會遇到一個請求被瀏覽器攔截了的問題,瀏覽器在檢測到當前頁面發起的請求不屬於當前域就會將其攔截,這是因為瀏覽器的“同源策略”。
那么,什么是同源策略呢?
同源策略用於限制頁面發起不同域(源)的請求,用於提高請求的安全性。
如果兩個頁面的協議、端口、IP地址(域名)都相同的話,那么這兩個頁面就是同源,也就是同一個域。舉例:
以http://192.168.10.1:8080/index.html為對照源,
http://192.168.10.1:8080/auth/login.html與它是同源;
http://192.168.10.1:8181/index.html與它不是同源,因為端口不一樣;
http://192.168.10.30:8080/index.html與它不是同源,因為IP地址不一樣。
有些人會問,既然域不一樣就會攔截,為什么我用了xxxCDN的css文件,這個請求沒有被攔截呢?
這里要提一些並不是所有的跨域請求都會被攔截的。
1.通常瀏覽器不會攔截一些跨域資源嵌入的請求。
這種所謂的資源嵌入,就是類似於<img>
標簽中的src,<script>
中的src,<link>
中的href這樣的請求,這樣的請求是直接請求資源嵌入到你的頁面中的。所以你使用某個cdn的css文件不會被攔截。【所以有種方式就是通過這種嵌入的方式來進行解決跨域的問題】
2.通常瀏覽器不會攔截一些跨域寫資源的請求。
這種所謂的跨域寫資源,就是所謂的超鏈接請求,頁面重定向,非XMLHttpRequest方式的表單提交(普通的form表單提交)等等。
3.通常瀏覽器會攔截跨域讀資源的請求。
XMLHttpRequest提交表單,XMLHttpRequest請求資源,【常見於異步操作】除此之外,要再次強調的是,同源策略是瀏覽器的安全策略。所以如果你直接通過postman這些能夠不借助瀏覽器來發http請求的軟件來發請求的話,它是不會攔截你的跨域請求的。
一個可以用於測試的例子:
(當我把下面的html文件部署到一個web服務器(tomcat,apache等)中的時候,此時這個網頁處於的域應該是我的本機地址localhost:80
,此時我根據上述的三個方面來測試瀏覽器是否攔截。結果是只有最后一個是被攔截的。【不要不部署就直接打開這個頁面,否則的話它只是一個普通的本地文件,而非網絡文件,此時瀏覽器不會認為這是一個域】)
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>用於測試跨域</title>
</head>
<body>
<!-- 跨域資源嵌入,允許 -->
<img src="https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1565884598468&di=bb6ccc7a3280b183e4e13c852bc8353c&imgtype=0&src=http%3A%2F%2Fs04.lmbang.com%2FM00%2FCA%2FDE%2FDpgiA1uPAOKAXmJfAADir1hWa-A750.gif" alt="">
<!-- 跨域資源寫操作,允許 -->
<a href="http://www.baidu.com">百度</a>
<form action="http://www.baidu.com" method="post" >
用戶名:<input type="text" name="username" value="" placeholder="">
<input type="submit" name="提交" value="提交">
</form>
<!-- 跨域資源讀操作,禁止 -->
<button onclick="senddata()">XMLHttpRequest請求</button>
<script type="text/javascript">
var xmlhttp=new XMLHttpRequest();
function senddata(){
xmlhttp.open("GET","https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1565884598468&di=bb6ccc7a3280b183e4e13c852bc8353c&imgtype=0&src=http%3A%2F%2Fs04.lmbang.com%2FM00%2FCA%2FDE%2FDpgiA1uPAOKAXmJfAADir1hWa-A750.gif",true);
xmlhttp.send();
</script>
</body>
</html>
補充:
為什么瀏覽器會使用同源策略?它想解決什么問題?
首先,先談一下cookie吧,cookie主要用於存儲一些當前網站的一些數據,在一些舊的web開發中有的還會把用戶登錄信息存儲到cookie中。那么,從安全的角度來考慮的話,你應該希望你的網站的cookie不能被另外一個網站使用(不然cookie中的數據就非常容易被別人竊取了),所以這就引入了域的概念,通過域來限制資源的使用,攔截跨域的資源請求。
如何允許跨域
有很多手段來解決跨域,但常見的用於解決跨域調用接口的問題就是CORS
CORS
- 如何允許跨域,一種解決方法就是目的域告訴請求者允許什么來源域來請求,那么瀏覽器就會知道B域是否允許A域發起請求。
- CORS("跨域資源共享"(Cross-origin resource sharing))就是這樣一種解決手段。
CORS使得瀏覽器在向目的域發起請求之前先發起一個OPTIONS方式的請求到目的域獲取目的域的信息,比如獲取目的域允許什么域來請求的信息。
此時目的域通常需要在響應頭中添加以下信息:
- Access-Control-Allow-Origin:用來聲明什么域可以向當前域發起請求。
- Access-Control-Allow-Methods:用來聲明可以向當前域發起什么類型的請求。
- Access-Control-Max-Age:用來指定本次OPTIONS請求的有效期,單位為秒,在此期間不用發出另一條OPTIONS請求。
- Access-Control-Allow-Headers:用來允許你附加什么特殊的請求頭來發起請求。【有些前后端分離項目會把token放到header中,這時候這個請求頭就需要Access-Control-Allow-Headers來聲明了】
【在OPTIONS請求成功后,瀏覽器會把這些信息記錄下來,用來判斷發往目的域的請求是否需要攔截。如果OPTIONS請求失敗,那么原本要發起的請求就不會發送。】
但有時候發請求是不會觸發OPTIONS請求的。如果這個請求符合以下條件的話:
1.請求的方式是GET、POST或HEAD。
2.請求頭屬於Accept,Accept-Language,Content-Language,Content-Type ,Viewport-Width。
3.請求頭中Content-Type屬於application/x-www-form-urlencoded、multipart/form-data、text/plain中的一個。這種請求也被稱為“簡單請求”。
簡單請求的測試:
下面的例子可以用於測試簡單和非簡單請求是否會發OPTIONS請求
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>用於測試簡單請求</title>
</head>
<body>
<button onclick="senddata()">XMLHttpRequest請求</button>
<script type="text/javascript">
var xmlhttp=new XMLHttpRequest();
function senddata(){
xmlhttp.open("GET","https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1565884598468&di=bb6ccc7a3280b183e4e13c852bc8353c&imgtype=0&src=http%3A%2F%2Fs04.lmbang.com%2FM00%2FCA%2FDE%2FDpgiA1uPAOKAXmJfAADir1hWa-A750.gif",true);
xmlhttp.send();
//如果你有一個可以用來測試的接口,可以嘗試把這一段注釋了,來測試是否發送OPTIONS請求。
// xmlhttp.open("POST","http://localhost:8080/hello",true);
//下面通過加了一個請求頭,使得這個請求不是一個不發OPTIONS的請求。
// xmlhttp.setRequestHeader("Content-Type", "application/json")
// xmlhttp.send();
}
</script>
</body>
</html>
后端的處理
下面基於Spring MVC框架來說明后端如何返回返回CORS響應頭(注:在spring mvc中,你可以直接使用@CrossOrigin
來簡單返回CORS響應頭。)。
前端請求測試代碼:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>用於測試OPTIONS</title>
</head>
<body>
<!-- 跨域資源讀操作,禁止 -->
<button onclick="senddata()">XMLHttpRequest請求</button>
<script type="text/javascript">
var xmlhttp=new XMLHttpRequest();
function senddata(){
//如果你有一個可以用來測試的接口,可以嘗試把這一段注釋了,來測試是否發送OPTIONS請求。
xmlhttp.open("POST","http://localhost:8080/hello",true);
//下面通過加了一個請求頭,使得這個請求不是一個不發OPTIONS的請求。
xmlhttp.setRequestHeader("Content-Type", "application/json")
xmlhttp.send();
}
</script>
</body>
</html>
當我們沒有部署接口的時候,我們就可以看到,頁面是發了一個OPTIONS請求的:
而且,瀏覽器控制台也提示以下信息:
當我們部署了接口的時候,我們先嘗試不返回正規的CORS響應頭。
由於沒有返回CORS響應頭,所以OPTIONS沒有請求到合適的CORS信息,所以請求就會被攔截,所以就會報下圖的兩個警告。
當我們在響應中添加CORS響應頭后,我們可以看到我們剛剛設置的CORS響應頭被OPTIONS請求成功了。
而且請求也被發出去了:
對於不同編程語言的如何使用CORS,可以自查。
補充:
- 除了正常的,例如通過js來處理跨域的。還有一些沙雕的手法,通過修改瀏覽器來不進行同源策略就是其中一種(治標不治本)。