前端开发中的跨域问题
在开始之前,我们先熟悉这样一个概念:同源策略。所谓同源策略,指的是‘同一个协议,同一个域名,同一个端口’。三者有任意一个不一样,均不可称之为同源。
URL | 说明 | 是否允许请求 |
http://www.a.com/a.js http://www.a.com/b.js |
同一协议,同一域名,同一端口 | 是 |
http://www.a.com/a.js https://www.a.com/b.js |
不同协议,同一域名,同一端口 | 否 |
http://www.a.com/a.js https://www.b.com/b.js |
不同协议,不同域名,同一端口 | |
http://www.a.com/a.js https://www.a.com:8000/b.js |
不同协议,不同域名,不同端口 | |
http://www.a.com/a.js http://www.b.com/b.js |
同一协议,不同域名,同一端口 | |
http://www.a.com/a.js http://www.a.com:8000/b.js |
同一协议,同一域名,不同端口 | |
http://www.a.com/a.js http://192.168.98.123/b.js |
域名与域名对应ip | |
http://www.a.com/a.js http://nba.a.com/b.js |
主域相同,子域不同 | |
http://www.a.com/a.js http://a.com/b.js |
同一域名,不同二级域名 |
从上边可以看出,引起跨域问题的场景很多。我们接下来就简单的来说一下跨域问题的存在及其解决方案。
避开跨域
比如在a.com域名下的某个页面需要跨域请求b.com下的资源文件。这时候我们可以把执行跨域的js文件放到b.com下。而在a.com中通过外联的方式引入b.com下的文件,这样js文件与图片等资源都在同一域名下,即可正常访问。使用场景比如:cdn,图像……
document.domain + iframe
对于主域相同,子域不同引起的跨域问题,我们可以通过document.domain的方式来解决。
具体可以在http://www.a.com/a.html和http://script.a.com/b.html中分别设置:document.domain = 'a.com',然后在a.html中创建一个iframe,这样就可以实现通信了。
http://www.a.com/a.html 部分设置如下:
document.domain = 'a.com'; function addIframe () { var iframe = document.createElement('iframe'); iframe.src = 'http://script.a.com/b.html'; iframe.style.display = 'none'; docuemnt.body.append(iframe); iframe.onload = function () { var ele = iframe.contentDocument || iframe.contentWindow.docuemnt; console.log(ele.getElementsByTagName('h1')[0].childNodes[0].nodeValue); } }
http://script.a.com/b.html 部分设置如下:
document.domain = 'a.com';
那么问题来了:
- 安全性:当一个站点收到攻击后,另外一个站点也会引起安全泄露
- 耦合性:当页面中有多个iframe时,要想能操作所有iframe,则必须设置相同的document.domain
location.hash + iframe
由于地址栏上的hash值发生改变,并不会触发页面刷新,所以可以利用这个原理来跨域请求数居。但是数据大小有限制。
假设a.com域名下的文件a.html需要跨域请求b.com下的文件b.html。采用location.hash来处理如下:在a.html中创建一个隐藏的iframe,设置src指向b.com下的b.html,把hash值当做参数传递过去。b.html在接收到请求后,通过修改a.html上的hash值来传递数据。然后在a.html上设置一个定时监测其地址栏hash值变化的定时器即可。
a.html部分代码如下:
function addFrame () { var ifr = document.createElement("iframe"); ifr.src = 'http://www.b.com/b.html#proto'; ifr.style.display = "none"; document.body.append(ifr); } function checkHash() { window.addEventListener("hashchange",function(e){ var hash = window.location.hash ? window.location.hash.subString(1) :""; console.log("你请求到的信息是:"+ hash); }); } setInterval(checkHah,1000);
b.html 部分代码如下:
if(location.hash === '#proto'){ var data = "要返回给a.html的数据"; try{ parent.location.hash = data ; }catch(e){ // ie、chrome的安全机制无法修改parent.location.hash, // 所以要利用一个中间的cnblogs域下的代理iframe var ifr = document.createElement("iframe"); ifr.src="http://www.a.com/test.html#"+data; ifr.style.display = "none"; document.body.append(ifr); } }
test.html 部分代码如下:
parent.parent.location.href = location.hash.substring(1);
这样做也会产生一些问题:
- 数据直接暴露在地址栏
- 数据传输量有限
- 对浏览器历史记录产生影响
- 地址栏上传输汉字问题
- 步骤繁琐
postMessage
postMessag是html5新增的API。用于支持web的实时消息传递。
使用方法:otherWindow.postMessage(message, targetOrigin);
- otherWindow:对发送信息页面的引用。可以是页面中iframe的contentWindow属性;window.open的返回值;通过name或下标从window.frames取到的值。
- message:要发送的消息
- targetOrigin:接受消息的窗口,设置为*则不做限制。
a.com/a.html 部分代码如下:
<iframe id="ifr" src="http://www.b.com/b.html"></iframe>
<script>
var ifr = document.getElementById("ifr"),
url = "http://www.b.com";
ifr.contentWindow.postMessage("hello world !",url); </script>
www.b.com/b.html 部分代码如下:
window.addEventListener("message",function(e){ if(e.origin === 'http://a.com'){ console.log(e.data) } },false)
动态创建script
尽管浏览器有同源策略的限制。但是通过script标签的src属性却可以访问其它域名下的文件,并可以执行其中的函数。下边先看一下判断js加载是否完毕的方法:
js.onload = js.onreadystatechange = function() { if(!this.readyState || this.readyState === "loaded" || this.readyState ==="complete"){ js.onload = js.onreadystatechange = null; } }
JSONP
创建一个回调函数,在远程服务器上执行这个函数,并把json数据当做参数传递。
JSONP原理
function addScript (src) { var spt = document.createElement("script"); spt.setAttribute("type","text/javascript"); spt.src= src; document.body.append(spt); } function handleFn(data){ //处理跨域回调数据 } window.onload = function() { addScript("http://localhost:2000/MyService.ashx?callback=handleFn") }
$.getJSON
先看一下,getJSON方法在跨域问题中的使用方法:
$.getJSON("http://localhost:20002/MyService.ashx?callback=?",function(data) { //处理回调函数 })
像上面那样,我们看到了一个:callback=?。这样getJSON方法才会知道是用JSONP方式去访问服务,callback后面的那个问号是内部自动生成的一个回调函数名。那么问题是:如果服务器上规定了回调函数名,我们应该怎么处理呢?答案是:ajax。
ajax
$.ajax({ url:"http://localhost:20002/MyService.ashx?callback=?", dataType:"jsonp", jsonpCallback:"person", success:function(data) { //处理回调函数 } })
jsonpCallback就是可以指定我们自己的回调方法名person,远程服务接受callback参数的值就不再是自动生成的回调名,而是person。dataType是指定按照JSOPN方式访问远程服务。
接下来才是我们这篇文章的重点。
跨域资源共享(CORS)
用于授权资源的跨域访问 。比如在前端开发中,A站点需要访问B站点下的某一个接口,从而得到我们想要的数据,这个时候涉及到一个重要的概念:Access-Control-Allow-Origin。如果服务器端在返回头中没有设置这个属性,或者设置的属性值不包括我们当前的域名A,那么这个时候,当前域名A是不被服务器允许进行跨域资源访问的。
上边这幅图描述了一个完整的CORS请求过程。
CORS请求的过程如下:
- 首先判断是不是简单请求,如果是,如:get请求,只需要在HTTP Response后添加Access-Control-Allow-Origin;
- 如果不是简单请求,如POST,PUT,DELETE等,这个时候浏览器会分2次进行请求:一次是预检请求preflight(method: OPTIONS),主要用于检测请求来源是否合法,并返回Header。第二次才是真正的请求,所以服务器必须处理OPTIONS应答。
服务器处理请求解析如下:
- 首先检测Http头部中是否有origin字段信息
- 如果没有或者不被允许,则当做普通请求处理,结束;
- 如果有并且是在允许范围内,则判断是否是复杂请求;
- 如果是复杂请求,则先执行预检请求preflight(method: OPTIONS),返回Allow-Headers、Allow-Methods等,内容为空;进如步骤6;
- 如果是简单请求,则直接进步步骤6;
- 执行请求,返回Allow-Origin、Allow-Credentials等,并返回正常内容。
在HTML5中,也有一些元素为CORS提供了支持,如img,video。此时需要设置crossOrigin属性,属性值可以是anonymous
或use-credentials。比如我们要用canvas访问跨域图片,就可以像下边这样操作:
var img = new Image(), canvas = document.createElement("canvas"), cxt = canvas.getContext("2d"); img.crossOrigin = "Anonymous";//使用CORS img.src="http://img5.imgtn.bdimg.com/it/u=104961686,3757525983&fm=27&gp=0.jpg"; img.onload = function() {
canvas.width = img.width; canvas.height = img.height; cxt.drawImage(img,0,0);
document.body.append(canvas); localStorage.setItem("imgData",canvas.toDataURL("image/jpg")) }