翻译
Handling File Uploads With Flask
Web 应用程序的一个常见特性是允许用户将文件上传到服务器 。在 RFC 1867 中协议记录了客户端上传文件的机制,我们最喜欢的 Web 框架 Flask 完全支持这一机制,但是对于许多开发者来说,还有许多实现细节未遵循该正式规范 。诸如在何处存储上传的文件,如何事后使用它们,或者如何保护服务器不受恶意文件上传的影响,这些都会产生很多混乱和不确定性 。
?
在本文中,我将向你展示如何为 Flask 服务器实现强大的文件上传功能,该功能不仅支持基于 Web 浏览器中的标准文件上传并且与基于 JavaScript 的上传小部件兼容:

文章插图
基本文件上传表单从高层次的角度来看,上传文件的客户端与其他任何表单数据提交一样 。换句话说,你必须定义一个包含文件字段的 HTML 表单 。
?
下面是一个简单的 HTML 页面,该表单接受一个文件:
<!doctype html><html><head><title>File Upload</title></head><body><h1>File Upload</h1><form method="POST" action="" enctype="multipart/form-data"><p><input type="file" name="file"></p><p><input type="submit" value="https://tazarkount.com/read/Submit"></p></form></body></html>
文章插图
你可能知道,
<form> 元素的 method 属性可以是 GET 或 POST 。使用 GET 时,数据将在请求 URL 的查询字符串中提交,而使用 POST 时,数据将进入请求主体 。在表单中包含文件时,必须使用 POST,因为不可能在查询字符串中提交文件数据 。?
没有文件的表单通常不包含
<form> 元素中的 enctype 属性 。此属性定义浏览器在将数据提交到服务器之前应该如何格式化数据 。HTML 规范为其定义了三个可能的值:application/x-www-form-urlencoded:
multipart/form-data:
text/plain:
?
实际的文件字段是我们用于大多数其他表单字段的标准
<input> 元素,其类型设置为 file 。在上面的示例中,我没有包含任何其他属性,但是file字段支持两个有时有用的属性:multiple:
<input type="file" name="file" multiple>accept:
<input type="file" name="doc_file" accept=".doc,.docx"><input type="file" name="image_file" accept="image/*">使用 Flask 接受文件提交对于常规表单,Flask 提供了对 request.form 字典中提交的表单字段的访问 。但是,文件字段包含在request.files 字典中 。request.form 和 request.files 字典实际上是“multi-dicts”,它是一种支持重复键的专门字典实现 。这是必要的,因为表单可以包含多个具有相同名称的字段,通常情况下是由多组复选框组成 。对于允许多个文件的文件字段,也会发生这种情况 。【使用 Flask 处理文件上传】暂时忽略诸如验证和安全性等重要方面,下面简短的 Flask 应用程序接受使用上一节中定义的表单上传的文件,并将提交的文件写入当前目录:
from flask import Flask, render_template, request, redirect, url_forapp = Flask(__name__)@app.route('/')def index():return render_template('index.html')@app.route('/', methods=['POST'])def upload_file():uploaded_file = request.files['file']if uploaded_file.filename != '':uploaded_file.save(uploaded_file.filename)return redirect(url_for('index'))upload_file() 函数使用@app.route装饰,以便在浏览器发送POST请求时调用该函数 。请注意,同一个根 URL 是如何在两个视图函数之间进行拆分的,并将 index() 设置为接受 GET 请求,将 upload_file``() 上传为 POST 请求 。?
uploaded_file 变量保存提交的文件对象 。这是 Flask 从 Werkzeug 导入的 FileStorage 类的实例 。FileStorage 中的 filename 属性提供客户端提交的文件名 。如果用户提交表单时没有在 file 字段中选择文件,那么文件名将是一个空字符串,因此始终检查文件名以确定文件是否可用是很重要的 。Flask 收到文件提交后,不会自动将其写入磁盘 。这实际上是一件好事,因为它使应用程序有机会查看和验证文件提交,这一点将在后面看到 。可以从 stream 属性访问实际文件数据 。如果应用程序只想将文件保存到磁盘,则可以调用
save() 方法,并将所需路径作为参数传递 。如果未调用文件的 save() 方法,则该文件将被丢弃 。??
是否要使用此应用程序测试文件上传? 为你的应用程序创建目录,并将上面的代码编写为 app.py 。然后创建一个模板子目录,并将上一节中的HTML页面编写为templates/index.html 。创建一个虚拟环境并在其上安装Flask,然后使用
flask run 运行该应用程序 。每次提交文件时,服务器都会把它的副本写到当前目录中 。?
在继续讨论安全性主题之前,我将讨论上面的代码的一些变体,你可能会发现这些变体很有用 。如前所述,可以将文件上传字段配置为接受多个文件 。如果像上面那样使用
request.files['file'],则只会得到一个提交的文件,但是使用 getlist() 方法,你可以在for循环中访问所有文件:for uploaded_file in request.files.getlist('file'):if uploaded_file.filename != '':uploaded_file.save(uploaded_file.filename)许多人在 Flask 中编写表单处理路由时,对 GET 和 POST 请求使用单个视图函数 。使用单视图函数的示例应用程序的版本编码如下:@app.route('/', methods=['GET', 'POST'])def index():if request.method == 'POST':uploaded_file = request.files['file']if uploaded_file.filename != '':uploaded_file.save(uploaded_file.filename)return redirect(url_for('index'))return render_template('index.html')最后,如果使用 Flask-WTF 扩展来处理表单,则可以使用 FileField 对象上传文件 。到目前为止,你看到的例子中使用的表单可以使用 Flask-WTF 编写如下:from flask_wtf import FlaskFormfrom flask_wtf.file import FileFieldfrom wtforms import SubmitFieldclass MyForm(FlaskForm):file = FileField('File')submit = SubmitField('Submit')注意,FileField 对象来自 flask_wtf 包,与大多数其他字段类不同,后者直接从 wtforms 包导入 。Flask-WTF 为文件字段提供了两个验证器,FileRequired 和 FileAllowed,前者执行类似于空字符串检查的检查,后者确保文件扩展名包含在允许的扩展名列表中 。?
当您使用 Flask-WTF 表单时,file 字段对象的
data 属性指向 FileStorage 实例,因此将文件保存到磁盘的工作方式与上面的示例相同 。保护文件上传上一节中给出的文件上传示例是一个非常简单的实现,不是很健壮 。Web 开发中最重要的规则之一是永远不要信任客户提交的数据,因此在使用常规表单时,像 Flask-WTF 这样的扩展会在接受表单和整合数据到应用程序中之前对所有字段进行严格验证 。对于包含文件字段的表单,也需要进行验证,因为如果不进行文件验证,服务器将为攻击敞开大门 。例如:
- 攻击者可以上传一个非常大的文件,以至于服务器中的磁盘空间完全被填满,从而导致服务器出现故障
- 攻击者可以使用文件名(例如../../../.bashrc或类似文件)的上传请求,以试图欺骗服务器重写系统配置文件 。
- 攻击者可以上传带有病毒或其他类型恶意软件的文件到应用程序需要使用的位置,例如,用户头像
MAX_CONTENT_LENGTH 选项控制请求主体可以拥有的最大大小 。虽然这不是一个特定于文件上传的选项,但设置一个最大的请求体大小有效地使 Flask使用413状态码丢弃大于允许的请求体大小的请求让我们修改上一节中的 app.py 示例,只接受最大为1 MB 的请求:
app.config['MAX_CONTENT_LENGTH'] = 1024 * 1024如果你试图上传一个大于1 MB 的文件,应用程序现在将拒绝它 。验证文件名我们不能完全相信客户端提供的文件名是有效的和可以安全使用的,所以随上传文件一起提供的文件名必须经过验证 。
?
要执行的一个非常简单的验证是确保文件扩展名是应用程序愿意接受的扩展名,这与使用 Flask-WTF 时F
FileAllowed 验证器所做的类似 。假设应用程序接受图像,那么它可以配置允许的文件扩展名列表:app.config['UPLOAD_EXTENSIONS'] = ['.jpg', '.png', '.gif']对于每个上传的文件,应用程序可以确保文件扩展名是允许的:filename = uploaded_file.filenameif filename != '':file_ext = os.path.splitext(filename)[1]if file_ext not in current_app.config['UPLOAD_EXTENSIONS']:abort(400)使用这种逻辑,任何不在允许的文件扩展名的文件名,都会出现400错误 。?
除了文件扩展名之外,验证文件名以及提供的任何路径也很重要 。如果你的应用程序不关心客户端提供的文件名,则处理上传的最安全方法是忽略客户端提供的文件名,而是生成自己的文件名,然后传递给
save() 方法 。这种技术工作良好的示例是头像上传 。每个用户的头像都可以使用用户 ID 保存为文件名,因此客户端提供的文件名可以丢弃 。如果你的应用程序使用 Flask-Login,则可以实现以下 save() 调用:uploaded_file.save(os.path.join('static/avatars', current_user.get_id()))在其他情况下,保留客户端提供的文件名可能更好,因此必须首先清理文件名 。对于这些情况,Werkzeug 提供了 secure_filename() 函数 。让我们通过在 Python shell 中运行一些测试来看看这个函数是如何工作的:>>> from werkzeug.utils import secure_filename>>> secure_filename('foo.jpg')'foo.jpg'>>> secure_filename('/some/path/foo.jpg')'some_path_foo.jpg'>>> secure_filename('../../../.bashrc')'bashrc'正如你在示例中看到的,无论文件名有多么复杂或多么恶意,secure_filename()函数都将其缩减为一个单位文件名 。?
让我们将
secure_filename() 合并到示例上传服务器中,并添加一个配置变量,该变量定义文件上传的专用位置 。下面是带有安全文件名的完整 app.py 源文件:import osfrom flask import Flask, render_template, request, redirect, url_for, abortfrom werkzeug.utils import secure_filenameapp = Flask(__name__)app.config['MAX_CONTENT_LENGTH'] = 1024 * 1024app.config['UPLOAD_EXTENSIONS'] = ['.jpg', '.png', '.gif']app.config['UPLOAD_PATH'] = 'uploads'@app.route('/')def index():return render_template('index.html')@app.route('/', methods=['POST'])def upload_files():uploaded_file = request.files['file']filename = secure_filename(uploaded_file.filename)if filename != '':file_ext = os.path.splitext(filename)[1]if file_ext not in app.config['UPLOAD_EXTENSIONS']:abort(400)uploaded_file.save(os.path.join(app.config['UPLOAD_PATH'], filename))return redirect(url_for('index'))注意secure_filename 函数将过滤所有非ASCII字符,因此,如果filename 是 "头像.jpg"之类的,则结果为"jpg",但没有格式,这是个问题,我建议使用uuid模块重命名上传的文件,以避免出现上述情况 。
验证文件内容我将要讨论的第三层验证是最复杂的 。如果您的应用程序接受某种文件类型的上传,那么理想情况下,它应该执行某种形式的内容验证,并拒绝任何不同类型的文件 。
?
如何实现内容验证在很大程度上取决于应用程序接受的文件类型 。对于本文中的示例应用程序,我使用的是图像,因此可以使用 Python 标准库中的 imghdr 包验证文件头实际上是一个图像 。
?
让我们编写一个
validate_image() 函数,对图像执行内容验证:import imghdrdef validate_image(stream):header = stream.read(512)stream.seek(0)format = imghdr.what(None, header)if not format:return Nonereturn '.' + (format if format != 'jpeg' else 'jpg')这个函数以一个字节流作为参数 。它首先从流中读取512个字节,然后重置流指针,因为稍后当调用 save ()函数时,我们希望它看到整个流 。前512字节的图像数据将足以识别图像的格式 。?
如果第一个参数是文件名,
imghdr.what() 函数可以查看存储在磁盘上的文件; 如果第一个参数是 None,数据在第二个参数中传递,则可以查看存储在内存中的数据 。FileStorage 对象为我们提供了一个流,因此最方便的选项是从它中读取安全数量的数据,并在第二个参数中将其作为字节序列传递 。?
imghdr.what() 的返回值是检测到的图像格式 。该函数支持多种格式,其中包括流行的
jpeg、 png 和 gif 。如果未检测到已知的图像格式,则返回值为 None 。如果检测到格式,则返回该格式的名称 。最方便的是将格式作为文件扩展名返回,因为应用程序可以确保检测到的扩展名与文件扩展名匹配,所以 validate_image() 函数将检测到的格式转换为文件扩展名 。这很简单,只需为除 jpeg 外的所有图像格式添加一个点作为前缀,jpeg 除外,通常使用 .jpg扩展名 。?
下面是完整的 app.py,包含前面几节中的所有特性和内容验证:
import imghdrimport osfrom flask import Flask, render_template, request, redirect, url_for, abortfrom werkzeug.utils import secure_filenameapp = Flask(__name__)app.config['MAX_CONTENT_LENGTH'] = 1024 * 1024app.config['UPLOAD_EXTENSIONS'] = ['.jpg', '.png', '.gif']app.config['UPLOAD_PATH'] = 'uploads'def validate_image(stream):header = stream.read(512)stream.seek(0)format = imghdr.what(None, header)if not format:return Nonereturn '.' + (format if format != 'jpeg' else 'jpg')@app.route('/')def index():return render_template('index.html')@app.route('/', methods=['POST'])def upload_files():uploaded_file = request.files['file']filename = secure_filename(uploaded_file.filename)if filename != '':file_ext = os.path.splitext(filename)[1]if file_ext not in app.config['UPLOAD_EXTENSIONS'] or \file_ext != validate_image(uploaded_file.stream):abort(400)uploaded_file.save(os.path.join(app.config['UPLOAD_PATH'], filename))return redirect(url_for('index'))在视图函数中唯一的变化就是加入了最后一个验证逻辑:if file_ext not in app.config['UPLOAD_EXTENSIONS'] or \file_ext != validate_image(uploaded_file.stream):abort(400)这个扩展检查首先确保文件扩展名在允许的列表中,然后确保通过查看数据流检测到的文件扩展名与文件扩展名相同 。?
在测试这个版本的应用程序之前,创建一个名为
- 春季老年人吃什么养肝?土豆、米饭换着吃
- 三八妇女节节日祝福分享 三八妇女节节日语录
- 老人谨慎!选好你的“第三只脚”
- 校方进行了深刻的反思 青岛一大学生坠亡校方整改校规
- 脸皮厚的人长寿!有这特征的老人最长寿
- 长寿秘诀:记住这10大妙招 100%增寿
- 春季老年人心血管病高发 3条保命要诀
- 眼睛花不花要看四十八 老年人怎样延缓老花眼
- 香槟然能防治老年痴呆症? 一天三杯它人到90不痴呆
- 老人手抖的原因 为什么老人手会抖
