表单处理 — Flask-WTF
本章你将学会用 Flask-WTF 规范化处理表单:定义表单类、自动 CSRF 保护、字段校验与错误提示。
为什么要用 Flask-WTF?
上一章的注册和登录表单是手写的:手动从 request.form.get() 取值、手动校验、手动显示错误。
Flask-WTF 基于 WTForms 为 Flask 封装,提供:
- 表单类定义,字段 + 校验规则集中管理
- 自动 CSRF 保护
- 模板中渲染字段和错误信息
form.validate_on_submit()统一处理 GET/POST 判断与校验
安装与配置
(venv) $ pip install flask-wtf
在 app.py 中配置 SECRET_KEY(CSRF Token 依赖此密钥):
实例
# 文件路径:app.py
app.config['SECRET_KEY'] = 'your-secret-key' # 生产环境用环境变量
app.config['WTF_CSRF_ENABLED'] = True # 默认开启,显式声明一下
app.config['SECRET_KEY'] = 'your-secret-key' # 生产环境用环境变量
app.config['WTF_CSRF_ENABLED'] = True # 默认开启,显式声明一下
定义表单类
实例
# 文件路径:app/forms.py(新建文件)
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, EmailField, BooleanField
from wtforms.validators import DataRequired, Length, Email, EqualTo, ValidationError
from app.models import User
class RegisterForm(FlaskForm):
"""注册表单"""
username = StringField('用户名', validators=[
DataRequired(message='用户名不能为空'),
Length(min=3, max=50, message='用户名长度 3-50 个字符')
])
email = EmailField('邮箱', validators=[
DataRequired(message='邮箱不能为空'),
Email(message='请输入有效的邮箱地址')
])
password = PasswordField('密码', validators=[
DataRequired(message='密码不能为空'),
Length(min=6, message='密码至少 6 位')
])
confirm_password = PasswordField('确认密码', validators=[
DataRequired(message='请再次输入密码'),
EqualTo('password', message='两次输入的密码不一致')
])
# 自定义验证器:检查用户名是否已被注册
def validate_username(self, field):
if User.query.filter_by(username=field.data).first():
raise ValidationError('该用户名已被使用。')
def validate_email(self, field):
if User.query.filter_by(email=field.data).first():
raise ValidationError('该邮箱已被注册。')
class LoginForm(FlaskForm):
"""登录表单"""
username = StringField('用户名', validators=[
DataRequired(message='用户名不能为空')
])
password = PasswordField('密码', validators=[
DataRequired(message='密码不能为空')
])
remember = BooleanField('记住我')
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, EmailField, BooleanField
from wtforms.validators import DataRequired, Length, Email, EqualTo, ValidationError
from app.models import User
class RegisterForm(FlaskForm):
"""注册表单"""
username = StringField('用户名', validators=[
DataRequired(message='用户名不能为空'),
Length(min=3, max=50, message='用户名长度 3-50 个字符')
])
email = EmailField('邮箱', validators=[
DataRequired(message='邮箱不能为空'),
Email(message='请输入有效的邮箱地址')
])
password = PasswordField('密码', validators=[
DataRequired(message='密码不能为空'),
Length(min=6, message='密码至少 6 位')
])
confirm_password = PasswordField('确认密码', validators=[
DataRequired(message='请再次输入密码'),
EqualTo('password', message='两次输入的密码不一致')
])
# 自定义验证器:检查用户名是否已被注册
def validate_username(self, field):
if User.query.filter_by(username=field.data).first():
raise ValidationError('该用户名已被使用。')
def validate_email(self, field):
if User.query.filter_by(email=field.data).first():
raise ValidationError('该邮箱已被注册。')
class LoginForm(FlaskForm):
"""登录表单"""
username = StringField('用户名', validators=[
DataRequired(message='用户名不能为空')
])
password = PasswordField('密码', validators=[
DataRequired(message='密码不能为空')
])
remember = BooleanField('记住我')
常用字段类型
| 字段类 | HTML 标签 | 说明 |
|---|---|---|
| StringField | <input type="text"> | 单行文本 |
| PasswordField | <input type="password"> | 密码输入框 |
| EmailField | <input type="email"> | 邮箱输入框 |
| TextAreaField | <textarea> | 多行文本 |
| BooleanField | <input type="checkbox"> | 复选框 |
| SelectField | <select> | 下拉选择 |
常用验证器
| 验证器 | 作用 |
|---|---|
| DataRequired() | 字段不能为空 |
| Length(min=, max=) | 字符长度限制 |
| Email() | 邮箱格式校验 |
| EqualTo('field_name') | 与另一个字段值一致 |
| Optional() | 允许字段为空(跳过后续验证器) |
在视图函数中使用表单
实例
# 文件路径:app/blueprints/auth.py(重构后的 register 和 login)
from flask import Blueprint, render_template, redirect, url_for, flash
from flask_login import login_user, logout_user, login_required, current_user
from app.forms import RegisterForm, LoginForm
from app.models import User, db
auth_bp = Blueprint('auth', __name__)
@auth_bp.route("/register", methods=['GET', 'POST'])
def register():
if current_user.is_authenticated:
return redirect(url_for('main.index'))
form = RegisterForm()
# form.validate_on_submit() 在 POST 请求时校验表单
# GET 请求或校验失败时返回 False
if form.validate_on_submit():
user = User(username=form.username.data, email=form.email.data)
user.set_password(form.password.data)
db.session.add(user)
db.session.commit()
login_user(user)
flash(f'注册成功,欢迎你,{user.username}!', 'success')
return redirect(url_for('main.index'))
# GET 请求或校验失败时,渲染带表单的模板
return render_template('register.html', form=form)
@auth_bp.route("/login", methods=['GET', 'POST'])
def login():
if current_user.is_authenticated:
return redirect(url_for('main.index'))
form = LoginForm()
if form.validate_on_submit():
user = User.query.filter_by(username=form.username.data).first()
if user is None or not user.check_password(form.password.data):
flash('用户名或密码错误。', 'error')
return render_template('login.html', form=form)
login_user(user, remember=form.remember.data)
flash(f'欢迎回来,{user.username}!', 'success')
next_page = request.args.get('next')
return redirect(next_page or url_for('main.index'))
return render_template('login.html', form=form)
from flask import Blueprint, render_template, redirect, url_for, flash
from flask_login import login_user, logout_user, login_required, current_user
from app.forms import RegisterForm, LoginForm
from app.models import User, db
auth_bp = Blueprint('auth', __name__)
@auth_bp.route("/register", methods=['GET', 'POST'])
def register():
if current_user.is_authenticated:
return redirect(url_for('main.index'))
form = RegisterForm()
# form.validate_on_submit() 在 POST 请求时校验表单
# GET 请求或校验失败时返回 False
if form.validate_on_submit():
user = User(username=form.username.data, email=form.email.data)
user.set_password(form.password.data)
db.session.add(user)
db.session.commit()
login_user(user)
flash(f'注册成功,欢迎你,{user.username}!', 'success')
return redirect(url_for('main.index'))
# GET 请求或校验失败时,渲染带表单的模板
return render_template('register.html', form=form)
@auth_bp.route("/login", methods=['GET', 'POST'])
def login():
if current_user.is_authenticated:
return redirect(url_for('main.index'))
form = LoginForm()
if form.validate_on_submit():
user = User.query.filter_by(username=form.username.data).first()
if user is None or not user.check_password(form.password.data):
flash('用户名或密码错误。', 'error')
return render_template('login.html', form=form)
login_user(user, remember=form.remember.data)
flash(f'欢迎回来,{user.username}!', 'success')
next_page = request.args.get('next')
return redirect(next_page or url_for('main.index'))
return render_template('login.html', form=form)
模板中渲染表单
实例
<!-- 文件路径:app/templates/register.html -->
{% extends 'base.html' %}
{% block title %}注册 - RUNOOB 博客{% endblock %}
{% block content %}
<div class="auth-form">
<h2>注册</h2>
<form method="post">
<!-- form.hidden_tag() 自动生成 CSRF Token 隐藏字段 -->
{{ form.hidden_tag() }}
<div class="form-group">
{{ form.username.label }}
{{ form.username(class="form-input") }}
{% if form.username.errors %}
{% for error in form.username.errors %}
<span class="field-error">{{ error }}</span>
{% endfor %}
{% endif %}
</div>
<div class="form-group">
{{ form.email.label }}
{{ form.email(class="form-input") }}
{% for error in form.email.errors %}
<span class="field-error">{{ error }}</span>
{% endfor %}
</div>
<div class="form-group">
{{ form.password.label }}
{{ form.password(class="form-input") }}
{% for error in form.password.errors %}
<span class="field-error">{{ error }}</span>
{% endfor %}
</div>
<div class="form-group">
{{ form.confirm_password.label }}
{{ form.confirm_password(class="form-input") }}
{% for error in form.confirm_password.errors %}
<span class="field-error">{{ error }}</span>
{% endfor %}
</div>
{{ form.submit(class="btn-submit") }}
</form>
<p class="form-footer">
已有账号?<a href="{{ url_for('auth.login') }}">立即登录</a>
</p>
</div>
{% endblock %}
{% extends 'base.html' %}
{% block title %}注册 - RUNOOB 博客{% endblock %}
{% block content %}
<div class="auth-form">
<h2>注册</h2>
<form method="post">
<!-- form.hidden_tag() 自动生成 CSRF Token 隐藏字段 -->
{{ form.hidden_tag() }}
<div class="form-group">
{{ form.username.label }}
{{ form.username(class="form-input") }}
{% if form.username.errors %}
{% for error in form.username.errors %}
<span class="field-error">{{ error }}</span>
{% endfor %}
{% endif %}
</div>
<div class="form-group">
{{ form.email.label }}
{{ form.email(class="form-input") }}
{% for error in form.email.errors %}
<span class="field-error">{{ error }}</span>
{% endfor %}
</div>
<div class="form-group">
{{ form.password.label }}
{{ form.password(class="form-input") }}
{% for error in form.password.errors %}
<span class="field-error">{{ error }}</span>
{% endfor %}
</div>
<div class="form-group">
{{ form.confirm_password.label }}
{{ form.confirm_password(class="form-input") }}
{% for error in form.confirm_password.errors %}
<span class="field-error">{{ error }}</span>
{% endfor %}
</div>
{{ form.submit(class="btn-submit") }}
</form>
<p class="form-footer">
已有账号?<a href="{{ url_for('auth.login') }}">立即登录</a>
</p>
</div>
{% endblock %}
Flask-WTF 的 CSRF 保护是自动的:
form.hidden_tag()会生成一个隐藏的<input>包含随机 Token,服务器在接受 POST 请求前验证此 Token。任何外站提交的表单都因为没有正确的 Token 而被拒绝。
手动 request.form vs Flask-WTF 对比
| 方面 | 手动 request.form | Flask-WTF |
|---|---|---|
| 代码行数 | 每个字段重复取/验/报三步 | 表单类定义一次 |
| CSRF 保护 | 需手动实现 | 自动 |
| 自定义验证 | 手动 if/else | validate_xxx 方法 |
| 错误展示 | 手动拼接 | form.field.errors 自动收集 |
| 适用场景 | 简单表单(如搜索框) | 复杂表单(注册、登录、资料编辑) |
本章小结
本章你用 Flask-WTF 重构了认证表单:FlaskForm 定义字段和验证器、validate_xxx 自定义验证、form.validate_on_submit() 统一处理 POST 校验、form.hidden_tag() 自动 CSRF 保护、模板中渲染错误信息。
表单处理现在更规范、更安全。
