跨域

什么是同源策略

浏览器出于安全方面的考虑,只允许与本域下的接口交互。不同源的客户端脚本在没有明确授权的情况下,不能读写对方的资源。

“同源”指的是”三个相同“。

  • 协议相同
  • 域名相同
  • 端口相同

同源政策的目的,是为了保证用户信息的安全,防止恶意的网站窃取数据。
随着互联网的发展,“同源政策”越来越严格。目前,如果非同源,共有三种行为受到限制。

  • Cookie、LocalStorage 和 IndexedDB 无法读取。
  • DOM 无法获得。
  • AJAX 请求无效(可以发送,但浏览器会拒绝接受响应)。
    虽然这些限制是必要的,但是有时很不方便,合理的用途也受到影响。下面,将详细介绍,如何规避上面三种限制。

什么是跨域?跨域有几种实现形式

跨域:允许不同域的接口进行交互
实现方式:

  • JSONP
  • CORS
  • 降域
  • postMessage

JSONP

JSONP是一种数据调用方式,是解决JSON跨域获取的解决方案。因此JSONP其实是一个非官方的协议。
JSONP是服务器与客户端跨源通信的常用方法。最大特点就是简单适用,老式浏览器全部支持,服务器改造非常小。

JSONP 的主要缺点有两个,
是只能 GET 不能 POST,因为是通过<script>引用的资源,参数全都显式的放在URL里,和 AJAX 没有关系。
是存在安全隐患,动态插入<script>标签其实就是一种脚本注入,可能遭遇XSS

JSONP原理
网页通过添加一个<script>元素,向服务器请求JSON数据,这种做法不受同源政策限制;服务器收到请求后,将数据放在一个指定名字的回调函数里传回来。

JSONP实现
定义数据处理函数_fun
创建script标签,src的地址执行后端接口,最后加个参数callback=_fun
服务端在收到请求后,解析参数,计算返还数据,输出 fun(data) 字符串。
fun(data)会放到script标签做为js执行。此时会调用fun函数,将data做为参数。

<body> 
  <div class="container"> 
    <ul class="news"> 
      <li>第11日前瞻:中国冲击4金 博尔特再战</li> 
      <li>男双力争会师决赛 </li> 
      <li>女排将死磕巴西!</li> 
    </ul> 
    <button class="change">换一组</button> 
  </div>

<script> 
  $('.change').addEventListener('click', function(){ 
    var script = document.createElement('script'); // 创建元素节点 
    script.src = 'http://localhost:8080/getNews?callback=appendHtml'; 
   // 引入后端接口  后面加上 callback=fun 确定函数名
    document.head.appendChild(script); 
   // 从后端返回的数据作为js代码执行 调用定义好的函数
    document.head.removeChild(script); 
  })
 function appendHtml(news){ 
    var html = ''; 
    for( var i=0; i<news.length; i++){ 
      html += '<li>' + news[i] + '</li>'; 
    } 
    $('.news').innerHTML = html; 
 } 
 function $(id){ 
   return document.querySelector(id); 
 } 
</script> 
</body> 

app.get('/getNews', function(req, res){ 
  var news = [ 
     "第11日前瞻:中国冲击4金 博尔特再战200米羽球", 
     "正直播柴飚/洪炜出战 男双力争会师决赛", 
     "女排将死磕巴西!郎平安排男陪练模仿对方核心", 
     "没有中国选手和巨星的110米栏 我们还看吗?", 
     "中英上演奥运金牌大战",
   ] 
   var data = []; 
   for(var i=0; i<3; i++){ 
     var index = parseInt(Math.random()*news.length); 
     data.push(news[index]); 
     news.splice(index, 1); 
    } 
    var cb = req.query.callback; 
    if(cb){ 
      res.send(cb + '('+ JSON.stringify(data) + ')');  
       // JSON.stringify(arr) = "["aa","bb","cc"]" 
      //  '('+ JSON.stringify(data) + ')'  = '([data1,data2,data3....])'
    }else{ res.send(data); } 
})

