#Flask-WTF 插件:表单类定义与 CSRF 防护
📂 所属阶段:第二阶段 — 交互与数据(核心篇)
🔗 相关章节:数据验证 · Jinja2 模板引擎(下)
#1. 为什么需要 Flask-WTF?
#1.1 原始表单的问题
# ❌ 原始表单:手动处理,数据脏乱
@app.route("/register", methods=["GET", "POST"])
def register():
if request.method == "POST":
name = request.form.get("name")
email = request.form.get("email")
# 没有验证、没有类型转换、容易出错#1.2 Flask-WTF 的优势
✅ Flask-WTF 解决方案:
1. 类定义表单:结构清晰、可复用
2. 自动 CSRF 防护:防止跨站请求伪造
3. 字段验证器:内置 + 自定义验证规则
4. 表单渲染:Jinja2 自动集成
5. 错误处理:每个字段独立的错误信息#2. 安装与配置
#2.1 安装
pip install flask-wtf email-validator#2.2 配置
# app/__init__.py
from flask_wtf.csrf import CSRFProtect
csrf = CSRFProtect()
def create_app():
app = Flask(__name__)
app.config["SECRET_KEY"] = "your-secret-key" # 必须设置!用于 CSRF 签名
csrf.init_app(app)#3. 定义表单
#3.1 用户注册表单
# app/forms/auth.py
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, BooleanField, SubmitField
from wtforms.validators import DataRequired, Email, Length, EqualTo, ValidationError
from app.models import User
class RegisterForm(FlaskForm):
name = StringField("用户名", validators=[
DataRequired(message="用户名不能为空"),
Length(min=2, max=20, message="用户名长度需在 2-20 个字符之间"),
])
email = StringField("邮箱", validators=[
DataRequired(message="邮箱不能为空"),
Email(message="请输入有效的邮箱地址"),
])
password = PasswordField("密码", validators=[
DataRequired(message="密码不能为空"),
Length(min=8, message="密码至少 8 个字符"),
])
password_confirm = PasswordField("确认密码", validators=[
DataRequired(message="请再次输入密码"),
EqualTo("password", message="两次输入的密码不一致"),
])
agree_terms = BooleanField("我已阅读并同意《用户协议》", validators=[
DataRequired(message="请勾选用户协议"),
])
submit = SubmitField("注册")
# 自定义验证:检查邮箱是否已被注册
def validate_email(self, field):
if User.query.filter_by(email=field.data).first():
raise ValidationError("该邮箱已被注册")#3.2 登录表单
class LoginForm(FlaskForm):
email = StringField("邮箱", validators=[
DataRequired(message="邮箱不能为空"),
Email(message="请输入有效的邮箱地址"),
])
password = PasswordField("密码", validators=[
DataRequired(message="密码不能为空"),
])
remember_me = BooleanField("记住我")
submit = SubmitField("登录")#3.3 文章表单
class ArticleForm(FlaskForm):
title = StringField("标题", validators=[
DataRequired(message="标题不能为空"),
Length(max=200, message="标题最多 200 个字符"),
])
summary = StringField("摘要", validators=[
Length(max=300, message="摘要最多 300 个字符"),
])
content = TextAreaField("正文", validators=[
DataRequired(message="正文不能为空"),
Length(min=10, message="正文至少 10 个字符"),
])
category = SelectField("分类", coerce=int, validators=[
DataRequired(message="请选择分类"),
])
tags = StringField("标签", description="用逗号分隔多个标签")
is_published = BooleanField("立即发布")
submit = SubmitField("保存草稿")
# 动态设置 category 选项
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
from app.models import Category
self.category.choices = [(c.id, c.name) for c in Category.query.all()]#4. 在路由中使用表单
#4.1 基础用法
# app/routes/auth.py
from flask import render_template, redirect, url_for, flash
from flask_login import login_user, logout_user
from app.forms.auth import LoginForm, RegisterForm
from app.models import User
from app.extensions import db
@auth_bp.route("/register", methods=["GET", "POST"])
def register():
form = RegisterForm()
if form.validate_on_submit():
# 表单数据全部验证通过
user = User(
name=form.name.data,
email=form.email.data,
password_hash=generate_password_hash(form.password.data), # 后续章节
)
db.session.add(user)
db.session.commit()
flash("注册成功!请登录。", "success")
return redirect(url_for("auth.login"))
return render_template("auth/register.html", form=form)
@auth_bp.route("/login", methods=["GET", "POST"])
def login():
form = LoginForm()
if form.validate_on_submit():
user = User.query.filter_by(email=form.email.data).first()
if user and check_password_hash(user.password_hash, form.password.data):
login_user(user, remember=form.remember_me.data)
flash("登录成功!", "success")
next_page = request.args.get("next")
return redirect(next_page or url_for("main.index"))
else:
flash("邮箱或密码错误", "danger")
return render_template("auth/login.html", form=form)#4.2 validate_on_submit vs validate
# validate_on_submit() = (request.method == "POST") and form.validate()
# 推荐用法:GET 请求时返回 False,不做验证
# validate() 单独使用:每次请求都验证(调试用)#5. 在模板中渲染表单
#5.1 自动渲染
<!-- templates/auth/register.html -->
{% extends "base.html" %}
{% block content %}
<div class="auth-form">
<h2>注册</h2>
<!-- 显示表单的非字段错误 -->
{% if form.errors %}
<div class="alert alert-danger">
{% for errors in form.errors.values() %}
{% for error in errors %}
<p>{{ error }}</p>
{% endfor %}
{% endfor %}
</div>
{% endif %}
<form method="POST" action="{{ url_for('auth.register') }}">
<!-- CSRF Token(必须!) -->
{{ form.hidden_tag() }}
<!-- 字段渲染 -->
<div class="form-group">
{{ form.name.label }}
{{ form.name(class="form-control") }}
{% if form.name.errors %}
<small class="text-danger">{{ form.name.errors[0] }}</small>
{% endif %}
</div>
<div class="form-group">
{{ form.email.label }}
{{ form.email(class="form-control", placeholder="your@email.com") }}
{% if form.email.errors %}
<small class="text-danger">{{ form.email.errors[0] }}</small>
{% endif %}
</div>
<div class="form-group">
{{ form.password.label }}
{{ form.password(class="form-control") }}
{% if form.password.errors %}
<small class="text-danger">{{ form.password.errors[0] }}</small>
{% endif %}
</div>
<div class="form-group">
{{ form.password_confirm.label }}
{{ form.password_confirm(class="form-control") }}
{% if form.password_confirm.errors %}
<small class="text-danger">{{ form.password_confirm.errors[0] }}</small>
{% endif %}
</div>
<div class="form-group">
{{ form.agree_terms() }}
{{ form.agree_terms.label }}
</div>
{{ form.submit(class="btn btn-primary btn-block") }}
</form>
</div>
{% endblock %}#5.2 使用宏自动渲染
<!-- templates/macros/form_macros.html -->
{% macro render_field(field) %}
<div class="form-group{% if field.errors %} has-error{% endif %}">
{{ field.label }}
{% if field.type == "BooleanField" %}
{{ field() }} {{ field.label }}
{% else %}
{{ field(class="form-control", **kwargs)|safe }}
{% endif %}
{% if field.description %}
<small class="form-text text-muted">{{ field.description }}</small>
{% endif %}
{% if field.errors %}
<small class="text-danger">{{ field.errors[0] }}</small>
{% endif %}
</div>
{% endmacro %}
<!-- 使用宏 -->
{% from "macros/form_macros.html" import render_field %}
<form method="POST">
{{ form.hidden_tag() }}
{{ render_field(form.name) }}
{{ render_field(form.email) }}
{{ render_field(form.password) }}
{{ form.submit(class="btn btn-primary") }}
</form>#6. CSRF 防护详解
#6.1 CSRF 攻击原理
攻击者网站:
<form action="https://your-blog.com/delete/42" method="POST">
<input name="_method" value="DELETE">
</form>
<script>document.forms[0].submit();</script>
用户已登录 → 访问攻击者网站 → 自动提交删除表单 → 文章 42 被删除!#6.2 Flask-WTF 如何防护
CSRF Token 流程:
1. 服务器生成随机 Token,存入 Session
2. 渲染表单时通过 {{ form.hidden_tag() }} 注入 Token
3. 用户提交表单时,Token 必须匹配,否则 400 错误
4. 攻击者网站无法获取目标站点的 Cookie,无法伪造 Token#6.3 全局禁用 CSRF(API 场景)
# 仅对特定端点禁用
@csrf.exempt
@app.route("/api/webhook", methods=["POST"])
def webhook():
# Webhook 来自第三方,无法提供 CSRF token
pass#7. 小结
Flask-WTF 使用流程:
1. 安装:pip install flask-wtf email-validator
2. 配置:app.config["SECRET_KEY"] = "..."
3. 定义表单:class XxxForm(FlaskForm)
4. 路由使用:form = XxxForm(); if form.validate_on_submit(): ...
5. 模板渲染:{{ form.hidden_tag() }} + {{ form.field() }}💡 最佳实践:所有涉及数据修改的表单(POST/PUT/DELETE)必须使用 Flask-WTF 并开启 CSRF 防护。只读页面可以用普通表单。
🔗 扩展阅读

