标签归档:CSRF

在 Flask 程序中实现 CKEditor 图片上传和 CSRF 保护

《Flask Web 开发实战》第 2 个实战项目是一个博客(Bluelog),这个项目本来没有添加图片上传支持,很多人想要自己实现,结果因为项目中同时使用了 CSRFProtect 扩展,它会默认验证所有 POST 请求,进而导致上传图片的请求出错。反馈的人多了,我就做了一些事情来改善这个问题:

  • 发布了 Flask-CKEditor 0.4.3,内置了对 CSRFProtect 扩展的支持。
  • 在 Bluelog 中实现了图片上传(7537416)。

如果你想实际体验,需要更新本地的 Bluelog 仓库,并更新 Flask-CKEditor 的版本。这篇文章会单独介绍使用 Flask-CKEditor 实现图片上传的方法,顺便说一下新的 CSRF 保护支持。

基础工作

使用扩展并不是必须的,但是可以简化大量的工作。这里将通过 Flask-CKEditor 来实现相关功能,首先安装(或更新)它:

$ pip install -U Flask-CKEditor

关于创建编辑器输入框等基础内容,可以参考这篇文章

实现图片上传

在使用文本编辑器写文章时,上传图片是一个很常见的需求。在CKEditor中,图片上传可以通过File Browser插件实现。在服务器端的Flask程序中,你需要做三件事:

  1. 创建一个视图函数来处理并保存上传文件
  2. 创建一个视图函数来获取图片文件,类似Flask内置的 static 端点
  3. 将配置变量 `CKEDITOR_FILE_UPLOADER` 设为这个视图函数的 URL 或端点值

完整的代码示例如下所示:

from flask_ckeditor import upload_success, upload_fail

app.config['CKEDITOR_FILE_UPLOADER'] = 'upload'

@app.route('/files/<path:filename>')
def uploaded_files(filename):
    path = '/the/uploaded/directory'
    return send_from_directory(path, filename)

@app.route('/upload', methods=['POST'])
def upload():
    f = request.files.get('upload')  # 获取上传图片文件对象,键必须为'upload'
    # Add more validations here
    extension = f.filename.split('.')[1].lower()
    if extension not in ['jpg', 'gif', 'png', 'jpeg']:  # 验证文件类型示例
        return upload_fail(message='Image only!')  # 返回upload_fail调用
    f.save(os.path.join('/the/uploaded/directory', f.filename))
    url = url_for('uploaded_files', filename=f.filename)
    return upload_success(url=url) # 返回upload_success调用

在处理上传文件的视图函数中,你必须返回 `upload_success()` 调用,每将 `url` 参数设置为获取上传文件的 URL。通常情况下,除了保存文件,你还需要对上传的图片进行验证和处理(大小、格式、文件名处理等等,具体可以访问这篇《Flask文件上传(一):原生实现》了解),在验证未通过时,你需要返回 `upload_fail()` 调用,并使用 `message` 参数传入错误消息。

当设置了 `CKEDITOR_FILE_UPLOADER` 配置变量后,你可以在编辑区域点开图片按钮打开的弹窗中看到一个新的上传标签。另外,你也可以直接将图片文件拖拽到编辑区域进行上传,或复制文件并粘贴到文本区域进行上传(CKEditor >= 4.5)。

提示 对应的示例程序在examples/image-upload/目录下。

如果你使用的 CKEditor 版本小于 4.5,则使用下面的方式实现:

from flask import send_from_directory

app.config['CKEDITOR_FILE_UPLOADER'] = 'upload'  # this value can be endpoint or url

@app.route('/files/<filename>')
def uploaded_files(filename):
    path = '/the/uploaded/directory'
    return send_from_directory(path, filename)

@app.route('/upload', methods=['POST'])
@ckeditor.uploader
def upload():
    f = request.files.get('upload')
    f.save(os.path.join('/the/uploaded/directory', f.filename))
    url = url_for('uploaded_files', filename=f.filename)
    return url

CSRF 保护

如果你想为图片上传的请求添加 CSRF 保护,可以通过 CSRFProtect 实现(Flask-WTF 内置),首先安装 Flask-WTF:

$ pip install flask-wtf

同时要确保 Flask-CKEditor 使用了最新版本(0.4.3)

然后初始化扩展:

from flask_wtf import CSRFProtect

csrf = CSRFProtect(app) 

Flask-CKEditor 0.4.3 版本内置了对 CSRFProtect 的支持,当使用 CSRFProtect 时,只需要把配置变量 `CKEDITOR_ENABLE_CSRF` 设为 `True` 即可开启 CSRF 保护:

app.config['CKEDITOR_ENABLE_CSRF'] = True

顺便说一下 Flask-CKEditor 内部的实现方式, Flask-CKEditor 需要把 CSRF 令牌放到上传图片的 AJAX 请求首部,这通过 CKEditor 4.9.0 版本新添加的一个配置选项 fileTools_requestHeaders 实现,这个配置可以用来想文件上传请求插入自定义的首部字段 。所以,如果想要实现 CSRF 保护,CKEditor 的版本需要大于或等于 4.9.0。