下载工作台
Flask Web 开发

WTForms 表单与 CSRF

试读上半部分 · 解锁后可读全文

第 4 章 · WTForms 表单与 CSRF

本章目标:使用 Flask-WTF 定义 ProductForm 并完成商品新建/编辑页面;掌握字段校验ValidationError 与自定义 validate_* 钩子;在模板中正确渲染字段并展示错误信息;理解 CSRFProtect 与隐藏字段 csrf_token 的原理;完成 GET 展示 → POST 校验 → 重定向的完整流程。

学时建议:4~5 小时(含 2 小时跟练)

前置:完成 flask-web ch03(模板继承、flash、admin 蓝图列表页)。


4.1 场景说明:运营录入商品

shop-demo 用 Django ModelForm 绑定 Product 模型;api-demo 在 ch05 接入数据库前,先用 WTForms 定义表单类,字段与未来将建的 Product 模型对齐。

场景字段校验规则
新建商品name, slug, price, stock, category_id必填、价格 > 0
编辑商品同上slug 唯一(ch05 查库)
安全所有 POSTCSRF Token

对外 API https://api.example.com 使用 Token 鉴权(ch07);管理页使用 Session + CSRF 防御跨站伪造。


4.2 安装 Flask-WTF

pip install "flask-wtf>=1.2,<2"
pip freeze >> requirements.txt
作用
flask-wtfFlask 集成 WTForms
WTForms字段定义与校验(依赖)

api_demo/extensions.py 更新:

from flask_wtf.csrf import CSRFProtect

csrf = CSRFProtect()

api_demo/__init__.py 注册:

from api_demo.extensions import csrf

def create_app(config_name=None):
    # ...
    csrf.init_app(app)
    # ...

api_demo/config.py 确认:

class Config:
    SECRET_KEY = os.environ.get("SECRET_KEY", "dev-only-change-in-prod")
    WTF_CSRF_ENABLED = True
    WTF_CSRF_TIME_LIMIT = None

4.3 定义 ProductForm

api_demo/forms/product.py

from flask_wtf import FlaskForm
from wtforms import DecimalField, IntegerField, SelectField, StringField
from wtforms.validators import DataRequired, NumberRange, Length, ValidationError


class ProductForm(FlaskForm):
    name = StringField(
        "商品名称",
        validators=[DataRequired(message="请输入名称"), Length(max=120)],
    )
    slug = StringField(
        "URL 标识 (slug)",
        validators=[DataRequired(), Length(max=64)],
    )
    price = DecimalField(
        "售价",
        places=2,
        validators=[DataRequired(), NumberRange(min=0.01, message="价格须大于 0")],
    )
    stock = IntegerField(
        "库存",
        validators=[DataRequired(), NumberRange(min=0, message="库存不能为负")],
    )
    category_id = SelectField(
        "分类",
        coerce=int,
        choices=[],
        validators=[DataRequired()],
    )

    def validate_slug(self, field):
        blocked = {"duplicate-slug"}
        if field.data in blocked:
            raise ValidationError("该 slug 已被占用")
字段类型对应 HTML典型校验器
StringField<input type="text">DataRequired, Length
DecimalFieldtext + 解析NumberRange
IntegerFieldnumberNumberRange
SelectField<select>DataRequired

FlaskForm 子类自动带 CSRF 字段,模板中需渲染 form.hidden_tag()


4.4 视图:创建与编辑

api_demo/routes/admin.py(表单相关片段):

from flask import Blueprint, flash, redirect, render_template, url_for

from api_demo.forms.product import ProductForm

bp = Blueprint("admin", __name__, url_prefix="/admin")

_PRODUCTS = {}
_NEXT_ID = 1
CATEGORY_CHOICES = [(1, "数码"), (2, "家居"), (3, "食品")]


def _fill_category_choices(form):
    form.category_id.choices = CATEGORY_CHOICES


@bp.route("/products/create", methods=["GET", "POST"])
def product_create():
    form = ProductForm()
    _fill_category_choices(form)

    if form.validate_on_submit():
        global _NEXT_ID
        pid = _NEXT_ID
        _NEXT_ID += 1
        _PRODUCTS[pid] = {
            "id": pid,
            "name": form.name.data,
            "slug": form.slug.data,
            "price": float(form.price.data),
            "stock": form.stock.data,
            "category_id": form.category_id.data,
        }
        flash(f"商品「{form.name.data}」创建成功", "success")
        return redirect(url_for("admin.product_list"))

    return render_template("admin/product_form.html", form=form, title="新建商品")


@bp.route("/products/<int:product_id>/edit", methods=["GET", "POST"])
def product_edit(product_id):
    product = _PRODUCTS.get(product_id)
    if not product:
        flash("商品不存在", "error")
        return redirect(url_for("admin.product_list"))

    form = ProductForm(data=product)
    _fill_category_choices(form)

    if form.validate_on_submit():
        product.update({
            "name": form.name.data,
            "slug": form.slug.data,
            "price": float(form.price.data),
            "stock": form.stock.data,
            "category_id": form.category_id.data,
        })
        flash("保存成功", "success")
        return redirect(url_for("admin.product_list"))

    return render_template("admin/product_form.html", form=form, title="编辑商品")
方法行为
GET展示空表单或预填数据
POSTvalidate_on_submit() = 校验通过且为 POST
成功flash + redirect(PRG 模式)
失败重新渲染模板,字段旁显示错误

以下内容需解锁后阅读

试读已结束。解锁本章 ¥5.00,或开通年度会员畅读全部教程。
年度会员 ¥199.00/年; 小紫 AI 工作台有效会员 ¥99.00/年

正文仅在服务端鉴权后下发,未付费无法获取下半部分内容。