这个系列是学习《Flask Web开发:基于Python的Web应用开发实战》的部分笔记
网站需要能提供一个表格,让用户提供信息进行注册、写点东西
处理POST请求中提交的表单数据
- 用 flask 的请求对象,
request.form
,但功能很初级,需要做很多重复、额外的操作 - 一个名为 Flask-WTF 的扩展,将 WTForms 集成到 flask 程序,可以帮助完成很多事情
CSRF(Cross-Site Request Forgery,跨站请求伪造)攻击
恶意网站在受害者不知情的情况下,伪造请求,以受害者名义(利用用户浏览器中的 cookie )发送给受害者已登录的受攻击站点,在自身没有授权的情况下执行用户的某些权限的操作。
IBM CSRF、
wiki CSRF、
wiki cookie
为了进行防御,需要在响应的表单(不在 cookie 中)中添加一个攻击者无法伪造的信息,即随机产生的token,然后在提交表单的请求中送回,进行比对验证
在程序的配置中设置一个密钥,启用 CSRF 保护
CSPR_ENABLED = True # 启用 CSPR (跨站请求伪造) 保护,在表单中使用,隐藏属性
SECRET_KEY = 'this-is-safe-and-you-never-guess-it' # 建立一个用于加密的密钥,验证表单
使用:
如果是使用 Flask-WTF 自动化表单的一些操作,会自动用密钥生成一个随机的加密令牌,放入响应中,并要求在用户提交的请求中送回,通过对比是否一致,判断表单的真伪
如果是手动写,添加
{{ form.hidden_tag() }}
表单类
表单的创建,可以通过继承从 Flask-WTF 导入的Form
父类实现
from flask.ext.wtf import Form # 表单类,从第三方扩展的命名空间 导入
表单类中需要定义 属性/字段,值是字段类型类
,就是将要在 HTML 中显示的表单各个字段,其实就是对 HTML 表单各种标签的包装
from wtforms import StringField, BooleanField, SubmitField, PasswordField, TextAreaField, SelectField # 字段类型类,字符串、布尔值、提交、密码、文本区域、选择框
字段类型类(说明文本,验证器列表)
验证器列表
,检查用户填写表单时输入的内容是否符合我们的期望,有多个验证器时,需要同时通过验证
from wtforms.validators import DataRequired, Required, Length, Email , Regexp, EqualTo # 验证器,直接从 wtforms.validators 导入
# 普通用户的资料编辑表单
class EditProfileForm(Form):
name = StringField('Real name', validators=[Length(0, 64)]) # 因为是可选,允许长度为0
location = StringField('Location', validators=[Length(0, 64)])
about_me = TextAreaField('About me') # 文本区域,可以多行,可以拉动
submit = SubmitField('Submit')
可以在 表单类 中,通过定义validate_
开头的类方法,自定义验证器,用ValidationError
定义报错的提示信息。自定义的验证器会和在用户提交表单时自动被调用
from wtforms import ValidationError
# 管理员的资料编辑表单
class EditProfileAdminForm(Form):
# 检查提交的昵称
# 如果字段值没有变,跳过验证
# 如果新的与旧的不同,但与其他用户的昵称冲突,报错
# 如果有变化,且与其他用户不冲突,验证通过
def validate_username(self, field):
if field.data != self.user.username and User.query.filter_by(username=field.data).first():
raise ValidationError('Username already in use.')
渲染
将表单渲染成 HTML
如果是 WTF()
{% import "bootstrap/wtf.html" as wtf %}
{{ wtf.quick_form(form) }} # 渲染时,将 form 作为参数传递给模板
如果是手动写
<form method="POST"> # 表单提交方式为 POST
{{ form.hidden_tag() }}
{{ form.name.label }} {{ form.name() }}
{{ form.submit() }}
</form>
在视图中的处理
导入定义的表单类
from .forms import EditProfileForm
在匹配 URL 和 HTTP 的请求方式时,需要添加 POST 方法,默认只处理 GET 请求
其实 HTTP 中 GET方式 和 POST方式 都可以提交表单中填写的数据,区别是,GET方式会将数据以
查询字符串
的形式放到 URL 中提交,POST方式 会将数据保存在 HTTP 主体中提交
# 个人主页编辑页面
@main.route('/edit_profile', methods=['GET', 'POST'])
实例化表单类
form = EditProfileForm()
查看提交的数据是否能被所有验证器验证通过,如果通过,通过form.字段名.data
获取指定字段的内容,并保存到数据库,否则,设置表单字段为当前值(如果是修改或编辑页面),或直接返回空表单(注册、登陆 页面)
if form.validate_on_submit():
current_user.name = form.name.data
current_user.location = form.location.data
current_user.about_me = form.about_me.data
db.session.add(current_user)
db.session.commit()
flash('Your profile has been updated')
return redirect(url_for('.user', username=current_user.username)) # 提交后,转到个人主页,显示编辑结果
# 如果是 GET,或 验证器不通过,显示目前的资料内容
form.name.data = current_user.name
form.location.data = current_user.location
form.about_me.data = current_user.about_me
return render_template('edit_profile.html', form=form)
登陆页面的例子:
from .forms import LoginForm
# 登录页面,填写表单、认证
@auth.route('/login', methods = ['GET', 'POST']) # 接收 url 为 `/login`, HTTP 方式为 'GET' 和 'POST' 的请求
def login():
form = LoginForm() # 创建实例,表示表单
if form.validate_on_submit(): # 如果 通过 post 提交的表单,数据通过了所有验证器的检查
user = User.query.filter_by(email=form.email.data).first()
if user is not None and user.verify_password(form.password.data):
login_user(user, form.remember_me.data)
return redirect(request.args.get('next') or url_for('main.index'))
flash('Invalid username or password.')
return render_template('auth/login.html', form=form)
刷新
如果提交表单后,点击浏览器刷新按钮,会要求在浏览器再次提交表单前进行确认
这是因为,刷新页面时,浏览器会重新发送最后一次发送过的请求
为了避免用户遇到这种情况,需要避免让 POST 请求作为浏览器最后发出的一个请求
POST/重定向/GET 模式
对于用户的 POST 请求,如果验证通过,使用重定向作为响应,使得浏览器向 响应中的重定向URL 发送 GET 请求,这样就可以正常刷新了
返回重定向响应的方法
return redirect(url_for('auth.login'))
使用redirect()
函数,将目标 URL 作为参数
# 注册页面
@auth.route('/register', methods = ['GET', 'POST']) # 初次 get 请求获取空白表单,然后 post 请求提交填写后的表单
def register():
form = RegistrationForm()
if form.validate_on_submit():
return redirect(url_for('auth.login')) # 重定向到登陆页面, 让用户登陆。浏览器向 login url 发送 get 请求。
return render_template('auth/register.html', form=form)
通用密钥
SECRET_KEY 不仅可以用于 CSRF 还可以用于加密 cookie
cookie 是网站为了辨别用户身份而储存在用户本地终端(Client Side)上的数据
默认情况下,用户会话保存在客户端 cookie 中,使用设置的 SECRET_KEY 进行加密签名。如果篡改了 cookie 中的内容,签名就会失效,会话也会随之 失效。
在下一个返回的响应中显示当前处理结果的消息
请求完成后,有时需要让用户知道状态发生了变化。
通过函数flash()
和get_flashed_messages()
,可以将当前请求处理的结果,在下一个返回的响应中显示
两步:
- 在 view 中,用函数
flash()
定义、收集消息
# 退出,跳转到主页
@auth.route('/logout')
@login_required # 保护路由,只允许已登陆用户访问。Flask-Login 提供的装饰器,将用户重定向到 登陆页面
def logout():
logout_user() # 删除并重设用户会话
flash('You have been logged out.')
return redirect(url_for('main.index'))
当前处理的响应是重定向URL,然后在浏览器向 重定向的URL 请求的响应中显示
需要注意,可以多次调用 flash() 收集多条消息,形成队列,但所有消息都只能显示一次
- 在模板中调用函数
get_flashed_messages()
显示所有收集的消息
因为可能队列中有多条消息,所以需要用 for 循环获取
{% for message in get_flashed_messages() %}
{{ message }}
{% endfor %}