CORS
CORS是跨源资源分享(Cross-Origin Resource Sharing)的缩写。它是W3C标准,是跨源AJAX请求的根本解决方法。
相比JSONP只能发GET请求,CORS允许任何类型的请求。
CORS需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE浏览器不能低于IE10。
整个CORS通信过程,都是浏览器自动完成,不需要用户参与。对于开发者来说,CORS通信与同源的AJAX通信没有差别,代码完全一样。浏览器一旦发现AJAX请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉。
因此,实现CORS通信的关键是服务器。只要服务器实现了CORS接口,就可以跨源通信。

CORS实现
当你使用 XMLHttpRequest发送请求时,浏览器发现该请求不符合同源策略,会给该请求加一个请求头:Origin,后台进行一系列处理
如果确定接受请求则在返回结果中加入一个响应头:Access-Control-Allow-Origin;
浏览器判断该相应头中是否包含 Origin 的值
如果有则浏览器会处理响应,我们就可以拿到响应数据,
如果不包含浏览器直接驳回,这时我们无法拿到响应数据。
所以 CORS 的表象是让你觉得它与同源的 ajax 请求没啥区别,代码完全一样。

浏览器将CORS请求分成两类:简单请求(simple request)和非简单请求(not-so-simple request)。
只要同时满足以下两大条件,就属于简单请求。
请求方法是以下三种方法之一。
HEAD
GET
POST
HTTP的头信息不超出以下几种字段。
Accept
Accept-Language
Content-Language
Last-Event-ID
Content-Type:只限于三个值application/x-www-form-urlencoded、multipart/form-data、text/plain
凡是不同时满足上面两个条件,就属于非简单请求。
浏览器对这两种请求的处理,是不一样的。

基本流程
对于简单请求,浏览器直接发出CORS请求。具体来说,就是在头信息之中,增加一个Origin字段。
下面是一个例子,浏览器发现这次跨源AJAX请求是简单请求,就自动在头信息之中,添加一个Origin字段。

GET /cors HTTP/1.1
Origin: http://api.bob.com
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...

上面的头信息中,Origin字段用来说明,本次请求来自哪个源(协议 + 域名 + 端口)。服务器根据这个值,决定是否同意这次请求。
如果Origin指定的源,不在许可范围内,服务器会返回一个正常的HTTP回应。浏览器发现,这个回应的头信息没有包含Access-Control-Allow-Origin字段(详见下文),就知道出错了,从而抛出一个错误,被XMLHttpRequest的onerror回调函数捕获。注意,这种错误无法通过状态码识别,因为HTTP回应的状态码有可能是200。
如果Origin指定的域名在许可范围内,服务器返回的响应,会多出几个头信息字段。

Access-Control-Allow-Origin: http://api.bob.com
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: FooBar
Content-Type: text/html; charset=utf-8

上面的头信息之中,有三个与CORS请求相关的字段,都以Access-Control-开头。
(1)Access-Control-Allow-Origin
该字段是必须的。它的值要么是请求时Origin字段的值,要么是一个*,表示接受任意域名的请求。
(2)Access-Control-Allow-Credentials
该字段可选。它的值是一个布尔值,表示是否允许发送Cookie。默认情况下,Cookie不包括在CORS请求之中。设为true,即表示服务器明确许可,Cookie可以包含在请求中,一起发给服务器。这个值也只能设为true,如果服务器不要浏览器发送Cookie,删除该字段即可。
(3)Access-Control-Expose-Headers
该字段可选。CORS请求时,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。如果想拿到其他字段,就必须在Access-Control-Expose-Headers里面指定。上面的例子指定,getResponseHeader('FooBar')可以返回FooBar字段的值。

