跨域问题实际就是浏览器的同源策略造成的。
同源策略限制了从同一个源加载的文档或脚本如何与另一个源的资源进行交互。这是浏览器的一个用于隔离潜在恶意文件的重要安全机制。同源指的是:
协议
、端口号
、域名
必须一致。
下表给出了与 URL http://store.company.com/dir/page.html 的源进⾏对⽐的示例:
同源策略:protocol(协议)、domain(域名)、port(端口)三者必须一致
同源策略主要限制了三个方面:
同源政策的⽬的主要是为了保证⽤户的信息安全,它只是对 js 脚本
的⼀种限制,并不是对浏览器的限制,对于⼀般的img
、或者script
脚本请求都不会有跨域的限制,这是因为这些操作都不会通过响应结果来进⾏可能出现安全问题的操作。
下⾯是MDN对于CORS的定义:
跨域资源共享(CORS) 是⼀种机制,它使⽤额外的 HTTP 头来告诉浏览器 让运⾏在⼀个 origin(domain)上的Web应⽤被准许访问来⾃不同源服务器上的指定的资源。当⼀个资源从与该资源本身所在的服务器不同的域、协议或端⼝请求⼀个资源时,资源会发起⼀个跨域HTTP 请求。
CORS需要浏览器
和服务器
同时⽀持,整个CORS过程都是浏览器完成的,⽆需⽤户参与
。因此实现CORS的关键就是服务器,只要服务器实现了CORS请求,就可以跨源通信了。
浏览器将CORS分为简单请求和⾮简单请求:
简单请求不会触发CORS预检请求。若该请求满⾜以下两个条件,就可以看作是简单请求:
1)请求方法是以下三种方法之一:
2)HTTP的头信息不超出以下⼏种字段:
对于简单请求,浏览器会直接发出CORS请求,它会在请求的头信息中增加⼀个Orign字段,该字段⽤来说明本次请求来⾃哪个源(协议+端⼝+域名),服务器会根据这个值来决定是否同意这次请求。如果Orign指定的域名在许可范围之内,服务器返回的响应就会多出以下信息头:
Access-Control-Allow-Origin: http://api.bob.com // 和Orign⼀直
Access-Control-Allow-Credentials: true // 表示是否允许发送Cookie
Access-Control-Expose-Headers: FooBar // 指定返回其他字段的值
Content-Type: text/html; charset=utf-8 // 表示⽂档类型
如果Orign指定的域名不在许可范围之内,服务器会返回⼀个正常的HTTP回应,浏览器发现没有上⾯的Access-Control-Allow-Origin头部信息,就知道出错了。这个错误⽆法通过状态码识别,因为返回的状态码可能是200。
在简单请求中,在服务器内,⾄少需要设置字段: Access-Control-Allow-Origin
⾮简单请求是对服务器有特殊要求的请求,⽐如请求⽅法为DELETE或者PUT等。⾮简单请求的CORS请求会在正式通信之前进⾏⼀次HTTP查询请求,称为预检请求
。
浏览器会询问服务器,当前所在的⽹⻚是否在服务器允许访问的范围内,以及可以使⽤哪些HTTP请求⽅式和头信息字段,只有得到肯定的回复,才会进⾏正式的HTTP请求,否则就会报错。
预检请求使⽤的请求⽅法是OPTIONS
,表示这个请求是来询问的。他的头信息中的关键字段是Orign
,表示请求来⾃哪个源。除此之外,头信息中还包括两个字段:
服务器在收到浏览器的预检请求之后,会根据头信息的三个字段来进⾏判断,如果返回的头信息在中有Access-Control-Allow-Origin
这个字段就是允许跨域请求,如果没有,就是不同意这个预检请求,就会报错。
服务器回应的CORS的字段如下:
Access-Control-Allow-Origin: http://api.bob.com // 允许跨域的源地址
Access-Control-Allow-Methods: GET, POST, PUT // 服务器⽀持的所有跨域请求的⽅法
Access-Control-Allow-Headers: X-Custom-Header // 服务器⽀持的所有头信息字段
Access-Control-Allow-Credentials: true // 表示是否允许发送Cookie
Access-Control-Max-Age: 1728000 // ⽤来指定本次预检请求的有效期,单位为秒
只要服务器通过了预检请求,在以后每次的CORS请求都会⾃带⼀个Origin
头信息字段。服务器的回应,也都会有⼀个Access-Control-Allow-Origin
头信息字段。
在⾮简单请求中,⾄少需要设置以下字段:
'Access-Control-Allow-Origin'
'Access-Control-Allow-Methods'
'Access-Control-Allow-Headers'
减少OPTIONS请求次数:
OPTIONS请求次数过多就会损耗⻚⾯加载的性能,降低⽤户体验度。所以尽量要减少OPTIONS请求次数,可以后端在请求的返回头部添加:Access-Control-Max-Age:number
。它表示预检请求的返回结果可以被缓存多久,单位是秒。该字段只对完全⼀样的URL的缓存设置⽣效,所以设置了缓存时间,在这个时间范围内,再次发送请求就不需要进⾏预检请求了。
CORS中Cookie相关问题:
在CORS请求中,如果想要传递Cookie,就要满⾜以下三个条件:
withCredentials
默认情况下在跨域请求,浏览器是不带 cookie 的。但是我们可以通过设置 withCredentials
来进⾏传递 cookie
// 原⽣ xml 的设置⽅式
var xhr = new XMLHttpRequest();
xhr.withCredentials = true;
// axios 设置⽅式
axios.defaults.withCredentials = true;
Access-Control-Allow-Credentials
设置为 trueAccess-Control-Allow-Origin
设置为falsejsonp的原理就是利⽤标签没有跨域限制,通过
标签src属性,发送带有callback参数的GET请求,服务端将接⼝返回数据拼凑到callback函数中,返回给浏览器,浏览器解析执⾏,从⽽前端拿到callback函数返回的数据。
1)原生JS实现
<script>
let script = document.createElement("script");
script.type = "text/javascript";
//传参一个回调函数名给后端,方便后端返回时执行这个在前端定义的回调函数
script.src =
"https://www.domain2.com:8080/login?user=admin&callback=handleCallback";
document.head.appendChild(script);
//回调执行函数
function handleCallback(res) {
alert(JSON.stringify(res));
}
</script>
服务端返回如下(返回时即执行全局函数):
handleCallback({"success":true,"user":"admin"})
2)Vue axios实现
this.$http=axios;
this.$http.jsonp('http://www.domain2.com:8080/login'{
params:{},
jsonp:'handleCallback'
}).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 = 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...');
postMessage是HTML5 XMLHttpRequest Level 2中的API,且是为数不多可以跨域操作的window属性之⼀,它可⽤于解决以下⽅⾯的问题:
⽤法:postMessage(data,origin)⽅法接受两个参数:
JSON.stringify()
序列化。1) a.html:(domain1.com/a.html)
<script>
<iframe id="iframe" src="http://www.domain2.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.domain2.com');
};
// 接受domain2返回数据
window.addEventListener('message', function(e) {
alert('data from domain2 ---> ' + e.data);
}, false);
</script>
2) b.html:(domain2.com/b.html)
<script>
// 接收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);
</script>
nginx代理跨域,实质和CORS跨域原理⼀样,通过配置⽂件设置请求响应头Access-Control-AllowOrigin…
等字段
1)nginx配置解决iconfont跨域
浏览器跨域访问js、css、img等常规静态资源被同源策略许可,但iconfont字体⽂件(eot|otf|ttf|woff|svg)例外,此时可在nginx的静态资源服务器中加⼊以下配置。
location / {
add_header Access-Control-Allow-Origin *;
}
2)nginx反向代理接⼝跨域
跨域问题:同源策略仅是针对浏览器的安全策略。服务器端调⽤HTTP接⼝只是使⽤HTTP协议,不需要同源策略,也就不存在跨域问题。
实现思路:通过Nginx配置⼀个代理服务器域名与domain1相同,端⼝不同)做跳板机,反向代理访问domain2接⼝,并且可以顺便修改cookie中domain信息,⽅便当前域cookie写⼊,实现跨域访问。
#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;
}
}
node中间件实现跨域代理,原理⼤致与nginx相同,都是通过启⼀个代理服务器,实现数据的转发,也可以通过设置cookieDomainRewrite
参数修改响应头中cookie中域名,实现当前域的cookie写⼊,⽅便接⼝登录认证。
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);
console.log('Proxy server is listen at port 3000...');
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
}
}