常見跨域方法原理及其用例
一、常見跨域方法
1) JSONP跨域 需要目標伺服器配合一個callback函式
2) AJAX跨域 CORS
3) 使用window.name+iframe來進行跨域
4) window.postMessage:跨文件通訊 API(Cross-document messaging)
5) 跨子域:修改document.domain
6) 通過Nginx反向代理
7) WebSocket
二、原理及其用例
JSONP跨域:
原理: <script>可跨域請求資源,json格式被原生 JavaScript支援,客戶端與伺服器端配合 客戶端動態定義並實現一個函式,將函式新增到請求的目標 URL中,通過建立 <script src="URL">跨域請求資源 伺服器端接受到請求,獲取新增在請求 URL中的函式,將需要的資料以引數的形式傳入獲取到的函式中並返回 客戶端獲取到帶有引數(需要的資料)的函式,執行該函式(客戶端已經定義並實現了該函式),處理資料 用例:客戶端程式碼:
<!DOCTYPE HTML> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <title>JSONP</title> <style> body, input, select, button, h1 { font-size: 28px; line-height:1.7; } </style> </head> <body> <h1>員工查詢</h1> <label>請輸入員工編號:</label> <input type="text" id="keyword" /> <button id="search">查詢</button> <p id="searchResult"></p> <script> /** * 原理: * <script>可跨域請求資源,json格式被原生 JavaScript支援,客戶端與伺服器端配合 * 客戶端動態定義並實現一個函式,將函式新增到請求的目標 URL中,通過建立 <script src="URL">跨域請求資源 * 伺服器端接受到請求,獲取新增在請求 URL中的函式,將需要的資料以引數的形式傳入獲取到的函式中並返回 * 客戶端獲取到帶有引數(需要的資料)的函式,執行該函式(客戶端已經定義並實現了該函式),處理資料*/ function myJSONP(url) { //建立一個十位數的隨機數 var randomNumber = Math.random().toString().substring(2, 12); // 生成 cbname(jsonp請求用到的回撥函式,後面會新增到URL中) var cbname = "callbackName" + randomNumber; // 將 cbname函式掛載到 myJSONP函式上(即 myJSONP裡面有一個 cbname函式) var myJSONP_cbname = "myJSONP." + cbname; //實現 cbname回撥函式 myJSONP[cbname] = function (response) { try { // var data=JSON.parse(data); //返回的資料已經是json格式,所以不用轉換,否則錯誤 if (response.success) document.querySelector("#searchResult").innerHTML=response.msg;//請求成功 else document.querySelector("#searchResult").innerHTML="出現錯誤:"+response.msg;//請求失敗 } finally { //請求完成,刪除函式以及移除指令碼 delete myJSONP[cbname]; script.parentNode.removeChild(script); } }; //建立script用於傳送請求 var script = document.createElement("script"); //將 myJSONP裡面的 cbname函式新增到URL中 if (url.indexOf("?") === -1) { url += "?callback=" + myJSONP_cbname; } else { url += "&callback=" + myJSONP_cbname; } //將指令碼的 src指向請求URL,然後將指令碼新增到頁面中,觸發http請求 script.src = url; document.body.appendChild(script); } document.querySelector("#search").onclick=function(){ // jsonp跨域請求(模擬跨域請求) // jsonp.html 在瀏覽器中開啟的地址為: http://localhost/jsonp.html // jsonp.php 伺服器地址為: http://127.0.0.1:80/jsonp.php var url="http://127.0.0.1:80/jsonp.php?number="+document.querySelector("#keyword").value; myJSONP(url); } </script> </body> </html>
伺服器端程式碼:
<?php //設定頁面內容是html編碼格式是utf-8 // header("Content-Type: text/plain;charset=utf-8"); header("Content-Type: application/json;charset=utf-8"); //header("Content-Type: text/xml;charset=utf-8"); //header("Content-Type: text/html;charset=utf-8"); //header("Content-Type: application/javascript;charset=utf-8"); //定義一個多維陣列,包含員工的資訊,每條員工資訊為一個數組 $staff = array ( array("name" => "洪七", "number" => "101", "sex" => "男", "job" => "總經理"), array("name" => "郭靖", "number" => "102", "sex" => "男", "job" => "開發工程師"), array("name" => "黃蓉", "number" => "103", "sex" => "女", "job" => "產品經理") ); //判斷如果是get請求,則進行搜尋;如果是POST請求,則進行新建 //$_SERVER是一個超全域性變數,在一個指令碼的全部作用域中都可用,不用使用global關鍵字 //$_SERVER["REQUEST_METHOD"]返回訪問頁面使用的請求方法 if ($_SERVER["REQUEST_METHOD"] == "GET") { search(); } elseif ($_SERVER["REQUEST_METHOD"] == "POST"){ create(); } //通過員工編號搜尋員工 function search(){ $jsonp = $_GET["callback"]; //檢查是否有員工編號的引數 //isset檢測變數是否設定;empty判斷值為否為空 //超全域性變數 $_GET 和 $_POST 用於收集表單資料 if (!isset($_GET["number"]) || empty($_GET["number"])) { echo $jsonp . '({"success":false,"msg":"引數錯誤"})'; return; } //函式之外宣告的變數擁有 Global 作用域,只能在函式以外進行訪問。 //global 關鍵詞用於訪問函式內的全域性變數 global $staff; //獲取number引數 $number = $_GET["number"]; $result = $jsonp . '({"success":false,"msg":"沒有找到員工。"})'; //遍歷$staff多維陣列,查詢key值為number的員工是否存在,如果存在,則修改返回結果 foreach ($staff as $value) { if ($value["number"] == $number) { $result = $jsonp . '({"success":true,"msg":"找到員工:員工編號:' . $value["number"] . ',員工姓名:' . $value["name"] . ',員工性別:' . $value["sex"] . ',員工職位:' . $value["job"] . '"})'; break; } } echo $result; } //建立員工 function create(){ //判斷資訊是否填寫完全 if (!isset($_POST["name"]) || empty($_POST["name"]) || !isset($_POST["number"]) || empty($_POST["number"]) || !isset($_POST["sex"]) || empty($_POST["sex"]) || !isset($_POST["job"]) || empty($_POST["job"])) { echo '{"success":false,"msg":"引數錯誤,員工資訊填寫不全"}'; return; } //TODO: 獲取POST表單資料並儲存到資料庫 //提示儲存成功 echo '{"success":true,"msg":"員工:' . $_POST["name"] . ' 資訊儲存成功!"}'; } ?>
執行結果:
CORS請求分成兩類:簡單請求(simple request)和非簡單請求(not-so-simple request)
原理:詳情點選這裡(阮一峰的這篇部落格講得很清楚)
用例:
AJAX跨域之CORS "跨域資源共享"(Cross-origin resource sharing)GET請求之簡單請求:
客戶端程式碼:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Document</title> <style> body, input, select, button, h1 { font-size: 28px; line-height:1.7; } </style> </head> <body> <h1>查詢員工</h1> <label>請輸入員工編號:</label> <input type="text" id="keyword" /> <button id="search">查詢</button> <p id="searchResult"></p>
<script> function handlerResponse(response){ var response=JSON.parse(response); if (response.success) document.querySelector("#searchResult").innerHTML=response.msg;//請求成功 else document.querySelector("#searchResult").innerHTML="出現錯誤:"+response.msg;//請求失敗 } document.querySelector("#search").onclick=function(){ // CORS跨域請求(模擬跨域請求) // AjaxCORS.html 在瀏覽器中開啟的地址為: http://localhost/AjaxCORS.html // AjaxCORS.php 伺服器地址為: http://127.0.0.1:80/AjaxCORS.php var url="http://127.0.0.1:80/AjaxCORS.php?number="+document.querySelector("#keyword").value; ajaxGET(url,handlerResponse); } function ajaxGET(url,callback){ var xhr=new XMLHttpRequest(); xhr.open("GET",url); xhr.send(null); xhr.onreadystatechange=function(){ if(xhr.readyState==4 && xhr.status==200){ callback(xhr.responseText); } } } </script> </body> </html>
伺服器端程式碼:
<?php //設定頁面內容是html編碼格式是utf-8 // header("Content-Type: text/plain;charset=utf-8"); header('Access-Control-Allow-Origin:*'); header('Access-Control-Allow-Methods:POST,GET'); header('Access-Control-Allow-Credentials:true'); header("Content-Type: application/json;charset=utf-8"); //header("Content-Type: text/xml;charset=utf-8"); //header("Content-Type: text/html;charset=utf-8"); //header("Content-Type: application/javascript;charset=utf-8"); //定義一個多維陣列,包含員工的資訊,每條員工資訊為一個數組 $staff = array ( array("name" => "洪七", "number" => "101", "sex" => "男", "job" => "總經理"), array("name" => "郭靖", "number" => "102", "sex" => "男", "job" => "開發工程師"), array("name" => "黃蓉", "number" => "103", "sex" => "女", "job" => "產品經理") ); //判斷如果是get請求,則進行搜尋;如果是POST請求,則進行新建 //$_SERVER是一個超全域性變數,在一個指令碼的全部作用域中都可用,不用使用global關鍵字 //$_SERVER["REQUEST_METHOD"]返回訪問頁面使用的請求方法 if ($_SERVER["REQUEST_METHOD"] == "GET") { search(); } elseif ($_SERVER["REQUEST_METHOD"] == "POST"){ create(); } //通過員工編號搜尋員工 function search(){ //檢查是否有員工編號的引數 //isset檢測變數是否設定;empty判斷值為否為空 //超全域性變數 $_GET 和 $_POST 用於收集表單資料 if (!isset($_GET["number"]) || empty($_GET["number"])) { echo '{"success":false,"msg":"引數錯誤"}'; return; } //函式之外宣告的變數擁有 Global 作用域,只能在函式以外進行訪問。 //global 關鍵詞用於訪問函式內的全域性變數 global $staff; //獲取number引數 $number = $_GET["number"]; $result = '{"success":false,"msg":"沒有找到員工。"}'; //遍歷$staff多維陣列,查詢key值為number的員工是否存在,如果存在,則修改返回結果 foreach ($staff as $value) { if ($value["number"] == $number) { $result = '{"success":true,"msg":"找到員工:員工編號:' . $value["number"] . ',員工姓名:' . $value["name"] . ',員工性別:' . $value["sex"] . ',員工職位:' . $value["job"] . '"}'; break; } } echo $result; }?>
執行結果:
AJAX跨域之CORS "跨域資源共享"(Cross-origin resource sharing)POST請求之非簡單請求:
客戶端程式碼:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="Content-Type" content="application/json; charset=utf-8"> <title>AjaxCORS</title> <style> body, input, select, button, h1 { font-size: 28px; line-height:1.7; } </style> </head> <body> <h1>新建員工</h1> <label>請輸入員工姓名:</label> <input type="text" id="staffName" /><br> <label>請輸入員工編號:</label> <input type="text" id="staffNumber" /><br> <label>請選擇員工性別:</label> <select id="staffSex"> <option>女</option> <option>男</option> </select><br> <label>請輸入員工職位:</label> <input type="text" id="staffJob" /><br> <button id="save">儲存</button> <p id="createResult"></p> <script> function handlerResponse(response){ var response=JSON.parse(response); if (response.success) document.querySelector("#createResult").innerHTML=response.msg;//請求成功 else document.querySelector("#createResult").innerHTML="出現錯誤:"+response.msg;//請求失敗 } document.querySelector("#save").onclick=function(){ // CORS跨域請求(模擬跨域請求) // AjaxCORS.html 在瀏覽器中開啟的地址為: http://localhost/AjaxCORS.html // AjaxCORS.php 伺服器地址為: http://127.0.0.1:80/AjaxCORS.php var url="http://127.0.0.1:80/AjaxCORS.php"; var data= { "name": document.querySelector("#staffName").value, "number": document.querySelector("#staffNumber").value, "sex": document.querySelector("#staffSex").value, "job": document.querySelector("#staffJob").value }; data=JSON.stringify(data); ajaxPOST(url,data,handlerResponse); } function ajaxPOST(url,data,callback){ var xhr=new XMLHttpRequest(); xhr.open("POST",url); xhr.setRequestHeader("Content-type","application/json"); xhr.send(data); xhr.onreadystatechange=function(){ if(xhr.readyState==4 && xhr.status==200){ callback(xhr.responseText); } } } </script> </body> </html>
伺服器端程式碼:
<?php //設定頁面內容是html編碼格式是utf-8 header('Access-Control-Allow-Origin:*'); header('Access-Control-Allow-Headers:content-type'); // header('Access-Control-Allow-Methods:POST,GET,OPTION'); // header('Access-Control-Allow-Credentials:true'); // header("Content-Type: text/plain;charset=utf-8"); header("Content-Type: application/json;charset=utf-8"); //定義一個多維陣列,包含員工的資訊,每條員工資訊為一個數組 $staff = array ( array("name" => "洪七", "number" => "101", "sex" => "男", "job" => "總經理"), array("name" => "郭靖", "number" => "102", "sex" => "男", "job" => "開發工程師"), array("name" => "黃蓉", "number" => "103", "sex" => "女", "job" => "產品經理") ); //判斷如果是get請求,則進行搜尋;如果是POST請求,則進行新建 //$_SERVER是一個超全域性變數,在一個指令碼的全部作用域中都可用,不用使用global關鍵字 //$_SERVER["REQUEST_METHOD"]返回訪問頁面使用的請求方法 if ($_SERVER["REQUEST_METHOD"] == "GET") { search(); } elseif ($_SERVER["REQUEST_METHOD"] == "POST"){ create(); }//建立員工 function create(){ //判斷資訊是否填寫完全 $data=json_decode(file_get_contents('php://input'),true); //轉換成陣列 if (!$data["name"] || !$data["number"] || !$data["sex"] || !$data["job"]) { echo '{"success":false,"msg":"引數錯誤,員工資訊填寫不全"}'; return; } //TODO: 獲取POST表單資料並儲存到資料庫 //提示儲存成功 echo '{"success":true,"msg":"員工:' . $data["name"] . ' 資訊儲存成功!"}'; } ?>
執行結果:
首先是預檢請求 ,使用 OPTION方法
然後是正式請求,使用 POST方法:
即:非簡單請求分為 ,"預檢"請求 + 簡單請求。
使用window.name+iframe來進行跨域: 原理: 通過瀏覽器的 window.name 屬性實現跨域請求(每個瀏覽器視窗都有一個 window.name屬性) 原理:無論是否同源,只要在 “同一個窗口裡”,前一個網頁設定了這個屬性,後一個網頁就可以讀取它,如頁面 A中 有一個 iframe,iframe.src指向頁面 B,若頁面 B設定了 window.name屬性,那麼頁面 A中的那個 iframe就能獲 取到頁面 B中的 window.name(雖然 A中的 iframe能讀取到 B中的 window.name屬性,但是由於iframe在頁面 A中, 而 iframe.src指向頁面 B,瀏覽器會因為頁面 A與 iframe不同源而阻止獲取,因此還需要設定 iframe.src指向頁面 A所在的域) 用例: 頁面 a.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <!-- 假設這個頁面是域 www.aaa.com 下面的頁面 A.html --> <title>a.html</title> </head> <body> <h2>domainA/a.html</h2> <button id="btn">get_data_by_iframe_window.name</button> <h3 id="data"></h3> <script> //獲取按鈕繫結事件,新增一個 iframe var btn=document.getElementById("btn"); btn.onclick=function(){ var ifr=document.createElement("iframe"); //模擬跨域請求 // a.html 在瀏覽器中開啟的地址為: http://localhost/a.html // b.html 需要請求的頁面地址為: http://127.0.0.1:80/b.html ifr.src="http://127.0.0.1/b.html"; //新增 iframe到當前頁面中,並設定為不可見 ifr.style.display = 'none'; var body=document.getElementsByTagName("body")[0]; body.appendChild(ifr); //iframe.src會觸發 iframe.onload事件,因此使用標記來判斷 iframe.src是否已經更改 var flag=true; ifr.onload=function(){ if(flag){ flag=false; //為了不讓瀏覽器阻止不同源獲取window.name屬性的值,這裡需要設定 iframe與當前頁面在同一個域 //(也可以指向其他頁面,只要與建立當前 iframe的頁面在同一個域都可以) ifr.src="http://localhost/a.html"; } else{ //contentWindow屬性返回<iframe>元素的Window物件,由此獲取 b.html頁面中設定的 window.name屬性的值 document.querySelector("#data").innerText=ifr.contentWindow.name; //獲取資料完成,刪除 iframe body.removeChild(ifr); } } } /** * 假設當前頁面為 domainA/p.html,裡面有 iframe.src=domianA/a.html * 如果開始時候 iframe.src=domainB/b.html,後來 iframe.src=domianA/a.html時, * 那麼必須是先執行完 iframe.src=domainB/b.html這就語句後面的程式碼後,iframe才會屬於域 domainA, * 而在執行完 iframe.src=domainB/b.html這就語句後面的程式碼前,iframe依然屬於域 domainB。 * * 如:假設在頁面 domainA/p.html中 屬於域 domainA,iframe在 p.html中建立 * * 首先 * iframe.src=domainB/b.html * 執行完其他程式碼 現在屬於域 domainB * * 然後 * iframe.src=domianA/a.html 現在屬於域 domainB * 執行完其他程式碼 現在屬於域 domainA * */ </script> </body> </html>
頁面 b.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>b.html</title> </head> <body> <script> //b.html中設定 window.name的值(需要傳輸的資料) window.name = "我是需要傳輸的資料,來自 domainB/b.html"; </script> </body> </html>
執行結果:
window.postMessage:跨文件通訊 API(Cross-document messaging)
原理:
* HTML5為了解決跨域問題引入了一個全新的API:跨文件通訊 API(Cross-document messaging)。 * 這個API為window物件新增了一個window.postMessage方法,允許跨視窗通訊,不論這兩個視窗是否同源。* 使用方法: otherWindow.postMessage(message, targetOrigin, [transfer]); * otherWindow: * 其他視窗的一個引用,比如iframe的contentWindow屬性、執行window.open返回的視窗物件、或者是命名過或數值索引的window.frames * message: * 將要傳送到其他 window的資料。 * targetOrigin: * 通過視窗的origin屬性來指定哪些視窗能接收到訊息事件,其值可以是字串"*"(表示無限制)或者一個URI * 只有當目標視窗與 targetOrigin的源完全相同時訊息才能被成功傳送,只要協議、主機名、埠其中一項不同訊息都不會被髮送 * (這個引數就顯得尤為重要,必須保證它的值與這條包含密碼的資訊的預期接受者的origin屬性完全一致,來防止密碼被惡意的第三方截獲) * [transfer]: * 是一串和message 同時傳遞的 Transferable 物件. 這些物件的所有權將被轉移給訊息的接收方,而傳送一方將不再保有所有權。
* 父視窗和子視窗都可以通過message事件,監聽對方的訊息。 * message事件的事件物件event,提供以下三個屬性。 * event.source:記錄呼叫 window.postMessage()方法的視窗資訊 * event.origin: 表示呼叫 window.postMessage()方法時,呼叫頁面的當前狀態 * event.data: 要傳送到其他 window的資料 用例: 頁面 a.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>a.html</title> </head> <body> <script> /** * 本例中:模擬跨域 * a.html : http://localhost/a.html * b.html : http://127.0.0.1/b.html */ var popup=window.open("http://127.0.0.1/b.html","title b.html"); // targetOrigin為:http://127.0.0.1 協議: http 主機名: 127.0.0.1 埠號: 預設(80) // b.html中的域為:http://127.0.0.1 協議: http 主機名: 127.0.0.1 埠號: 預設(80) // 可見目標視窗與 targetOrigin的源完全相同 popup.postMessage("aaaaaa 在a.html中 通過 postMessage 傳送","http://127.0.0.1/b.html"); //監聽子視窗資訊 window.addEventListener("message",function(event){ //event.origin: 表示呼叫 window.postMessage()方法時,呼叫頁面的當前狀態 //在本例中,這裡監聽的是 b.html中 evnet.source.postMessage()事件,event.source.postMessage()的當前狀態還是屬於域 http://127.0.0.1 //console.log(event.origin); //輸出: http://127.0.0.1 if (event.origin !== 'http://127.0.0.1') return; console.log(event.data); }); </script> </body> </html>
頁面 b.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>b.html</title> </head> <body> <script> //監聽父視窗資訊 window.addEventListener("message",function(event){ //event.origin: 表示呼叫 window.postMessage()方法時,呼叫頁面的當前狀態 //在本例中,這裡監聽的是 a.html中 popup.postMessage()事件,popup.postMessage()的當前狀態還是屬於域 http://localhost //console.log(event.origin); //輸出: http://localhost if (event.origin !== 'http://localhost') return; console.log(event.data); //event.source:記錄呼叫 window.postMessage()方法的視窗資訊 //在本例中,這裡監聽的是 a.html中 popup.postMessage()事件,因此這裡 event.source記錄的是 a.html頁面視窗的資訊 // targetOrigin為:http:localhost 協議: http 主機名: localhost 埠號: 預設(80) // a.html中的域為:http:localhost 協議: http 主機名: localhost 埠號: 預設(80) // 可見目標視窗與 targetOrigin的源完全相同 event.source.postMessage("bbbbbb 在b.html中 通過 postMessage 傳送","http:localhost"); }); //當然也可以使用 window.opener傳送資訊 window.opener.postMessage("bbbbbb 在b.html中 通過 window.opener.postMessage 傳送","http:localhost"); </script> </body> </html>
執行結果:
跨子域:修改document.domain
原理:兩個文件上一層級的域名相同,下一層級(或該層級以下的域名不同),將兩個文件的 document.domain都修改為上一層級的域名(這樣他們的 document.domain就一樣了)
用例:
頁面一 http://a.test.com/a.html 域為: http://a.test.com 設定該頁面的 document.domain=test.com 設定 document.cookie= "hello=world" 頁面二 http://b.test.com/b.html 域為: http://b.test.com 設定該頁面的 document.domain=test.com 這裡 console.log(document.cookie) 輸出結果包含 "hello=world"
通過反向代理 (Reverse Proxy)
原理:反向代理(Reverse Proxy)方式是指以代理伺服器來接受Internet上的連線請求,然後將請求轉發給內部網路上的伺服器;並將從伺服器上得到的結果返回給Internet上請求連線的客戶端,此時代理伺服器對外就表現為一個伺服器。跨域時,頁面A 、頁面 B不同域,但是頁面 A與代理伺服器在同一個域,頁面 A將請求傳送給代理伺服器(同一個域),由代理伺服器到頁面 B獲取所需要的資料(跨域是瀏覽器阻止跨域,伺服器不存在跨域問題),然後代理伺服器將獲取的資料返回給頁面 A(同一個域)
用例:無
WebSocket 跨域
原理:WebSocket是一種通訊協議,使用ws://
(非加密)和wss://
(加密)作為協議字首。該協議不實行同源政策,只要伺服器支援,就可以通過它進行跨源通訊。
用例:無
參考:
MDN 官網:https://developer.mozilla.org/
阮一峰部落格-瀏覽器同源政策及其規避方法:http://www.ruanyifeng.com/blog/2016/04/same-origin-policy.html
阮一峰部落格-跨域資源共享 CORS 詳解:http://www.ruanyifeng.com/blog/2016/04/cors.html