第二部分 Blog例子
第八章 用户验证
大部分程序需要追踪用户身份。当用户连接到程序,通过一系列步骤使自己的身份被识别,这就是用户验证。一旦程序知道这个用户是谁,它就能提供定制化的体验。
大部分验证方法要求用户提供一个身份标识(用户名称或者邮件地址)和一个密码。本章将创建一个完整的身份认证系统。
Flask认证扩展
优秀的Python认证包有很多,但它们都没有单蹦完成所有的任务的。本章的用户认证解决方案联合使用了多个包共同完成这一任务。下面是我们要用的包清单:
- Flask-Login: 登陆后用户会话管理
- Werkzeug: 密码加密和密码验证
- itsdangerous: 加密安全令牌的生成和验证
除了特定的认证包之外,下面是我们要用到的常规目的的一些扩展。
- Flask-Mail: 发送验证邮件
- Flask-Bootstrap:HTML模板
- Flask-WTF: web表单
密码加密
在设计开发web程序过程中,存储在数据库的用户信息的安全性往往被忽视。如果攻击者黑进服务器,访问了你的用户数据库,你就有用户安全的风险。并且这种风险要比你想象的要大得多。这是众所周知的一个事实:大部分用户在多个网站上使用同样的密码,所以即使你没有存储任何敏感信息,只需获得你数据库里的用户密码,攻击者就可以访问其他网站上你这些用户的帐户了。(译注:奇葩如此。当年CSDN这样的大网站竟然都是明文存储密码。在2011年其密码数据库泄露后,众多网站纷纷中招——用户们都是一个密码|甚至一套名码走天下。
)
安全存储用户密码的关键就是不要存储明文密码,你应该存储它的hash(译注:哈希|散列
)值。密码哈希函数获取输入的明文密码后对其一次或多次加密转换,其结果是一个新的字符串序列,跟原文密码毫不相似。哈希密码可以与明文密码(译注:用户输入
)进行校验,因为哈希函数是可重复的:同样的输入一定会得到同样的结果输出。
密码哈希是一个复杂的任务,很难确保没有疏漏。我不建议你自己实现加密验证这套方案,应该采用社区广泛认可的库。如果有兴趣学习加密验证,你可以去读读这些文章:Salted Password Hashing-Doing it Right
使用Werkzeug哈希密码
Werkzeug的安全模块通过两个函数方便地实现了密码哈希加密,分别用于注册和验证阶段:
- generate_password_hash(password, method=pbkdf2:sha1, salt_length=8) :这个函数接收纯文本密码并返回经过哈希加密的一个字符串,该返回值可以放心的存入数据库。method和salt_length两个参数的默认值基本可以满足大部分用例需要了。(
译注:当然,这两个参数可以指定别的值,从而带来更高的安全性。总的来说随着cpu技术进步,暴力破解<穷尽所有组合进行逐一猜测>所需要的时间也在相应缩短——理论上所有密码都可以被猜出来,当然所需要的时间可能无法接受——所以很对定长的用户密码,提高加密的复杂性/更长的salt/更多次的hash,使破解需要的时间尽可能长,是另一种安全保护考虑。
) - check_password_hash(hash, password) : 这个函数分别从数据库获得散列后的密码,从用户获取输入的明文密码,进行比较。一旦这两个密码一致就返回True。
例子8-1展示了第五章编写的User模型中密码加密部分的变更:
Example 8-1. app/models.py: Password hashing in User model
from werkzeug.security import generate_password_hash, check_password_hash
class User(db.Model):
# ...
password_hash = db.Column(db.String(128))
@property
def password(self):
raise AttributeError('password is not a readable attribute')
@password.setter
def password(self, password):
self.password_hash = generate_password_hash(password)
def verify_password(self, password):
return check_password_hash(self.password_hash, password)
密码哈希函数通过User类的password只写属性实现的。如果试图读取password属性,将返回一个错误。设置该属性时,setter方法会呼叫werkzeug的generate_password_hash()函数并把返回结果写到类字段password_hash。原始密码 一旦哈希就无法恢复(<small>译注:你只能比对哈希值而不能从哈希值还原原始密码</small>)
verify_password方法获取用户输入的密码,并传递给werkzeug的check_pasword_hash()函数来验证是否与保存在User模型中的已经哈希过的密码一致。如果这个方法返回True,密码就是正确的。
密码哈希功能已经完成,你可以在shell中进行测试:
(venv) $ python manage.py shell
>>> u = User()
>>> u.password = 'cat'
>>> u.password_hash
'pbkdf2:sha1:1000$duxMk0OF$4735b293e397d6eeaf650aaf490fd9091f928bed'
>>> u.verify_password('cat')
True
>>> u.verify_password('dog')
False
>>> u2 = User()
>>> u2.password = 'cat'
>>> u2.password_hash
'pbkdf2:sha1:1000$UjvnGeTP$875e28eb0874f44101d6b332442218f66975ee89'
注意,虽然使用了同一个密码,用户u和u2的哈希密码值完全不同。为了确认这一功能将来也能正常工作,我们把上面的测试写成一个单元测试以便于能重复测试。例子8-2展示了tests包中的一个新模块,它有三个新测试来检测User模型的最近更改:
Example 8-2. tests/test_user_model.py: Password hashing tests
import unittest
from app.models import User
class UserModelTestCase(unittest.TestCase):
def test_password_setter(self):
u = User(password = 'cat')
self.assertTrue(u.password_hash is not None)
def test_no_password_getter(self):
u = User(password = 'cat')
with self.assertRaises(AttributeError):
u.password
def test_password_verification(self):
u = User(password = 'cat')
self.assertTrue(u.verify_password('cat'))
self.assertFalse(u.verify_password('dog'))
def test_password_salts_are_random(self):
u = User(password='cat')
u2 = User(password='cat')
self.assertTrue(u.password_hash != u2.password_hash)
创建身份认证蓝图
我们在第七章介绍了蓝图:把程序创建移动到工厂函数中后,定义全局路由。用户验证相关的路由可以添加到auth蓝图。要保持良好的代码结构,针对不同的程序功能集合设置不同的蓝图就是很好的办法。
auth蓝图保存在同名的python包里。蓝图包构造函数创建蓝图对象并从一个views.py模块中导入路由。请看来例子8-3:
Example 8-3. app/auth/__init__.py: Blueprint creation
from flask import Blueprint
auth = Blueprint('auth', __name__)
from . import views
app/auth/views.py
模块,如例子8-4所示,导入了蓝图和验证相关的的路由定义(译注:使用装饰器@auth.route()
)。现在已经添加了一个'/login'路由,来显示同名的占位符模板。
Example 8-4. app/auth/views.py: Blueprint routes and view functions
from flask import render_template
from . import auth
@auth.route('/login')
def login():
return render_template('auth/login.html')
传给render_template()的模板文件被保存在auth文件夹。这个文件夹必须在app/templates
中创建,因为Flask默认所有模板都在这里。把蓝图模板保存在各自同名文件夹,避免了和main了蓝图或未来其他蓝图的模板名称冲突。
提醒:经过配置,蓝图也可以拥有自己独立的模板文件夹(<small>译注:不在app/templates中?</small>)。当配置了多个模板文件夹之后,render_template()函数将先搜索程序指定的模板文件夹,再搜索蓝图指定的模板文件夹。
auth蓝图需要添加到create_app()工厂函数中的程序,如例子8-5:
Example 8-5. app/__init__.py: Blueprint attachment
def create_app(config_name):
# ...
from .auth import auth as auth_blueprint
app.register_blueprint(auth_blueprint, url_prefix='/auth')
return app
蓝图注册中的url_prefix参数是可选的。如果启用,在这个蓝图中定义的路由都会使用这个前缀(此处是/auth)注册。例如,/login路由会被注册成/auth/login,在开发服务器上的完整路径就是http://localhost:5000/auth/login
使用Flask-login实现用户验证
当用户登录进入程序,必须记录下他们的验证授权状态,这样即使转到别的页面也可以记住已登录的用户。Flask-Login是个小巧但非常有用的扩展,它并没有绑定某个特定的验证方式,只是限于用户验证管理这方面(译注:不限制于某种验证方法(账号密码方式,openID,或其他),只负责管理用户是否可以验证通过,以及记住用户会话,退出系统??
)。
开始之前,我们需要在虚拟环境中安装该扩展:
(venv) $ pip install flask-login
准备登录用的用户模型
为了配合User模型一起工作,Flask-Login扩展需要模型实现几个方法。如表8-1:
Table 8-1. Flask-Login user methods
Method Description
is_authenticated() Must return True if the user has login credentials or False otherwise.
is_active() Must return True if the user is allowed to log in or False otherwise. A False return value can be used for disabled accounts.
is_anonymous() Must always return False for regular users.
get_id() Must return a unique identifier for the user, encoded as a Unicode string.
这四个方法可以在模型类中直接以方法的形式实现,但我们有个更简单的备选——Flask-Login提供了一个UserMixin类,它已经默认实现了这些方法足以满足大部分应用场景。更新后的用户模型如里子8-6所示:
Example 8-6. app/models.py: Updates to the User model to support user logins
from flask.ext.login import UserMixin
class User(UserMixin, db.Model):
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key = True)
email = db.Column(db.String(64), unique=True, index=True)
username = db.Column(db.String(64), unique=True, index=True)
password_hash = db.Column(db.String(128))
role_id = db.Column(db.Integer, db.ForeignKey('roles.id'))
注意,email字段也被添加上了。在这个程序里,用户使用email来登录系统,相比较用户名,一般很少有人会忘了自己邮件地址。
Flask-Login在工厂函数中进行初始化,如例子8-7所示:
Example 8-7. app/__init__.py: Flask-Login initialization
from flask.ext.login import LoginManager
login_manager = LoginManager()
login_manager.session_protection = 'strong'
login_manager.login_view = 'auth.login'
def create_app(config_name):
# ...
login_manager.init_app(app)
# ...
Login_Manager对象的属性session_protection可以被设置成None,basic或者strong,来提供不同的安全等级对付用户会话攻击。如果设置为strong,Flask-Login将持续追踪客户端ip地址和浏览器标识,一旦发现变动就会要求重新登录。 login_view属性设置了登录页面的端点('auth.login')。你可能记着,login路由是在蓝图里的,这就需要添加蓝图名为前缀。
最终,Flask-Login要求程序设置一个回调函数,用来载入用户,作为身份识别。如例子8-8所示:
Example 8-8. app/models.py: User loader callback function
from . import login_manager
@login_manager.user_loader
def load_user(user_id):
return User.query.get(int(user_id))
载入用户的回调函数接收一个Unicode字符串格式的用户标识id。如果该标识有效,回调函数的返回值则肯定是一个用户对象,否则返回None而不会报错。
保护路由
为了保护路由只允许已登录用户访问,Flask-Login提供了login_required装饰器。用例如下:
from flask.ext.login import login_required
@app.route('/secret')
@login_required
def secret():
return 'Only authenticated users are allowed!'
如果一个未经验证的用户试图访问该路由,Flask-Login就会中断请求,把用户转到登录页面。
添加登录表单
将要显示给用户的登录表单页面,包含一个文本字段用来输入email地址,一个密码字段,一个"remember me"选框和一个提交按钮。例子8-9显示了Flask-WTF类:
Example 8-9. app/auth/forms.py: Login form
from flask.ext.wtf import Form
from wtforms import StringField, PasswordField, BooleanField, SubmitField
from wtforms.validators import Required, Email
class LoginForm(Form):
email = StringField('Email', validators=[Required(), Length(1, 64),Email()])
password = PasswordField('Password', validators=[Required()])
remember_me = BooleanField('Keep me logged in')
submit = SubmitField('Log In')
email字段利用了WTFForms提供的Length(),Email()验证器。PassowrdField类将显示为type="passowrd"的一个<input>元素。BooleanField类则显示为一个选择框。
与登录页面关联的模板是auth/login.html。这个模板只需要使用Flask-Bootstrap的wtf.quick_form()宏来显示表单即可。图8-1显示了浏览器中的登录表单:
在base.html模板中的导航条使用了jinja2的条件语句,根据当前用户的登录状态来显示“登录”或“退出”链接。这个条件语句显示在例子8-10:
Example 8-10. app/templates/base.html: Sign In and Sign Out navigation bar links
<ul class="nav navbar-nav navbar-right">
{% if current_user.is_authenticated() %}
<li><a href="{{ url_for('auth.logout') }}">Sign Out</a></li>
{% else %}
<li><a href="{{ url_for('auth.login') }}">Sign In</a></li>
{% endif %}
</ul>
条件语句中的current_user变量由Flask-login定义,在视图函数和模板中自动生效。这个变量包含了当前已登录的用户或者一个匿名用户对象——如果未登录的话。匿名用户对象给is_authenticated()方法的响应是False,所以很方便就能知道当前用户是否已经登录。
用户登录
视图函数login()的实现如例子8-11:
Example 8-11. app/auth/views.py: Sign In route
from flask import render_template, redirect, request, url_for, flash
from flask.ext.login import login_user
from . import auth
from ..models import User
from .forms import LoginForm
@auth.route('/login', methods=['GET', 'POST'])
def login():
form = LoginForm()
if form.validate_on_submit():
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)
视图函数创建了一个LoginForm对象,并像第四章中那样使用了这个简单表单。当这个请求的类型是GET,视图函数就只渲染这个模板——依次显示表单。如果这个表单被提交——请求类型是POST,Flask-WTF的validate_on_submit()函数就验证表单变量,然后试图登录用户。
为了登录用户,这个函数开始利用表单提供的email从数据库中加载用户。如果指定email的用户存在,就传递表单的password值给verify_password方法调用之。如果密码有效,Flask-login的login_user()函数就把用户登入系统,并察看表单的“remember me”的布尔值。如果该值为False,用户会话将在浏览器关闭的时候过期失效,用户下次就必须重新登录。如果值为True,就会给浏览器设置一个长期的cookie,用以恢复用户会话。(译注:由于cookie被加密存储在客户端,这样下次用户打开网站时浏览器就会读取cookie自动把用户登录进系统。
)
根据我们在第四章讨论过的POST/Redirect/GET模式,POST请求提交登录信息并以重定向为结束,但也有两个可能的URL跳转方向。如果是阻止为验证用户访问某URL而显示登录表单,Flask_login会把这一URL保存在“next”查询字符串参数中,你可以从request.args字典中访问。如果next查询字符串参数无不可用,就以重定向到home页面取代之。(译注:即,如果登录页面有历史url--你在登录前试图访问的地址,成功登录后就自动转向该url;反之,跳转到首页。
)如果用户提供的email或密码无效,就会设置闪现消息并重新向用户显示登录表单。
提醒:在生产服务器上,登录路由必须设置为基于安全http(
译注:https??
),保证提交给服务器的表单数据经过加密传输。没有安全http的话,登录信息在传输时会被拦截——这样在服务器上加密密码的所有努力统统白费了。
登录模板需要更新来显示表单。如例子8-12所示:
Example 8-12. app/templates/auth/login.html: Render login form
{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}
{% block title %}Flasky - Login{% endblock %}
{% block page_content %}
<div class="page-header">
<h1>Login</h1>
</div>
<div class="col-md-4">
{{ wtf.quick_form(form) }}
</div>
{% endblock %}
退出登录
退出路由的实现 如例子8-13所示:
Example 8-13. app/auth/views.py: Sign Out route
from flask.ext.login import logout_user, login_required
@auth.route('/logout')
@login_required
def logout():
logout_user()
flash('You have been logged out.')
return redirect(url_for('main.index'))
Flask-login调用了logout_user()函数来移除并重设用户会话 ,来退出登录状态。退出操作 设置了一个闪现消息确认完成,并重定向到首页。
测试登录
为了测试登录函数正常工作,应该更新首页代码来显示已登录的用户名欢迎信息。模板中的欢迎信息段如例子8-14:
Example 8-14. app/templates/index.html: Greet the logged-in user
Hello,
{% if current_user.is_authenticated() %}
{{ current_user.username }}
{% else %}
Stranger
{% endif %}!
在这个模板里,我们再次使用了current_user.is_authenticated()来判断用户是否登录成功。
由于我们还没有创建用户注册功能,现在只能先从shell中添加一个新用户:
(venv) $ python manage.py shell
>>> u = User(email='john@example.com', username='john', password='cat')
>>> db.session.add(u)
>>> db.session.commit()
上面注册的用户登录后,将在首页显示对他的欢迎信息,如图8-2所示
用户注册
要成为程序的一个正式用户,必须向系统注册自己信息才能登录。在登录页面上添加一个链接,供用户跳转到注册页面,来输入email地址,用户名和密码来实现注册。
添加注册表单
注册表单用于注册页面,来供用户输入email地址,用户名和密码。如例子8-15:
Example 8-15. app/auth/forms.py: User registration form
from flask.ext.wtf import Form
from wtforms import StringField, PasswordField, BooleanField, SubmitField
from wtforms.validators import Required, Length, Email, Regexp, EqualTo
from wtforms import ValidationError
from ..models import User
class RegistrationForm(Form):
email = StringField('Email', validators=[Required(), Length(1, 64),Email()])
username = StringField('Username', validators=[Required(), Length(1, 64), Regexp('^[A-Za-z][A-Za-z0-9_.]*$', 0,'Usernames must have only letters, numbers, dots or underscores')])
password = PasswordField('Password',validators=[Required(), EqualTo('password2', message='Passwords must match.')])
password2 = PasswordField('Confirm password', validators=[Required()])
submit = SubmitField('Register')
def validate_email(self, field):
if User.query.filter_by(email=field.data).first():
raise ValidationError('Email already registered.')
def validate_username(self, field):
if User.query.filter_by(username=field.data).first():
raise ValidationError('Username already in use.')
这个表单使用了WTForms内置验证器Regexp来确保username字段只包含字母、数字、下划线和点号。这个验证器在表达式后面的两个参数分别是正则表达式标志位(原文the regular expression flags
<small> 不明白。</small>查看WTForm文档:*flags* – The regexp flags to use, for example re.IGNORECASE. Ignored if regex is not a string.意思是像re.ignorecase<忽略大小写>的话,如果regex是非字符串就忽略,不执行验证???更迷糊了
)和供验证失败显示的错误信息。
为了安全,要求输入两次密码。因此要保证这两次输入的内容一致,就得使用WTForms的另外一个验证器EqualTo进行验证。这个验证器被添加到密码字段,把另外一个字段名作为参数。
表单还有两个自定义的验证器(作为表单的方法实现)。一旦表单定义了一个方法,其格式是前缀validate_+字段名,那么除了常规验证器之外这个方法也会被调用。本例中,自定义的email和username验证器可以确保这两个值不会与数据库中已有值重复。如验证失败,自定义验证器将抛出一个带有文本错误信息为参数的ValidationError错误。
显示模板就是/tmplates/auth/register.html
。跟登录(login)模板一样,它也通过wft.quick_form()
来显示表单。注册页面如图8-3:
注册页面需要从登录页面链接过来,以便于没有系统帐户的用户能找到注册地方。更改如例子8-16所示:
Example 8-16. app/templates/auth/login.html: Link to the registration page
<p>
New user?
<a href="{{ url_for('auth.register') }}">Click here to register
</a>
</p>
注册新用户
对用户注册的处理没有什么奇特之处。当提交注册表并验证通过后,一个新帐户就被添加到数据库里。完成这一任务的视图函数如例子8-17所示:
Example 8-17. app/auth/views.py: User registration route
@auth.route('/register', methods=['GET', 'POST'])
def register():
form = RegistrationForm()
if form.validate_on_submit():
user = User(email=form.email.data,username=form.username.data,password=form.password.data)
db.session.add(user)
flash('You can now login.')
return redirect(url_for('auth.login'))
return render_template('auth/register.html', form=form)
帐户确认
对于某些类型的程序来说,确认用户信息真实有效非常重要。一个最常见的要求是可以通过用户的邮件联系该用户。
要验证email地址,程序只需在用户注册完成后立即向用户的email发送一封确认邮件。用户的新帐户被初始化为未确认,直到根据邮件中的指示操作完成才会正式启用。帐户确认过程一般是点击一个特定的包含确认令牌的URL链接。
使用itsdangerous生成确认令牌
最简单的帐户确认链接就是一个包含在email中的http://www.example.com/auth/confirm/<id>
格式的URL,此处的id就是用户在数据库中的主键id的数值。当用户点击链接,视图函数就会处理该路由,把id作为确认参数来更新用户的确认状态。
但这绝对不是一个安全的处理,任何用户只要能识别出确认链接的格式就能通过URL来发送随机数字确认任意帐户。所以,用一个包含同样但加密了的信息的令牌来替换url中的id就更安全些。
如可能你还记得我们在第四章用户会话中的那些讨论,Flask使用了加密签名的cookies来保护用户会话不受攻击。这些安全cookie都是使用itsdangerouse包来加密的。同样,我们可以把这一思路用于确认令牌。
下面是一个简短的shell会话,展示了itsdangerous如何生成包含用户id的安全令牌:
(venv) $ python manage.py shell
>>> from manage import app
>>> from itsdangerous import TimedJSONWebSignatureSerializer as Serializer
>>> s = Serializer(app.config['SECRET_KEY'], expires_in = 3600)
>>> token = s.dumps({ 'confirm': 23 })
>>> token
'eyJhbGciOiJIUzI1NiIsImV4cCI6MTM4MTcxODU1OCwiaWF0IjoxMzgxNzE0OTU4fQ.ey ...'
>>> data = s.loads(token)
>>> data
{u'confirm': 23}
itsdangerous提供了多种令牌生成方式。其中,TimedJSONWebSignatureSerializer类会生成带有超时限制的JSON Web签名(JWS,包含了时间戳。)。这个类的构造函数需要一个加密密匙(key)作为参数,你可以使用Flask程序配置中的SECRET_KEY。
发送确认邮件
当前/register路由在完成添加用户之后就会重定向到/index。在重定向之前,这个路由需要发送确认邮件。变更如例子8-19所示:
Example 8-19. app/auth/views.py: Registration route with confirmation email
from ..email import send_email
@auth.route('/register', methods = ['GET', 'POST'])
def register():
form = RegistrationForm()
if form.validate_on_submit():
# ...
db.session.add(user)
db.session.commit()
token = user.generate_confirmation_token()
send_email(user.email, 'Confirm Your Account','auth/email/confirm', user=user, token=token)
flash('A confirmation email has been sent to you by email.')
return redirect(url_for('main.index'))
return render_template('auth/register.html', form=form)
注意,即使程序配置中设置了请求结束时自动提交,也必须显式调用db.session.commit()
。这是因为只有提交到数据库后,新用户才会取得id,这样生成确认令牌时才有id可用——不能延迟提交(译注:程序配置是在请求结束后再提交,在这里明显是晚了:Config类中 SQLALCHEMY_COMMIT_ON_TEARDOWN = True
)。
认证蓝图使用的email模板存储在templates/auth/email
文件夹,与其他模板分离。就行第六章讨论的那样,每个email有两个正文模板——纯文本和富文本格式。例子8-20展示了确认邮件模板的纯文本格式,你可以在我的Github库里找到HTML版本的模板。
Example 8-20. app/auth/templates/auth/email/confirm.txt: Text body of confirmation email
Dear {{ user.username }},
Welcome to Flasky!
To confirm your account please click on the following link:
{{ url_for('auth.confirm', token=token, _external=True) }}
Sincerely,
The Flasky Team
Note: replies to this email address are not monitored.
默认情况下,url_for()会生成一个相对路径的url,所以,像url_for('auth.conrim',token='abc' )将返回字符串'/auth/confirm/abc'。这个对于通过邮件来发送的地址来说当然是无效的——在程序内部使用相对地址没有问题,是因为浏览器会利用当前页面的主机名和端口把它转换成绝对地址。但通过email发送出的url可是没有转换需要的上下文的。所以_external=True
参数就用上了,这样url_for()就可以生成一个包含协议名称(http://或https://
)以及主机名,端口信息的完整url(绝对URL地址
)。
确认帐户的视图函数如例子8-21所示:
Example 8-21. app/auth/views.py: Confirm a user account
from flask.ext.login import current_user
@auth.route('/confirm/<token>')
@login_required
def confirm(token):
if current_user.confirmed:
return redirect(url_for('main.index'))
if current_user.confirm(token):
flash('You have confirmed your account. Thanks!')
else:
flash('The confirmation link is invalid or has expired.')
return redirect(url_for('main.index'))
这个路由有装饰器login_required保护,所以用户点击链接访问这个视图函数之前需要登录进入系统才行。这个函数首先检查登录用户是否已经确认,要是已经确认就会直接跳转到首页。这主要是避免用户多次|误点确认链接导致不必要的工作。
因为实际的令牌校验工作是在User模型中完成,所以视图函数要做的也就是调用confirm方法,然后根据结果闪现消息。如果确认成功,User模型的confirmed属性就会改变并被添加到会话中,在请求结束的时候会被提交到数据库更新。
程序可以自行决定未经确认的用户在确认帐户之前可以做什么。一个可能是允许未确认用户登录,但是只显示一个要求进行帐户确认的页面。
这一步可以使用Flask的before_request钩子,在第二章中有过描述。在蓝图中,before_request钩子只会响应本蓝图的请求。要想在蓝图中使用整个程序范围的钩子,就必须使用before_app_request装饰器了。例子8-22说明了如何实现这一处理器:
Example 8-22. app/auth/views.py: Filter unconfirmed accounts in before_app_request handler
@auth.before_app_request
def before_request():
if current_user.is_authenticated() and not current_user.confirmed and request.endpoint[:5] != 'auth.':
return redirect(url_for('auth.unconfirmed'))
@auth.route('/unconfirmed')
def unconfirmed():
if current_user.is_anonymous() or current_user.confirmed:
return redirect('main.index')
return render_template('auth/unconfirmed.html')
如果三个条件为真,before_app_request处理器将中断一个请求:
- 用户已登录(current_user.is_authenticated()的返回为True)
- 该用户帐户未经确认
- 请求的端点|结束点(可写作request.endpoint,
译注:也就是路由名称
)超出了认证蓝图(auth)的范围。要访问的这个认证蓝图的路由需要授权,比如那些允许用户确认帐户或执行其他帐户管理的函数(译注:迷糊了:Access to the authentication routes needs to be granted, as those are the routes that will enable the user to confirm the account or perform other account management functions.
)
如果这三个条件都符合,那个就会触发一个重定向,转到/auth/unconfirmed
路由,显示一个关于帐户确认信息的页面。
警告:before_request或before_app_request回调返回一个响应或者一个重定向,Flask将把它发送给客户端,而不会再调用与这一请求相关的视图函数。这样,回调就能在必要的时候中断请求。
显示的确认信息页面(图8-4)只是给未确认用户显示一个模板,介绍如何确认其帐户,并附加一个链接用户重新请求确认邮件——如原始确认邮件丢失。例子8-23路由将再次发送一个确认邮件:
Example 8-23. app/auth/views.py: Resend account confirmation email
@auth.route('/confirm')
@login_required
def resend_confirmation():
token = current_user.generate_confirmation_token()
send_email('auth/email/confirm','Confirm Your Account', user, token=token)
flash('A new confirmation email has been sent to you by email.')
return redirect(url_for('main.index'))
这个路由使用current_user(已登录用户)作为目标用户,再次重复了注册路由的工作(给用户发送确认邮件)。这个路由其也是被login_required装饰器保护的,这样确保访问路由的发送这一请求用户已登录。图8-4
帐户管理
程序的用户可能不时的需要更改自己的帐户信息,我们可以根据本章介绍的技术来给认证蓝图添加一些功能:
-
密码更改
具有安全意识的用户可能不定期更改自己的密码。这个功能很容易实现,因为一旦用户登录系统,我们可以比较安全的询问其旧密码和输入新密码,通过表单进行新旧替换即可。 -
密码重置
为了防止合法用户遗忘密码后被挡在系统之外,我们需要提供一个密码重置的选项。为了安全实现密码重试,需要像进行帐户确认那样使用一个加密令牌。这样,一旦用户请求密码重设,习题集就给注册的Emailizhi发送一个重设令牌。用户点击邮件中的链接,验证令牌通过后,通过表单输入新的密码即可完成重置。 -
变更Email地址
用户可能也需要更改其注册的email地址,同样需要通过一封确认邮件来验证这一新email地址。用户在表单中输入新地址然后系统就给新地址发送令牌。一旦服务器回收到令牌,就可以验证并更新用户对象了。在服务器等待回收令牌期间,他会把新地址保存到一个数据库新字段中(附加email地址),或者可以把心邮件地址和id一起保存到令牌中。
注:以上三个功能还是参照注册,确认帐户两步,利用current_user,分别生成token后进行更新即可
在下一章,我们将扩展用户子系统,加入用户角色。