常见跨域方法原理及其用例
一、常见跨域方法
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跨域:
客户端代码:
<!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"] . ' 信息保存成功!"}'; } ?>
运行结果:
AJAX跨域 CORS:
原理:跨域是浏览器行为(浏览器阻止跨域),服务器不存在跨域问题,客户端发出请求到服务器,服务器端通过设置相关信息(允许哪些域、允许哪些头部、允许哪些数据类型、是否允许发送cookie等),服务器返回响应信息(包含前面设置的相关信息)给客户端,客户端接接受响应,看到这些信息,允许对应的跨域请求。
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"> <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方法:
即:非简单请求分为 ,"预检"请求 + 简单请求。
<!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 * 没更换 iframe.src 之前执行完其他代码 现在属于域 domainB * * 然后 * iframe.src=domianA/a.html 现在属于域 domainB * 每更换 iframe.src 之前执行完其他代码 现在属于域 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)
原理:
<!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想要的资源,但不同域)、代理服务器P(跟A同一个域),页面 A将请求发送给代理服务器(同一个域),由代理服务器P向服务器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