首先什么是跨域,简单地理解就是由于JavaScript同源策略的限制,a.com 域名下的js没法操做b.com或是c.a.com域名下的对象
javascript
什么是同源策略?css
同源策略/SOP(Same origin policy)是一种约定,由Netscape公司1995年引入浏览器,它是浏览器最核心也最基本的安全功能,若是缺乏了同源策略,浏览器很容易受到XSS、CSFR等攻击。所谓同源是指"协议+域名+端口"三者相同,即使两个不一样的域名指向同一个ip地址,也非同源。html
JSONP 是 JSON with padding(填充式 JSON 或参数式 JSON)的简写。前端
JSONP实现跨域请求的原理简单的说,就是动态建立<script>标签,而后利用<script>的src 不受同源策略约束来跨域获取数据。 html5
JSONP 由两部分组成:回调函数和数据回调函数是当响应到来时应该在页面中调用的函数。回调函数的名字通常是在请求中指定的。而数据就是传入回调函数中的 JSON 数据java
动态建立<script>
标签,设置其src,回调函数在src中设置:
node
var script = document.createElement("script");
script.src = "https://api.douban.com/v2/book/search?q=javascript&count=1&callback=handleResponse"
document.body.insertBefore(script, document.body.firstChild);
复制代码
在页面中,返回的JSON做为参数传入回调函数中,咱们经过回调函数来来操做数据。
webpack
function handleResponse(response){
// 对response数据进行操做代码
}
复制代码
postMessage是html5引入的API,postMessage()方法容许来自不一样源的脚本采用异步方式进行有效的通讯,能够实现跨文本文档,多窗口,跨域消息传递.多用于窗口间数据通讯,这也使它成为跨域通讯的一种有效的解决方案.
nginx
发送数据:web
otherWindow.postMessage(message, targetOrigin, [transfer]);
复制代码
otherWindow
窗口的一个引用,好比iframe的contentWindow属性,执行window.open返回的窗口对象,或者是命名过的或数值索引的window.frames.
message
要发送到其余窗口的数据,它将会被[!结构化克隆算法](https://developer.mozilla.org/en-US/docs/DOM/The_structured_clone_algorithm)序列化.这意味着你能够不受什么限制的将数据对象安全的传送给目标窗口而无需本身序列化.
targetOrigin
经过窗口的origin属性来指定哪些窗口能接收到消息事件,指定后只有对应origin下的窗口才能够接收到消息,设置为通配符"*"表示能够发送到任何窗口,但一般处于安全性考虑不建议这么作.若是想要发送到与当前窗口同源的窗口,可设置为"/"
transfer | 可选属性
是一串和message同时传递的**Transferable**对象,这些对象的全部权将被转移给消息的接收方,而发送一方将再也不保有全部权.
接收数据: 监听message事件的发生
window.addEventListener("message", receiveMessage, false) ;
function receiveMessage(event) {
var origin= event.origin;
console.log(event);
}复制代码
event对象的打印结果截图以下:
event对象的四个属性
CORS 须要浏览器和后端同时支持。IE 8 和 9 须要经过 XDomainRequest 来实现。
浏览器会自动进行 CORS 通讯,实现 CORS 通讯的关键是后端。只要后端实现了 CORS,就实现了跨域。
服务端设置 Access-Control-Allow-Origin 就能够开启 CORS。 该属性表示哪些域名能够访问资源,若是设置通配符则表示全部网站均可以访问资源。
虽然设置 CORS 和前端没什么关系,可是经过这种方式解决跨域问题的话,会在发送请求时出现两种状况,分别为简单请求和复杂请求。
只要同时知足如下两大条件,就属于简单请求
条件1:使用下列方法之一:
条件2:Content-Type 的值仅限于下列三者之一:
请求中的任意 XMLHttpRequestUpload 对象均没有注册任何事件监听器; XMLHttpRequestUpload 对象可使用 XMLHttpRequest.upload 属性访问。
不符合以上条件的请求就确定是复杂请求了。 复杂请求的CORS请求,会在正式通讯以前,增长一次HTTP查询请求,称为"预检"请求,该请求是 option 方法的,经过该请求来知道服务端是否容许跨域请求。
咱们用PUT
向后台请求时,属于复杂请求,后台需作以下配置:
// 容许哪一个方法访问我
res.setHeader('Access-Control-Allow-Methods', 'PUT')
// 预检的存活时间
res.setHeader('Access-Control-Max-Age', 6)
// OPTIONS请求不作任何处理
if (req.method === 'OPTIONS') {
res.end()
}
// 定义后台返回的内容
app.put('/getData', function(req, res) {
console.log(req.headers)
res.end('我不爱你')
})
复制代码
接下来咱们看下一个完整复杂请求的例子,而且介绍下CORS请求相关的字段
// index.html
let xhr = new XMLHttpRequest()
document.cookie = 'name=xiamen' // cookie不能跨域
xhr.withCredentials = true // 前端设置是否带cookie
xhr.open('PUT', 'http://localhost:4000/getData', true)
xhr.setRequestHeader('name', 'xiamen')
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) {
console.log(xhr.response)
//获得响应头,后台需设置Access-Control-Expose-Headers
console.log(xhr.getResponseHeader('name'))
}
}
}
xhr.send()复制代码
//server1.js
let express = require('express');
let app = express();
app.use(express.static(__dirname));
app.listen(3000);
复制代码
在前端网站开发过程当中,网络请求指向nodejs提供的接口,nodejs服务端再发起请求指向跨域的服务器,而后依次返回到前端页面,这样就完成了跨域的访问,基本上就知足了跨域访问的问题了
前端代码
var xhr = new XMLHttpRequest();
// 浏览器是否读写cookie
xhr.withCredentials = true;
// 访问http-proxy-middleware代理服务器
xhr.open('get', 'http://www.127.0.0.1:3000/login?user=admin', true);
xhr.send();复制代码
express+ http-proxy-middleware
var express = require('express');
var proxy = require('http-proxy-middleware');
var app = express();
app.use('/', proxy({
// 代理跨域接口
target: 'http://www.127.0.0.1:8080',
changeOrigin: true,
// 修改响应头信息,实现跨域并容许带cookie
onProxyRes: function(proxyRes, req, res) {
res.header('Access-Control-Allow-Origin', 'http://www.127.0.0.1');
res.header('Access-Control-Allow-Credentials', 'true');
},
// 修改响应信息中的cookie域名
cookieDomainRewrite: 'www.127.0.0.1' // 能够为false,表示不修改
}));
app.listen(3000);复制代码
Koa+Koa2-cors
var Koa = require('koa');
var cors = require('koa2-cors');
var app = new Koa();
app.use(cors({
origin: function(ctx) {
if (ctx.url === '/') {
return false;
}
return '*';
},
exposeHeaders: ['WWW-Authenticate', 'Server-Authorization'],
maxAge: 5,
credentials: true,
allowMethods: ['GET', 'POST', 'DELETE'],
allowHeaders: ['Content-Type', 'Authorization', 'Accept'],
}));
app.listen(3000);复制代码
WebSocket protocol是HTML5一种新的协议。它实现了浏览器与服务器全双工通讯,同时容许跨域通信,是server push技术的一种很好的实现。
原生WebSocket API使用起来不太方便,咱们使用Socket.io,它很好地封装了webSocket接口,提供了更简单、灵活的接口,也对不支持webSocket的浏览器提供了向下兼容。
前端
<div>user input:<input type="text"></div>
<script src="./socket.io.js"></script>
<script>
var socket = io('http://www.domain2.com:8080');
// 链接成功处理
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>复制代码
Nodejs socket后台
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('8080');
console.log('Server is running at port 8080...');
// 监听socket链接
socket.listen(server).on('connection', function(client) {
// 接收信息
client.on('message', function(msg) {
client.send('hello:' + msg);
console.log('data from client: ---> ' + msg);
});
// 断开处理
client.on('disconnect', function() {
console.log('Client socket has closed.');
});
});复制代码
window.name属性的独特之处:name值在不一样的页面(甚至不一样域名)加载后依旧存在,而且能够支持很是长的 name 值(2MB)。
其中a.html和b.html是同域的,都是http://localhost:3000
;而c.html是http://localhost:4000
// a.html(http://localhost:3000/b.html)
<iframe src="http://localhost:4000/c.html" frameborder="0" onload="load()" id="iframe"></iframe>
<script>
let first = true
// onload事件会触发2次,第1次加载跨域页,并留存数据于window.name
function load() {
if(first){
// 第1次onload(跨域页)成功后,切换到同域代理页面
let iframe = document.getElementById('iframe');
iframe.src = 'http://localhost:3000/b.html';
first = false;
}else{
// 第2次onload(同域b.html页)成功后,读取同域window.name中数据
console.log(iframe.contentWindow.name);
}
}
</script>复制代码
b.html为中间代理页,与a.html同域,内容为空。
// c.html(http://localhost:4000/c.html)
// c.html(http://localhost:4000/c.html)
<script>
window.name = '我要跨域'
</script>
复制代码
经过iframe的src属性由外域转向本地域,跨域数据即由iframe的window.name从外域传递到本地域。这个就巧妙地绕过了浏览器的跨域访问限制,但同时它又是安全操做。
实现原理: 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页面全部对象。
// a.html
<iframe id="iframe" src="http://www.domain2.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<iframe id="iframe" src="http://www.domain1.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
<script>
// 监听b.html传来的hash值
window.onhashchange = function () {
// 再经过操做同域a.html的js回调,将结果传回
window.parent.parent.onCallback('hello: ' + location.hash.replace('#user=', ''));
};
</script>复制代码
该方式只能用于二级域名相同的状况下,好比 a.test.com
和 b.test.com
适用于该方式。 只须要给页面添加 document.domain ='test.com'
表示二级域名都相同就能够实现跨域。
实现原理:两个页面都经过js强制设置document.domain为基础主域,就实现了同域。
咱们看个例子:页面a.zf1.cn:3000/a.html
获取页面b.zf1.cn:3000/b.html
中a的值
// a.html
<body>
helloa
<iframe src="http://b.zf1.cn:3000/b.html" frameborder="0" onload="load()" id="frame"></iframe>
<script>
document.domain = 'zf1.cn'
function load() {
console.log(frame.contentWindow.a);
}
</script>
</body>复制代码
// b.html
<body>
hellob
<script>
document.domain = 'zf1.cn'
var a = 100;
</script>
</body>
复制代码
浏览器跨域访问js、css、img等常规静态资源被同源策略许可,但iconfont字体文件(eot|otf|ttf|woff|svg)例外,此时可在nginx的静态资源服务器中加入如下配置。
location / {
add_header Access-Control-Allow-Origin *;
}复制代码
跨域原理: 同源策略是浏览器的安全策略,不是HTTP协议的一部分。服务器端调用HTTP接口只是使用HTTP协议,不会执行JS脚本,不须要同源策略,也就不存在跨越问题。
实现思路:经过nginx配置一个代理服务器(域名与domain1相同,端口不一样)作跳板机,反向代理访问domain2接口,而且能够顺便修改cookie中domain信息,方便当前域cookie写入,实现跨域登陆。
nginx具体配置:
#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;
}
}复制代码
前端代码
var xhr = new XMLHttpRequest();
// 前端开关:浏览器是否读写cookie
xhr.withCredentials = true;
// 访问nginx中的代理服务器
xhr.open('get', 'http://www.domain1.com:81/?user=admin', true);
xhr.send();复制代码
Nodejs后台
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...');复制代码
推荐使用nginx解决跨域问题,有如下优势: