Сообщить об ошибке.

Расширение Flask-WTF для приложения Flask в Python

Простая интеграция Flask и WTForms, включая CSRF

Расширение Flask-WTF представляет собой простую в использовании интеграцию фреймворка Flask и модуля WTForms, включая CSRF-токены для защиты форм, загрузку файлов и поддержку reCAPTCHA.

Основные функции расширение Flask-WTF:

  • Интеграция с модулем WTForms.
  • Безопасная форма с токеном CSRF.
  • Глобальная защита CSRF.
  • Поддержка reCAPTCHA.
  • Загрузка файлов, которая работает с Flask-Uploads.
  • Интернационализация с использованием Flask-Babel.

Установка или обновления расширения Flask-WTF:

$ python3 -m pip install -U Flask-WTF

Содержание:


Пример использования расширения 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-форм при помощи расширения Flask-WTF.

Защищенные HTML-формы.

Без определения какой-либо конфигурации, 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-токеном"

Загрузка файлов при помощи расширения Flask-WTF.

Класс поля 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!')
    ])

Поддержка Recaptcha.

Расширение 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 с использованием Recaptcha.

Это микро приложения 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>

Защита HTML-форм CSRF-токеном.

Любое представление, использующее 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.

Использование CSRF-токена в HTML-форме шаблона.

При использовании класса FlaskForm визуализируйте поле CSRF формы как обычно.

<form method="post">
    {{ form.csrf_token }}
</form>

Если в шаблоне не используются поля, созданные при помощи класса FlaskForm, то необходимо вручную отобразить скрытое поле формы с токеном.

<form method="post">
    <input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
</form>

Защита CSRF-токеном запросов AJAX.

При отправке 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-токена.

Если проверка 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. Но при необходимости можно исключить некоторые функции-представления с помощью декоратора @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()

Список параметров конфигурации расширения Flask-WTF.

  • 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.