withCredentials 属性
上面说到,CORS请求默认不包含Cookie信息(以及HTTP认证信息等)。如果需要包含Cookie信息,一方面要服务器同意,指定Access-Control-Allow-Credentials字段。
Access-Control-Allow-Credentials: true
另一方面,开发者必须在AJAX请求中打开withCredentials属性。
var xhr = new XMLHttpRequest();
xhr.withCredentials = true;
否则,即使服务器同意发送Cookie,浏览器也不会发送。或者,服务器要求设置Cookie,浏览器也不会处理。
但是,如果省略withCredentials设置,有的浏览器还是会一起发送Cookie。这时,可以显式关闭withCredentials。
xhr.withCredentials = false;
需要注意的是,如果要发送Cookie,Access-Control-Allow-Origin就不能设为星号,必须指定明确的、与请求网页一致的域名。同时,Cookie依然遵循同源政策,只有用服务器域名设置的Cookie才会上传,其他域名的Cookie并不会上传,且(跨源)原网页代码中的document.cookie也无法读取服务器域名下的Cookie。

简单例子:
浏览器判断该相应头中是否包含 Origin 的值 
如果不包含浏览器直接驳回,这时我们无法拿到响应数据。
//前端
$('.change').addEventListener('click', function () {
    var xhr = new XMLHttpRequest();
    xhr.open('get', 'http://b.jrg.com:8080/getNews', true);
    xhr.send();
    xhr.onreadystatechange = function () {
        if (xhr.readyState === 4 && xhr.status === 200) {
            appendHtml(JSON.parse(xhr.responseText))
        }
    };
    window.xhr = xhr
});
结果会发生报错:没有响应头被允许


 如果有则浏览器会处理响应,我们就可以拿到响应数据,
//后端
res.header("Access-Control-Allow-Origin", "http://a.jrg.com:8080");
//res.header("Access-Control-Allow-Origin", "*");
可实现CORS跨域:响应头被允许

降域

Cookie 是服务器写入浏览器的一小段信息,只有同源的网页才能共享。但是,两个网页一级域名相同,只是二级域名不同,浏览器允许通过设置document.domain共享 Cookie。(这种方法又被称为 降域)

举例来说,A网页是http://w1.example.com/a.html,B网页是http://w2.example.com/b.html,那么只要设置相同的document.domain,两个网页就可以共享Cookie。
注意,这种方法只适用于 Cookie 和 iframe 窗口,LocalStorage 和 IndexedDB 无法通过这种方法,规避同源政策,而要使用下文介绍的PostMessage API。
另外,服务器也可以在设置 Cookie 的时候,指定 Cookie 的所属域名为一级域名,比如.example.com。
Set-Cookie: key=value; domain=.example.com; path=/
这样的话,二级域名和三级域名不用做任何设置,都可以读取这个Cookie。


iframe元素可以在当前网页之中,嵌入其他网页。每个iframe元素形成自己的窗口,即有自己的window对象。iframe窗口之中的脚本,可以获得父窗口和子窗口。但是,只有在同源的情况下,父窗口和子窗口才能通信;如果跨域,就无法拿到对方的DOM。
比如,父窗口运行下面的命令,如果iframe窗口不是同源,就会报错。

 document.getElementById("myIFrame").contentWindow.document
 // Uncaught DOMException: Blocked a frame from accessing a cross-origin frame.

上面命令中,父窗口想获取子窗口的DOM,因为跨域导致报错。
反之亦然,子窗口获取主窗口的DOM也会报错。
window.parent.document.body
// 报错

这种情况不仅适用于iframe窗口,还适用于window.open方法打开的窗口,只要跨域,父窗口与子窗口之间就无法通信。
如果两个窗口一级域名相同,只是二级域名不同,那么设置上一节介绍的document.domain属性,就可以规避同源政策,拿到DOM。
父页面 子页面的script代码里都加上
doucument.domain = 'example.com'

对于完全不同源的网站,目前有两种方法,可以解决跨域窗口的通信问题。
片段识别符(fragment identifier)
跨文档通信API(Cross-document messaging)


postmessage

