前端常见跨域解决方案

undefined

啊。本来是打算昨天总结这块内容的。然而,突然忘了之前是在哪个地方看到的文章,找了半天硬是没找到,奈何就放弃了。好在今天乱刷着各种技术文章,终于看到了!!那么,请看跨域!

什么是跨域?

跨域是指一个域下的文档或者脚本试图去请求另一个域下的资源。这里描述的是广义的跨域。

  • 资源跳转:A链接、重定向、表单提交
  • 资源嵌入: <link>、<script>/<img>/<frame>等dom标签,以及样式中的background:url()等的文件外链
  • 脚本请求:js发起的ajax请求、dom和js对象的跨域操作。

然而,我们一般讨论的都是狭义的跨域,也就是由浏览器同源策略限制引起的一类请求场景。

什么是同源策略

同源策略/SOP(same origin policy)是一种约定,是浏览器最核心也是最基本的安全功能,如果缺乏了同源策略,浏览器很容易受到XSS、CSRF(跨站请求伪造)等算计。

场景:
假设用户在访问银行网站的时候并没有登出。他又跑到了任意其他网站上,刚好这个网站上有恶意的js代码,在后台请求银行网站的信息。因为用户目前仍然是银行站点的登录状态,那么恶意代码就可以在银行站点做任何事情。

所以同源策略限制以下几种行为:

  1. Cookie、LocalStorage 和 IndexDB无法读取
  2. DOM 和 JS对象无法获取
  3. AJAX请求不能发送

所谓的同源就是指“协议 + 域名 + 端口”三者要相同,即便两个不同的域名指向同一个ip地址,也非同源。具体的跨域场景看下表:

URL 说明 是否允许通信
http://www.domain.com/a.js
http://www.domain.com/b.js
http://www.domain.com/lab/c.js
同一域名,不同文件或路径 允许
http://www.domain.com:8000/a.js
http://www.domain.com/b.js
同一域名,不同端口 不允许
http://www.domain.com/a.js
https://www.domain.com/b.js
同一域名,不同协议 不允许
http://www.domain.com/a.js
http://x.domain.com/b.js
http://domain.com
主域相同,子域不同 不允许
http://www.domain.com/a.js
http://www.domain2.com/b.js
http://domain.com
不同域名 不允许

跨域的解决方案

然而跨域的需求却是存在的,两个互相可信的站点之间可以保持信息沟通是存在需求的。于是,想办法实现跨域吧~~

jsonp实现跨域

有关jsonp的内容已经在之前的文章Ajax的实现(ajax与jsonp)/)说得挺详细的了,直接跳转吧~

但是jsonp有一个缺点:只能实现get一种请求。

document.domain + iframe跨域

此方案同样有所缺点:仅限主域相同,子域不同的跨域应用场景。
实现原理: 两个页面都通过js强制设置document.domain为基础主域,就实现了同域。

父窗口:

1
2
3
4
5
<iframe id="iframe" src="http://child.domain.com/b.html"></iframe>
<script>
document.domain = 'domain.com';
var user = 'admin';
</script>

子窗口

1
2
3
4
<script>
document.domain = 'domain.com';
alert('get js data from parent' + window.parent.user);
</script>

location.hash + iframe

实现原理:A域与B域要相互通信,通过中间页c来实现。三个页面,不同域之间利用iframelocation.hash传值,相同域之间直接用js访问来通信。

具体实现:A域:a.html => B域:b.html => A域:c.html, a 与 b 不同于只能通过hash值单向通信,b 与 c 也不同于也只能单向通信,但是c 与 a 同域,所以c可通过parent.parent访问a页面的所有对象。

