web應用中的跨域問題以及解決
一、什麼是跨域
1、跨域的概念
同一個源下的資源與另一個源下的資源進行交換。切記 跨域僅僅是針對瀏覽器而言的,像兩個服務端之間執行http請求的則不屬於跨域。
跨域出現的場景:
1、前後端分離情況下,前臺請求不同源的後臺服務
2、微服務下,服務例項在多個不同源的服務上
3、資源共享,公共的靜態資源如圖片,音訊視訊等存放在一個服務中,其他web頁面訪問該服務獲取資源(其他web頁面與存放靜態資源的服務不同源)
2、為什麼出現跨域
瀏覽器的同源 : 瀏覽器針對 相同的協議(http、https),相同的ip,埠(port),或者域名相同的兩個頁面,則認為這兩個頁面為同源,例如 http://www.baidu.com/aaa.html 和http://www.baidu.com/bbb.html即可以被瀏覽器認為這兩個頁面為同源。而如下http://www.baidu.com/aaa.html 和http://www.58.cn/bbb.html即可以被瀏覽器認為這兩個頁面不同源。
瀏覽器的同源策略為:為了安全考慮,瀏覽器限制了同一個源載入的文件或指令碼不允許與另一個源的資源進行互動。這是一個用於隔離潛在惡意檔案的重要安全機制。
二、模擬跨域
- 本機啟動兩個程式,將其埠設定為不同,則這兩個應用程式即為不同源的應用程式,兩個應用程式分別為127.0.0.1:7070(A應用)和127.0.0.1:7071(B應用)
A應用的前臺頁面為
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <title>跨域測試</title> </head> <body> <div class="container"> <div class="starter-template"> <!--標籤的單擊事件 所屬應用A呼叫ajax操作訪問應用B的api (屬於跨域請求)--> <input id="btn" type="button" value="跨域獲取資料" onclick="test_ajax_cors()"/> <textarea id="text" style="width: 400px; height: 100px;"></textarea> </div> </div> <script type="text/javascript" th:src="@{/js/jquery-3.2.1.min.js}"></script> <script type="text/javascript" th:src="@{/js/home.js}"></script> </body> </html>
對應的js
//使用ajax 進行跨域請求
function test_ajax_cors(){
$.ajax({
method : 'get',
url : 'http://127.0.0.1:7071/jsonp',
success : function (message) {
alert(message);
},
async : true
})
}
直接訪問7071返回資料
通過應用A頁面(應用A再跨域訪問應用B)訪問出現如下:
跨域請求常見錯誤 ,其實在使用ajax啟動另一個同源應用的時候,請求已經到達了該應用,但是在響應到瀏覽器後,瀏覽器進行解析的時候基於同源安全策略丟擲上圖的錯誤。
三、解決方案
1、使用jsonp來解決跨域請求
使用jsonp解決跨域問題的原理:在頁面上直接發起一個跨域的ajax請求是不可以的,但是,在頁面上引入不同域上的js指令碼卻是可以的,所以我們可以在頁面構造一個<script>,通過該標籤的src屬性請求另一個非同源的資源。
- 原生js實現跨域請求
應用A的js/html:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>跨域測試</title>
</head>
<body>
<div class="container">
<div class="starter-template">
<input id="btn" type="button" value="跨域獲取資料" onclick="test_jsonp()"/>
<textarea id="text" style="width: 400px; height: 100px;"></textarea>
</div>
</div>
<script type="text/javascript" th:src="@{/js/jquery-3.2.1.min.js}"></script>
<script type="text/javascript" >
//應用A訪問應用B後回撥函式
function showData(response){
$("#text").html(response);
}
function test_jsonp(){
//構造一個js指令碼(利用了在頁面上可以引入不同域上的js指令碼)來實現跨域請求,
//而且後面引數需要有相應的回撥引數傳遞給不同源的應用中,
//由不同源的服務進行資料的填充,並響應到該頁面中
$("head").append("<script src='http://localhost:7071/jsonp?callback=showData'>
<\/script>");
}
</script>
</body>
</html>
應用B的服務端處理
/**
*使用jsonp來實現跨域請求的操作
* 所謂的jsonp 其實就是 json(資料) padding(填充)
* 填充則是由不同源的服務端進行處理,即先獲取到請求的回撥引數名稱
* 本應用中script 訪問的http://localhost:7071/jsonp?callback=showData 中的showData 為回撥函式名稱
* 為該回調函式填充資料引數
* @throws IOException
*/
@RequestMapping("/jsonp")
public void jsonp(HttpServletRequest request, HttpServletResponse response,Model model) throws IOException {
//獲取前臺提供的回撥引數
//前端傳過來的回撥函式名稱
String callback = request.getParameter("callback");
//用回撥函式名稱包裹返回資料,這樣,返回資料就作為回撥函式的引數傳回去了
String data = "jsonp";
//給訪問的頁面指令碼填充資料,並響應回去
String result = callback + "('"+ data +"')";
//在回撥函式中填充資料,並相應給另一個應用的script
response.getWriter().write(result);
}
- 使用jquery的ajax來實現跨域請求
//使用jquery的ajax來進行跨域請求
function test_ajax_jsonp(){
$.ajax({
method : 'get',
url : 'http://127.0.0.1:7071/jsonp',
dataType:'jsonp', //設定資料型別為jsonp型別 跨域請求的核心
jsonpCallback: "showData", //指定回撥函式名稱
success : function (message) {
$("#text").html(message);
},
async : true
})
將按鈕單擊事件繫結該函式即可,其他保持不變
檢視請求可以看到,jquery呼叫ajax 設定dataType:jsonp,則發起跨域請求的時候,會自動帶上callback回撥函式完成跨域請求操作。
jsonp缺點:只支援GET,不支援POST請求,不安全XSS,且需要服務端進行相關的資料填充操作處理。
2、postMessage:配合使用iframe
應用A
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>跨域測試</title>
</head>
<body>
<div class="container">
<div class="starter-template">
<form>
<p>
<label for="message">給iframe發一個資訊:</label>
<input type="text" name="message" value="i am a project" id="message" />
<input type="submit" />
</p>
</form>
<h4>目標iframe傳來的資訊:</h4>
<p id="test">暫無資訊</p>
<iframe id="iframe" src="http://127.0.0.1:7071/postMessage"></iframe>
</div>
</div>
<script type="text/javascript" >
//獲取子視窗
var win = document.getElementById("iframe").contentWindow;
document.querySelector('form').onsubmit=function(e){
//使用html的postMessage()方法
/*
postMessage是通訊物件的一個方法,所以向iframe通訊,
就是iframe物件呼叫postMessage方法。postMessage有兩個引數,缺一不可。
第一個引數是要傳遞的資料,第二個引數是允許通訊的域,“*”代表不對訪問的域進行判斷,可理解為允許所有域的通訊。
*/
win.postMessage(document.getElementById("message").value,"*");
if (e.preventDefault){
e.preventDefault();
e.returnValue = false;
}
};
//獲取子類的回覆訊息
window.onmessage=function(e){
document.getElementById("test").innerHTML = e.origin + "say:" + e.data;
};
</script>
</body>
</html>
應用B
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>跨域測試</title>
</head>
<body>
<div class="container">
<div class="starter-template">
<form>
<p>
<label for="message">給另一個應用回覆資訊:</label>
<input type="text" name="message" value="i am b project" id="message" />
<input type="submit" />
</p>
</form>
<p id="test">暫無資訊。</p>
</div>
</div>
<script type="text/javascript" >
//獲取父類視窗
var parentwin = window.parent;
//監聽postMessage事件獲取不同源傳遞過來的資訊
window.onmessage=function(e){
document.getElementById("test").innerHTML = e.origin + "say:" + e.data;
parentwin.postMessage('HI! 你好 <span>i am b project</span>。',"*");
};
</script>
</body>
</html>
3、cors:需要後臺配合進行相關的設定
使用CORS可以幫助我們快速實現跨域訪問,只需在服務端進行授權即可,無需在前端新增額外設定,比傳統的JSONP跨域更安全和便捷。
請求頭資訊由瀏覽器檢測到跨域自動新增,無需過多幹預,重點放在Response headers,它可以幫助我們在伺服器進行跨域授權,例如允許哪些原始域可放行,是否需要攜帶Cookie資訊等。即為使用者進行跨域請求的時候,請求是可以到達不同源的服務處的,但是在進行返回的時候,如果沒有設定相應的跨域請求會出現跨域請求錯誤
所以cors跨域請求的核心是請求不同源的響應返回的時候,設定Response headers
方式1:返回新的CorsFilter
@Configuration
public class CorsConfig {
//配置相應的響應訊息頭資訊 Response Header *表示匹配所有
private CorsConfiguration buildConfig() {
CorsConfiguration corsConfiguration = new CorsConfiguration();
//Access-Control-Allow-Origin
//表示允許哪些原始域進行跨域訪問。(字元陣列)
corsConfiguration.addAllowedOrigin("*");
//Access-Control-Allow-Headers
//表示跨域請求的頭部的允許範圍。
corsConfiguration.addAllowedHeader("*");
//Access-Control-Allow-Methods
//表示跨域請求的方式的允許範圍。(例如只授權GET/POST)
corsConfiguration.addAllowedMethod("*");
/*
Access-Control-Allow-Credentials
表示是否允許客戶端獲取使用者憑據。(布林型別)
使用場景:例如現在從瀏覽器發起跨域請求,並且要附帶Cookie資訊給伺服器。
則必須具備兩個條件:1. 瀏覽器端:傳送AJAX請求前需設定通訊物件XHR的withCredentials
屬性為true。 2.伺服器端:設定Access-Control-Allow-Credentials為true。
兩個條件缺一不可,否則即使伺服器同意傳送Cookie,瀏覽器也無法獲取。
*/
corsConfiguration.setAllowCredentials(true);
/*
Access-Control-Expose-Headers
表示暴露哪些頭部資訊,並提供給客戶端。
(因為基於安全考慮,如果沒有設定額外的暴露,
跨域的通訊物件XMLHttpRequest只能獲取標準的頭部資訊)
Access-Control-Max-Age
表示預檢請求 [Preflight Request] 的最大快取時間。
*/
return corsConfiguration;
}
@Bean
public CorsFilter corsFilter() {
//url 路徑對映
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", buildConfig());
return new CorsFilter(source);
}
}
方式2:重寫WebMvcConfigurer
public class MyWebMvcConfigurer implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
//新增對映路徑
registry.addMapping("/**")
//放行哪些原始域
.allowedOrigins("*")
//是否傳送Cookie資訊
.allowCredentials(true)
//放行哪些原始域(請求方式)
.allowedMethods("GET","POST", "PUT", "DELETE")
//放行哪些原始域(頭部資訊)
.allowedHeaders("*");
//暴露哪些頭部資訊(因為跨域訪問預設不能獲取全部頭部資訊)
//.exposedHeaders("Header1", "Header2");
}
}
方式3:使用註解(@CrossOrigin)
@RequestMapping("/cors")
@CrossOrigin(origins = "http://127.0.0.1:7070",allowCredentials = "true")
public String corsLogin(String message,Model model) {
model.addAttribute("messgae",message);
return "postMessage";
}
方式4:手工設定響應頭(HttpServletResponse )
cors配置參考:http://www.spring4all.com/article/177
上面配置結束後,就可以使用正常的ajax進行處理,而不用考慮前端進行處理複雜跨域請求。
4、document.domain:僅限於同一域名下的子域
5、websocket:需要後臺配合修改協議,不相容
6、proxy:使用代理去避開跨域請求,需要修改nginx、apache等的配置
至此,有關跨域問題的探討到此結束,有什麼不正確的還望不吝賜教,手寫不易,幫到你的話,希望您點個贊。