Расширение Flask-WTF представляет собой простую в использовании интеграцию фреймворка Flask и модуля WTForms, включая CSRF-токены для защиты форм, загрузку файлов и поддержку reCAPTCHA.
Основные функции расширение Flask-WTF:
Установка или обновления расширения Flask-WTF:
$ python3 -m pip install -U Flask-WTF
Сразу начнем с примера, который дает хорошее представление о расширении Flask-WTF.
Создание формы:
from flask_wtf import FlaskForm # типы поля HTML-формы и их валидаторы # импортируются из модуля WTForms from wtforms import StringField from wtforms.validators import DataRequired class MyForm(FlaskForm): name = StringField('name', validators=[DataRequired()])
Из модуля WTForms необходимо импортировать только типы полей и их валидаторы, кроме поля для загрузки файла. Обратите внимание, что расширение Flask-WTF, автоматически создает скрытое поле маркера CSRF-токена. Его можно отобразить в своем шаблоне:
<form method="POST" action="/"> {# отображение скрытого поля CSRF-токена #} {{ form.csrf_token }} {{ form.name.label }} {{ form.name(size=20) }} <input type="submit" value="Go"> </form>
Если в созданной форме есть несколько скрытых полей, то их можно отобразить в одном блоке с помощью метода form.hidden_tag()
.
<form method="POST" action="/"> {# отображение всех скрытых полей формы #} {{ form.hidden_tag() }} {{ form.name.label }} {{ form.name(size=20) }} <input type="submit" value="Go"> </form>
Проверка форм в обработчиках представлений:
@app.route('/submit', methods=['GET', 'POST']) def submit(): form = MyForm() if form.validate_on_submit(): return redirect('/success') return render_template('submit.html', form=form)
Обратите внимание, что не нужно передавать объект запроса request.form
в расширение Flask-WTF, он загрузится автоматически. А удобный метод form.validate_on_submitvalidate_on_submit()
проверит, действительно ли это POST-запрос.
Если в приложении Flask формы проходят проверку на качество заполнения полей, то скорее всего, нужно добавить в шаблоны возможность отображения сообщений об ошибках, возникших при заполнении полей формы. Используя поле form.name
из приведенного выше примера, это можно сделать следующим образом:
{% if form.name.errors %} <ul class="errors"> {% for error in form.name %} <li>{{ error }}</li> {% endfor %} </ul> {% endif %}
Все легко и просто...
Без определения какой-либо конфигурации, HTML-формы, созданные при помощи класса FlaskForm()
расширения Flask-WTF будут безопасными, с защитой CSRF. Не меняйте это поведении.
Но если необходимо отключить защиту CSRF, то можно сделать так:
form = FlaskForm(meta={'csrf': False})
CSRF защиту можно отключить глобально (с помощью конфигурации), хотя этого делать не следует:
WTF_CSRF_ENABLED = False
Чтобы сгенерировать CSRF-токен для HTML-формы, необходим секретный ключ. Обычно он совпадает с секретным ключом приложения Flask. Если есть необходимость использовать другой секретный ключ, то его необходимо определить на этапе конфигурирования расширения Flask-WTF:
WTF_CSRF_SECRET_KEY = 'a random string'
Более подробно о CSRF защите смотрите ниже в подразделе "Защита HTML-форм CSRF-токеном"
Класс поля FileField
HTML-формы, предоставляемый Flask-WTF, отличается от поля, который имеет модуль WTForms. Класс FileField
сразу проверит, что файл не является пустым экземпляром FileStorage
, в противном случае данные будут None
.
from flask_wtf import FlaskForm # поле для загрузки файла и валидатор # импортируются из расширения Flask-WTF from flask_wtf.file import FileField, FileRequired from werkzeug.utils import secure_filename class PhotoForm(FlaskForm): photo = FileField(validators=[FileRequired()]) @app.route('/upload', methods=['GET', 'POST']) def upload(): form = PhotoForm() if form.validate_on_submit(): f = form.photo.data filename = secure_filename(f.filename) f.save(os.path.join( app.instance_path, 'photos', filename )) return redirect(url_for('index')) return render_template('upload.html', form=form)
Не забываем установить тип данных HTML-формы enctype="multipart/form-data"
, в противном случае атрибут объекта запроса request.files
будет пустым.
<form method="POST" enctype="multipart/form-data"> ... </form>
Расширение Flask-WTF автоматически обрабатывает передачу данных файла в атрибут объекта запроса request.form
. Если данные передаются явно, то нужно вручную объединить request.form
с request.files
, чтобы атрибут form
мог видеть данные файла.
form = PhotoForm() # эквивалентно from flask import request from werkzeug.datastructures import CombinedMultiDict form = PhotoForm(CombinedMultiDict((request.files, request.form)))
Расширение Flask-WTF поддерживает проверку загрузки файлов с помощью классов FileRequired
и FileAllowed
. Их можно использовать как с классами Flask-WTF, так и с классом FileField
модуля WTForms.
Класс FileAllowed
хорошо работает с расширением Flask-Uploads
.
from flask_uploads import UploadSet, IMAGES from flask_wtf import FlaskForm from flask_wtf.file import FileField, FileAllowed, FileRequired images = UploadSet('images', IMAGES) class UploadForm(FlaskForm): upload = FileField('image', validators=[ FileRequired(), FileAllowed(images, 'Images only!') ])
Класс FileAllowed
также можно использовать без расширения Flask-Uploads, передав разрешенные к загрузке расширения файлов напрямую.
class UploadForm(FlaskForm): upload = FileField('image', validators=[ FileRequired(), FileAllowed(['jpg', 'png'], 'Images only!') ])
Расширение Flask-WTF также обеспечивает поддержку Recaptcha через класс поля HTML-формы RecaptchaField()
:
from flask_wtf import FlaskForm, RecaptchaField from wtforms import TextField class SignupForm(FlaskForm): username = TextField('Username') recaptcha = RecaptchaField()
Поддержка и поведение Recaptcha связано с рядом переменных конфигурации, некоторые из которых необходимо настроить.
RECAPTCHA_PUBLIC_KEY
: требуется открытый ключ.RECAPTCHA_PRIVATE_KEY
: требуется закрытый ключ.RECAPTCHA_API_SERVER
: необязательный параметр своего сервера API Recaptcha.RECAPTCHA_PARAMETERS
: необязательный параметр, словарь параметров JavaScript (api.js).RECAPTCHA_DATA_ATTRS
: необязательный параметр, словарь параметров атрибутов данных.Пример RECAPTCHA_PARAMETERS
и RECAPTCHA_DATA_ATTRS
:
RECAPTCHA_PARAMETERS = {'hl': 'zh', 'render': 'explicit'} RECAPTCHA_DATA_ATTRS = {'theme': 'dark'}
Для удобства при тестировании приложения, если app.testing
имеет значение True
, то поле recaptcha
всегда будет действительным.
И это можно легко настроить в шаблонах приложения Flask:
<form action="/" method="post"> {{ form.username }} {{ form.recaptcha }} </form>
Это микро приложения Flask имеет всего 2 файла: flask-recaptcha.py
и файл шаблона index.html
, который необходимо поместить в папку templates
, расположенную на уровне файла приложения flask-recaptcha.py
. Так же, должны быть установлены модули: flask
, wtforms
и flask_wtf
.
Файл приложения flask-recaptcha.py
:
# файл flask-recaptcha.py from flask import Flask, render_template, from flask import session, url_for, flash, redirect from wtforms import TextAreaField from wtforms.validators import DataRequired from flask_wtf import FlaskForm from flask_wtf.recaptcha import RecaptchaField DEBUG = True SECRET_KEY = "secret" # ключи для локального хоста. Измените по мере необходимости. RECAPTCHA_PUBLIC_KEY = "6LeYIbsSAAAAACRPIllxA7wvXjIE411PfdB2gt2J" RECAPTCHA_PRIVATE_KEY = "6LeYIbsSAAAAAJezaIq3Ft_hSTo0YtyeFG-JgRtu" app = Flask(__name__) app.config.from_object(__name__) class CommentForm(FlaskForm): comment = TextAreaField("Comment", validators=[DataRequired()]) recaptcha = RecaptchaField() @app.route("/") def index(form=None): if form is None: form = CommentForm() comments = session.get("comments", []) return render_template("index.html", comments=comments, form=form) @app.route("/add/", methods=("POST",)) def add_comment(): form = CommentForm() if form.validate_on_submit(): comments = session.pop("comments", []) comments.append(form.comment.data) session["comments"] = comments flash("You have added a new comment") return redirect(url_for("index")) return index(form) if __name__ == "__main__": app.run()
Файл шаблона index.html
, который необходимо поместить в папку templates
:
{% Файл шаблона `index.html` %} <html> <body> {% for comment in comments %} <p>{{ comment }}</p> {% endfor %} <form method="POST" action="{{ url_for('add_comment') }}"> {{ form.csrf_token }} <p> {{ form.comment.label }}<br> {{ form.comment(rows=5, cols=40) }} </p> <p> {% for error in form.recaptcha.errors %} {{ error }} {% endfor %} {{ form.recaptcha }} </p> <p> <input type="submit" value="Add comment"> </p> </form> </body> </html>
Любое представление, использующее FlaskForm
для обработки запроса, уже получает защиту CSRF автоматически. Если есть функции-представления, которые не используют HTML-формы, созданные на основе класса FlaskForm
или отправляют запросы AJAX, то для защиты нужно использовать расширение CSRF, входящее в состав .
Чтобы включить глобальную защиту CSRF для приложения Flask, зарегистрируйте расширение CSRFProtect
.
from flask_wtf.csrf import CSRFProtect csrf = CSRFProtect(app) def create_app(): app = Flask(__name__) csrf.init_app(app)
Примечание. Защита CSRF требует секретного ключа для безопасной подписи токена. По умолчанию для этого будет использоваться секретный ключ приложения Flask SECRET_KEY
. Если нужно использовать отдельный токен, то можно установить параметр конфигурации WTF_CSRF_SECRET_KEY
.
При использовании класса FlaskForm
визуализируйте поле CSRF формы как обычно.
<form method="post"> {{ form.csrf_token }} </form>
Если в шаблоне не используются поля, созданные при помощи класса FlaskForm
, то необходимо вручную отобразить скрытое поле формы с токеном.
<form method="post"> <input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/> </form>
При отправке AJAX запроса нужно добавить в него заголовок X-CSRFToken
. Например, в jQuery, можно настроить все запросы на отправку токена следующим образом.
<script type="text/javascript"> var csrf_token = "{{ csrf_token() }}"; $.ajaxSetup({ beforeSend: function(xhr, settings) { if (!/^(GET|HEAD|OPTIONS|TRACE)$/i.test(settings.type) && !this.crossDomain) { xhr.setRequestHeader("X-CSRFToken", csrf_token); } } }); </script>
Если проверка CSRF не удалась, то возникнет ошибка CSRFError
. По умолчанию возвращается ответ с причиной сбоя и кодом 400. Можно настроить ответ при ошибке с помощью декоратора экземпляра приложения Flask @app.errorhandler()
.
from flask_wtf.csrf import CSRFError @app.errorhandler(CSRFError) def handle_csrf_error(e): return render_template('csrf_error.html', reason=e.description), 400
Настоятельно рекомендуется защитить все представления с помощью CSRF. Но при необходимости можно исключить некоторые функции-представления с помощью декоратора @csrf.exempt
.
@app.route('/foo', methods=('GET', 'POST')) @csrf.exempt def my_handler(): # ... return 'ok'
Можно исключить сразу все представления схемы blueprint
.
csrf.exempt(account_blueprint)
Также можно отключить CSRF защиту во всех представлениях по умолчанию, установив значение параметра конфигурации WTF_CSRF_CHECK_DEFAULT=False
и выборочно вызывать метод csrf.protect()
только при необходимости. Это также позволяет выполнить некоторую предварительную обработку запросов перед проверкой наличия маркера CSRF.
@app.before_request def check_csrf(): if not is_oauth(request): csrf.protect()
WTF_CSRF_ENABLED
: если False
, то отключает всю защиту CSRF. По умолчанию True
.WTF_CSRF_CHECK_DEFAULT
: при использовании защиты CSRF этоn параметр контролирует, защищено ли каждое представление. По умолчанию True
.WTF_CSRF_SECRET_KEY
: данные для генерации токенов безопасности. Если он не установлен, используется SECRET_KEY
.WTF_CSRF_METHODS
: словарь HTTP-методов для защиты CSRF. По умолчанию: {'POST', 'PUT', 'PATCH', 'DELETE'}
.WTF_CSRF_FIELD_NAME
: имя поля HTML-формы и сеансовый ключ, который содержит токен CSRF. По умолчанию csrf_token
.WTF_CSRF_HEADERS
: список возможных HTTP-заголовков для поиска токена CSRF, если он не указан в форме. По умолчанию ['X-CSRFToken', 'X-CSRF-Token']
.WTF_CSRF_TIME_LIMIT
: максимальный возраст в секундах для токенов CSRF. По умолчанию - 3600 секунд. Если установлено значение None
, то токен CSRF действителен в течение всего сеанса.WTF_CSRF_SSL_STRICT
: следует ли применять политику, проверяя, что реферер соответствует хосту. Применяется только к запросам HTTPS
. По умолчанию True
.WTF_I18N_ENABLED
: если False
, то отключает поддержку расширения Flask-Babel I18N. Также нужно установить значение False
, если необходимо напрямую использовать встроенные сообщения модуля WTForms. По умолчанию True
.