a.html:(http://www.domain1.com/a.html)

1
2
3
4
5
6
7
8
9
10
11
<iframe id="iframe" src="http://www.domain2.com/b.html" style="display=none;">
<script>
var iframe = document.getElementById('iframe');
// 向b.html传hash值
setTimeout(function () {
iframe.src = iframe.src + '#user=admin';//改变url的hash值
}, 1000);
// 开放给同域c.html的回调方法
function onCallback(res) {
alert('data from c.html:' + res);
}

b.html:(http://www.domain2.com/b.html)

1
2
3
4
5
6
7
<iframe id="iframe" src="http://www.domain1.com/c.html" display="none;">
<script>
var iframe = document.getElementById('iframe');
window.onhasChange = function() {
iframe.src =iframe.src + location.hash; //通过hash传递给c页面
}
</script>

c.html:(http://www.domain1.com/c.html)

1
2
3
4
5
<script>
window.onhasChange = function() {
window.parent.parent.onCallback('hello:' + location.hash);
};
</script>

window.name + iframe

window.name的独特之处:name值在不同的页面(甚至不同的域名)加载后依旧存在,并且可以支持非常长的name值(2MB);
实现原理:通过动态创建一个iframe,访问跨域页,在跨域成功后跳转回同域的代理页,利用name长期存在的特性便可以获取到数据传递给同域的本页

a.html:(http://www.domain1.com/a.html)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
var proxy = function(url, callback) {
var state = 0;
var iframe = document.createElement('iframe');
// 加载跨域页
iframe.src = url;
// 这里会触发两次onoload事件,第一次是加载跨域页,第二次是加载代理页
iframe.onload = function() {
if(state === 0) {
// 这里触发的是加载跨域页,切换到代理页
iframe.contentWindow.location = 'http://www.domain1.com/proxy.html';
state = 1;
} else if (state === 1) {
// 这里触发的是加载代理页,读取完同域window.name后
callback(iframe.contentWindow.name);
// 销毁iframe,释放内存,保证不被其他域iframe js访问
destoryFrame();
}
}
// 销毁iframe
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);
})

proxy.html

中间代理页,与a.html同域,内容为空即可

b.html

1
2
3
<script>
window.name = 'data data data ...';
<script>

总结: 通过iframe的src属性由外域转向本地域,跨域数据即由iframe的window.name从外域传递到本地域。这个就巧妙地绕过了浏览器的跨域访问限制,但同时它又是安全的操作。

postMessage跨域

postMessage是HTML5 XMLHttpRequest Level 2中的API,且是为数不多可以跨域操作的window属性之一,它可用于解决以下方面的问题:

  1. 页面和其打开的新窗口的数据传递(属于两个tab页之间的通信)
  2. 多窗口之间消息传递
  3. 页面与嵌套的iframe消息传递
  4. 上面三个场景的跨域数据传递

用法:postMesssage(data, origin)方法接受两个参数

  • data: html5规范支持任意基本类型或可复制的对象,但部分浏览器只支持字符串,所以传参时最好用JSON.stringify()序列化
  • origin: 协议 + 主机 + 端口号,也可以设置为* ,表示可以传递给任意窗口,如果要指定和当前窗口同源的话,设置为’/‘

a.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<iframe id="iframe" src="http://www.domain2.com/b.html" style="display:none;">
<script>
var iframe = document.getElementById('iframe');
iframe.onload = function() {
var data = {
name: 'aym'
};
// 向domain2传递跨域数据
iframe.contentWindow.postMesssage(JSON.stringify(data), 'http://www.domain2.com')
}
// 接受domain2的返回数据
window.addEventListener('message', function(e) {
alert('data from domain2' + e.data)
}, 1000)

b.html

1
2
3
4
5
6
7
8
9
10
11
12
<script>
window.addEventListener('message', function(e) {
alert(e.data);
var data = JSON.parse(e.data);
if(data) {
data.num = 16;
window.parent.postMesssage(JSON.stringify(data), 'http://www.domain1.com');
}
})
</scirpt>

跨域资源共享(CORS)

普通跨域请求: 只需要服务端设置Access-Control-Alloww-Origin即可,前端无须设置。

带cookie请求: 前后端都需要设置字段,另外需注意:所带cookie为跨域请求接口所在域的cookie,而非当前页。

目前,所有浏览器都支持该功能(IE8+:IE8/9需要使用XDomainRequest对象来支持CORS)),CORS也已经成为主流的跨域解决方案。

CORS和JSONP对比

  • JSONP只能实现GET请求,而CORS支持所有类型的HTTP请求。
  • 使用CORS,开发者可以使用普通的XMLHttpRequest发起请求和获得数据,比起JSONP有更好的错误处理。
  • JSONP主要被老的浏览器支持,它们往往不支持CORS,而绝大多数现代浏览器都已经支持了CORS)。

CORS与JSONP相比,无疑更为先进、方便和可靠。

有关CORS的详情,可以跳转详解CORS跨域资源共享

前端设置:

  1. 原生ajax
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    var xhr = new XMLHttpRequest(); //IE8 9需要使用window.XDomainRequest兼容
    // 前端设置是否带cookie
    xhr.withCredentials = true;
    xhr.open('post', 'http://www.domain2.com:8080/login', ture);
    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);
    }
    }

服务端设置:

若后端设置成功,前端浏览器控制台则不会出现跨域报错信息,反之,说明没设成功。

1
2
3
4
5
6
// 跨域后台设置
res.writeHead(200, {
'Access-Control-Allow-Credentials': 'true', // 后端允许发送Cookie
'Access-Control-Allow-Origin': 'http://www.domain1.com', // 允许访问的域(协议+域名+端口)
'Set-Cookie': 'l=a123456;Path=/;Domain=www.domain2.com;HttpOnly' // HttpOnly:脚本无法读取cookie
});

nginx代理跨域(未写)

这里nginx的使用。。。没学过,所以只当挖了个坑吧 因为和下面的nodejs结合vue实现差不多,所以,以后有机会接触再回来补这里的

nodejs中间件代理跨域

  1. Vue框架的跨域(1次跨域)
    利用node + webpack + webpack-dev-server代理接口跨域。在开发环境下,由于vue渲染服务和接口代理服务都是webpack-dev-server同一个,所以页面与代理接口之间不再跨域,无须设置headers跨域信息了。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    module.exports = {
    entry: {},
    module: {},
    ...
    devServer: {
    historyApiFallback: true,
    proxy: [{
    context: '/login',
    target: 'http://www.domain2.com:8080', // 代理跨域目标接口
    changeOrigin: true,
    cookieDomainRewrite: 'www.domain1.com' // 可以为false,表示不修改
    }],
    noInfo: true
    }
    }

在一次慕课网的学习,提到到dev-sever.js进行手动设置代理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 手动添加接口代理 axios
var apiRoutes = express.Router()
apiRoutes.get('/getDiscList', function (req, res) {
let url = 'https://c.y.qq.com/splcloud/fcgi-bin/fcg_get_diss_by_tag.fcg'
axios.get(url, {
headers: {
referer: 'https://c.y.qq.com',
host: 'c.y.qq.com'
},
params: req.query
}).then((response) => {
res.json(response.data)
}).catch((err) => {
console.log(err);
})
})

  1. 非vue(2次跨域)
    利用node + express + http-proxy-middleware搭建一个proxy服务器。

前端代码

1
2
3
4
5
6
7
8
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();

代理服务器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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...');

WebSocket协议跨域

详细可以看webSocket那一章吧~~
这里推荐使用Socket.io??