使用Python的Flask框架表单插件Flask-WTF实现Web登录验证
表单是让用户与我们的网页应用程序交互的基本元素。Flask 本身并不会帮助我们处理表单,但是 Flask-WTF 扩展让我们在我们的 Flask 应用程序中使用流行的 WTForms 包。这个包使得定义表单和处理提交容易一些。 Flask-WTF # 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。 登录表单可以替我们做一些事情。它能够保证我们应用程序的安全以防止 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 会主动帮我们去做。好哇! 自定义验证 现在我们来定义一个简单的注册表单,其实这个表单和登录的表单几乎一样。只是会在后面给它添加一些自定义的验证器。 # ourapp/forms.py from flask_wtf import Form from wtforms import StringField,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',Email(),Unique( User,User.email,message='There is already an account with that email.')]) password = PasswordField('Password',validators=[DataRequired()]) 渲染表单 {# 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 登录 我们要把一些提供给用户的公共账号服务商定义到一个列表里面,这个列表就放到配置文件中吧 (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 链接后显示的截图: (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |