本博客配套的源码在这里
最近我在做一个系统的全栈开发,遇到了这样一个问题。
首先,我的前端是一个来自百度的开源框架——Amis,它封装自React.js,基于JSON配置。我下载了Amis提供的SDK文件夹,并进行了代码开发。但是我在部署整个系统的时候遇到了跨域问题。原因是,我的前端不是以服务的形式运行的,它是一组在浏览器中打开的HTML页面。
如果我在浏览器中打开一个HTML页面,当前采用的协议通常是HTTP或HTTPS,域名通常是"localhost"或者是HTML文件所在的服务器的域名,端口通常是80(HTTP)或443(HTTPS)。由于我是通过文件路径直接打开HTML文件,那么协议、域名和端口都是本地文件系统的相关信息。
而我需要在前端中调用后端的接口,尽管后端IP与前端一致,但PORT和前端不同,因此在浏览器中访问系统时,触发了浏览器的同源策略,导致我的前端无法访问后端接口。
根据同源策略,浏览器会阻止页面中的JavaScript代码向不同域名、协议或端口的资源发出跨域请求。这意味着如果我的HTML页面和后端服务的域名、协议或端口不一致,浏览器会阻止这种跨域请求。
我上网查阅了资料,发现,在遵守同源策略的前提下,可以采取以下方法来实现前端页面对后端服务的访问:
下面我将对这三种方法进行实践。
前端工程1我写了一个HTML页面,放在HTML目录下。在点击按钮后,显示弹窗,如果后端顺利返回,则将返回的文本显示到弹窗上;如果发生异常,在弹窗上显示异常信息。
home.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>调用后端服务</title>
</head>
<body>
<h1>调用后端服务示例</h1>
<button id="getDataBtn">获取数据</button>
<script>
document.getElementById('getDataBtn').addEventListener('click', function() {
fetch('http://127.0.0.1:2020/api/hello')
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.text();
})
.then(data => {
alert(data);
})
.catch(error => {
alert('发生错误: ' + error.message);
});
});
</script>
</body>
</html>
我创建了一个Flask项目,因为Flask足够的简单、快捷,但是你也可以使用任何你熟悉的语言和框架。
使用之前需要本地有python环境,并执行pip install flask
来安装依赖,并在我提供的源码的FLASK目录下执行python app.py
来运行项目。
app.py
from flask import Flask
app = Flask(__name__)
@app.route('/api/hello', methods=['GET'])
def get_data():
return jsonify(data='hello,flask!')
if __name__ == '__main__':
app.run(host='127.0.0.1', port=2020,debug=True)
我想找到不同形式的前端对应的跨域问题的解决方案,因此我创建了两种前端,除了上面的HTML页面的形式,还包括了服务的形式。
我创建了一个vue.js脚手架项目,并在里面写了和前端工程1类似的代码。关于怎么快速上手vue,可以看我的另一篇博客:这里
在VUE/vue-app目录下执行npm run serve
运行项目。
App.vue
<template>
<div>
<button @click="getData">获取数据</button>
<div v-if="showModal" class="modal">
<div class="modal-content">
<span v-if="responseData">{{ responseData }}</span>
<span v-if="error">{{ error }}</span>
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
showModal: false,
responseData: null,
error: null
};
},
methods: {
getData() {
fetch('http://127.0.0.1:2020/api/hello')
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.text();
})
.then(data => {
this.responseData = data;
this.showModal = true;
})
.catch(error => {
this.error = '发生错误: ' + error.message;
this.showModal = true;
});
}
}
};
</script>
<style>
.modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
}
.modal-content {
background-color: white;
padding: 20px;
border-radius: 5px;
}
</style>
在浏览器中打开home.html页面,点击按钮后,无法成功获取后端数据,触发了浏览器的同源策略。
下面就来着手解决这个问题。
在后端服务中配置允许特定域名的跨域请求,通过设置响应头来允许跨域访问。这样可以让前端页面在浏览器中向后端服务发出跨域请求。
在 Flask 项目中应用
CORS(app)
的底层原理涉及到在 HTTP响应中添加特定的头部信息,以允许跨域请求访问资源。Flask-CORS 扩展简化了这个过程,它通过在响应中添加适当的 CORS头部信息来实现跨域资源共享。 具体来说,当你在 Flask 项目中调用CORS(app)
时,Flask-CORS会自动为你的应用程序添加 CORS 头部信息,包括Access-Control-Allow-Origin
、Access-Control-Allow-Methods
、Access-Control-Allow-Headers
等。这些头部信息告诉浏览器哪些跨域请求是被允许的,从而解决了跨域请求被浏览器阻止的问题。
Flask-CORS 简化了这个过程。
这种方法只能对前端工程是服务(前端工程2)
的情况生效,原因就是后端需要配置前端的ip、端口等信息,而前端工程1是以文件的形式
打开前端页面并访问后端的。
当以文件路径的形式在浏览器中打开HTML页面文件时,页面中调用后端的API时,无法直接获取调用者的IP和端口。这是因为以文件路径形式打开HTML页面时,页面的请求是直接从文件系统发出的,而不是通过网络协议进行通信,因此无法获取调用者的IP和端口信息。
如果需要获取调用者的IP和端口信息,需要将HTML页面部署到一个服务器上,然后通过服务器地址访问页面,这样页面中的请求就会通过网络协议进行通信,从而可以获取调用者的IP和端口信息。
from flask import Flask,request
from flask_cors import CORS
app = Flask(__name__)
# 配置前端vue的ip、端口
CORS(app, resources={r"/api/hello": {"origins": "http://127.0.0.1:8080"}})
@app.route('/api/hello', methods=['GET'])
def get_data():
return jsonify(data='hello,flask!')
if __name__ == '__main__':
app.run(host='127.0.0.1', port=2020,debug=True)
成功访问到了后端!
JSONP(JSON with Padding)是一种利用<script>标签的跨域技术,它允许在不受同源策略限制的情况下从其他域中获取数据。
底层原理是利用
<script>
标签的跨域特性来实现跨域请求。JSONP是一种在客户端与服务器之间进行跨域数据传输的技术,它允许从其他域中获取数据,绕过了浏览器的同源策略限制。 JSONP的基本原理是通过在页面上动态创建一个<script>
标签,该标签的 src 属性指向包含 JSON 数据的 URL 地址。这个 URL地址会将 JSON 数据包裹在一个函数调用中,这个函数是在客户端预先定义好的。服务器返回的数据会被当做 JavaScript代码执行,从而触发客户端预先定义的函数,实现对数据的处理和展示。
需要注意的是,JSONP存在一些安全性方面的问题,主要是潜在的跨站脚本攻击(XSS)风险。因为JSONP是通过动态创建<script>
标签来获取数据的,所以如果被恶意注入了恶意代码,就有可能导致安全问题。因此,在使用JSONP时需要谨慎处理返回的数据,确保数据的安全性。
home.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>调用后端服务</title>
</head>
<body>
<h1>使用JSONP</h1>
<div id="result"></div>
<button id="getDataBtn">获取数据</button>
<script>
document.getElementById('getDataBtn').addEventListener('click', function() {
var script = document.createElement('script');
script.src = 'http://127.0.0.1:2020/api/hello?callback=handleData';
document.body.appendChild(script);
});
function handleData(data) {
document.getElementById('result').innerText = data.result;
}
</script>
</body>
</html>
失败!触发了浏览器的CORB策略!
这个错误表明浏览器使用了CORB(Cross-Origin Read Blocking)机制来阻止跨域读取。CORB是一种安全机制,用于防止恶意网站从跨域响应中读取敏感数据。浏览器对JSONP请求进行了CORB阻止。JSONP本身存在安全风险,因为它是通过动态创建<script>标签来获取数据的,这可能导致恶意网站注入恶意代码。因此,浏览器会对JSONP请求进行CORB阻止。
在开发环境中,可以设置代理服务器来转发前端请求到后端服务,使得前端页面和后端服务在同一个域名下,从而避免跨域问题。我以Nginx为例:
Nginx 解决跨域问题的底层原理主要是通过设置 HTTP 响应头来实现跨域资源共享(CORS)。当浏览器发起跨域请求时,会先发送一个OPTIONS 预检请求,以确定是否允许跨域访问。Nginx 可以通过设置响应头来响应这个预检请求,从而允许跨域访问。
具体来说,Nginx 可以通过设置 add_header 指令来添加Access-Control-Allow-Origin、Access-Control-Allow-Methods、Access-Control-Allow-Headers
等 CORS 相关的响应头,允许特定域名的跨域请求访问资源。这样一来,浏览器就能够允许跨域请求的发送和接收,从而解决了跨域问题。
关于Nginx详细的学习和使用可以参考我的这篇博客:Nginx
下载: 下载 Nginx 的 Windows 版本安装文件(https://nginx.org/en/download.html)。
解压: 下载完成后,解压缩安装文件到你选择的目录。
启动: 在解压文件夹中双击nginx.exe就能启动nginx;或者在当前目录下打开终端执行nginx
命令。如果一切顺利,在浏览器中输入 http://localhost
将访问 Nginx 的欢迎页面。
配置文件: nginx.conf
文件位于安装目录下的 conf
文件夹中,可用文本编辑器打开并进行修改。
这一章我以前端工程1为例,前端工程2是vue服务,执行npm run build
对项目打包成静态文件,都存放在了dist目录中,后续nginx配置流程和前端工程1是一样的。
nginx.conf:
#user nobody;
worker_processes 1;
#error_log logs/error.log;
#error_log logs/error.log notice;
#error_log logs/error.log info;
#pid logs/nginx.pid;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
server {
listen 8000;
server_name 192.168.2.107;
# 配置后端工程:
# 访问http://192.168.2.107:8000/flask/
# 时相当于访问http://192.168.2.107:2020/
location /flask/ {
proxy_pass http://192.168.2.107:2020/;
}
# 配置前端工程1:这个要放在最下面
# 访问http://192.168.2.107:8000/home.html
# 时相当于在浏览器中访问HTML文件夹
location / {
root "D:\\0 project\\cors\\HTML";
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}
}
在nginx安装目录下执行nginx -s reload
刷新配置。
将原来的127.0.0.1改成服务器局域网ip(仅对局域网可见),如果有公网ip是最好的,但是我电脑没配置这个。
if __name__ == '__main__':
app.run(host='192.168.2.107', port=2020,debug=True)
home.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Nginx示例</title>
</head>
<body>
<h1>Nginx示例</h1>
<button id="getDataBtn">获取数据</button>
<script>
document.getElementById('getDataBtn').addEventListener('click', function() {
fetch('http://192.168.2.107:8000/flask/api/hello')
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.text();
})
.then(data => {
alert(data);
})
.catch(error => {
alert('发生错误: ' + error.message);
});
});
</script>
</body>
</html>
跨域问题通过Nginx解决了!