使用Python的Flask框架表单插件Flask-WTF实现Web登录验证


Posted in Python onJuly 12, 2016

表单是让用户与我们的网页应用程序交互的基本元素。Flask 本身并不会帮助我们处理表单,但是 Flask-WTF 扩展让我们在我们的 Flask 应用程序中使用流行的 WTForms 包。这个包使得定义表单和处理提交容易一些。

Flask-WTF
我们想要使用 Flask-WTF 做的第一件事情(在安装它以后,GitHub项目页:https://github.com/lepture/flask-wtf )就是在 myapp.forms 包中定义一个表单。

# ourapp/forms.py

from flask_wtf import Form
from wtforms import StringField, PasswordField
from wtforms.validators import DataRequired, Email

class EmailPasswordForm(Form):
 email = StringField('Email', validators=[DataRequired(), Email()])
 password = PasswordField('Password', validators=[DataRequired()])

在 Flask-WTF 0.9 版本以前,Flask-WTF 提供了针对 WTForms 字段以及验证器的自己的封装。你可能看到外面一大堆的代码是从 flask.ext.wtforms 中不是从 wtforms 中导入 TextField,PasswordField。
在 Flask-WTF 0.9 版本以后,我们应该直接从 wtforms 中导入这些字段和验证器。
我们定义的表单是一个用户登录表单。我们把它叫做 EmailPasswordForm(),我们可以重用这个同样的表单类(Form)去做其它的一些事情,像注册表单。这里我们没有去定义一个又长又没有用的表单,而是选择一个很常用的表单,只是为了给你们介绍使用 Flask-WTF 定义表单的方式。也许以后在正式项目中会定义一个特别复杂表单。对于表单中包含字段名称,我们建议使用一个清楚的名称,并且在一个表单中保持唯一。不得不说,对于一个长的表单,我们可能要给出一个更符合上文的字段名称。

登录表单可以替我们做一些事情。它能够保证我们应用程序的安全以防止 CSRF 漏洞,验证用户输入并且渲染适当的标记,这些标记是我们为表单定义的字段。

CSRF 保护和验证
CSRF 表示跨站请求伪造。CSRF 攻击是指第三方伪造(像一个表单提交)请求到一个应用程序的服务器。一个易受攻击的服务器假设从一个表单来的数据是来自它自己的网站并且采取相应的操作。

作为一个例子,比方说,一个邮件提供商可以让你通过提交一个表单来删除你的账号。表单发送一个 POST 请求到服务器上的 account_delete 端点并且当表单被提交的时候删除登录的账号。我们可以在自己的网站上创建一个表单,该表单发送一个 POST 请求到同一个 account_delete 端点。现在,如果我们让某人点击我们表单的提交按钮(或者通过 JavaScript 来这样做),邮件提供商提供的登录账号就会被删除掉。当然邮件提供商还不知道表单提交并不是发生在他们的网站上。

因此如何才能阻止 POST 请求来自别的网站?WTForms 通过在渲染每一个表单的时候生成一个唯一的令牌使得成为可能。生成的令牌会被传回到服务器,伴随着 POST 请求的数据,在表单被接受之前令牌必须接受服务器的验证。关键的是令牌是与存储在用户会话(cookies)的一个值有关并且会在一段时间后失效(默认是 30 分钟)。这种方式就能够保证提交一个有效表单的人就是加载页面的人(或者至少是使用同一电脑的人),而且他们只能在加载页面 30 分钟内这样做。

要开始使用 Flask-WTF 保护 CSRF,我们需要为我们的登录页定义一个视图。

# ourapp/views.py

from flask import render_template, redirect, url_for

from . import app
from .forms import EmailPasswordForm

@app.route('/login', methods=["GET", "POST"])
def login():
 form = EmailPasswordForm()
 if form.validate_on_submit():

  # Check the password and log the user in
  # [...]

  return redirect(url_for('index'))
 return render_template('login.html', form=form)

如果表单已经被提交和验证的话,我们可以继续登录的逻辑。如果它没有被提交的话(例如,只是一个 GET 请求),我们就要把表单对象传递给我们的模板,以便它能够被渲染。下面就是我们使用 CSRF 保护的时候模板的样子。

{# ourapp/templates/login.html #}

{% extends "layout.html" %}
{% endraw %}
<html>
 <head>
  <title>Login Page</title>
 </head>
 <body>
  <form action="{{ url_for('login') }}" method="post">
   <input type="text" name="email" />
   <input type="password" name="password" />
   {{ form.csrf_token }}
  </form>
 </body>
</html>

{% raw %}{{ form.csrf_token }}{% endraw %} 渲染了一个隐藏的字段,该字段包含那些奇特的 CSRF 令牌,并且当 WTForms 验证表单的时候会寻找这个字段。我们不用担心包含处理令牌的逻辑,WTForms 会主动帮我们去做。好哇!

自定义验证
除了由 WTForms 提供的内置的表单验证器(例如,Required(),Email() 等等),我们能创建我们自己的验证器。我们将通过编写一个 Unique() 验证器来说明如何创建自己的验证器,Unique() 验证器是用来检查数据库并且确保用户提供的值在数据库中不存在。这能够用于确保用户名或者邮箱地址还没有使用。没有 WTForms 的话,我们可能要在视图中做这些事情,但是现在我们可以在表单本身做些事情。

现在我们来定义一个简单的注册表单,其实这个表单和登录的表单几乎一样。只是会在后面给它添加一些自定义的验证器。

# ourapp/forms.py

from flask_wtf import Form
from wtforms import StringField, PasswordField
from wtforms.validators import DataRequired, Email

class EmailPasswordForm(Form):
 email = StringField('Email', validators=[DataRequired(), Email()])
 password = PasswordField('Password', validators=[DataRequired()])

现在我们要添加我们的验证器用来确保它们提供的邮箱地址不存在数据库中。我们把这个验证器放在一个新的 util 模块,util.validators。

# ourapp/util/validators.py
from wtforms.validators import ValidationError

class Unique(object):
 def __init__(self, model, field, message=u'This element already exists.'):
  self.model = model
  self.field = field

 def __call__(self, form, field):
  check = self.model.query.filter(self.field == field.data).first()
  if check:
   raise ValidationError(self.message)

这个验证器假设我们是使用 SQLAlchemy 来定义我们的模型。WTForms 期待验证器返回某种可调用的对象(例如,一个可调用的类)。

在 Unique() 的 \_\_init\_\_ 中我们可以指定哪些参数传入到验证器中,在本例中我们要传入相关的模型(例如,在我们例子中是传入 User 模型)以及要检查的字段。当验证器被调用的时候,如果定义模型的任何实例匹配表单中提交的值,它将会抛出一个 ValidationError。我们也可以添加一个具有通用默认值的消息,它将会被包含在 ValidationError 中。

现在我们可以修改 EmailPasswordForm,使用我们自定义的 Unique 验证器。

# ourapp/forms.py

from flask_wtf import Form
from wtforms import StringField, PasswordField
from wtforms.validators import DataRequired

from .util.validators import Unique
from .models import User

class EmailPasswordForm(Form):
 email = StringField('Email', validators=[DataRequired(), Email(),
  Unique(
   User,
   User.email,
   message='There is already an account with that email.')])
 password = PasswordField('Password', validators=[DataRequired()])

渲染表单
WTForms 也能帮助我们为表单渲染成 HTML 表示。WTForms 实现的 Field 字段能够渲染成该字段的 HTML 表示,所以为了渲染它们,我们只必须在我们模板中调用表单的字段。这就像渲染 csrf_token 字段。下面给出了一个登录模板的示例,在里面我们使用 WTForms 来渲染我们的字段。

{# ourapp/templates/login.html #}

{% extends "layout.html" %}
<html>
 <head>
  <title>Login Page</title>
 </head>
 <body>
  <form action="" method="post">
   {{ form.email }}
   {{ form.password }}
   {{ form.csrf_token }}
  </form>
 </body>
</html>

我们可以自定义如何渲染字段,通过传入字段的属性作为参数到调用中。

<form action="" method="post">
 {{ form.email.label }}: {{ form.email(placeholder='yourname@email.com') }}
 <br>
 {% raw %}{{ form.password.label }}: {{ form.password }}{% endraw %}
 <br>
 {% raw %}{{ form.csrf_token }}{% endraw %}
</form>

处理 OpenID 登录
现实生活中,我们发现有很多人都不知道他们拥有一些公共账号。一部分大牌的网站或服务商都会为他们的会员提供公共账号的认证。举个栗子,如果你有一个 google 账号,其实你就有了一个公共账号,类似的还有 Yahoo, AOL, Flickr 等。
为了方便我们的用户能简单的使用他们的公共账号,我们将把这些公共账号的链接添加到一个列表,这样用户就不用自手工输入了。

我们要把一些提供给用户的公共账号服务商定义到一个列表里面,这个列表就放到配置文件中吧 (fileconfig.py):

CSRF_ENABLED = True
SECRET_KEY = 'you-will-never-guess'
 
OPENID_PROVIDERS = [
 { 'name': 'Google', 'url': 'https://www.google.com/accounts/o8/id' },
 { 'name': 'Yahoo', 'url': 'https://me.yahoo.com' },
 { 'name': 'AOL', 'url': 'http://openid.aol.com/<username>' },
 { 'name': 'Flickr', 'url': 'http://www.flickr.com/<username>' },
 { 'name': 'MyOpenID', 'url': 'https://www.myopenid.com' }]

接下来就是要在我们的登录视图函数中使用这个列表了:

@app.route('/login', methods = ['GET', 'POST'])
def login():
 form = LoginForm()
 if form.validate_on_submit():
  flash('Login requested for OpenID="' + form.openid.data + '", remember_me=' + str(form.remember_me.data))
  return redirect('/index')
 return render_template('login.html',
  title = 'Sign In',
  form = form,
  providers = app.config['OPENID_PROVIDERS'])

我们从 app.config 中引入了公共账号服务商的配置列表,然后把它作为一个参数通过 render_template 函数引入到模板。

接下来要做的我想你也猜得到,我们需要在登录模板中把这些服务商链接显示出来。

<!-- extend base layout -->
{% extends "base.html" %}
 
{% block content %}
<script type="text/javascript">
function set_openid(openid, pr)
{
 u = openid.search('<username>')
 if (u != -1) {
  // openid requires username
  user = prompt('Enter your ' + pr + ' username:')
  openid = openid.substr(0, u) + user
 }
 form = document.forms['login'];
 form.elements['openid'].value = openid
}
</script>
<h1>Sign In</h1>
<form action="" method="post" name="login">
 {{form.hidden_tag()}}
 <p>
  Please enter your OpenID, or select one of the providers below:<br>
  {{form.openid(size=80)}}
  {% for error in form.errors.openid %}
  <span style="color: red;">[{{error}}]</span>
  {% endfor %}<br>
  |{% for pr in providers %}
  <a href="javascript:set_openid('{{pr.url}}', '{{pr.name}}');">{{pr.name}}</a> |
  {% endfor %}
 </p>
 <p>{{form.remember_me}} Remember Me</p>
 <p><input type="submit" value="Sign In"></p>
</form>
{% endblock %}

这次的模板添加的东西似乎有点多。一些公共账号需要提供用户名,为了解决这个我们用了点 javascript。当用户点击相关的公共账号链接时,需要用户名的公共账号会提示用户输入用户名, javascript 会把用户名处理成可用的公共账号,最后再插入到 openid 字段的文本框中。

下面这个是在登录页面点击 google 链接后显示的截图:

使用Python的Flask框架表单插件Flask-WTF实现Web登录验证

Python 相关文章推荐
python中使用xlrd、xlwt操作excel表格详解
Jan 29 Python
浅析Python多线程下的变量问题
Apr 28 Python
Python实现的堆排序算法原理与用法实例分析
Nov 22 Python
使用python实现链表操作
Jan 26 Python
Python实战购物车项目的实现参考
Feb 20 Python
Python中psutil的介绍与用法
May 02 Python
Python3视频转字符动画的实例代码
Aug 29 Python
python 实现二维列表转置
Dec 02 Python
Python grequests模块使用场景及代码实例
Aug 10 Python
利用python如何实现猫捉老鼠小游戏
Dec 04 Python
python中numpy.empty()函数实例讲解
Feb 05 Python
python基于selenium爬取斗鱼弹幕
Feb 20 Python
Python的Flask框架标配模板引擎Jinja2的使用教程
Jul 12 #Python
深度定制Python的Flask框架开发环境的一些技巧总结
Jul 12 #Python
Python的面向对象编程方式学习笔记
Jul 12 #Python
Python使用cookielib模块操作cookie的实例教程
Jul 12 #Python
Python网络编程中urllib2模块的用法总结
Jul 12 #Python
Python中内置的日志模块logging用法详解
Jul 12 #Python
Swift 3.0在集合类数据结构上的一些新变化总结
Jul 11 #Python
You might like
PHP程序开发范例学习之表单 获取文本框的值
2011/08/08 PHP
PHP中strtr字符串替换用法详解
2014/11/26 PHP
PHP基于ICU扩展intl快速实现汉字转拼音及按拼音首字母分组排序的方法
2017/05/03 PHP
CSS JavaScript 实现菜单功能 改进版
2008/12/09 Javascript
ExtJS4 Grid改变单元格背景颜色及Column render学习
2013/02/06 Javascript
JQuery打造省市下拉框联动效果
2014/05/18 Javascript
JS+CSS实现感应鼠标渐变显示DIV层的方法
2015/02/20 Javascript
Java File类的常用方法总结
2015/03/18 Javascript
jQuery中trigger()与bind()用法分析
2015/12/18 Javascript
jquery组件WebUploader文件上传用法详解
2020/10/23 Javascript
微信小程序 简单DEMO布局,逻辑,样式的练习
2016/11/30 Javascript
js实现消息滚动效果
2017/01/18 Javascript
BootStrap fileinput.js文件上传组件实例代码
2017/02/20 Javascript
如何选择jQuery版本 1.x? 2.x? 3.x?
2017/04/01 jQuery
React Native 环境搭建的教程
2017/08/19 Javascript
layui上传图片到服务器的非项目目录下的方法
2019/09/26 Javascript
微信小程序中的上拉、下拉菜单功能
2020/03/13 Javascript
如何使用JavaScript实现无缝滚动自动播放轮播图效果
2020/08/20 Javascript
uin-app+mockjs实现本地数据模拟
2020/08/26 Javascript
[28:28]Ti4 冒泡赛第二天NEWBEE vs NaVi 2
2014/07/15 DOTA
[52:10]LGD vs Optic Supermajor小组赛D组胜者组决赛 BO3 第二场 6.3
2018/06/04 DOTA
python自定义时钟类、定时任务类
2021/02/22 Python
Python中BeautifuSoup库的用法使用详解
2019/11/15 Python
Python实现图片识别加翻译功能
2019/12/26 Python
在python中list作函数形参,防止被实参修改的实现方法
2020/06/05 Python
python的help函数如何使用
2020/06/11 Python
浅析Python 简单工厂模式和工厂方法模式的优缺点
2020/07/13 Python
凯特方迪化妆品官网:Kat Von D Beauty
2016/11/15 全球购物
Urban Outfitters英国官网:美国平价服饰品牌
2016/11/25 全球购物
Boston Proper官网:美国女装品牌
2017/10/30 全球购物
演讲稿开场白
2014/01/13 职场文书
秋天的图画教学反思
2014/05/01 职场文书
初三新学期计划书
2014/05/03 职场文书
球队口号
2014/06/18 职场文书
赞美教师的句子
2019/09/02 职场文书
python绘制箱型图
2021/04/27 Python