跨域请求
全称:非同源策略请求(同源即同协议、同域名和同端口),而ajax专门用于处理同源策略请求,因此对于非同源请求,特别是在前后端分离的项目当中,我们就会面临这类问题。
这里要声明一下:跨域是浏览器行为,而不是服务器行为(当前台向服务器发起请求时,后台并没有阻止请求操作,但后台返回数据时,浏览器基于安全考虑,若非同源请求,将可能禁止对请求数据的处理)
常用跨域解决方案
基于Jsonp
我们可以发现在html当中含有src
属性的标签,如<srcipt>
/<img>
/<link>
/<iframe>
标签,在请求资源时都不存在跨域请求的限制。而jsonp则是基于<script>
标签去请求资源,服务器则在返回数据资源时将其包在一个本地可调用的全局函数里返回,然后本地则调用这个函数
原理详解
首先因由于同源策略,我们无法通过ajax请求来获取不符合条件的资源,但是假如我们用<script>
标签请求的js文件中有这样的语句:
test({"x": 1})
而我们前台的请求如下:
<script src="http://xxx.xxx.com/xxx.js"></script>
那么获取到请求以后就会执行test
这个函数,并将一个对象作为参数传入,此时如果我们的前台本身有test
这个函数,那么就可以成功执行这段代码。换个说法,如果现在我们通过访问一个后台接口获取到跟前面一样的字符串(之前是通过访问一个远程js资源得到的),那么因为都是用<script>
标签获取的,获取的结果也一样,只是请求的url稍微有些改变,可以发现结果都是是一样的:正常执行test
这个函数,例如下面这段代码:
<script src="http://xxx.xxx.com/api"></script>
<script>
function test(data) {
console.log(data);
}
</script>
那么控制台就会成功输出data的内容。这就是jsonp的原理,可以看出和ajax请求的本质是完全不一样的,ajax是基于XmlHttpRequest,而jsonp则是基于标签动态请求,可以说是一种伪请求
缺点:由于是基于标签的src属性请求的,只能使用get请求、并且安全性不好
基于CORS跨域资源共享配置
CORS是十分常用的一种跨域解决方案,其只需要在服务端配置对应的响应头属性即可,而无需前端做任何操作,最关键的就是在返回头里配置Access-Control-Allow-Origin
属性,举例(这里基于flask进行示例):
from flask import Flask, make_response
import json
app = Flask(__name__)
@app.route('/test', methods=['GET'])
def user_info():
response = make_response(json.dumps({'data': 'content'}))
response.headers['Access-Control-Allow-Origin'] = '*'
return response
if __name__ == '__main__':
app.run(debug=True, port=5000)
还有一些其他访问控制的配置如下:
Access-Control-Allow-Methods: POST, GET, PUT, DELETE, HEAD, OPTIONS
# 允许的请求方式
#(注意只有POST, GET以及HEAD是默认允许的请求,其他的请求都必须先发送一个OPTIONS的预请求
# 当预请求得到认可后才可以进行请求,而只有在该配置当中配置的方法才会得到认可)
Access-Control-Allow-Headers: 'Content-Type, ...'
# 允许请求的头部信息,没有设置在里面的header都是不允许的
Access-Control-Max-Age: '1000'
# 设置允许跨域的时间,此时在有效期内无需再发送预请求进行验证,直接发请求就可以了,单位是s
Access-Control-Allow-Credentials: true
# 是否允许发送cookie
详细参考:https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Access-Control-Allow-Credentials
缺点:cors配置源地址只能配置*
(多源)或者一个地址,并且如果配置的是*
,那么基于安全性问题,请求时将无法携带cookie
注:
如果不想配置成*
,又希望多个地址可以,那么可以在程序当中判断访问的地址是否是允许的,如果是,则加入到cors配置当中
基于Http Proxy
由于cors的弊端,又出了Http Proxy的方案,需要在webpack中配置,常在基于vue、react的前后端分离项目中使用,首先要安装对应模块包:
npm install webpack webpack-dev-server
然后在webpack的配置文件webpack.config.js
中配置:
devServer: {
port: 8080,
proxy: {
'/': {
target: 'http://127.0.0.1:6666',
// 代理的地址
// secure: false,
// 如果是https接口,需要配置这个参数
changeOrigin: true
// 是否跨域
}
}
}
vue-cli中配置
如果是基于vue-cli的项目工程,那么这里介绍两种方式实现proxy
代理的配置:
- 第一种:配置
devServer
,在build/webpack.base.conf.js
中的module.exports
进行和上面相同的配置 - 第二种:配置
proxyTable
,直接在config/index.js
中的module.exports.dev
配置如下:
module.exports = {
dev: {
...
proxyTable: {
// 代理配置
'/': {
target: 'http://127.0.0.1:6666',
changeOrigin: true,
// pathRewrite: {
// '^/apis': '/api' //重写的路径
// }
}
},
},
...
}
注:
配置proxy之后,实际上前端请求还是发给本身的node服务器(查看网络请求就可以发现),例如前端是8080
,配置了6666
的代理,那么前端会先请求本身,即8080
,然后8080
再通过node去访问6666
,由于服务器之间的请求不存在跨域问题,所以6666
返回数据给node,node再返回给前端,由于同源,所以此时数据也就没有跨域问题了
注:
在vue-cli3.0+以后,则直接在项目根目录下创建文件vue.config.js
,并添加如下内容即可:
module.exports = {
devServer: {
proxy: {
"/": {
target: "http://127.0.0.1:6666/",
changeOrigin: true
}
}
}
};
基于nginx配置CORS
在nginx中也可以配置cors
,举例:
server {
listen 80;
server_name localhost;
...
location / {
root html;
index index.html index.htm;
proxy_pass http://xxx;
add_header Access-Control-Allow-Origin *; # 配置cors
...
}
基于nginx配置反向代理
假如前台服务端口为http://127.0.0.1:3000
,后台服务端接口为http://127.0.0.1:8000/api
,那么因为不同源,必然存在跨域问题,此时可以在nginx中进行配置如下:监听3000
端口,并配置/api
的反向代理地址为:http://127.0.0.1:8000/api
,配置文件示例如下:
server {
listen 3000;
server_name localhost;
...
location /api/{
...
proxy_pass http://127.0.0.1:8000/api/; # 配置反向代理
}
}
再将前台请求的接口改为:http://127.0.0.1:3000/api
,即可解决跨域问题。
(原理:经过nginx的反向代理配置,现在访问http://127.0.0.1:3000/api
就相当于访问http://127.0.0.1:8000/api
,而前台请求的url因为改成了http://127.0.0.1:3000/api
,在浏览器看来前台和请求接口同源,也就不存在跨域问题了)
其他跨域解决方案
基于修改本地host文件(不推荐)
例如前台地址:127.0.0.1:6666
,而后台接口地址:http://api.xxx.com
,此时如果想要访问后台接口,可以在本地host文件中加一行:
127.0.0.1:6666 http://api.xxx.com
但这种方式实际上只是在模仿同源请求,并没有实质地解决跨域问题
基于Iframe的postMessage
postMessage方法允许页面间基于Iframe进行消息传递,例如A和B页面进行消息传递:
- a.html
<html>
<body>
<iframe
id="iframe"
src="http://127.0.0.1:5500/b.html"
style="display: none;"
></iframe>
</body>
<script>
iframe.onload = function() {
iframe.contentWindow.postMessage("来自A的信息...", "http://127.0.0.1:5500/b.html");
// 发送消息给页面B
};
// 监听页面B发来的消息
window.onmessage = function(ev) {
console.log("A收到B的信息:", ev.data);
};
</script>
</html>
- b.html
<html>
<body></body>
<script>
// 监听页面A发来的消息
window.onmessage = function(ev) {
console.log("B收到A的信息:", ev.data);
ev.source.postMessage("回信...", "*");
};
</script>
</html>
基于H5的web socket
由于原生web socket
不太好使用,这里使用socket.io
(对web socket
进行了封装的框架)进行示例:
- 服务端(基于node):
const server = require("http").createServer();
const io = require("socket.io")(server);
io.on("connection", client => {
client.on("event", data => {
console.log(data);
});
client.on("message", msg => {
console.log(msg);
});
client.on("disconnect", () => {
console.log("server has closed!");
});
});
server.listen(3000);
- 客户端:
<html>
<body></body>
<script src="https://cdn.bootcss.com/socket.io/2.3.0/socket.io.js"></script>
<script>
let socket = io('http://127.0.0.1:3000/');
socket.on('connect', () => {
socket.on('message', msg => {
console.log(msg);
})
socket.on('disconnect', () => {
console.log('server has closed!');
})
})
socket.send('test');
</script>
</html>
web socket使用参考:https://zhuanlan.zhihu.com/p/74326818
使用WebSocket进行跨域数据请求参考:https://blog.csdn.net/itkingone/article/details/83818278