项目中和面试经常遇到跨域问题,老生常谈的问题,今天就来讲讲跨域:
浏览器不支持跨域?
- 用户登录A网站,在cookie里面存入了登录信息,用户再登录B网站,如果没有同源策略,B网站能够轻易拿到用户A网站信息,去伪造用户向A网站发送信息。
- DOM元素,iframe嵌入页面,如果没有同源策略,嵌入一个淘宝页面,伪造成淘宝钓鱼网站,用户输入账户密码可以通过dom操作拿到账户信息。
- ajax,LocalStorage也不支持跨域,不能随意拿取信息。
总的来说浏览器不支持跨域是为了安全,但在项目中有些情况下需要去跨域。
先讲讲什么是同源策略
- 协议
- 域名
- 端口
3个都一样为同域,其中某一个不一样那么就跨域了。
列如你的网站域名是http://www.xxx.com
- https://www.xxx.com (跨域,协议不一致)
- http://aaa.xxx.com(跨域,域名不一致)
- http://www.xxx.com:8080 (跨域,端口不一致)
- http://www.xxx.com/aaa (同域)
但是在浏览器中link标签(加载css) img(加载图片) script(加载js) 标签不受同源策略,可以随便跨域,去加载其他域下面的资源。
jsonp
由于script标签不受同源策略,可以用script去跨域,原理就是创建一个script标签,src地址去引入其他域下的js文件并且带入参数和回调函数,js文件返回一个执行函数,去执行window下的回调函数。
例如现在要向www.xxx.com去请求数据,首先我们在window下声明一个函数a,然后创建一个script标签<script src="www.xxx.com?params=xxx&cb=a"></script>
,params代表请求参数,cb指定成功后需要调用的回调函数,这个请求返回一个js文件内容a({data: 'xxx'})
,执行了最开始在window下声明的函数a并且传入我们需要的数据。
下面来看看,如果封装一个简单的jsonp函数
function jsonp({ url, params }) {
return new Promise((resolve, reject) => {
// 创建script标签
const script = document.createElement('script');
// window下声明回调函数
window.cb = function (data) {
resolve(data);
document.body.removeChild(script);
}
const arr = []
// 拼接参数
for (let key in params) {
arr.push(`${key}=${params[key]}`);
}
script.src = `${url}?${arr.join('&')&cb=cb}`
// 加载文件
document.body.appendChild(script);
})
}
jsonp({
url: 'www.xxx.com/xxx',
params: {xx: xx},
}).then((data) => {
// data数据
})
后端实现
const express = require('express');
const app = express();
app.get('/xxx', function(req, res) {
const { xx, cb } = req.query;
// 返回cb(xxx)
res.end(`${cb}(获取到数据${xx})`);
})
app.listen(80)
jsonp缺点: 只支持get请求并且不安全,如果加载第三方返回script标签,会出现恶意攻击(xss)。
cors
解决jsonp缺点 支持get post put delet请求,由服务端控制,安全性高,前端正常发送ajax请求,项目中最常用的方式。
上面说了同源策略是浏览器的行为,其实我们的请求能够到达服务器,只是浏览器给屏蔽掉了数据,cors就是利用http header头告诉浏览器一些信息,浏览器放开同源策略。
简单请求
- 必须是以下三种方法
- get
- post
- head(什么是head请求? -> 只返回响应头,不会返回响应内容,http1.0定义的方法,前端用的很少)
- 请求头只能包含以下字段
Accept
Accept-Language
Content-Language
Last-Event-ID
Content-Type:application/x-www-form-urlencoded multipart/form-data text/plain
如果不满足上面的简单请求,那么就是非简单请求,非简单请求的时候浏览器会首先发出一个预检请求OPTIONS到服务器,把将要发送的请求方法,请求头给服务器,如果服务器返回成功,那么浏览器才会发出正式的请求,否则报错。
服务器可以设置的header头
-
Access-Control-Allow-Origin: www.baidu.com
: 允许百度这个域下的请求,可以配置多个域,以逗号隔开,还能设置为*(*代表所有域都能请求但是origin写*不能允许携带cookie凭证) -
Access-Control-Allow-Methods: POST,GET,PUT,DELETE
: 允许哪些方法,以逗号隔开 -
Access-Control-Allow-Headers: name,token
: 表示服务器支持name和token字段 -
Access-Control-Allow-Credentials: true
: 允许cookie跨域 -
Access-Control-Max-Age: 6
: 预检请求的有效期,单位为秒,相当于把预检请求缓存下来,下次直接发送正式请求,不用再去预检是否服务器支持该请求方法 -
Access-Control-Expose-Headers: token
: 允许用js获取响应头里的token值 -
Access-Control-Request-Headers: token
: 指定浏览发送请求时,需要带上的额外请求头
用express简单实现一个cors跨域服务端
const express = require('express');
const app = express();
app.use((req, res, next) => {
res.setHeader('Access-Control-Allow-Origin', 'http://localhost:8080');
res.setHeader('Access-Control-Allow-Methods', 'POST,GET,PUT,DELETE');
res.setHeader('Access-Control-Allow-Headers', 'name,token');
res.setHeader('Access-Control-Allow-Credentials', true);
res.setHeader('Access-Control-Max-Age', 6);
res.setHeader('Access-Control-Expose-Headers', 'token');
res.setHeader('Access-Control-Request-Headers', 'token');
// 如果为预检请求,直接返回同意,避免浪费之后处理资源
if (req.method === 'OPTIONS') {
res.end();
}
next();
});
app.put('/uset', (req, res) => {
res.end('yes');
});
postMessage
除了与服务器通信需要跨域,有时候会存在iframe加载跨域,window.open()一个标签页跨域通信,html5为了解决这个问题新增了postMessage方法。
postMessage(需要传递的参数, 目标源)
iframe:
localhost:3000 下 a.html
<iframe
src="http://localhost:4000/b.html"
id="frame"
onload="load()"
></iframe>
<script>
function load () {
const i = document.getElementById('frame')
i.contentWindow.postMessage('send', 'http://localhost:4000/')
}
window.addEventListener('message', function(e) {
console.log(e.data) // 接收到ok
})
</script>
localhost:5000 下 b.html
<script>
window.addEventListener('message', function(e) {
console.log(e.data) 接收到send
e.source.postMessage('ok', e.origin)
}, false)
</script>
注意: 一定要在onload结束之后再发送postMessage,否则子页面接收不到消息
window.open()
localhost:3000 下 a.html
<input type="button" value="打开窗口" onclick="open_new()">
<script>
function open_new() {
const newWindow = window.open('http://localhost:4000/b.html');
// 跨域无法监听onload 如果同域下跨域使用
newWindow.onload = function () {
newWindow.postMessage('open', 'http://localhost:4000/')
}
setTimeout(() => {
newWindow.postMessage('open', 'http://localhost:4000/')
}, 1000);
}
window.addEventListener('message', function(e) {
console.log(e.data)
})
</script>
localhost:5000 下 b.html
<script>
window.addEventListener('message', function(e){
console.log(e)
}, false);
window.opener.postMessage('Nice to see you', 'http://localhost:3000/');
</script>
window.open()如果是跨域,则无法监听onload事件
document.domain
该方法存在一定的限制条件,必须是2个网页的一级域名相同,用法也很简单,可以使2个页面共享cookie。
例如: a.xxx.com
和b.xxx.com
:
同时设置docment.domain = 'xxx.com'
服务器设置cookie时也设置到xxx.com
这样的话,在这个一级域名下的所有二三级域名,都可以互通cookie
window.name
利用window.name改变网页地址,该值不变的特点,可以做到跨域。
a网站iframe加载b网站,b网站把需要传输的数据放入window.name中,然后重定向到a网站下同域的网址,这时a网站可以顺利的拿到window.name属性。
localhost: 3000 下的a.html
<iframe id="fra" src="http://localhost:4000/b.html" onload="load()"></iframe>
<script>
function load(){
const f = document.getElementById('fra');
console.log(f.contentWindow.name);
}
</script>
localhost: 4000 下的b.html
<script>
window.name = 'hello'
window.location = 'http://localhost:3000/c.html'
</script>
localhost: 3000 下的c.html
无需任何代码,建一个空的html文件
location.hash
hash值得变化不会导致页面刷新,通过a页面改变b页面hash值,b页面不能直接通过parent去修改a页面的hash值,需要通过加载一个a域下的代理iframe修改,从而实现跨域通信。
localhost: 3000 下的a.html
<iframe id="fra" src="http://localhost:4000/b.html" onload="load()"></iframe>
<script>
function load(){
const f = document.getElementById('fra');
f.src = 'http://localhost:4000/b.html#hello'
}
window.addEventListener('hashchange', function() {
console.log(window.location.hash)
})
</script>
localhost: 4000 下的b.html
<script>
addEventListener('hashchange', function() {
console.log(window.location.hash)
const ifr = document.createElement('iframe');
ifr.style.display = 'none';
ifr.src = 'http://localhost:3000/c.html#yes';
document.body.appendChild(ifr);
})
</script>
localhost: 3000 下的c.html
<script>
parent.parent.location.hash = window.location.hash.substring(1);
</script>
other
以上就是项目中最常用到的跨域方式,还有一种与服务器通信方式websocket不受浏览器同源策略。平常在开发中经常用到的webpack-dev-server,nginx,http-server...利用的是代理去请求。
先是本地起了一个代理服务器,前端发送http到代理服务器,由代理服务器请求后端的接口,跨域是浏览器的行为,2个服务器之间是没有同源策略的,所有代理服务器拿到数据后,再返回给前端。代理服务器和前端同源。