跨域处理

跨域

  • 1、什么是跨域
  • 2、跨域方法
    • 2.1 JSONP
    • 2.2 CORS
    • 2.3 window.postMessage
    • 2.4 window.name
    • 2.5 document.domain
  • 3、跨域方式间比较

1、什么是跨域

概念:只要不遵循同源策略的请求,都被当作是跨域的。

同源策略(same-origin policy):浏览器出于安全方面的考虑,只允许本域下的接口交互。不同源的客户端脚本在没有明确授权的情况下,不能读取对方的资源。

这里的本域指:

  • 同协议: 如均为httphttps协议
  • 同域名: 如github.com/jazengithub.com/Tony
  • 同端口: 如均为80端口或8080端口

也就是说只要协议、端口、域名这三者中有一者不同,则可以说是跨域的。
下面是一个关于JavaScript能否跨域通信的例子,以 http://www.aaa.com/a.js 访问以下URL的结果。见下表:

URL 说明 是否允许通信
http://www.aaa.com/b.js 同一域名下
http://www.aaa.com/script/b.js 同一域名下,不同文件夹
http://www.aaa.com:8080/b.js 同一域名,不同端口 ×
https://www.aaa.com/b.js 同一域名,不同协议 ×
http://127.0.0.1/b.js 域名和域名对应的IP ×
http://script.aaa.com/b.js 主域相同,子域不同 ×
http://aaa.com/b.js 同一域名,不同二级域名(同上) ×
http://www.bbb.com/b.js 不同域名 ×

对于端口和协议的不同,只能通过后台来解决。前端针对不同域名,则有以下几种解决方案。

2.1、JSONP

JSONP(JSON with Padding):一种用于解决AJAX跨域问题的方案。(把JSON包裹在回调函数中传回,这个padding就可以理解为这个回调函数)

原理: AJAX由于受到同源策略的限制无法跨域,但带有 src 属性的标签(例如<script><img><iframe>)并不受同源限制(也应用这个特性来使用CDN)。因此可以通过向页面中动态添加 <script> 标签来完成对跨域资源的访问。

实现方式: 在网页添加一个 <script> 元素,向服务器请求JSON数据;服务器收到请求后,将数据放在一个指定名字的回调函数里传回来。具体步骤如下:

  1. 定义处理函数 foo
  2. 创建 script 标签, src的属性为请求发送的目标地址,并且在最后加上callback=foo
  3. 服务端在收到请求后,解析参数并计算返回数据,输出 foo(data) 字符串
  4. foo(data) 会放到script标签作为JS执行。此时会调用foo(data)作为参数

例:

function addScript(url){
    var script = document.createElement('script');       // 创建一个 script 标签
    script.src = url;      // 将script标签的 src 属性指向目标地址。需要调用时就传入对应的 url
    document.body.appendChild(script);     // 添加这个标签以供加载
}

// 传入一个回调函数,并且使得回调函数的名字为 foo
addScript("http://localhost:8080/jsonp?callback=foo"); 

// 返回的是 foo(data) 字符串,被包含在script标签中 以JS的方式解析执行
// 因为存在下面这个同名函数, 所以上述JS的执行结果就是  函数foo  的执行结果
function foo(data){
    console.log(data)
}

当上述代码的 data是一个对象时,则可以调用对象的属性来输出更多内容。后台处理部分如下:

app.get('/jsonp',function(req,res) {
    var data = {"jsonp": "succuess"}; 
    var cb = req.query.callback;
    if(cb)
        res.send(cb+'('+JSON.stringify(data)+')');
    else
        res.send(data);
    // foo({"jsonp": "succuess"})
})

而对于jQuery,跨域的实现方式是封装在了$.ajax()中。但是由上述代码可知跨域与XMLHttpRequest可以说并没有什么关系,但是解决了AJAX的“痛点” (两者本质上也是有区别的:ajax的核心是通过XMLHttpRequest获取非本页内容,而JSONP的核心则是动态添加<script>标签来调用服务器提供的js脚本)。

下面是jQuery的实现方式:

