前端跨域的处理
由于浏览器的同源策略保护机制,浏览器不能执行来自其他来源的脚本。通过js在不同的域之间进行数据传输或通信,比如用ajax向一个不同的域请求数据,或者通过js获取页面中不同域的框架中(iframe)的数据的操作就叫跨域。
所谓同源,就是指协议、域名(IP)、端口三者都相同。只要有其中一者不相同,都是跨域,无法进行数据的交流。例如:
注意的是,localhost 与 本机的IP 地址也属于跨域。
浏览器执行javascript脚本时,会检查这个脚本属于哪个页面,如果不是同源页面,就不会被执行。
解决方法主要有以下几种:
1、在工程化的前端项目中,在本机进行开发时,后台的接口可能会出现跨域,可以设置 webpack 的服务器代理,将本机的请求转发到与接口同源的地址。
首先安装插件:webpack-dev-server,然后再 config 文件夹里找到 index.js 文件,在 dev 对象下的 proxyTable 字段添加对应的需要替换的地址。
module.exports = {
...
devServer: { //本地开发服务器设置
...
port: 8080, //当前的端口号
proxyTable: { //服务器代理设置
'/v3/admin': { //需要代理的接口形式
target: 'http://localhost:8700', //将当前端口号代理到8700
changeOrigin: true,
secure: false
}
}
}
}
以上设置就是将所有包含 '/v3/admin' 字段的接口,从原来的 localhost: 8080 代理到 localhost: 8700 ,变成 'http://localhost: 8700/v3/admin' 从而获取到 8700 端口的内容。
2、使用 CORS 进行数据传输。
CORS是一个W3C标准,全称是"跨域资源共享"(Cross-origin resource sharing)。
它允许浏览器向跨源服务器,发出XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制。
CORS标准新增了一组 HTTP 首部字段,允许服务器声明哪些源站有权限访问哪些资源。另外,规范要求,对那些可能对服务器数据产生副作用的 HTTP 请求方法(特别是GET以外的 HTTP 请求,或者搭配某些 MIME 类型的POST请求),浏览器必须首先使用OPTIONS方法发起一个预检请求(preflight request),从而获知服务端是否允许该跨域请求。服务器确认允许之后,才发起实际的 HTTP 请求。在预检请求的返回中,服务器端也可以通知客户端,是否需要携带身份凭证(包括Cookies和 HTTP 认证相关数据)。
CORS需要浏览器和服务器同时支持,浏览器需要 ie8 以上。
浏览器端的写法:
function CORSRequest(method,url,opation,callback) {
var xhr = new XMLHttpRequest();
if ("withCredentials" in xhr) {
// 此时即支持CORS的情况
// 检查XMLHttpRequest对象是否有“withCredentials”属性
// “withCredentials”仅存在于XMLHTTPRequest level 2对象里
} else {
// 否则检查是否支持XDomainRequest
// XDomainRequest仅存在于IE中,是IE8 和 IE9 用于支持CORS请求的方式
xhr = new XDomainRequest();
}
xhr.open(method, url, true);
if(method=="POST"){ //可以设置不同请求方法的操作,所有的请求都可以分别设置
xhr.setRequestHeader("Content-type","application/x-www-form-urlencoded"); //设置请求头
xhr.send(opation); //传输数据到服务器
}else{
xhr.send();
}
xhr.onload = function(){
callback(xhr.responseText);
}
};
function notice(data) {
console.log(data)
}
CORSRequest('POST','http://example.com','传递给服务器的数据',notice);
服务器端的写法:
Apache:
Apache需要使用mod_headers模块来激活HTTP头的设置,它默认是激活的。你只需要在Apache配置文件的, , 或的配置里加入以下内容即可:
Header set Access-Control-Allow-Origin *
PHP:
<?php
header("Access-Control-Allow-Origin:*"); //允许所有来源访问
header('Access-Control-Allow-Method:POST,GET'); //允许访问的方式
以上的配置的含义是允许任何域发起的请求都可以获取当前服务器的数据。当然,这样有很大的危险性,恶意站点可能通过XSS攻击我们的服务器。所以我们应该尽量有针对性的对限制安全的来源,例如:
<?php
header("Access-Control-Allow-Origin: http://blog.csdn.net");
CORS 的优点:
CORS与JSONP相比,无疑更为先进、方便和可靠。
1、 JSONP只能实现GET请求,而CORS支持所有类型的HTTP请求。
2、 使用CORS,开发者可以使用普通的XMLHttpRequest发起请求和获得数据,拥有 onerror 和 onabort 方法,比起JSONP有更好的错误处理。
3、 JSONP主要被老的浏览器支持,它们往往不支持CORS,而绝大多数现代浏览器都已经支持了CORS。
注意点:
当发送的请求是非简单请求,浏览器会在正式通信之前,增加一次HTTP查询请求,称为"预检"请求(preflight)。浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的XMLHttpRequest请求,否则就报错。
一般前端框架比如"ExtJS"、"AngularJS", 框架监测到访问的域名可能存在跨域的话会先发送一个OPTIONS请求,验证是否可进行通信,如果返回可通信才会真正发起一个POST、GET请求。
下图是框架发起的OPTIONS请求,当如果服务器的Nginx并没有设置允许跨域请示的时候,它会返回一个405状态码。
axios 框架也会发送 OPTIONS 请求。
关于CROS 的更多内容,可以进入以下链接了解:
https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Access_control_CORS
http://www.ruanyifeng.com/blog/2016/04/cors.html
3、JSONP
在js中,我们直接用XMLHttpRequest请求不同域上的数据时,是不可以的。但是,在页面上引入不同域上的js脚本文件却是可以的,jsonp正是利用这个特性来实现的。
JSONP是一种非正式传输协议,该协议的一个要点就是允许用户传递一个约定的参数(一般是callback)给服务端,然后服务端返回数据时会将这个callback参数作为函数名来包裹住JSON数据,这样客户端就可以随意定制自己的函数来自动处理返回数据了。
比如,有个a.html页面,它里面的代码需要利用ajax获取一个不同域上的json数据,假设这个json数据地址是http://example.com/data.php,那么a.html中的代码就可以这样:
我们看到获取数据的地址后面还有一个callback参数,按惯例是用这个参数名,但是你用其他的也一样。当然如果获取数据的jsonp地址页面不是你自己能控制的,就得按照提供数据的那一方的规定格式来操作了。关键是参数值必须是当前页面 script 设置的数据处理回调函数的名字。
因为是当做一个js文件来引入的,所以http://example.com/data.php返回的必须是一个能执行的js文件,所以这个页面的php代码可能是这样的:
最终那个页面输出的结果是:
所以通过http://example.com/data.php?callback=dosomething得到的js文件,就是我们之前定义的dosomething函数,并且它的参数就是我们需要的json数据,这样我们就跨域获得了我们需要的数据。
这样jsonp的原理就很清楚了,通过script标签引入一个js文件,这个js文件载入成功后会执行我们在url参数中指定的函数,并且会把我们需要的json数据作为参数传入。所以jsonp是需要服务器端的页面进行相应的配合的。
知道jsonp跨域的原理后我们就可以用js动态生成script标签来进行跨域操作了,而不用特意的手动的书写那些script标签。
一个跨域获取淘宝关键字搜索建议的例子:
效果:
优点:
不受同源策略的限制;
兼容性更好,在更加古老的浏览器中都可以运行,不需要XMLHttpRequest或ActiveX的支持;
请求完毕后可以通过调用callback的方式回传结果。
缺点:
只支持GET请求而不支持POST等其它类型的HTTP请求;
只支持跨域HTTP请求这种情况,不能解决不同域的两个页面之间如何进行JavaScript调用的问题。
4、设置代理服务器
例如www.123.com/index.html需要调用www.456.com/server.php,可以写一个接口www.123.com/server.php,由这个接口在后端去调用www.456.com/server.php并拿到返回值,然后再返回给index.html,这就是一个代理的模式。相当于绕过了浏览器端,自然就不存在跨域问题。
例如:
a.php 后台获取跨域的数据
<?php
$url="https://api.douban.com/v2/movie/subject/1764796”; //接口地址
$content=file_get_contents($url);
echo $content;
?>
前端只要将 XMLHttpRequest 的请求地址设置为 'a.php' 就可以。
5、使用 window.name
window对象有个name属性,该属性有个特征:即在一个窗口(window)的生命周期内,窗口载入的所有的页面都是共享一个window.name的,每个页面对window.name都有读写的权限,window.name是持久存在一个窗口载入过的所有页面中的,并不会因新页面的载入而进行重置。
比如:有一个页面a.html,它里面有这样的代码:
再看看b.html页面的代码:
a.html页面载入后3秒,跳转到了b.html页面,结果为:
我们看到在b.html页面上成功获取到了它的上一个页面a.html给window.name设置的值。如果在之后所有载入的页面都没对window.name进行修改的话,那么所有这些页面获取到的window.name的值都是a.html页面设置的那个值。当然,如果有需要,其中的任何一个页面都可以对window.name的值进行修改。注意,window.name的值只能是字符串的形式,这个字符串的大小最大能允许2M左右甚至更大的一个容量,具体取决于不同的浏览器,但一般是够用了。
上面的例子中,我们用到的页面a.html和b.html是处于同一个域的,但是即使a.html与b.html处于不同的域中,上述结论同样是适用的,这也正是利用window.name进行跨域的原理。
此方法需要与 iframe 配合使用。
比如有一个www.example.com/a.html页面,需要通过a.html页面里的js来获取另一个位于不同域上的页面www.cnblogs.com/data.html里的数据。
data.html页面里的代码很简单,就是给当前的window.name设置一个a.html页面想要得到的数据值。data.html里的代码:
那么在a.html页面中,我们怎么把data.html页面载入进来呢?显然我们不能直接在a.html页面中通过改变window.location来载入data.html页面,因为我们想要即使a.html页面不跳转也能得到data.html里的数据。答案就是在a.html页面中使用一个隐藏的iframe来充当一个中间人角色,由iframe去获取data.html的数据,然后a.html再去得到iframe获取到的数据。
充当中间人的iframe想要获取到data.html的通过window.name设置的数据,只需要把这个iframe的src设为www.cnblogs.com/data.html就行了。然后a.html想要得到iframe所获取到的数据,也就是想要得到iframe的window.name的值,还必须把这个iframe的src设成跟a.html页面同一个域才行,不然根据前面讲的同源策略,a.html是不能访问到iframe里的window.name属性的。这就是整个跨域过程。
看下a.html页面的代码:
上面的代码只是最简单的原理演示代码,你可以对使用js封装上面的过程,比如动态的创建iframe,动态的注册各种事件等等,当然为了安全,获取完数据后,还可以销毁作为代理的iframe。
6、使用 window.postMessage
window.postMessage(message,targetOrigin) 方法是html5新引进的特性,可以使用它来向其它的window对象发送消息,无论这个window对象是属于同源或不同源,目前IE8+、FireFox、Chrome、Opera等浏览器都已经支持window.postMessage方法。
调用postMessage方法的window对象是指要接收消息的那一个window对象,该方法的第一个参数message为要发送的消息,类型只能为字符串;第二个参数targetOrigin用来限定接收消息的那个window对象所在的域,如果不想限定域,可以使用通配符 * 。
需要接收消息的window对象,可是通过监听自身的message事件来获取传过来的消息,消息内容储存在该事件对象的data属性中。
上面所说的向其他window对象发送消息,其实就是指一个页面有几个框架的那种情况,因为每一个框架都有一个window对象。下面看一个简单的示例,有两个页面
我们运行a页面后得到的结果:
我们看到b页面成功的收到了消息。
使用postMessage来跨域传送数据还是比较直观和方便的,但是缺点是IE6、IE7不支持,所以用不用还得根据实际需要来决定。