HTML5为了解决跨域问题,引入了一个全新的API:跨文档通信 API(Cross-document messaging)。
这个API为window对象新增了一个window.postMessage方法,允许跨窗口通信,不论这两个窗口是否同源。
举例来说,父窗口aaa.com向子窗口bbb.com发消息,调用postMessage方法就可以了。
var popup = window.open('http://bbb.com', 'title');
popup.postMessage('Hello World!', 'http://bbb.com');
postMessage方法的第一个参数是具体的信息内容,第二个参数是接收消息的窗口的源(origin),即“协议 + 域名 + 端口”。也可以设为*,表示不限制域名,向所有窗口发送。
子窗口向父窗口发送消息的写法类似。
window.opener.postMessage('Nice to see you', 'http://aaa.com');
父窗口和子窗口都可以通过message事件,监听对方的消息。
window.addEventListener('message', function(e) {
console.log(e.data);
},false);
message事件的事件对象event,提供以下三个属性。
event.source:发送消息的窗口
event.origin: 消息发向的网址
event.data: 消息内容
下面的例子是,子窗口通过event.source属性引用父窗口,然后发送消息。
window.addEventListener('message', receiveMessage);
function receiveMessage(event) {
event.source.postMessage('Nice to see you!', '*');
}
上面代码有几个地方需要注意。首先,receiveMessage函数里面没有过滤信息的来源,任意网址发来的信息都会被处理。其次,postMessage方法中指定的目标窗口的网址是一个星号,表示该信息可以向任意网址发送。通常来说,这两种做法是不推荐的,因为不够安全,可能会被恶意利用。
event.origin属性可以过滤不是发给本窗口的消息。

 window.addEventListener('message', receiveMessage);
 function receiveMessage(event) {
 if (event.origin !== 'http://aaa.com') return;
 if (event.data === 'Hello World') {
    event.source.postMessage('Hello', event.origin);
 } else {
  console.log(event.data);
}
}

LocalStorage
通过window.postMessage,读写其他窗口的 LocalStorage 也成为了可能。

 <div class="ct"> 
   <h1>使用postMessage实现跨域</h1> 
   <div class="main"> 
   <input type="text" placeholder="http://a.jrg.com:8080/a.html"> 
   </div> 
 <iframe src="http://localhost:8080/b.html" frameborder="0" ></iframe> 
 </div> 

 <script> 
 //URL: http://a.jrg.com:8080/a.html 
 $('.main input').addEventListener('input', function(){ 
   console.log(this.value); 
   window.frames[0].postMessage(this.value,'*'); // postMessage 
  }) 
 window.addEventListener('message',function(e) { 
   $('.main input').value = e.data 
   console.log(e.data); 
 }); 
function $(id){ return document.querySelector(id); } 
</script> 

 <input id="input" type="text" placeholder="http://b.jrg.com:8080/b.html"> 

<script> 
// URL: http://b.jrg.com:8080/b.html  
$('#input').addEventListener('input', function(){ 
  window.parent.postMessage(this.value, '*'); // postMessage 
 }) 
window.addEventListener('message',function(e) { 
   $('#input').value = e.data 
   console.log(e.data); 
 }); 
function $(id){ return document.querySelector(id); } 
</script>
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335

推荐阅读更多精彩内容

  • 欢迎关注微信公众号:全栈工厂 本文主要参考跨域资源共享 CORS 详解[http://www.ruanyifeng...
    liqingbiubiu阅读 1,798评论 0 3
  • 跨域是什么 同源策略 在讲解什么是跨域之前先要清楚什么是同源策略,“同源政策”(same-origin polic...
    JRG_Orange阅读 943评论 0 52
  • 1.什么是同源策略浏览器出于安全方面的考虑,只允许与本域下的接口交互。不同源的客户端脚本在没有明确授权的情况下,不...
    24_Magic阅读 486评论 0 0
  • Section1、为什么要跨域? 自古以来(1995年起),为了用户的信息安全,浏览器就引入了同源策略。那么同源策...
    qhaobaba阅读 375评论 0 0
  • 当你突然离开的时候 我驻守的世界便空无一物 平凡人怎会胸襟开阔 逃避不过留在心里的失落 待久了冬季里那一片白 习惯...
    君凉阅读 328评论 5 21