$.ajax({
    url: "http://localhost:8080/jsonp",
    dataType: 'jsonp',
    jsonp: "callback",
    // jsonpCallback: "foo"
})
.done(function(res) { 
    console.log(res);
}) 

// 后台代码不变,仍输出 "jsonp": "succuess"

实际完整请求为:http://localhost:8080/jsonp?callback=foo&_=54681548735 ,最后的随机字符串是jQuery添加的。这个请求的参数含义如下:

  • dataType: 'jsonp',用于表示这是一个 JSONP 请求
  • jsonp: 'callback',用于告知服务器根据这个参数获取回调函数的名称,通常约定就叫 callback。
  • jsonpCallback: 'foo',回调函数的名称,即前面callback参数的值。(该参数可省略,jQuery 会自动生成一个随机字符串作为函数名)

注意: JSONP 存在安全隐患,动态插入<script>标签其实就是一种脚本注入(Dynamic script injection)。因为JavaScript没有任何权限与访问控制的概念,通过动态脚本注入的代码可以完全控制整个页面,所以引入外部来源的代码须多加小心。

2.2、CORS

CORS(Cross-origin resource sharing):跨域资源共享,是一个W3C标准。

它允许浏览器向跨源服务器,发出XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制。

原理:服务器端对于CORS的支持,主要就是通过设置 Access-Control-Allow-Origin 来进行的。当浏览器检测到服务器设置 Access-Control-Allow-Origin HTTP响应头之后,就会允许跨域请求。其中AJAX代码的相对路径要设置成其他域的绝对路径,也就是将要跨域访问的接口地址。

例:

document.querySelector("button").addEventListener("click", function(){
    var xhr = new XMLHttpRequest();   // 新建AJAX请求
    xhr.open("GET", "http://localhost:8080/cors");  // URL请求的是绝对路径
    xhr.send();
    xhr.onload = function(){
        if(this.status == 200||this.status == 304)
             console.log(xhr.responseText);
     }
})

// 后端代码
app.get('/cors',function(req,res) {
    var data = {"cors": "succuess"};
    res.header("Access-Control-Allow-Origin", "*");  // 通配符 一切网站的请求都接受
//res.header("Access-Control-Allow-Origin", "http://localhost:8080");  仅接受来自该域名的请求
    res.send(data);
})

当设置header("Access-Control-Allow-Origin", "http://localhost:8080")时,仅能允许访问来自localhost且端口为8080的请求。即使是同IP127.0.0.1:8080的请求 也不会允许访问。

2.3、window.postMessage

window.postMessage() 方法可以安全地实现跨源通信。该方法被调用时,会在所有页面脚本执行完毕之后向目标窗口派发一个 MessageEvent消息。

window.postMessage(data,origin)方法接受两个参数:

  1. data:要传递的数据。
  2. origin:字符串参数,指明目标窗口的源,协议+主机+端口号[+URL],URL会被忽略,所以可以不写。postMessage()方法只会将message传递给指定窗口,也可以将参数设置为"*"以传递给任意窗口,如需指定和当前窗口同源则设置为"/"。

postMessage()是HTML5的一个API,使用这种方法最重要的就是发送消息接受消息。在页面A中向页面B发送请求,然后在页面B中监听请求并获取A发送的数据,即可实现跨域。

  • 页面A发送消息:调用postMessage API向目标窗口B 发送消息 window.postMessage(data, origin)
  • 页面B接收消息:目标窗口B监听message事件 window.addEventListener('message',function (e) { console.log(e.origin,e.data) })

例:页面A localhost:8080/index.htm 代码

<iframe src="http://127.0.0.1:8080/s/second.html"></iframe>

<script>
window.onload = function(){
  // 向页面B发送数据“hello world”  - postMessage
  window.frames[0].postMessage("Hello World", "http://127.0.0.1:8080/s/second.html");
}
</script>

页面B http://127.0.0.1:8080/s/second.html 代码

