什么是跨域?
我认识的跨域是指一个域下的文档或脚本试图去请求另一个域下的资源。
为什么会出现跨域?
出于浏览器的同源策略限制。它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,则浏览器的正常功能可能都会受到影响。可以说Web是构建在同源策略基础之上的,浏览器只是针对同源策略的一种实现。同源策略会阻止一个域的javascript脚本和另外一个域的内容进行交互。所谓同源(即指在同一个域)就是两个页面具有相同的协议(protocol),主机(host)和端口号(port)
同源策略限制以下几种行为:
1、Cookie、LocalStorage 和 IndexDB 无法读取
2、DOM 和 Js对象无法获得
3、AJAX 请求不能发送
常见跨域场景
URL 说明 是否允许通信
http://www.huhaowb.com/a.js
http://www.huhaowb.com/b.js
同一域名,不同文件或路径 允许
http://www.huhaowb.com/lab/c.js
http://www.huhaowb.com:8000/a.js
http://www.huhaowb.com/b.js
同一域名,不同端口 不允许
http://www.huhaowb.com/a.js
https://www.huhaowb.com/b.js
同一域名,不同协议 不允许
http://www.huhaowb.com/a.js
http://192.168.1.12/b.js
域名和域名对应相同ip 不允许
http://www.huhaowb.com/a.js
http://x.huhaowb.com/b.js
主域相同,子域不同 不允许
http://huhaowb.com/c.js
http://www.huhaowb1.com/a.js
http://www.huhaowb2.com/b.js
不同域名 不允许
跨域解决方案
通过jsonp跨域
JSONP 是服务器与客户端跨源通信的常用方法。最大特点就是简单适用,兼容性好(兼容低版本IE),缺点是只支持get请求,不支持post请求。
核心思想:网页通过添加一个 <script>
元素,向服务器请求 JSON 数据,服务器收到请求后,将数据放在一个指定名字的回调函数的参数位置传回来。
1.原生实现:
<script>
var script = document.createElement('script');
script.type = 'text/javascript';
// 传参一个回调函数名给后端,方便后端返回时执行这个在前端定义的回调函数
script.src = 'http://localhost:3000/article?user=admin&callback=handleCallback';
document.head.appendChild(script);
// 回调执行函数
function handleCallback(res) {
alert(JSON.stringify(res));
}
</script>
可以看到,创建了一个script标签(即会发送一个get请求到src指向的地址),src地址是"localhost:3000/article",这个src地址,就是我们请求的服务端接口。注意,这里我们有是那个参数,user和callback,user不说了,这跟我们平时普通的get请求参数无异。主要说下callback这个参数,callback参数就是核心所在。为什么要定义callback呢?首先我们知道,这个get请求已经被发出去了,那么我们如何接口请求回来的数据呢,callback=handleCallback则可以帮我们做这件事。
我们声明了一个handleCallbackc函数,但没有执行,你可以想一下,如果服务端接口到get请求,返回的是handleCallback({message:'hello'})
,这样的话在服务端不就可以把数据通过函数执行传参的方式实现数据传递了吗。
node.js代码实现:
var qs = require('querystring');
var http = require('http');
var server = http.createServer();
server.on('request', function(req, res) {
var params = qs.parse(req.url.split('?')[1]);
var handleCallback = params.callback;
// jsonp返回设置
res.writeHead(200, { 'Content-Type': 'text/javascript' });
res.write(handleCallback + '(' + JSON.stringify(params) + ')');
res.end();
});
server.listen('8080');
console.log('Server is running at port 8080...');
2.ajax实现:
$.ajax({
url: 'http://localhost:3000/article',
type: 'get',
dataType: 'jsonp', // 请求方式为jsonp
jsonpCallback: "handleCallback", // 自定义回调函数名
data: {}
});
总结:
需要注意的是,callback参数定义的方法是需要前后端定义好的,具体什么名字,商讨好就可以了。其实jsonp的整个过程就类似于前端声明好一个函数,后端返回执行函数,执行函数参数中携带所需的数据。
document.domain + iframe跨域
这种跨域的方式最主要的是要求主域名相同。什么是主域名相同呢?
www.huhaowb.com aaa.huhaowb.com ad.huhaowb.com 这三个主域名都是huhaowb.com,而主域名不同的就不能用此方法。
实现原理:两个页面都通过js强制设置document.domain为基础主域,就实现了同域。
1.父窗口:(http://www.huhaowb.com/a.html)
<iframe id="iframe" src="http://child.huhaowb.com/b.html"></iframe>
<script>
document.domain = 'huhaowb.com';
var user = 'admin';
</script>
2.子窗口:(http://child.huhaowb.com/b.html)
<script>
document.domain = 'huhaowb.com';
// 获取父窗口中变量
alert('get js data from parent ---> ' + window.parent.user);
</script>
location.hash + iframe跨域
实现原理: a与b跨域相互通信,通过中间页c来实现。 三个页面,不同域之间利用iframe的location.hash传值,相同域之间直接js访问来通信。
具体实现:A域:a.html -> B域:b.html -> A域:c.html,a与b不同域只能通过hash值单向通信,b与c也不同域也只能单向通信,但c与a同域,所以c可通过parent.parent访问a页面所有对象。
1.a.html:(http://www.huhaowb1.com/a.html)
<iframe id="iframe" src="http://www.huhaowb2.com/b.html"></iframe>
<script>
var iframe = document.getElementById('iframe');
// 向b.html传hash值
setTimeout(function() {
iframe.src = iframe.src + '#user=admin';
}, 1000);
// 开放给同域c.html的回调方法
function onCallback(res) {
alert('data from c.html ---> ' + res);
}
</script>
2.b.html:(http://www.huhaowb2.com/b.html)
<iframe id="iframe" src="http://www.huhaowb1.com/c.html"></iframe>
<script>
var iframe = document.getElementById('iframe');
// 监听a.html传来的hash值,再传给c.html
window.onhashchange = function () {
iframe.src = iframe.src + location.hash;
};
</script>
3.c.html:(http://www.huhaowb1.com/c.html)
<script>
// 监听b.html传来的hash值
window.onhashchange = function () {
// 再通过操作同域a.html的js回调,将结果传回
window.parent.parent.onCallback('hello: ' + location.hash.replace('#user=', ''));
};
</script>
window.name + iframe 跨域
window.name属性可设置或者返回存放窗口名称的一个字符串。他的神器之处在于name值在不同页面或者不同域下加载后依旧存在,没有修改就不会发生变化,并且可以存储非常长的name(2MB)
假设index页面请求远端服务器上的数据,我们在该页面下创建iframe标签,该iframe的src指向服务器文件的地址(iframe标签src可以跨域),服务器文件里设置好window.name的值,然后再在index.html里面读取改iframe中的window.name的值。
<body>
<script type="text/javascript">
iframe = document.createElement('iframe');
iframe.style.display = 'none';
var state = 0;
iframe.onload = function() {
if(state === 1) {
var data = JSON.parse(iframe.contentWindow.name);
console.log(data);
iframe.contentWindow.document.write('');
iframe.contentWindow.close();
document.body.removeChild(iframe);
} else if(state === 0) {
state = 1;
iframe.contentWindow.location = 'http://huhaowb.com/cross-domain/proxy.html';
}
};
iframe.src = 'http://aaa.huhaowb.com/test.html';
document.body.appendChild(iframe);
</script>
</body>
test.html
<script>
window.name = 'This is test pages!';
</script>
proxy.html中间代理页,与index.html同域,内容为空即可。
补充说明:其实location.hash和window.name都是差不多的,都是利用全局对象属性的方法,然后这两种方法和jsonp也是一样的,就是只能够实现get请求
postMessage跨域
一、作用:
- 页面和其打开的新窗口的数据传递
- 多窗口之间消息传递
- 页面与嵌套的iframe消息传递
- 上面三个场景的跨域数据传递
二、发送消息
otherWindow.postMessage(message, targetOrigin);
otherWindow指的是目标窗口,也就是要给哪一个window发送消息,是window.frames属性的成员或者是window.open方法创建的窗口。
message html5规范支持任意基本类型或可复制的对象,但部分浏览器只支持字符串,所以传参时最好用JSON.stringify()序列化。
targetOrigin 协议+主机+端口号,也可以设置为"*“,表示可以传递给任意窗口,如果要指定和当前窗口同源的话设置为”/"。
三、接收消息
window.addEventListener("message", receiveMessage, false);
function receiveMessage(event){
console.log(event.origin)
console.log(event.data)
console.log(event.source)
}
event对象有三个属性,分别是origin,data和source
event.origin 表示postMessage的发送来源,包括协议,域名和端口;
event.data 表示接收到的消息;
event.source 表示发送消息的窗口对象的引用;
代码:
- a.html:(http://www.huhaowb1.com/a.html)
<iframe id="iframe" src="http://www.huhaowb2.com/b.html" style="display:none;"></iframe>
<script>
var iframe = document.getElementById('iframe');
iframe.onload = function() {
var data = {
name: 'huhao'
};
// 向huhaowb2传送跨域数据
iframe.contentWindow.postMessage(JSON.stringify(data), 'http://www.huhaowb2.com');
};
// 接受huhaowb2返回数据
window.addEventListener('message', function(e) {
alert('data from huhaowb2 ---> ' + e.data);
}, false);
</script>
- b.html:(http://www.huhaowb1.com/b.html)
<script>
// 接收huhaowb1的数据
window.addEventListener('message', function(e) {
alert('data from huhaowb1 ---> ' + e.data);
var data = JSON.parse(e.data);
if (data) {
data.number = 16;
// 处理后再发回huhaowb1
window.parent.postMessage(JSON.stringify(data), 'http://www.huhaowb1.com');
}
}, false);
</script>
跨域资源共享(CORS)
支持CORS请求的浏览器一旦发现ajax请求跨域,会对请求做一些特殊处理,对于已经实现CORS接口的服务端,接受请求,并做出回应。
有一种情况比较特殊,如果我们发送的跨域请求为“非简单请求”,浏览器会在发出此请求之前首先发送一个请求类型为OPTIONS的“预检请求”,验证请求源是否为服务端允许源,这些对于开发这来说是感觉不到的,由浏览器代理。
总而言之,客户端不需要对跨域请求做任何特殊处理。
简单请求与非简单请求
浏览器对跨域请求区分为“简单请求”与“非简单请求”
“简单请求”满足以下特征:
(1) 请求方法是以下三种方法之一:
HEAD
GET
POST
(2)HTTP的头信息不超出以下几种字段:
Accept
Accept-Language
Content-Type:application/x-www-form-urlencoded、 multipart/form-data、text/plain (Content-Type 的值仅限于三者之一)
不满足这些特征的请求称为“非简单请求”,例如:content-type=applicaiton/json , method = PUT/DELETE…
简单请求
浏览器判断跨域为简单请求时候,会在Request Header中添加 Origin (协议 + 域名 + 端口)字段 , 它表示我们的请求源,CORS服务端会将该字段作为跨源标志。
CORS接收到此次请求后 , 首先会判断Origin是否在允许源(由服务端决定)范围之内,如果验证通过,服务端会在Response Header 添加 Access-Control-Allow-Origin、Access-Control-Allow-Credentials等字段。
必须字段:
Access-Control-Allow-Origin:表示服务端允许的请求源,*标识任何外域,多个源 , 分隔
可选字段
Access-Control-Allow-Credentials:false 表示是否允许发送Cookie,设置为true 同时,ajax请求设置withCredentials = true,浏览器的cookie就能发送到服务端
Access-Control-Expose-Headers:调用getResponseHeader()方法时候,能从header中获取的参数
浏览器收到Respnose后会判断自己的源是否存在 Access-Control-Allow-Origin允许源中,如果不存在,会抛出“同源检测异常”。
总结:简单请求只需要CORS服务端在接受到携带Origin字段的跨域请求后,在response header中添加Access-Control-Allow-Origin等字段给浏览器做同源判断
非简单请求
进行非简单请求时候 , 浏览器会首先发出类型为OPTIONS的“预检请求”,请求地址相同 ,
CORS服务端对“预检请求”处理,并对Response Header添加验证字段,客户端接受到预检请求的返回值进行一次请求预判断,验证通过后,主请求发起。
例如:发起 content-type=application/json 的非简单请求,这时候传参要注意为json字符串
这里可以看到,浏览器连续发送了两个jsonp.do请求 , 第一个就是“预检请求”,类型为OPTIONS,因为我们设置了content-type这个属性,所以预检请求的Access-Control-Expose-Headers必须携带content-type,否则预检会失败。
预检通过后,主请求与简单请求一致。
vue框架的跨域
利用node + webpack + webpack-dev-server代理接口跨域。在开发环境下,由于vue渲染服务和接口代理服务都是webpack-dev-server同一个,所以页面与代理接口之间不再跨域,无须设置headers跨域信息了。
webpack.config.js部分配置:
module.exports = {
entry: {},
module: {},
...
devServer: {
historyApiFallback: true,
proxy: {
'^/api': {
target: 'http://www.huhaowb.com', //要跨域的域名
ws: true, // 是否启用websockets
changeOrigin: true, //是否允许跨越
pathRewrite: {
'^/api': '' //将你的地址代理位这个 /api 接下来请求时就使用这个/api来代替你的地址
},
}
}
noInfo: true
}
}
//接下来发起请求这样写就不会出现跨域问题了
axios({
url: "/api/usercomment",
method: "post",
}).then(res => {
console.log(res);
})