前面的话
我们经常听到跨域这词,这是由于浏览器同源策略限制的一类请求场景。这样做的目的使得 浏览器不容易受到攻击。
什么是同源策略?
同源策略(Same origin policy)是一种约定,所谓同源是指“协议、域名、端口”三者都相同,如果没有同源策略,浏览器很容易受到XSS、CSRF等攻击。
同源策略限制了什么?
1)Cookie、LocalStorage和IndexDB无法获取
2) DOM 和JS对象无法获取
3)AJAX请求不能发送
不满足协议、域名、端口相同的都不能通信:
跨域的解决方法
- CORS(跨域资源共享)
- 通过jsonp跨域
- document.domain + iframe跨域
- location.hash + iframe跨域
- window.name + iframe跨域
- postMessage跨域
- WebSocket协议跨域
- nginx代理跨域
- nodejs中间件代理跨域
CORS (Cross-origin resource sharing跨域资源共享)
CORS的背后基本思想:使用自定义的HTTP头部让浏览器与服务器进行沟通,从而决定请求响应是应该成功还是应该失败。
- 使用node创建两个http服务器,监听端口8081和8084
- cors.html运行在8081下
cors1.js服务,监听8081
var express = require('express');
var app = express();
// public下有cors.html页面
app.use(express.static('public')).listen(8081);
cors.html请求代码:
<script>
let xhr = new XMLHttpRequest();
document.cookie = 'name=xiaoqi';// cookie不能跨域
xhr.withCredentials = true;// 前端设置是否带cookie
xhr.open('PUT', 'http://localhost:8084/getData', true);
xhr.setRequestHeader('name', 'xiaoqi');// 设置请求头
xhr.onreadystatechange = function() {
if(xhr.readyState===4 && xhr.status===200){
console.log(xhr.response);
// 返回响应头的name字段的值
console.log(xhr.getResponseHeader('name'));
}
}
xhr.send();
</script>
cors2.js服务,监听8084:
var express = require('express');
var app = express();
// 设置白名单
let whiteList = ['http://localhost:8081'];
app.use(function(req, res, next) {
let origin = req.headers.origin;
if(whiteList.includes(origin)){
// 设置哪个源可以访问我
res.setHeader('Access-Control-Allow-Origin', origin);
// 允许携带哪个头来访问我
res.setHeader('Access-Control-Allow-Headers', 'name');
// 允许哪个方法访问我
res.setHeader('Access-Control-Allow-Methods', 'PUT');
// 允许携带cookie
res.setHeader('Access-Control-Allow-Credentials', 'true');
// 预检的存在时间
res.setHeader('Access-Control-Max-Age',6);
// 允许返回的头
res.setHeader('Access-Control-Expose-Headers', 'name');
if(req.method === 'OPTIONS'){
res.end()// OPTIONS请求不做任何处理
}
}
next();
})
app.put('/getData', function(req, res){
console.log(req.headers);
res.setHeader('name', 'jw');
res.end('hhh');
});
app.use(express.static('public1')).listen(8084);
显然8081与8084不在同一个域,在后端(cors2.js)代码中自动设置HTTP的响应头,来规定哪些域能访问。
上面代码的结果:
通过jsonp跨域
jsonp跨域原理是通过<script>
标签来实现的。在HTML页面中通过相应的<script>
标签从不同域名下加载资源,这种方式是被浏览器运行的。
注意: jsonp只能实现GET方法一种请求
我们也可以动态创建script标签,再请求一个带参网址实现跨域通信。
前端页面请求:
- 原生实现
<script> var script = document.createElement('script'); script.type = 'text/javascript'; script.src = 'http://localhost:8081?user=admin&callback=onBack'; document.head.appendChild(script); // 回调函数 function onBack(res) { // 把一个对象转为json的字符串类型 alert(JSON.stringify(res)); } // 服务器下的返回 // onBack({"status": true,"user": "admin"}); </script>
- jquery ajax:
$.ajax({ url: 'http://localhost:8081', type: 'get', dataType: 'jsonp', // 请求方式为jsonp jsonpCallback: "handleCallback", // 自定义回调函数名 data: {} });
- vue.js
this.$http.jsonp('http://localhost:8081', { params: {}, jsonp: 'handleCallback' }).then((res) => { console.log(res); })
后端node.js代码实现:
var qs = require('qs');
var querystring = require('querystring');
var http = require('http');
// 创建服务器
var server = http.createServer();
// 监听请求
server.on('request', function(req, res) {
// qs.parse()将URL解析成对象的形式: params为{ user: 'admin', callback: 'onBack' }
var params = qs.parse(req.url.split('?')[1]);
var fn = params.callback;// 'onBack'
// jsonp返回设置
res.writeHead(200, {
'Content-Type': 'text/javascript'
})
// res.write用来向请求的客户端发送响应的内容
res.write(fn + '(' + JSON.stringify({"status": true, "user": "admin"})+ ')');
res.end();
})
// 监听端口8081
server.listen('8081');
console.log('Server is running at port 8081...');
请求结果:
document.domain + iframe跨域
document.domain是用来得到当前网页的域名。
比如,在百度(https://www.baidu.com)页面控制台中输入:
alert(document.domain); // `www.baidu.com`
我们也可以给document.domain属性赋值,不过是有限的, 只能赋值为当前域名或者一级域名。
alert(document.domain = "baidu.com"); // `baidu`
alert(document.domain = "www.baidu.com"); // `www.baidu.com`
上面的赋值都是成功的:
下面的赋值则无效:
alert(document.domain = "qq.com"); // `baidu`
alert(document.domain = "www.qq.com"); // `www.baidu.com`
qq.com与baidu.com的一级域名不相同,所以出错。
如何使用document.domain + iframe实现跨域?
- 实现原理:两个页面都通过js强制设置document.domain为基础主域,就实现了同域。
- 前提条件: 这两个域名必须属于同一个一级域名,并且所用协议,端口相同,否则无法使用其方法来实现跨域。
注意:该方案仅限主域相同、子域不同的跨域场景
实例:
news.baidu.com里的一个网页(news.html)引入了map.baidu.com里的一个
网页(map.html),这两个域是不相同的,但它们的主域都相同,都是baidu.com,
可以使用该方法实现跨域,将两个页面的domain都改为“baidu.com”.在news.html
与map.html中都加入:document.domain = "baidu.com"
代码:
new.baidu.com下的news.html:
<iframe id="iframe1" src="http://map.baidu.com/map/html"></iframe>
<script>
document.domain = 'baidu.com';
var iframe1 = document.getElementById("iframe1");
// 返回iframe中的文档。兼容写法
var doc = iframe1.contentDocument || iframe1.contentWindow.document;
var p1 = doc.getElementById('p1');
alert(p1.innerHTML);
</script>
动态设置iframe的src更好,这样可以避免阻塞页面加载。
map.baidu.com下的map.html:
<p id="p1"> 我是map.baidu.com中的p</p>
<script>
document.domain = 'baidu.com';
</script>
iframe阻塞页面加载
这里iframe,就不得不说一下使用它的性能问题。
-
及时触发 window 的 onload 事件是非常重要的。onload 事件触发使浏览器的 “忙” 指示器停止,告诉用户当前网页已经加载完毕。当 onload 事件加载延迟后,它给用户的感觉就是这个网页非常慢。
-
window 的 onload 事件需要在所有 iframe 加载完毕后(包含里面的元素)才会触发。在 Safari 和 Chrome 里,通过 JavaScript 动态设置 iframe 的 SRC 可以避免这种阻塞情况。
location.hash + iframe跨域
原理: 利用location.hash来进行传值。在url:http://a.com#helloword中的#helloword就是location.hash,改变hash并不会导致页面刷新,所以可以利用hash值来进程数据传递。
例如:在域名a.com下的a1.html文件要与域名b.com下的b.html文件进行数据传递。
步骤:
- a1.html下创建一个隐藏的iframe(不影响页面格局),其src指向域名b.com下的b.html页面(http://b.com#hello) (#hello为hash值,用来传递数据)
- b.html响应请求后,可以通过修改a1.html的hash值来传递数据。(只不过在不同域名下,IE、Chrome是不允许直接在b.html中直接通过parent.lacation.hash来改变a1.html的hash值;fireFox可以)。所以为了兼容,在IE、Chrome环境下,要借助a.com域名下的一个代理iframe(a2.html)。
- 如果是IE、Chrome浏览器,就在b.html文件下创建一个隐藏的iframe,其src指向a2.html。通过parent.parent.location.hash来改变a1.html的hash值。
代码如下:
a.com下的a.html:
<script>
// a1.html
// 创建一个隐藏的iframe
var iframe = document.createElement('iframe');
iframe.src = "http://b.com/b.html#hello";
iframe.style.display = 'none';
document.body.appendChild(iframe);
// 监听hash值
function checkHash() {
var data = location.hash? location.hash.substring(1): '';
console.log(data);
}
// 每隔1s检测以下hash值是否变化
setTimeout(checkHash,1000);
</script>
b.com下的b.html:
<script>
// b.html
// 模拟简单的location.hash值的改变
switch(location.hash){
case "#hello":
// 如果hash值是hello,执行回调函数
callback();
break;
case '#hhh':
// ...
}
function callback(){
try {
// 如果是FireFox可以直接修改a1.html的hash值
parent.location.hash = '#hello';
} catch (error) {
// 如果是在IE、Chrome下,就要创建a.com下的一个代理
var iframe = document.createElement('iframe');
iframe.style.display = 'none';
// 将其hash值设置为b.html的hash值
iframe.src = 'http://a.com/a2.html#hello';
document.body.appendChild(iframe);
}
}
</script>
域名a.com下的a2.html:
<script>
// a2.html
// a2.html与a1.html同域,可以通过a2.html来改变a1.html的hash值。
parent.parent.location.hash = self.location.hash.substring(1);
</script>
这种方法有很多缺点:数据都暴露在URL、数据容量有限等。
window.name + iframe跨域
window.name属性的独特之处:name值在不同的页面(甚至不同域名)加载后依旧存在,并且可以支持非常长的 name 值(2MB)。
利用这个特性,可以与iframe结合实现跨域。
实例:
- 使用node创建两个http服务器,监听端口8081和8088
- a.html运行在8081下
- b.html运行在8088下
代码如下:
监听端口8081:
// 导入 express
const express = require('express');
const app = express();
// 将public目录导入,public下有a.html文件
app.use(express.static('public')).listen(8081);
localhost8081下的a.html代码:
<script>
var iframe = document.createElement('iframe');
iframe.src = "http://localhost:8088/b.html";
iframe.style.display = 'none';
var flag = 0;
// onload会触发两次,第一次触发保存b.html下的数据,第二次重定向之后触发,读取同域的window.name
iframe.onload = function() {
if(flag == 1){
// 返回b.html下的数据数据
console.log("跨域资源", iframe.contentWindow.name);
}else if(flag == 0) {
// 得到b.html下的数据之后,立即将src设置为同域的proxy.html
flag = 1;
// iframe.src重定向
iframe.contentWindow.location = "http://localhost:8081/proxy.html";
}
}
document.body.appendChild(iframe);
</script>
上面的代码中将iframe.src重定向,指向了http://localhost:8081/proxy.html
,这个与a.html同域,并且就是一个空文件。为什么这么做?原因是:尽管window.name的值在不同域也存在,但是iframe有一个特性规定:如果a.html页面和该页面里的iframe框架的src不同源的话,就无法操作框架里的任何东西。 既然要同源,那就当保存了b.html下的name之后,将src指向一个同域的空页面,这样就可以顺利的读出b.html下的window.name值。
注意: 改变iframe的src之后会重新触发onload。
监听端口8088:
// 导入 express
const express = require('express');
const app = express();
// 将public目录导入,b.html文件里面
app.use(express.static('public1')).listen(8088);
localhost8088下的b.html代码:
<script>
window.name = "This is domain data";
</script>
结果:
postMessage跨域
postMessage是HTML5 XMLHttpRequest Level2中的API,且是为数不多可以跨域操作的window属性之一。
postMessage(data,origin)方法的使用:
- data: html5规范支持任意类型,但部分浏览器只支持字符串,所以传参时最好用JSON.stringify()序列化。
- origin:协议+主机+端口号,也可以设置为“*”,表示可以传递给任何窗口,如果要指定和当前同源的话就设置为“/”
实例:
还是像上面一样:
- 使用node创建两个http服务器,监听端口8081和8088
- aa.html运行在8081下
- bb.html运行在8088下
localhost8081下的aa.html代码:
<script>
var iframe = document.createElement('iframe');
iframe.style.display = 'none';
iframe.src = "http://localhost:8088/bb.html";
document.body.appendChild(iframe);
iframe.onload = function() {
var data = {
name: 'xiaoqi'
}
// 向bb.html传输跨域数据
iframe.contentWindow.postMessage(JSON.stringify(data),"http://localhost:8088");
}
// 接受bb.html返回数据
window.onmessage = function(event) {
console.log('data from bb.html:' + event.data);
}
</script>
localhost8088下的bb.html代码:
<script>
// 接受aa.html的数据
window.onmessage = function(event) {
console.log('data from aa.html:' + event.data);
// 将aa.html传来的数据转对象
var data = JSON.parse(event.data);
if(data) {
data.number = 16;
}
// 将处理后的数据再发回aa.html
window.parent.postMessage(JSON.stringify(data), 'http://localhost:8081');
}
</script>
结果:
WebSocket协议跨域
WebSocket 是HTML5的之中新协议。它实现了浏览器与服务器全双工通信,同时允许跨域,是server push技术的一种很好的实现。
原生WebSocket API使用起来不太方便,我们使用Socket.io,它很好的封装了webSoket接口,提供了更简单的接口,也对不支持webSocket的浏览器提供了向下兼容。
前端代码:
<div>use input: <input type="text"> </div>
<script src="https://cdn.bootcss.com/socket.io/2.2.0/socket.io.js"></script>
<script>
// 通过io()生成客户端所需要的socket对象
var socket = io('http://localhost:8081');
//socket.on()用于接收服务器端发来的消息 连接成功处理
socket.on('connect', function() {
// 监听服务端消息
socket.on('message', function(msg) {
console.log('data from server:' + msg);
});
// 监听服务端关闭
socket.on('disconnect', function() {
console.log('Server socket has closed');
});
});
//
document.getElementsByTagName('input')[0].onblur = function(){
// 向服务端发消息
socket.send(this.value);
}
</script>
后端代码:
var http = require('http');
var socket = require('socket.io');
// 启动http服务
var server = http.createServer(function (req, res) {
res.writeHead(200,{
'Content-type': 'text/html'
});
res.end();
})
server.listen(8081);
console.log('server is running at port 8081');
// 监听socket连接
socket.listen(server).on('connection', function (client){
// 监听客户端传来的信息
client.on('message', function (msg) {
console.log('data from client:'+ msg);
// 向客户端发消息
client.send(msg);
})
// 监听客户端断开连接
client.on('disconnect', function() {
console.log('Client socket has closed');
});
});
nginx代理跨域与nodejs中间件代理跨域见参考文章3
参考: