目录
1、通过 jsonp(JSON with padding) 跨域
同源策略
- 端口相同
- 域名相同
- 协议相同
同源政策的目的
是为了保证用户信息的安全,防止恶意的网站窃取数据。是浏览器做的努力
同源策略限制范围
- Cookie、LocalStorage 和 IndexDB 无法读取
- DOM 无法获得
- AJAX 请求不能发送
什么是跨域?
跨域,是指浏览器不能执行其他网站的脚本。它是由浏览器的同源策略造成的。
协议、域名、端口号都相同才是同一个域。只要有一个不同,就算跨域。
需要注意的是:主域名相同,子域名不同也算跨域,例如:email.qq.com 和 zone.qq.com 就属于主域相同,子域不同,也算是跨域。
跨域的 N 种方式
1、通过 jsonp(JSON with padding) 跨域
img、iframe 和 script 等标签的 src 是不受域名限制的,所以我们可以引用网络上的图片和脚本等资源,比如在线使用 jquery,使用在线的图片等。jsonp 正是利用了这一点。
实现原理
- 动态地创建一个script标签,这个script标签的src就是请求的地址;
- 这个script标签插入到DOM中,浏览器就根据src地址访问服务器资源
- 返回的资源是一个文本,但是因为是在script标签中,浏览器会执行它
- 而这个文本恰好是函数调用的形式,即函数名(数据),浏览器会把它当作JS代码来执行即调用这个函数
- 只要提前约定好这个函数名,并且这个函数存在于window对象中,就可以把数据传递给处理函数。
缺点:只能实现 get 一种请求
1)原生 js 实现
<script>
var script = document.createElement('script');
script.type = 'text/javascript';
// 传参并指定回调执行函数为 onBack
script.src = 'http://www.demo2.com:8080/login?user=admin&callback=onBack';
document.head.appendChild(script);
// 回调执行函数
function onBack(res) {
alert(JSON.stringify(res));
}
</script>
服务端返回如下(返回时即执行全局函数 onBack):
onBack({"status": true, "user": "admin"})
2)jquery ajax 实现
$.ajax({
url: 'http://www.demo2.com:8080/login',
type: 'get',
dataType: 'jsonp', // 请求方式为jsonp
jsonpCallback: "onBack", // 自定义回调函数名
data: {}
});
3)vue.js 实现
this.$http.jsonp('http://www.demo2.com:8080/login', {
params: {},
jsonp: 'onBack'
}).then((res) => {
console.log(res);
})
后端 nodejs 代码示例:
var querystring = 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 fn = params.callback;
// jsonp返回设置
res.writeHead(200, { 'Content-Type': 'text/javascript' });
res.write(fn + '(' + JSON.stringify(params) + ')');
res.end();
});
server.listen('8080');
console.log('Server is running at port 8080...');
2、document.domain + iframe 跨域
实现原理
两个页面都通过js强制设置 document.domain 为基础主域,就实现了同域。
缺点:仅限主域相同子域不同的跨域场景
- 父窗口(http://www.demo.com/a.html)
<iframe id="iframe" src="http://child.demo.com/b.html"></iframe> <script> document.domain = 'demo.com'; var user = 'admin'; </script>
- 子窗口(http://child.demo.com/b.html)
<script> document.domain = 'demo.com'; // 获取父窗口中变量 alert('get js data from parent ---> ' + window.parent.user); </script>
3、location.hash + iframe跨域
实现原理
a 与 b 跨域相互通信,通过中间页c来实现。 三个页面,不同域之间利用 iframe 的location.hash 传值,相同域之间直接 js 访问来通信。
- a.html(http://www.demo1.com/a.html)
<iframe id="iframe" src="http://www.demo2.com/b.html" style="display:none;"></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>
- b.html(http://www.demo2.com/b.html)
<iframe id="iframe" src="http://www.demo1.com/c.html" style="display:none;"></iframe> <script> var iframe = document.getElementById('iframe'); // 监听a.html传来的hash值,再传给c.html window.onhashchange = function () { iframe.src = iframe.src + location.hash; }; </script>
- c.html(http://www.demo1.com/c.html)
4、window.name + iframe跨域
window.name属性的独特之处:name值在不同的页面(甚至不同域名)加载后依旧存在,并且可以支持非常长的 name 值(2MB)。
- a.html(http://www.demo1.com/a.html)
var proxy = function(url, callback) { var state = 0; var iframe = document.createElement('iframe'); // 加载跨域页面 iframe.src = url; // onload事件会触发2次,第1次加载跨域页,并留存数据于window.name iframe.onload = function() { if (state === 1) { // 第2次onload(同域proxy页)成功后,读取同域window.name中数据 callback(iframe.contentWindow.name); destoryFrame(); } else if (state === 0) { // 第1次onload(跨域页)成功后,切换到同域代理页面 iframe.contentWindow.location = 'http://www.demo1.com/proxy.html'; state = 1; } }; document.body.appendChild(iframe); // 获取数据以后销毁这个iframe,释放内存;这也保证了安全(不被其他域frame js访问) function destoryFrame() { iframe.contentWindow.document.write(''); iframe.contentWindow.close(); document.body.removeChild(iframe); } }; // 请求跨域b页面数据 proxy('http://www.demo2.com/b.html', function(data){ alert(data); });
- proxy.html(http://www.demo1.com/proxy…)
中间代理页,与 a.html 同域,内容为空即可。
- b.html(http://www.demo2.com/b.html)
<script> window.name = 'This is demo2 data!'; </script>
总结:通过 iframe 的 src 属性由外域转向本地域,跨域数据即由 iframe 的 window.name 从外域传递到本地域。这个就巧妙地绕过了浏览器的跨域访问限制,但同时它又是安全操作。
5、postMessage 跨域
postMessage 是 HTML5 XMLHttpRequest Level 2 中的 API,且是为数不多可以跨域操作的window 属性之一,它可用于解决以下方面的问题:
a) 页面和其打开的新窗口的数据传递
b) 多窗口之间消息传递
c) 页面与嵌套的 iframe 消息传递
d) 上面三个场景的跨域数据传递
用法:postMessage(data,origin)方法接受两个参数
- data: html5 规范支持任意基本类型或可复制的对象,但部分浏览器只支持字符串,所以传参时最好用 JSON.stringify() 序列化。
- origin: 协议+主机+端口号,也可以设置为"*"(为安全考虑,不建议这样做),表示可以传递给任意窗口;如果要指定和当前窗口同源的话设置为"/"。
- a.html(http://www.demo1.com/a.html)
<iframe id="iframe" src="http://www.demo2.com/b.html" style="display:none;"></iframe> <script> var iframe = document.getElementById('iframe'); iframe.onload = function() { var data = { name: 'aym' }; // 向domain2传送跨域数据 iframe.contentWindow.postMessage(JSON.stringify(data), 'http://www.demo2.com'); }; // 接受domain2返回数据 window.addEventListener('message', function(e) { alert('data from demo2 ---> ' + e.data); }, false); </script>
- b.html(http://www.demo2.com/b.html)
<script> // 接收domain1的数据 window.addEventListener('message', function(e) { alert('data from demo1 ---> ' + e.data); var data = JSON.parse(e.data); if (data) { data.number = 16; // 处理后再发回domain1 window.parent.postMessage(JSON.stringify(data), 'http://www.demo1.com'); } }, false); </script>
6、跨域资源共享(CORS)
普通跨域请求:只服务端设置 Access-Control-Allow-Origin 即可,前端无须设置;
带 cookie 的跨域请求:前后端都需要设置。
1)前端设置
- 原生 ajax 示例
var xhr = new XMLHttpRequest(); // IE8/9需用window.XDomainRequest兼容 // 前端设置是否带cookie xhr.withCredentials = true; xhr.open('post', 'http://www.demo2.com:8080/login', true); xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); xhr.send('user=admin'); xhr.onreadystatechange = function() { if (xhr.readyState == 4 && xhr.status == 200) { alert(xhr.responseText); } };
- vue 框架示例,在 vue-resource 封装的 ajax 组件中加入以下代码
Vue.http.options.credentials = true
2)后端设置,若后端设置成功,前端浏览器控制台则不会出现跨域报错信息,反之,说明没设成功。
- NodeJs 后台示例
let http = require('http'); let server = http.createServer(); let qs = require('querystring'); server.on('request', function(req, res) { var postData = ''; // 数据块接收中 req.addListener('data', function(chunk) { postData += chunk; }); // 数据接收完毕 req.addListener('end', function() { postData = qs.parse(postData); // 跨域后台设置 res.writeHead(200, { 'Access-Control-Allow-Credentials': 'true', // 后端允许发送Cookie 'Access-Control-Allow-Origin': 'http://www.demo1.com', // 允许访问的域(协议+域名+端口) 'Set-Cookie': 'l=a123456;Path=/;Domain=www.demo2.com;HttpOnly' // HttpOnly:脚本无法读取cookie }); res.write(JSON.stringify(postData)); res.end(); }); }); server.listen('8080'); console.log('Server is running at port 8080...');
7、nodejs 中间件代理跨域
实现原理
同源策略是浏览器需要遵循的标准,而服务器向服务器请求无需遵循同源策略即不存在跨域问题。
代理服务器,需要做以下几个步骤:
- 接受客户端请求 。
- 将请求 转发给服务器。
- 拿到服务器 响应 数据。
- 将 响应 转发给客户端。
1)非vue框架的跨域(2次跨域)
利用node + express + http-proxy-middleware搭建一个proxy服务器。
- 前端代码示例
var xhr = new XMLHttpRequest(); // 前端开关:浏览器是否读写cookie xhr.withCredentials = true; // 访问http-proxy-middleware代理服务器 xhr.open('get', 'http://www.demo1.com:3000/login?user=admin', true); xhr.send();
- node 中间件服务器
var express = require('express'); var proxy = require('http-proxy-middleware'); var app = express(); app.use('/', proxy({ // 代理跨域目标接口 target: 'http://www.demo2.com:8080', changeOrigin: true, // 修改响应头信息,实现跨域并允许带cookie onProxyRes: function(proxyRes, req, res) { res.header('Access-Control-Allow-Origin', 'http://www.domain1.com'); res.header('Access-Control-Allow-Credentials', 'true'); }, // 修改响应信息中的cookie域名 cookieDomainRewrite: 'www.demo1.com' // 可以为false,表示不修改 })); app.listen(3000); console.log('Proxy server is listen at port 3000...');
- node 后台
var http = require('http'); var server = http.createServer(); var qs = require('querystring'); server.on('request', function(req, res) { var params = qs.parse(req.url.substring(2)); // 向前台写cookie res.writeHead(200, { 'Set-Cookie': 'l=a123456;Path=/;Domain=www.demo2.com;HttpOnly' // HttpOnly:脚本无法读取 }); res.write(JSON.stringify(params)); res.end(); }); server.listen('8080'); console.log('Server is running at port 8080...');
2)vue框架的跨域(1次跨域)
利用 node + webpack + webpack-dev-server 代理接口跨域。在开发环境下,由于vue渲染服务和接口代理服务都是同一个 webpack-dev-server,所以页面与代理接口之间不再跨域,无须设置 headers 跨域信息了。
webpack.config.js部分配置:
module.exports = {
entry: {},
module: {},
...
devServer: {
historyApiFallback: true,
proxy: [{
context: '/login',
target: 'http://www.demo2.com:8080', // 代理跨域目标接口
changeOrigin: true,
secure: false, // 当代理某些https服务报错时用
cookieDomainRewrite: 'www.demo1.com' // 可以为false,表示不修改
}],
noInfo: true
}
}
8、nginx 代理跨域
实现原理类似于Node中间件代理,需要你搭建一个中转nginx服务器,用于转发请求。
使用nginx反向代理实现跨域,是最简单的跨域方式。只需要修改nginx的配置即可解决跨域问题,支持所有浏览器,支持session,不需要修改任何代码,并且不会影响服务器性能。
实现思路
通过nginx配置一个代理服务器(域名与domain1相同,端口不同)做跳板机,反向代理访问domain2接口,并且可以顺便修改cookie中domain信息,方便当前域cookie写入,实现跨域登录。
- 先下载nginx,然后将nginx目录下的nginx.conf修改如下:
// proxy服务器 server { listen 81; server_name www.domain1.com; location / { proxy_pass http://www.domain2.com:8080; #反向代理 proxy_cookie_domain www.domain2.com www.domain1.com; #修改cookie里域名 index index.html index.htm; # 当用webpack-dev-server等中间件代理接口访问nignx时,此时无浏览器参与,故没有同源限制,下面的跨域配置可不启用 add_header Access-Control-Allow-Origin http://www.domain1.com; #当前端只跨域不带cookie时,可为* add_header Access-Control-Allow-Credentials true; } }
最后通过命令行 nginx -s reload
启动 nginx
- index.html
var xhr = new XMLHttpRequest(); // 前端开关:浏览器是否读写cookie xhr.withCredentials = true; // 访问nginx中的代理服务器 xhr.open('get', 'http://www.domain1.com:81/?user=admin', true); xhr.send();
- server.js
var http = require('http'); var server = http.createServer(); var qs = require('querystring'); server.on('request', function(req, res) { var params = qs.parse(req.url.substring(2)); // 向前台写cookie res.writeHead(200, { 'Set-Cookie': 'l=a123456;Path=/;Domain=www.domain2.com;HttpOnly' // HttpOnly:脚本无法读取 }); res.write(JSON.stringify(params)); res.end(); }); server.listen('8080'); console.log('Server is running at port 8080...');
9、WebSocket 协议跨域
WebSocket协议跨域WebSocket protocol是HTML5一种新的协议。它实现了浏览器与服务器全双工通信,同时允许跨域通讯,允许服务器端主动向浏览器端发送消息。
WebSocket和HTTP都是应用层协议,都基于 TCP 协议。但是WebSocket 是一种双向通信协议,在建立连接之后,WebSocket 的 server 与 client 都能主动向对方发送或接收数据。同时,WebSocket 在建立连接时需要借助 HTTP 协议,连接建立好了之后 client 与 server 之间的双向通信就与 HTTP 无关了。
我们先来看个例子:本地文件 socket.html 向 localhost:3000
发送数据和接受数据
- socket.html
<script> let socket = new WebSocket('ws://localhost:3000'); socket.onopen = function () { socket.send('Hello 3000');//向服务器发送数据 } socket.onmessage = function (e) { console.log(e.data);//接收服务器返回的数据 } </script>
参考: