跨域问题的产生及解决
开发过程中,或多或少都碰见过这样的情况,本地写了个ajax请求想从某某域下获取数据,代码如下:
$.ajax({
url:"https://www.baidu.com",
data:{
name:"name",
address:"beijing"
},
type:"post",
success:function(res){
},
error:function(err){
}
});
运行之后出现如下图中的报错。
浏览器提示:到http://www.baidu.com的请求被CORS策略阻止,资源的请求头上没有“Access-Control-Allow-Origin”标头,不允许访问。
简单来说就是出现跨域问题了!!!请求被阻止了 说到跨域,首先什么是跨域?
问题: 浏览器提示出现跨域了,,,,那这个请求服务器端是否能收到呢?
这个请求仍然会被发送到服务器,也就是服务器会收到这个请求。浏览器会拦截跨域请求只是拦截返回结果。
首先,请求被拦截之后会被改成
option
请求送达服务器。这样服务器知道有人在请求。
已经跨域了,服务器依然能收到请求,这样是有意义的,让服务器决定要不要运行是更为合理更为科学的。
一、什么是跨域?
跨域,是指一个域下的文档或脚本去请求另一个域下的的资源。
二、出现跨域问题为什么报错?
这是由于浏览器同源策略的限制。
所谓同源是指:协议、域名、端口三者都要相同,即使两个不同的域名指向同一个IP地址,也是非同源。 一句话总结:只要请求的资源URL的协议、域名、端口号三者任意一个与当前页面的不同就会出现跨域问题。
同源策略是一种约定,用于保障浏览器最基本最核心的安全问题,若是缺少同源策略浏览器的正常功能可能都会受到影响。试想一下,如果没有同源(同协议、同域名、同端口)策略,不同源的数据和资源(Cookie、localStorage等)就能随意访问,这种情况简直不敢想象。所以为了浏览器采用同源策略是非常必要的。
但是,但是,问题又来了,在本地请求react.development.js时
<script src="https://unpkg.com/react@16/umd/react.development.js"></script>
按照前面的说法,本地和react.development.js并不同源(本地域名为dev1.adc.com,react.development.js文件所在域域名为unpkg.com),也是跨域请求,为什么确可以正常请求呢?
原因就在于,同源策略只限制特定的跨域请求,对于像css/js/image这些静态文件的跨域请求并不干涉。
三、 同源策略限制的几种行为:
- Cookie、localStorage和indexDB读取
- DOM 和js对象获取
- ajax请求
在工作中,很容易遇见需要跨域请求数据的情况,如本地为http://localhost:9990,需要请求的接口地址在http://dev4.byy.com。按照前面的描述同源策略会限制这次请求。那该如何解决呢?
四、解决跨域问题的几种方法
本文只列出前端处理部分
1、jsonp
我们知道js、image、css之类的静态文件是不存在跨域问题的。而jsonp的原理正是这个。
首先前后端需要沟通确定好回调函数,并将其作为URL的参数。 后端接收到请求后,通过该参数获取到回调函数名称,并将处理结果作为回调函数的参数将其返回
。 收到结果后,因为是script标签,浏览器会当做是脚本继续执行,从而达到跨域获取数据的目的。
1.1 jsonp前端实现
<script>
var script=document.createElement("script");
script.src="http://dev4.byy.com?callback=jsonpCallback";
// 执行回调函数
document.head.appendChild(script);
function jsonpCallback(data){
.....
}
</script>
1.2 接收到的返回内容
jsonpCallback({
"name":"nanjing",
"address":"jiangsu"
});
1.3 jQuery方法实现jsonp
$.ajax({
dataType:"jsonp", // 必须写
url:"aaa/bbb/ccc",
success:function(result){
console.log(result);
},
error:function(error){
console.log(error);
}
})
// 直接使用jsonp()
$.jsonp({
url:"aaa/bbb/ccc",
data:{
name:"clicke"
},
success:function(res){},
error:function(error){}
})
1.4 jsonp解决跨域的优点
- 兼容性很好,低版本浏览器也能无压力的执行
- 不需要XMLHttpRequest或ActiveX的支持
1.5 jsonp解决跨域的缺点
- 只支持get请求(静态资源的请求方式都是get),请求的参数会追加在请求地址上,所以这种方式发送的数据是有限制的。
- 动态拆入script标签本身就是一种脚本注入,存在安全隐患。
2、CORS
CORS是一个W3C标准,全称是跨域资源共享(Cross-origin resource sharing)它允许浏览器向跨源服务器发出XMLHttpRequest请求,从而克服了ajax只能同源使用的限制。
CORS需要浏览器和服务器同时支持,才可以实现跨域请求,目前几乎所有浏览器都支持CORS,IE则不能低于IE10。CORS的整个过程都由浏览器自动完成,前端无需做任何设置,跟平时发送ajax请求并无差异。so,实现CORS的关键在于服务器,只要服务器实现CORS接口,就可以实现跨域通信。
CORS的原理:增加额外的的HTTP头部信息让浏览器和服务器进行沟通,从而决定接受还是拒绝该请求。 因此,想要通过CORS实现跨域,服务器端和前端需要协同处理。
服务器接收到请求之后,会根据自己的跨域规则进行验证 ,验证成功后直接返回请求结果 。
从前端角度看,将CORS请求分成简单请求和非简单请求两类 。
2.1 简单请求
当请求满足下面两点时,该请求可以视为简单请求,否则为非简单请求
- 请求方式为get、post、head三者之一
- HTTP请求头不超出以下几种字段:Accept/Accept-Encoding/Accept-Language/Cache-Control/Connection/Cookie/Host/If-Modified-Since/Referer/User-Agent/Content-Type/Content-Language。其中Content-Type仅限于三个值:application/x-www-form-urlencoded、multipart/form-data、text/pain。
2.1.1 前端实现
ajax中添加header字段,设置请求头
使用beforeSend方法设置请求头
$.ajax({
url:"http://dev4.byy.com/user/menusAndFunction",
xhrFields:{withCredentials:true},
beforeSend: function (XMLHttpRequest) {
var sessionid = "SessionID";
XMLHttpRequest.setRequestHeader(sessionid, sid);
},
success:function(res){
console.log("成功了",res);
},
error:function(err){
console.log("失败了",err);
}
});
2.1.2 请求结果
可以看到,简单请求和普通的ajax请求并无区别,前端无需做任何设置。 代码中的xhrFields:{withCredentials:true} 是因为接口 http://dev4.byy.com/user/menusAndFunction 的请求需要带携带cookie。
请求详情图(CORS与普通ajax请求对比)
2.2 非简单请求
非简单的CORS请求会增加一次HTTP查询请求,称之为预先检验请求preflight,用于查询要被跨域访问的服务器,是否允许当前域是否可以发送跨域请求。如果预先验证请求通过,浏览器才会发送真正的跨域请求。
为什么要预
2.2.1 一次非简单请求
$.ajax({
url:"http://dev4.byy.com/user/menusAndFunction",
type:"delete", // 请求方式非get/post/head,为非简单CORS请求
xhrFields:{withCredentials:true},
success:function(res){
console.log("成功了",res);
},
error:function(err){
console.log("失败了",err);
}
});
2.2.2 请求结果
这次非简单请求没能通过预检查请求
2.2.3 关于withCredentials 属性
CORS请求默认不发送Cookie ,如果要发送cookie到服务器。首先要在服务器端设置 Allow-Control-Allow-Credentials:true ,接着在ajax请求中设置withCredentials:true
2.3 CORS的优点
- 支持所有类型的HTTP请求
- 相较于jsonp有完善的错误处理机制,方便排查错误
2.4 CORS的缺点
- 浏览器的兼容性不太佳,IE浏览器需要IE10以上才支持
2.5 CORS 跨域一句话总结
洋洋洒洒写了这么毒,主要是讲一下什么是CORS,CROS如何分类。从前端来看,CORS和普通的ajax在写法上没有区别。不同的地方在于HTTP请求头和返回头多了某些特定字段(orign、allow-control-allow-origin、allow-control-allow-methods等) CORS主要是在服务器端实现。
3、document.domain
适用情况:一级域名相同而子域名不同
原理:在父页面和子页面中,通过js强制设置document.domain的值为一级域名实现同域,通过页面嵌套的方式实现页面间的相互操作。 document.domain只能从子域往高级的域名设置,往下设置或者往其他域名设置都是不允许的
假设现有两个页面 b.com域下的b.html a.b.com下的a.html 现在要实现两个页面间的通信
3.1 前端实现
// b.com/b.html
<iframe id="ifr" src="http://a.b.com/index.html"></iframe>
<script>
document.domain = 'b.com';
function aa(str) {
console.log(str);
}
window.onload = function () {
document.getElementById('#ifr').contentWindow.bb('aaa');
}
</script>
// a.b.com/a.html
<script>
document.domain="b.com";
function bb(str){
console.log(str);
}
parent.aa("bbb");
</script>
通过设置document.domain=“b.com” 把两个页面的document.domain都指向一级域名,之后这两个页面就可以像同一域下的两个页面互相访问了。
父页面中:通过ifr.contentWindow 或 ifr.contentDocument.window访问子页面 子页面中:通过window.parent或者parent访问父页面
4、window.postMessage
原理:HTML5允许窗口之间发送消息
适用条件:浏览器支持HTML5
4.1前端实现
dev4.byy.com/a.html
// localhost:9990/html/b.html
<script>
window.onload=function(){
document.getElementsByTagName("iframe")[0].contentWindow.postMessage("hello helllo","http://dev4.byy.com/a.html")
}
</script>
<iframe src="http://dev4.byy.com/a.html"></iframe>
// http://dev4.byy.com/a.html
<script>
window.addEventListener("message",function(){
console.log(event);
}
</script>
上图中:data:传输的数据 、origin:发送消息的源 。 必须等目标窗口完全加载完毕,发送的数据才能被监听到。
4.2 安全性
使用postMessage跨域交换数据是安全的,postMessage采用的是“双向安全机制”。发送方发送数据的时候回确认接收方的origin,而接收方监听到message事件后,也用event.origin判断是否来自于正确可靠的发送方。
从localhost:9990/html/b.html 给http://www.baidu.com发送消息
错误提示:不匹配,发送消息失败
5、navigation对象
原理:IE6/7上的一个漏洞,iframe共享navigation对象
适用条件:IE6、IE7
实现 – – a.com/index.html 与b.com/index.html 传递消息
//a.com/index.html
navigation.onData(){}
typeof navigation.getData()==="function"||navigation.getData()
// b.com/index.html
navigation.getData = function(){
$.get('/path/under/b.com')
.success(function(data){
typeof navigation.onData === 'function'
|| navigation.onData(data)
});
}
注:电脑上未安装IE6/7,未验证。只是提供一种解决方法。
6、nginx代理
原理:配置一个代理路径替换实际的访问路径,使得浏览器认为访问的资源都是属于相同协议、同域名、同端口的。可以认为nginx代理是给浏览器的一种障眼法
7、window.name
原理:window.name属性有一个特点,每個窗口都有獨立的window.name與之對應; 在一個窗口被關閉之前,窗口載入的所有頁面同時共享一個window.name,每個頁面都有對window.name的讀寫權限;window.name一直存在於當前窗口,即使有新的頁面載入也不會被改變;window.name可以存儲不草果2M的數據,數據格式按需自定義
代碼實現 – – localhost:9990/html/a.html 獲取 http://dev2.byy.com/html/b.html 的數據
// localhost:9990/html/a.html
<script>
function load () {
var iframe = document.getElementById('iframe');
iframe.onload = function () {
var window = iframe .contentWindow;
console.log(window.name);
}
iframe.src = 'proxy.html'; //在iframe页面加载完毕的时候,再让iframe与当前页面属于同一个域下,就可以拿到window.name
}
</script>
<iframe id="iframe" src="http://dev2.byy.com/html/b.html" onload="load()" style="display:none"></iframe>
執行結果
http://dev2.byy.com/html/b.html中設置window.name 成功獲取到http://dev2.byy.com/html/b.html中設置的window.name
8、chrome 设置 --disable-web-security 解决跨域
对于Chrome浏览器,命令行执行以下命令关闭安全限制,解决跨域
sudo open -a "/Applications/Google Chrome.app" --args --disable-web-security --user-data-dir=/Users/yourname/chromeDevUserData/
执行后会自打开浏览器,同时提示浏览器安全降低了。说明设置成功了。
这是一个临时的方法,并不明真正解决问题,每次退出浏览器再次打卡都需要执行上面的动作。
五、总结
实现跨域的方法非常多,比如localtion.hash 、web Sockets、flash URLLoader等等等等,本文档并没有列出全部。
由于本身条件限制,文档中提到的方法6未能实地验证。
这么多实现方法中个人倾向于JSONP+CORS 两种结合使用, 其中JSONP用于兼容较低版本的浏览。相较于文中列出的其他方法,前端不需要做任何配置,不需要额外使用标签iframe,代码上更加清晰简洁。