<h1>I'm second level index</h1>
<script>
    //  监听“消息”事件  来获取传递数据的内容(存在于event对象中)
    window.addEventListener('message', function (event) {
        var header = document.createElement("h2");
        header.innerText = event.data;    // 把收到的数据放入到新节点h2中
        document.body.appendChild(header);  // 插入节点以便显示
    }); 
</script>
window.postMessage result

2.4、window.name

window.name: 在一个窗口(window)的生命周期内,窗口载入的所有页面共享一个 window.name,每个页面对 window.name 都有读写权限, window.name 持久存在一个窗口载入过的所有页面中。

问题来源: 当我们设置一个iframe时,若不遵守同源策略,则无法获取其中的数据。当插入如下代码时,会因为违反同源策略而报错。

<iframe src="http://127.0.0.1:8080/s/second.html"></iframe>
<script>
window.onload = function(){
  console.log( window.frames[0].contentWindow.name ); 
}
Error message

原理: 当 iframe 的页面跳到其他地址时,其 window.name 值保持不变(name值大小限制为2MB,不同浏览器限制大小也不同)。浏览器跨域 iframe 禁止互相调用/传值。但是调用 iframewindow.name 却不变,正是利用这个特性来互相传值。

解决方法:目标域的 window.name 存储需要的数值,然后生成 iframe的 src 属性先指向目标域。当把iframe的 src 属性改变时,它window.name属性值不变。即将这个 src 转向本地域某个代理时,跨域数据就由iframe的 window.name 从外域传递到本地域。具体步骤如下:

  1. 在应用页面A 中创建一个iframe,将其src指向数据页面B。数据页面B中的 window.name 存储着数据
  2. 在应用页面A中监听iframe的onload事件,在此事件中设置iframe的src属性指向本地域的代理文件proxy.html(代理文件有自己创建,最好是空页面 方便加载)
  3. 获取数据后,为保证安全,以免被其他域的iframe访问,要销毁这个iframe。
window.name solution

栗子:

// 页面A: http://localhost:8080/index.html  代码
window.onload = function () {
  var state = 0;  // 设置一个状态码 来标明iframe的src加载状态
  var iframe = document.createElement("iframe");
  iframe.src = 'http://127.0.0.1:8080/s/second.html';  // 生成一个iframe并将其src指向目标页B 

// iframe的src改变后会重新加载一次, 所以onload事件会触发两次(两个if语句都会触发)
  iframe.onload = function () {
     if (state === 0) {
      iframe.contentWindow.location = "http://localhost:8080/proxy.html";  // 将iframe页面的src指向代理页面 使得与页面A同源 
      state = 1;  // 状态码变为1 即代表iframe已同源
    }
    if (state === 1){
      console.log(iframe.contentWindow.name);  // 获取数据
      /*
      // 获取数据后销毁iframe  
      iframe.contentWindow.document.write('');
      iframe.contentWindow.close();
      document.body.removeChild(iframe);
      */
    }
};
    document.body.appendChild(iframe);
}
// 页面B: http://127.0.0.1:8080/s/second.html  代码
window.name = "window.name success!";   // 这个字符串可以换成任意内容,但最后都会被转换为String

使用这种方式,加载时 iframe 会闪现一下 然后消失。可以把这个 iframe 加载到对应的盒子中,并利用css对其进行渲染, 比如设置opacity: 0; height: 0; width: 0;display:none 来避免这种闪动带来的不良体验。

扩展阅读: 《iframe跨域通信的通用解决方案》

2.5、document.domain

document.doamin():获取/设置当前文档的原始域部分, 用于同源策略。

原理:浏览器中不同域的框架之无法进行JS交互。比如也面A: http://www.a.xxx.com:8080/s/second.html,它里面有个iframe,src: http://www.b.xxx.com:8080/B/B.html,因为页面与iframe框架属于不同域,所以无法通过JS来获取iframe中的东西

// 页面A : http://www.a.xxx.com:8080/s/second.html
<h1>Page A</h1>
<iframe src="http://127.0.0.1:8080/s/second.html"></iframe>

