跨域处理

作者: jazenye | 来源:发表于2017-08-11 12:39 被阅读178次

跨域

  • 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、另外参考

前端常见跨域解决方案

相关文章

  • 关于设置env等环境变量的思考

    1、如何处理跨域后台处理跨域前端处理跨域浏览器处理跨域 前端本地处理跨域:代理线上跨域的处理方式:Nginx反向代...

  • 全局异常处理(跨域)

    全局异常处理 注意: 全局异常处理可能会出现跨域 解决跨域

  • web跨域解决方案

    围绕以下几点介绍: 什么是跨域? 常用的几种跨域处理方法? crossdomain.xml解决跨域问题 什么是跨域...

  • Spring Boot配置跨域请求

    配置跨域请求处理拦截

  • webpack处理前端跨域

    前后端分离开发的时候,遇到跨域问题,可以用webpack处理跨域问题

  • Axios跨域处理方案

    Ajax跨域问题使用jsonp处理 Axios跨域请求问题处理: 1、在config文件夹下的prod和dev的j...

  • Asp.Net Core WebAPI 跨域处理

    一、Core WebAPI中的跨域处理 在使用WebAPI项目的时候基本上都会用到跨域处理 Core WebAP...

  • Axios跨域处理方案

    Ajax跨域问题使用jsonp处理 Axios跨域请求问题处理: 1、在config文件夹下的prod和dev的j...

  • zuul路由的跨域配置

    跨域说明和规则 (方法一)在zull网关服务中统一处理跨域问题,但下面所有controller中去掉跨域注解---...

  • 前端踩过的坑

    关于前端 跨域问题 大概问题 1,前端无须处理,需要在后台配置 niginx的配置 处理跨域的域名 关于布局上 ...

网友评论

    本文标题:跨域处理

    本文链接:https://www.haomeiwen.com/subject/mnnnlxtx.html