跨域解决方案汇总
Posted princess-knight
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了跨域解决方案汇总相关的知识,希望对你有一定的参考价值。
一、同源与同源策略
我们知道,同源指的是协议、域名、端口号全部相同。同源策略(Same Origin Policy)是一种约定,它是浏览器最核心也是最基本的安全功能,如果缺少了同源策略,则浏览器的正常功能都可能会受到影响。Web是构建在同源策略基础之上的,浏览器只是针对同源策略的一种实现。同源策略是处于对用户安全的考量的,如果缺少了同源的限制,那又怎么能够确定别人的网站始终对你是友好的呢。针对非同源的情况制定了一些限制条件,1. 无法读取不同源的cookie、LocalStorage、indexDB。2. 无法获得不同源的DOM。3. 不能向不同源的服务器发送Ajax请求。
但是事实上,我们经常是需要借用非同源来提供数据的,所以这就牵涉到跨域方面的技术了。
二、JSONP
JSONP是指JSON Padding,JSONP是一种非官方跨域数据交换协议,由于script的src属性可以跨域请求,所以JSONP利用的就是浏览器的这个原理,需要通信时,动态插入一个javascript标签。请求的地址一般带有一个callback参数,假设需要请求的地址为http://localhost:3000?callback=show,服务器返回的代码一般是show()的JSON数据,而show函数恰恰是前端需要用的这个数据的函数。JSONP非常简单易用,自动补全API利用的就是JSONP。
一个简单的例子:
var script = doxument.createElement("script"); script.setAttribute("type", "text/javascript"); script.src="http://example.com/ip?callback=handleResponse"; document.body.appendChild(script); function handleResponse(data) { console.log(‘Your public IP address is: ‘+data.ip); }
JSONP解决跨域的本质:<script>标签可以请求不同域名下的资源,即<script>请求不受浏览器同源策略的影响。上例中的script会向http://example.com/服务器发送请求,这个请求的url后面带了个callback参数,是用来告诉服务器回调方法的方法名的。因为服务器收到请求后,会把相应的数据写进foo的参数,也就是服务器会返回如下的脚本:
foo({ "ip" : "8.8.8.8" });
这样浏览器通过<script>下载的资源就是上面的脚本了,<script>下载完就会立即执行,也就是说http://example.com/ip?callback=foo
这个请求返回后就会立即执行上面的脚本代码,而这个脚本代码就是调用回调方法和拿到json数据了。
我们再来看一个例子:
//请求代码 function jsonp(callback) { var script = document.createElement("script"); url = `https://localhost:3000?callback=${callback}`; script.setAttribute("src", url); document.querySelector("head").appendChild(script); } function show(data) { concole.log(`学生姓名为: ${data.name},年龄为: ${data.age},性别为: ${data.sex}`); } jsonp("show"); //响应代码 const student = { name: "Knight", age: 19, sex: "male" }; var callback = url.parse(req.url, true).query.callback; res.writeHead(200,{ "Content-Type": "application/json;charset=utf-8" }); res.end(`${callback}(${JSON.stringify(student)})`);
JSONP有一个很大问题,就是只能进行GET请求。
三、跨域源资源共享(CORS)
CORS是W3C制定的跨站资源分享标准,可以让AJAX实现跨域访问,定义了在必须访问跨域资源时浏览器与服务器该如何沟通。CORS背后的基本思想,就是使用自定义的HTTP头部让浏览器和服务器进行沟通,从而决定请求或相应应该成功还是失败。
比如一个简单的使用GET或POST的请求,它没有自定义的头部,而主体内容是text/plain。在发送该请求时,需要给它附加一个额外的Origin头部,其中包含请求页面的源信息(协议、域名、端口号),以便服务器根据该头部信息来决定是否给予响应。
Origin: http://www.example.com
如果服务器认为这个请求可以接受,就在Access-Control-Allow-Origin头部中发回相同的源信息(如果是公共资源,可以发“*”)。例如:
Access-Control-Allow-Origin: http://www.example.com
如果没有这个头部信息或信息不匹配,浏览器就会驳回请求。正常情况下,浏览器会处理请求。此时,请求和响应都不包含Cookie信息。
简单请求的跨域:
请求方式为GET或则POST;
假若请求是POST的话,Content-Type必须为下列之一:
application/x-www-form-urlencoded
mutipart/form-data
text/plain
不含有自定义头;
对于简单的跨域只进行一次http请求:
function ajaxPost(url, obj, header) { return new Promise((resolve, reject) => { var xhr=new XMLHttpRequest(), str = ‘‘ ; keys = Object.keys(obj); for(var i=0,len=keys.length;i<len;i++) { str +=`${keys[i]}=${obj[keys[i]]}&`; } str = str.substring(0, str.length - 1); xhr.open(‘post‘, url); xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); if(header instanceof Object) { for(var k in header) xhr.setRequestHeader(k, header[k]); } xhr.send(str); xhr.onreadystatechange = function() { if(xhr.readyState == 4) { if(xhr.status >= 200 && xhr.status < 300 || xhr.status == 304) { resolve(xhr.responseText); } else { reject(); } } } }); } ajaxPost("https://localhost:3000?page=cors", { name: "Knight", age: 19, sex: "male" }).then((text) => {console.log(text);}, ()=>{console.log("请求失败");}); //后端处理 var postData = ""; req.on("data", (data) => { postData += data; }); req.on("end", () => { postData = querystring.parse(postData); res.writeHead(200, { "Access-Control-Allow-Origin": "*", "Content-Type": "application/json;charset=utf-8" }); if(postData.name === student.name && Number(postData.age) === student.age && postData.sex === student.sex) { res.end(`yeah! ${postData.name} is a good guy!`); } else { res.end("No! a bad guy!"); } });
对于非简单请求来说,需要两次http请求,其中在请求之前有一次预请求。
function ajaxPost(url, obj, header) { return new Promise((resolve, reject) => { var xhr=new XMLHttpRequest(), str = ‘‘ ; keys = Object.keys(obj); for(var i=0,len=keys.length;i<len;i++) { str +=`${keys[i]}=${obj[keys[i]]}&`; } str = str.substring(0, str.length - 1); xhr.open(‘post‘, url); xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); if(header instanceof Object) { for(var k in header) xhr.setRequestHeader(k, header[k]); } xhr.send(str); xhr.onreadystatechange = function() { if(xhr.readyState == 4) { if(xhr.status >= 200 && xhr.status < 300 || xhr.status == 304) { resolve(xhr.responseText); } else { reject(); } } } }); } ajaxPost("https://localhost:3000?page=cors", { name: "Knight", age: 19, sex: "male" }, {"X-author": "Knight"}).then((text) => {console.log(text);}, ()=>{console.log("请求失败");}); //后端处理 var postData = ""; if(req.method == "OPTIONS") { res.writeHead(200, { "Access-Control-Max-Age": 3000, "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Headers": "X-author", "Content-Type": "application/json;charset=utf-8" }); res.end(); return void 0; } req.on("data", (data) => { postData += data; }); req.on("end", () => { postData = querystring.parse(postData); res.writeHead(200, { "Access-Control-Allow-Origin": "*", "Content-Type": "application/json;charset=utf-8" }); if(postData.name === student.name && Number(postData.age) === student.age && postData.sex === student.sex) { res.end(`yeah! ${postData.name} is a good guy!`); } else { res.end("No! a bad guy!"); } });
上面代码中,两个响应头: Access-Control-Allow-Headers,用来指明在实际的请求中,可以使用那些自定义的http请求头;Access-Control-Max-Age,用来指定此次预请求的结果的有效期,在有效期内则不会发出预请求,类似于缓存。
四、document.domain实现跨域
可以将子域和主域的document.domian设为同一个主域来实现跨域。但前提条件是,这两个域名必须属于同一个基础域名,所用的协议,端口都要一致,否则无法通过document.domain()来进行跨域。
example 1:
如果想要在你的http://www.knightboy.cn/a.html页面里使用<iframe>调用另一个http://knightboy.cn/b.html页面。这时候你想在a页面里面获取b页面里的DOM,然后进行操作。然后你会发现你不能获得b的DOM。document.getElementById("myIFrame").contentWindow.document或window.parent.document.body因为两个窗口不同源而报错。
这时候你只需要在a页面里和b页面里把document.domian设置成相同的值就可以在两个页面里操作Dom了。
example 2:
如果你在http://www.knightboy.cn/a.html页面里写入了document.cookie = "test=hello world";你在http://knightboy.cn/b.html页面是拿不到这个cookie的。
原因在于,Cookie是服务器写入浏览器的一小段信息,只有同源的网页才能共享。但是,两个网页一级域名相同,二级域名不同,浏览器允许通过设置document.domain来共享Cookie。另外,服务器也可以在设置Cookie的时候,指定Cookie的所属域名为一级域名。这样的话,二级域名和三级域名不用做任何设置便可以读取这个Cookie。
有一点需要注意的是:document.domain虽然可以读写,但只能设置成自身或者是高一级的父域且主域必须相同。所以只能解决一级域名相同二级域名不同的跨域问题。还有就是document.domain只适用于Cookie和iframe窗口,LocalStorage和IndexDB无法通过这种方法跨域。
五、window.name跨域
window对象有一个name属性,该属性有个特征:即在一个窗口(window)的生命周期内,窗口载入的所有页面都是共享一个window.name的,每个页面对window.name都有读写的权限,window.name是持久存在一个窗口载入过的所有页面中的,并不会因新页面的载入而进行重置。注意,window.name的值只能是字符串的形式,且这个字符串的大小最大能容许2M左右甚至更大的容量,因浏览器而异,但一般是够用的。
example 1:
现在在一个浏览器的一个标签页里打开http://www.knightboy.cn/a.html页面,你通过location.href = http://baidu.com/b.html,在同一个浏览器标签页里打开了不同域名下的页面。这时候这两个页面你可以使用window.name来传递参数。因为window.name指的是浏览器窗口的名字,只要浏览器窗口相同,那么无论在哪个页面里访问都是一样的。
example 2:
你的http://www.knightboy.cn/a.html页面里使用<iframe>调用另一个http://baidu.com/b.html页面。这时候你想在a页面里获取b页面里的DOM,然后进行操作。结果会发现不能获得b中的DOM。同样会因为不同源而报错,和上面提到的不同之处就是两个页面的一级域名也不相同。这时候document.domain就解决不了了。
浏览器窗口有window.name属性。这个属性的最大特点就是,无论是否同源,只要在同一个窗口里,前一个网页设置了这个属性,后一个网页可以读取它。比如当在b页面里设定window.name="hello",你再返回到a页面,在a页面访问window.name,可以得到hello。这种方法的优点是,window.name容量很大,可以放置非常长的字符串;缺点是必须监听子窗口window.name属性的变化,影响网页性能。
<!--a.html--> <DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>跨域</title> <script> function getData() {//iframe载入后执行该函数 var iframe = document.getElementById("proxy"); iframe.onload=function() {//a.html于iframe属于同源了,可以互相访问 var data = iframe.contentWindow.name;//获取iframe里的window.name,也就是data.html页面给它设置的数据 alert(data); } iframe.src="data.html";//这里的data.html为随便的一个页面,目的是,使得a.html能访问到iframe里的内容,也可设置成about:blank </script> <head> <body> <iframe id="proxy" src="http://baidu.com/b.html" style="display:none" onload="getData()"></iframe> </body> </html>
<!--b.html--> <script> window.name="this is some data you got from b.html"; <script>
六、window.postMessage方法跨域
以上是关于跨域解决方案汇总的主要内容,如果未能解决你的问题,请参考以下文章