使用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中的输入和输出功能进行读取和写入的教程
Apr 14 Python
解析Python中的二进制位运算符
May 13 Python
使用Python操作MySQL的一些基本方法
Aug 16 Python
浅谈Python中chr、unichr、ord字符函数之间的对比
Jun 16 Python
python3 模拟登录v2ex实例讲解
Jul 13 Python
python实现贪吃蛇小游戏
Mar 21 Python
Python3 Tkinkter + SQLite实现登录和注册界面
Nov 19 Python
python爬虫爬取监控教务系统的思路详解
Jan 08 Python
django ObjectDoesNotExist 和 DoesNotExist的用法
Jul 09 Python
sqlalchemy实现时间列自动更新教程
Sep 02 Python
python基于机器学习预测股票交易信号
May 25 Python
Python常用配置文件ini、json、yaml读写总结
Jul 09 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
老照片 - 几十年前的收音机与人
2021/03/02 无线电
PHP校验ISBN码的函数代码
2011/01/17 PHP
php如何连接sql server
2015/10/16 PHP
PHP ajax+jQuery 实现批量删除功能实例代码小结
2018/12/06 PHP
Jquery中增加参数与Json转换代码
2009/11/20 Javascript
jQuery+CSS 半开折叠效果原理及代码(自写)
2013/03/04 Javascript
利用webqq协议使用python登录qq发消息源码参考
2013/04/08 Javascript
JS获取IP、MAC和主机名的五种方法
2013/11/14 Javascript
JS循环遍历JSON数据的方法
2014/07/08 Javascript
js钢琴按钮波浪式图片排列效果代码分享
2015/08/26 Javascript
javascript计时器编写过程与实现方法
2016/02/29 Javascript
轻松掌握JavaScript状态模式
2016/09/07 Javascript
利用vue写todolist单页应用
2016/12/15 Javascript
原生js实现放大镜效果
2017/01/11 Javascript
vue2 自定义动态组件所遇到的问题
2017/06/08 Javascript
JavaScript门面模式详解
2017/10/19 Javascript
详解Javascript 中的 class、构造函数、工厂函数
2017/12/20 Javascript
react-router browserHistory刷新页面404问题解决方法
2017/12/29 Javascript
JS实现生成由字母与数字组合的随机字符串功能详解
2018/05/25 Javascript
解决Vue axios post请求,后台获取不到数据的问题方法
2018/08/11 Javascript
3分钟了解vue数据劫持的原理实现
2019/05/01 Javascript
vue中利用Promise封装jsonp并调取数据
2019/06/18 Javascript
详解Python中使用base64模块来处理base64编码的方法
2016/07/01 Python
python使用opencv进行人脸识别
2017/04/07 Python
Python读取Json字典写入Excel表格的方法
2018/01/03 Python
用pandas按列合并两个文件的实例
2018/04/12 Python
Python删除n行后的其他行方法
2019/01/28 Python
Python中字符串String的基本内置函数与过滤字符模块函数的基本用法
2019/05/27 Python
520使用Python实现“我爱你”表白
2020/05/20 Python
使用pytorch 筛选出一定范围的值
2020/06/28 Python
friso美素佳儿官方海外旗舰店:荷兰原产原罐
2017/07/03 全球购物
菲律宾票务网站:StubHub菲律宾
2018/04/21 全球购物
MVC的各个部分都有那些技术来实现?如何实现?
2016/04/21 面试题
酒店公关部经理岗位职责
2013/11/24 职场文书
2014年加油站站长工作总结
2014/12/23 职场文书
python异常中else的实例用法
2021/06/15 Python