文章目录
一、什么是跨域?1.1. 什么是同源策略?1.2. 同源策略限制以下几种行为
二、常见的跨域场景三、9种跨域解决方案1. JSONP跨域1.1. 原生JS实现1.2. jquery Ajax实现1.3. Vue axios实现
2. CORS 跨域资源共享 (前端不需要做任何改变)2.1. 简单请求2.2. 非简单请求2.2.1. 预检请求2.2.2. 预检请求的回应(服务端设置)
2.3. 跨域认证
3. nginx代理跨域3.1. nginx配置解决iconfont跨域3.2. nginx反向代理接口跨域
4. nodejs中间件代理跨域4.1. 非vue框架的跨域4.2. vue框架的跨域
5. document.domain + iframe跨域6. location.hash + iframe跨域7. window.name + iframe跨域8. postMessage跨域9. WebSocket协议跨域
四、小结
一、什么是跨域?
在前端领域中,跨域是指浏览器允许向服务器发送跨域请求,从而克服Ajax只能同源使用的限制。
1.1. 什么是同源策略?
同源策略是一种约定,由Netscape公司1995年引入浏览器,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,浏览器很容易受到XSS、CSFR等攻击。所谓同源是指"协议+域名+端口"三者相同, 即便两个不同的域名指向同一个ip地址,也非同源。
1.2. 同源策略限制以下几种行为
Cookie、LocalStorage 和 IndexDB 无法读取DOM和JS对象无法获得AJAX 请求不能发送,说的更直白一点,就是我们在前端使用AJAX 发送异步请求,如果这个请求的URL地址与当前地址栏中的URL地址协议不同、域名不同、端口不同时,都称为跨域,
二、常见的跨域场景
三、9种跨域解决方案
1. JSONP跨域
jsonp的原理就是利用
服务端返回如下(返回时即执行全局函数):
handleCallback({"success": true, "user": "admin"})
1.2. jquery Ajax实现
$.ajax({
url: 'http://www.domain2.com:8080/login',
type: 'get',
dataType: 'jsonp', // 请求方式为jsonp
jsonpCallback: "handleCallback", // 自定义回调函数名
data: {}
});
1.3. Vue axios实现
this.$http = axios;
this.$http.jsonp('http://www.domain2.com:8080/login', {
params: {},
jsonp: 'handleCallback'
}).then((res) => {
console.log(res);
})
后端node.js代码:
var querystring = require('querystring');
var http = require('http');
var server = http.createServer();
server.on('request', function(req, res) {
var params = querystring.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. CORS 跨域资源共享 (前端不需要做任何改变)
CORS是浏览器解决跨域问题的一种策略,在CORS中,浏览器把ajax发起的请求分为简单请求和非简单请求,分别对两种请求进行处理,再将ajax请求发往服务器。
缺点:
设置具体地址,有局限性设置多源(*)就不能允许携带cookie了
2.1. 简单请求
简单请求就是满足以下条件的:
请求方式为这几种: GET,POST,HEADHTTP头信息不超出以下几种: Accept Accept-Language Content-Language Last-Event-ID content-type只限于三种:text/plain,multipart/form-data,application/x-www-form-urlencoded
对于简单请求,浏览器直接发出CORS请求,在头信息中增加一个字段:Origin,表示请求源域名。
例子:
Accept:*/*
Accept-Encoding:gzip, deflate, br
Accept-Language:zh-CN,zh;q=0.8
Connection:keep-alive
Host:localhost:3001
Origin:http://localhost:3000
Origin:http://localhost:3000 字段告诉服务器请求源是 http://localhost:3000,服务器就可以做出相应响应。如果服务器发现Oringin字段的源不在接受范围,就会发送一个正常HTTP回应,浏览器发现这个回应的头信息中没有包含Accsess-Control-Allow-Origin字段,就抛出一个错误,被XMLHttpRequest的onerror捕获,即跨域问题出现了。
好了,知道报错原因,我们就可以从以下方法解决跨域问题了: 概括为一句话:在服务器设置接受的源信息。
实例
客户端请求:
var xhr = new XMLHttpRequest();
var url = 'http://localhost:3000';
xhr.open('GET',url);
xhr.send(null);
xhr.onreadystatechange = () => {
if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) { // 如果请求成功
text.innerHTML = xhr.response;
}
}
服务器设置(例子用的express):
var express = require('express');
var test = express();
test.get('/',(req,res) => {
res.set('Access-Control-Allow-Origin','http://localhost:3000');
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT');
res.header('Access-Control-Allow-Headers', 'X-Custom-Header');
res.header('Access-Control-Allow-Credentials', 'true');
res.send("hello CORS.");
})
CORS请求设置的响应头字段,都以 Access-Control-开头:
1)Access-Control-Allow-Origin:必选 它的值要么是请求时Origin字段的值,要么是一个*,表示接受任意域名的请求。
// 允许所有源进行跨域请求
res.header("Access-Control-Allow-Origin", "*");
// 或者 允许指定源进行跨域请求
res.header("Access-Control-Allow-Origin", "http://localhost:3000");
2)Access-Control-Allow-Methods:可选 列举出服务器接受的请求方式。3)Access-Control-Allow-Credentials:可选 它的值是一个布尔值,表示是否允许发送Cookie。默认情况下,Cookie不包括在CORS请求之中。设为true,即表示服务器明确许可,Cookie可以包含在请求中,一起发给服务器。这个值也只能设为true,如果服务器不要浏览器发送Cookie,删除该字段即可。
res.header("Access-Control-Allow-Methods", "PUT,POST,GET,DELETE,OPTIONS");
4)Access-Control-Expose-Headers:可选 CORS请求时,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。如果想拿到其他字段,就必须在Access-Control-Expose-Headers里面指定。上面的例子指定,getResponseHeader(‘FooBar’)可以返回FooBar字段的值。
res.header("Access-Control-Allow-Headers", "Content-Type,Content-Length, Authorization, Accept,X-Requested-With");
2.2. 非简单请求
非简单请求是那种对服务器有特殊要求的请求,比如请求方法是PUT或DELETE,或者Content-Type字段的类型是application/json。非简单请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为"预检"请求(preflight)。
2.2.1. 预检请求
浏览器发出预检"请求用的请求方法是OPTIONS,表示这个请求是用来询问的。请求头信息里面,关键字段是Origin,表示请求来自哪个源。除了Origin字段,"预检"请求的头信息包括两个特殊字段
OPTIONS /cors HTTP/1.1
Origin: http://api.bob.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: X-Custom-Header
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0..
1)Access-Control-Request-Method:必选 用来列出浏览器的CORS请求会用到哪些HTTP方法,上例是PUT。
2)Access-Control-Request-Headers:可选 该字段是一个逗号分隔的字符串,指定浏览器CORS请求会额外发送的头信息字段,上例是X-Custom-Header。
2.2.2. 预检请求的回应(服务端设置)
服务器收到"预检"请求以后,检查了Origin、Access-Control-Request-Method和Access-Control-Request-Headers字段以后,确认允许跨源请求,就可以做出回应。
1)Access-Control-Allow-Origin:必选
2)Access-Control-Allow-Methods:必选 它的值是逗号分隔的一个字符串,表明服务器支持的所有跨域请求的方法。注意,返回的是所有支持的方法,而不单是浏览器请求的那个方法。这是为了避免多次"预检"请求。
3)Access-Control-Allow-Headers 如果浏览器请求包括Access-Control-Request-Headers字段,则Access-Control-Allow-Headers字段是必需的。它也是一个逗号分隔的字符串,表明服务器支持的所有头信息字段,不限于浏览器在"预检"中请求的字段。
4)Access-Control-Allow-Credentials:可选 该字段与简单请求时的含义相同。
5)Access-Control-Max-Age:可选 用来指定本次预检请求的有效期,单位为秒。
CORS跨域示例
客户端发送PUT请求:
var xhr = new XMLHttpRequest();
var url = 'http://localhost:3000';
xhr.open('PUT',url);
xhr.setRequestHeader('X-Custom-Header', 'value');
xhr.send();
xhr.onreadystatechange = () => {
if(xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) {
text1.innerHTML = xhr.response;
}
}
非简单请求例子中,我设置了特殊头信息“X-Custom-Header”,告知服务器接受这个参数信息。
在服务器设置接受信息:
var express = require('express');
var test = express();
var responsePort = 3001;//服务器端口
test.all('*', (req, res) => {
res.set('Access-Control-Allow-Origin', 'http://localhost:3000');
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT');
res.header('Access-Control-Allow-Headers', 'X-Custom-Header');
res.header('Access-Control-Allow-Credentials', 'true');
res.send("hello CORS.");
})
至此,对于两种请求方式,都可以实现跨域访问了。
2.3. 跨域认证
到此,我们解决了跨域问题,但是还有一个问题,有些场景我们需要携带 Cookie去请求,那这个问题如何解决呢?
第一步:前端请求中做如下配置:
如果使用fetch请求,需要设置 credentials 为 include
fetch('http://localhost:3000', {
method: 'GET',
credentials: 'include'
})
如果使用xhr请求,需要设置 withCredentials 为 true
var xhr = new XMLHttpRequest();
xhr.withCredentials = true;
xhr.open('GET', 'http://localhost:3000');
xhr.send();
第二步:服务端响应头中设置 Access-Control-Allow-Credentials 字段为 true
res.header("Access-Control-Allow-Credentials", "true");
这样我们就可以携带 Cookie 去请求了。
3. nginx代理跨域
nginx代理跨域,实质和CORS跨域原理一样,通过配置文件设置请求响应头Access-Control-Allow-Origin…等字段。
3.1. nginx配置解决iconfont跨域
浏览器跨域访问js、css、img等常规静态资源被同源策略许可,但iconfont字体文件(eot|otf|ttf|woff|svg)例外,此时可在nginx的静态资源服务器中加入以下配置。
location / {
add_header Access-Control-Allow-Origin *;
}
3.2. nginx反向代理接口跨域
跨域问题: 同源策略仅是针对浏览器的安全策略。服务器端调用HTTP接口只是使用HTTP协议,不需要同源策略,也就不存在跨域问题。
实现思路: 通过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;
}
}
4. nodejs中间件代理跨域
4.1. 非vue框架的跨域
使用node + express + http-proxy-middleware搭建一个proxy服务器。
前端代码:
var xhr = new XMLHttpRequest();
// 前端开关:浏览器是否读写cookie
xhr.withCredentials = true;
// 访问http-proxy-middleware代理服务器
xhr.open('get', 'http://www.domain1.com:3000/login?user=admin', true);
xhr.send();
中间件服务器代码:
var express = require('express');
var proxy = require('http-proxy-middleware');
var app = express();
app.use('/', proxy({
// 代理跨域目标接口
target: 'http://www.domain2.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.domain1.com' // 可以为false,表示不修改
}));
app.listen(3000);
也可以使用cors插件
npm install cors
var express = require('express');
// 跨域资源共享
const cors = require("cors");
var app = express();
app.use(cors());
// 启动服务
app.listen(80, () => {
console.log("express server running at http://127.0.0.1");
});
4.2. vue框架的跨域
node + vue + webpack + webpack-dev-server搭建的项目,跨域请求接口,直接修改webpack.config.js配置。开发环境下,vue渲染服务和接口代理服务都是webpack-dev-server同一个,所以页面与代理接口之间不再跨域。生产环境无效
webpack.config.js部分配置:
module.exports = {
entry: {},
module: {},
...
devServer: {
historyApiFallback: true,
proxy: [{
context: '/login',
target: 'http://www.domain2.com:8080', // 代理跨域目标接口
changeOrigin: true,
secure: false, // 当代理某些https服务报错时用
cookieDomainRewrite: 'www.domain1.com' // 可以为false,表示不修改
}],
noInfo: true
}
}
5. document.domain + iframe跨域
此方案仅限主域相同,子域不同的跨域应用场景。
实现原理: 两个页面都通过js强制设置document.domain为基础主域,就实现了同域。
1)父窗口:(http://www.domain.com/a.html)
document.domain = 'domain.com';
var user = 'admin';
2)子窗口:(http://child.domain.com/a.html)
document.domain = 'domain.com';
// 获取父窗口中变量
console.log('get js data from parent ---> ' + window.parent.user);
6. 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.domain1.com/a.html)
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);
}
2)b.html:(http://www.domain2.com/b.html)
var iframe = document.getElementById('iframe');
// 监听a.html传来的hash值,再传给c.html
window.onhashchange = function () {
iframe.src = iframe.src + location.hash;
};
3)c.html:(http://www.domain1.com/c.html)
// 监听b.html传来的hash值
window.onhashchange = function () {
// 再通过操作同域a.html的js回调,将结果传回
window.parent.parent.onCallback('hello: ' + location.hash.replace('#user=', ''));
};
7. window.name + iframe跨域
window.name属性的独特之处:name值在不同的页面(甚至不同域名)加载后依旧存在,并且可以支持非常长的 name 值(2MB)。
1)a.html:(http://www.domain1.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.domain1.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.domain2.com/b.html', function(data){
alert(data);
});
2)proxy.html:(http://www.domain1.com/proxy.html) 中间代理页,与a.html同域,内容为空即可。
3)b.html:(http://www.domain2.com/b.html)
window.name = 'This is domain2 data!';
通过iframe的src属性由外域转向本地域,跨域数据即由iframe的window.name从外域传递到本地域。这个就巧妙地绕过了浏览器的跨域访问限制,但同时它又是安全操作。
8. postMessage跨域
postMessage是HTML5 XMLHttpRequest Level 2中的API,且是为数不多可以跨域操作的window属性之一,它可用于解决以下方面的问题:
页面和其打开的新窗口的数据传递多窗口之间消息传递页面与嵌套的iframe消息传递上面三个场景的跨域数据传递
用法:postMessage(data,origin)方法接受两个参数:
data: html5规范支持任意基本类型或可复制的对象,但部分浏览器只支持字符串,所以传参时最好用JSON.stringify()序列化。origin: 协议+主机+端口号,也可以设置为"*“,表示可以传递给任意窗口,如果要指定和当前窗口同源的话设置为”/"。
1)a.html:(http://www.domain1.com/a.html)
var iframe = document.getElementById('iframe');
iframe.onload = function() {
var data = {
name: 'aym'
};
// 向domain2传送跨域数据
iframe.contentWindow.postMessage(JSON.stringify(data), 'http://www.domain2.com');
};
// 接受domain2返回数据
window.addEventListener('message', function(e) {
alert('data from domain2 ---> ' + e.data);
}, false);
2)b.html:(http://www.domain2.com/b.html)
// 接收domain1的数据
window.addEventListener('message', function(e) {
alert('data from domain1 ---> ' + e.data);
var data = JSON.parse(e.data);
if (data) {
data.number = 16;
// 处理后再发回domain1
window.parent.postMessage(JSON.stringify(data), 'http://www.domain1.com');
}
}, false);
9. WebSocket协议跨域
WebSocket protocol是HTML5一种新的协议。它实现了浏览器与服务器全双工通信,同时允许跨域通讯,是server push技术的一种很好的实现。原生WebSocket API使用起来不太方便,我们使用http://Socket.io,它很好地封装了webSocket接口,提供了更简单、灵活的接口,也对不支持webSocket的浏览器提供了向下兼容。
1)前端代码:
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);
};
2)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.');
});
});
四、小结
以上就是9种常见的跨域解决方案,jsonp(只支持get请求,支持老的IE浏览器)适合加载不同域名的js、css,img等静态资源;CORS(支持所有类型的HTTP请求,但浏览器IE10以下不支持)适合做ajax各种跨域请求;Nginx代理跨域和nodejs中间件跨域原理都相似,都是搭建一个服务器,直接在服务器端请求HTTP接口,这适合前后端分离的前端项目调后端接口。document.domain+iframe适合主域名相同,子域名不同的跨域请求。postMessage、websocket都是HTML5新特性,兼容性不是很好,只适用于主流浏览器和IE10+。