iframe.onload = function () {
  var ifrDoc = iframe.contentDocument || iframe.contentWindow.document; 
  var h1 = ifrDoc.querySelector("h1").innerHTML; 
  console.log(h1);
  ifrdoc.querySelector("h1").innerHTML = "h2";  // 获取Page B元素并修改内容
}
// 页面B: http://www.b.xxx.com:8080/B/B.html
<h1>Page B</h1>
Error message of document.domain

但是不同框架间(父子或同辈),能够获取到彼此window对象。这时,document.domain就可以派上用场了。只要把 http://www.example.com/a.htmlhttp://example.com/b.html 这两个页面的document.domain都设成相同的域名就可以了。

解决方法: 在相同主域名不同子域名下的页面,设置document.domain使它们同域。

注意:document.domain的设置是有限制的,只能把document.domain设置成自身或更高一级的父域,且主域必须相同。

// 页面A : http://www.a.xxx.com:8080/s/second.html
document.domain = "xxx.com";

// 页面B: http://www.b.xxx.com:8080/B/B.html
document.domain = "xxx.com";
set document.domain

如果仅想获取信息而不展示页面,并且不想再HTML中添加无意义的标签。可以对页面A的JS代码进行如下设置:

// 页面A : http://www.a.xxx.com:8080/s/second.html
document.domain = "xxx.com"
  
var iframe = document.createElement("iframe");
iframe.src = "http://www.b.xxx.com:8080/B/B.html";
iframe.onload = function () {
  var ifrdoc = iframe.contentDocument || iframe.contentWindow.document; 
  var h1 = ifrdoc.querySelector("h1").innerHTML;
  console.log(h1);
}

iframe.style.display = "none";  // 取消iframe的展示
document.body.appendChild(iframe);

3、几种跨域方式的比较

跨域方式 优点 缺点
JSONP 兼容性好;简单适用,服务器改造小 只能用于GET方法;有一定安全隐患;没有关于 JSONP 调用的错误处理(比如脚本内容错误提示,无法获取404)
CORS 任何HTTP请求均可;前端请求简单方便 兼容性稍差(IE10以上)
window.postMessage 无需后端配合;移动端兼容性好 浏览器需要支持HTML5,获取窗口句柄后才能相互通信
window.name 无需前后端配置;兼容性好 传递的数据仅限于字符串(对象或其他会自动转化为字符串);需要再添一个代理页面;需要额外加载两个iframe
document.domain 所有浏览器都支持 需要主域相同且子域不同;只适用于父子window间通信,不能用于XMLHttpRequest;



CORS与JSONP对比:

  1. JSONP只能实现GET请求,而CORS支持所有类型的HTTP请求。
  2. 使用CORS,开发者可以使用普通的XMLHttpRequest发起请求和获得数据,比起JSONP有更好的错误处理。
  3. JSONP主要被老的浏览器支持,它们往往不支持CORS,而绝大多数现代浏览器都已经支持了CORS)。
    JSONP的优势在于支持老式浏览器,以及可以向不支持CORS的网站请求数据。

4、另外参考

前端常见跨域解决方案

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

推荐阅读更多精彩内容

  • 前端跨域的那些总结 做项目期间一直有遇到关于跨域方面的问题,之前由于没有上过生产环境,对于这方面的问题还不够全面,...
    R_JsBest阅读 406评论 0 1
  • 由于浏览器的同源策略保护机制,浏览器不能执行来自其他来源的脚本。通过js在不同的域之间进行数据传输或通信,比如用a...
    威少_吴阅读 1,354评论 0 2
  • 什么是跨域? 2.) 资源嵌入:、、、等dom标签,还有样式中background:url()、@font-fac...
    电影里的梦i阅读 2,320评论 0 5
  • 那是谁的手 将那西窗上的画布渲染 冬日的暖阳下 枯枝的身姿竟被他画的这般曼妙 静谧、优雅都不足以传达他的灵魂; 亦...
    王淡淡阅读 225评论 0 0
  • 转眼玺宝已经2岁多了,走出了吃睡屎屁尿的混沌生活,接下来我和你要一起摸索走一条心智成长之路。这条道很漫长,没有教科...
    艾米珺阅读 174评论 0 1