8.1 Flask的认证扩展

优秀的 Python 认证包很多,但没有一个能实现所有功能。本章介绍的认证方案使用了多个

包,并编写了胶水代码让其良好协作。


本章使用的包列表如下:

• Flask-Login:管理已登录用户的用户会话。

• Werkzeug:计算密码散列值并进行核对。

• itsdangerous:生成并核对加密安全令×××。


除了认证相关的包之外,本章还用到如下常规用途的扩展。

• Flask-Mail:发送与认证相关的电子邮件。

• Flask-Bootstrap:HTML 模板。

• Flask-WTF:Web 表单。


8.2 密码安全性

设计 Web 程序时,人们往往会高估数据库中用户信息的安全性。如果×××者***服务器获

取了数据库,用户的安全就处在风险之中,这个风险比你想象的要大。众所周知,大多数

用户都在不同的网站中使用相同的密码,因此,即便不保存任何敏感信息,×××者获得存

储在数据库中的密码之后,也能访问用户在其他网站中的账户。


若想保证数据库中用户密码的安全,关键在于不能存储密码本身,而要存储密码的散列

值。计算密码散列值的函数接收密码作为输入,使用一种或多种加密算法转换密码,最终

得到一个和原始密码没有关系的字符序列。核对密码时,密码散列值可代替原始密码,因

为计算散列值的函数是可复现的:只要输入一样,结果就一样。


使用Werkzeug实现密码散列


Werkzeug 中的 security 模块能够很方便地实现密码散列值的计算。这一功能的实现只需要

两个函数,分别用在注册用户和验证用户阶段。


• generate_password_hash(password, method=pbkdf2:sha1, salt_length=8):这个函数将

原始密码作为输入,以字符串形式输出密码的散列值,输出的值可保存在用户数据库中。

method 和 salt_length 的默认值就能满足大多数需求。


• check_password_hash(hash, password):这个函数的参数是从数据库中取回的密码散列

值和用户输入的密码。返回值为 True 表明密码正确。


示例 8-1 app/models.py:在 User 模型中加入密码散列

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)


计算密码散列值的函数通过名为 password 的只写属性实现。设定这个属性的值时,赋值

方法会调用 Werkzeug 提供的 generate_password_hash() 函数,并把得到的结果赋值给

password_hash 字段。如果试图读取 password 属性的值,则会返回错误,原因很明显,因

为生成散列值后就无法还原成原来的密码了。


verify_password 方 法 接 受 一 个 参 数( 即 密 码 ), 将 其 传 给 Werkzeug 提供的 check_

password_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.3 创建认证蓝本

我们在第 7 章介绍过蓝本,把创建程序的过程移入工厂函数后,可以使用蓝本在全局作用

域中定义路由。与用户认证系统相关的路由可在 auth 蓝本中定义。对于不同的程序功能,

我们要使用不同的蓝本,这是保持代码整齐有序的好方法。


auth 蓝本保存在同名 Python 包中。蓝本的包构造文件创建蓝本对象,再从 views.py 模块

中引入路由,代码如示例 8-3 所示。


示例 8-3 app/auth/__init__.py:创建蓝本

from flask import Blueprint
auth = Blueprint('auth', __name__)
from . import views

app/auth/views.py 模块引入蓝本,然后使用蓝本的 route 修饰器定义与认证相关的路由,

如示例 8-4 所示。这段代码中添加了一个 /login 路由,渲染同名占位模板。


示例 8-4 app/auth/views.py:蓝本中的路由和视图函数

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 蓝本和后续添加的蓝本发生模板命名冲突,可以把蓝本使用的模板保存在单独的

文件夹中。


我们也可将蓝本配置成使用其独立的文件夹保存模板。如果配置了多个模板

文件夹,render_template() 函数会首先搜索程序配置的模板文件夹,然后再

搜索蓝本配置的模板文件夹。


auth 蓝本要在 create_app() 工厂函数中附加到程序上,如示例 8-5 所示。


示例 8-5 app/__init__.py:附加蓝本

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,在开发 Web 服务器中,完整的 URL 就变成了 http://localhost:5000/auth/login。


8.4 使用Flask-Login认证用户

用户登录程序后,他们的认证状态要被记录下来,这样浏览不同的页面时才能记住这个状

态。Flask-Login 是个非常有用的小型扩展,专门用来管理用户认证系统中的认证状态,且

不依赖特定的认证机制。


使用之前,我们要在虚拟环境中安装这个扩展:

(venv) $ pip install flask-login


8.4.1 准备用于登录的用户模型


要想使用 Flask-Login 扩展,程序的 User 模型必须实现几个方法。需要实现的方法如表 8-1

所示:



表8-1 Flask-Login要求实现的用户方法


方  法                                         说  明

is_authenticated()              如果用户已经登录,必须返回 True,否则返回 False

is_active()                           如果允许用户登录,必须返回 True,否则返回 False。如果要禁用账户,可以返回 False

is_anonymous()                 对普通用户必须返回 False

get_id()                              必须返回用户的唯一标识符,使用 Unicode 编码字符串



这 4 个方法可以在模型类中作为方法直接实现,不过还有一种更简单的替代方案。FlaskLogin

提供了一个 UserMixin 类,其中包含这些方法的默认实现,且能满足大多数需求。


修改后的 User 模型如示例 8-6 所示。


示例 8-6 app/models.py:修改 User 模型,支持用户登录

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 字段。在这个程序中,用户使用电子邮件地址登录,因

为相对于用户名而言,用户更不容易忘记自己的电子邮件地址。



Flask-Login 在程序的工厂函数中初始化,如示例 8-7 所示。

示例 8-7 app/__init__.py:初始化 Flask-Login

from flask.ext.login import LoginManager


login_manager = LoginManager()
"""
    LoginManager 对象的session_protection 属性可以设为None、'basic' 或'strong',以提
    供不同的安全等级防止用户会话遭篡改。设为'strong' 时,Flask-Login 会记录客户端IP
    地址和浏览器的用户代理信息,如果发现异动就登出用户.
"""
login_manager.session_protection = 'strong' 

# login_view 属性设置登录页面的端点。
login_manager.login_view = 'auth.login' 

def create_app(config_name):
    # ...
    login_manager.init_app(app)
    # ...

回忆一下,登录路由在蓝本中定义,因此要在前面加上蓝本的名字。


最后,Flask-Login 要求程序实现一个回调函数,使用指定的标识符加载用户。这个函数的 定义如示例 8-8 所示。


示例 8-8 app/models.py:加载用户的回调函数

from . import login_manager

@login_manager.user_loader
def load_user(user_id):
   return User.query.get(int(user_id))


加载用户的回调函数接收以 Unicode 字符串形式表示的用户标识符。如果能找到用户,这

个函数必须返回用户对象;否则应该返回 None。


8.4.2 保护路由

为了保护路由只让认证用户访问,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 会拦截请求,把用户发往登录页面。


8.4.3 添加登录表单

呈现给用户的登录表单中包含一个用于输入电子邮件地址的文本字段、一个密码字段、一

个“记住我”复选框和提交按钮。这个表单使用的 Flask-WTF 类如示例 8-9 所示。

示例 8-9 app/auth/forms.py:登录表单

from flask.ext.wtf import Form
from wtforms import StringField, PasswordField, BooleanField, SubmitField
from wtforms.validators import Required, Length, 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')


电子邮件字段用到了 WTForms 提供的 Length() 和 Email() 验证函数。PasswordField 类表

示属性为 type="password" 的 <input> 元素。BooleanField 类表示复选框。


登录页面使用的模板保存在 auth/login.html 文件中。这个模板只需使用 Flask-Bootstrap 提

供的 wtf.quick_form() 宏渲染表单即可。登录表单在浏览器中渲染后的样子如图 8-1 所示。


base.html 模板中的导航条使用 Jinja2 条件语句,并根据当前用户的登录状态分别显示

“Sign In”或“Sign Out”链接。这个条件语句如示例 8-10 所示。

示例 8-10 app/templates/base.html:导航条中的 Sign In 和 Sign Out 链接


current_user.is_authenticated()  # 判断用户是否认证成功

<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。所以这个方法可用来判断当前用户是否

已经登录



8.4.4 登入用户

视图函数 login() 的实现如示例 8-11 所示。

示例 8-11 app/auth/views.py:登录路由

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 对象,用法和第 4 章中的那个简单表单一样。当请

求类型是 GET 时,视图函数直接渲染模板,即显示表单。当表单在 POST 请求中提交时,

Flask-WTF 中的 validate_on_submit() 函数会验证表单数据,然后尝试登入用户。



为了登入用户,视图函数首先使用表单中填写的 email 从数据库中加载用户。如果电子邮

件地址对应的用户存在,再调用用户对象的 verify_password() 方法,其参数是表单中填

写的密码。如果密码正确,则调用 Flask-Login 中的 login_user() 函数,在用户会话中把

用户标记为已登录。login_user() 函数的参数是要登录的用户,以及可选的“记住我”布

尔值,“记住我”也在表单中填写。如果值为 False,那么关闭浏览器后用户会话就过期

了,所以下次用户访问时要重新登录。如果值为 True,那么会在用户浏览器中写入一个长

期有效的 cookie,使用这个 cookie 可以复现用户会话。



按照第 4 章介绍的“Post/ 重定向 /Get 模式”,提交登录密令的 POST 请求最后也做了重定

向,不过目标 URL 有两种可能。用户访问未授权的 URL 时会显示登录表单,Flask-Login

会把原地址保存在查询字符串的 next 参数中,这个参数可从 request.args 字典中读取。

如果查询字符串中没有 next 参数,则重定向到首页。如果用户输入的电子邮件或密码不正

确,程序会设定一个 Flash 消息,再次渲染表单,让用户重试登录。



我们需要更新登录模板以渲染表单。修改内容如示例 8-12 所示。

示例 8-12 app/templates/auth/login.html:渲染登录表单

{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}
86 | 第 8 章
{% 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.4.5 登出用户

退出路由的实现如示例 8-13 所示。


示例 8-13 app/auth/views.py:退出路由

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() 函数,删除并重设用户

会话。随后会显示一个 Flash 消息,确认这次操作,再重定向到首页,这样登出就完成了。


8.5 注册新用户

如果新用户想成为程序的成员,必须在程序中注册,这样程序才能识别并登入用户。程序

的登录页面中要显示一个链接,把用户带到注册页面,让用户输入电子邮件地址、用户名

和密码。



8.5.1 添加用户注册表单

注册页面使用的表单要求用户输入电子邮件地址、用户名和密码。这个表单如示例 8-15

所示。



示例 8-15 app/auth/forms.py:用户注册表单

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 字段只包含字母、数字、

下划线和点号。这个验证函数中正则表达式后面的两个参数分别是正则表达式的旗标和验

证失败时显示的错误消息。


安全起见,密码要输入两次。此时要验证两个密码字段中的值是否一致,这种验证可使用

WTForms 提供的另一验证函数实现,即 EqualTo。这个验证函数要附属到两个密码字段中

的一个上,另一个字段则作为参数传入。


这个表单还有两个自定义的验证函数,以方法的形式实现。如果表单类中定义了以

validate_ 开头且后面跟着字段名的方法,这个方法就和常规的验证函数一起调用。


本例分别为 email 和 username 字段定义了验证函数,确保填写的值在数据库中没出现过。

自定义的验证函数要想表示验证失败,可以抛出 ValidationError 异常,其参数就是错误消息。

显示这个表单的模板是 /templates/auth/register.html。和登录模板一样,这个模板也使用

wtf.quick_form() 渲染表单。注册页面如图 8-3 所示。


登录页面要显示一个指向注册页面的链接,让没有账户的用户能轻易找到注册页面。改动

如示例 8-16 所示。

示例 8-16 app/templates/auth/login.html:链接到注册页面

<p>
 New user?
 <a href="{{ url_for('auth.register') }}">
 Click here to register
 </a>
</p>


8.5.2 注册新用户


处理用户注册的过程没有什么难以理解的地方。提交注册表单,通过验证后,系统就使用

用户填写的信息在数据库中添加一个新用户。处理这个任务的视图函数如示例 8-17 所示。


示例 8-17 app/auth/views.py:用户注册路由

@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)


8.6 确认账户

对于某些特定类型的程序,有必要确认注册时用户提供的信息是否正确。常见要求是能通

过提供的电子邮件地址与用户取得联系。


为验证电子邮件地址,用户注册后,程序会立即发送一封确认邮件。新账户先被标记成待

确认状态,用户按照邮件中的说明操作后,才能证明自己可以被联系上。账户确认过程

中,往往会要求用户点击一个包含确认令×××的特殊 URL 链接。


8.6.1 使用itsdangerous生成确认令×××

确认邮件中最简单的确认链接是 http://www.example.com/auth/confirm/<id> 这种形式的

URL,其中 id 是数据库分配给用户的数字 id。用户点击链接后,处理这个路由的视图函

数就将收到的用户 id 作为参数进行确认,然后将用户状态更新为已确认。

但这种实现方式显然不是很安全,只要用户能判断确认链接的格式,就可以随便指定 URL

中的数字,从而确认任意账户。解决方法是把 URL 中的 id 换成将相同信息安全加密后得

到的令×××。


回忆一下我们在第 4 章对用户会话的讨论,Flask 使用加密的签名 cookie 保护用户会话,

防止被篡改。这种安全的 cookie 使用 itsdangerous 包签名。同样的方法也可用于确认令

×××上。


下面这个简短的 shell 会话显示了如何使用 itsdangerous 包生成包含用户 id 的安全令×××:


(venv) $ python manage.py shell
>>> from manage import app
>>> from itsdangerous import TimedJSONWebSignatureSerializer as Serialize
>>> 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 签名(JSON Web Signatures,JWS)。这个类的构造函数接收

的参数是一个密钥,在 Flask 程序中可使用 SECRET_KEY 设置。

dumps() 方法为指定的数据生成一个加密签名,然后再对数据和签名进行序列化,生成令

×××字符串。expires_in 参数设置令×××的过期时间,单位为秒。

为了解码令×××,序列化对象提供了 loads() 方法,其唯一的参数是令×××字符串。这个方法

会检验签名和过期时间,如果通过,返回原始数据。如果提供给 loads() 方法的令×××不正

确或过期了,则抛出异常.


我们可以将这种生成和检验令×××的功能可添加到 User 模型中。改动如示例 8-18 所示。

示例 8-18 app/models.py:确认用户账户

from itsdangerous import TimedJSONWebSignatureSerializer as Serializer
from flask import current_app
from . import db
class User(UserMixin, db.Model):
     # ...
     confirmed = db.Column(db.Boolean, default=False)
     
     def generate_confirmation_token(self, expiration=3600):
         s = Serializer(current_app.config['SECRET_KEY'], expiration)
         return s.dumps({'confirm': self.id})
         
         
     def confirm(self, token):
         s = Serializer(current_app.config['SECRET_KEY'])
         try:
             data = s.loads(token)
         except:
             return False
             
         if data.get('confirm') != self.id:
             return False

         self.confirmed = True
         db.session.add(self)
         return True

generate_confirmation_token() 方法生成一个令×××,有效期默认为一小时。confirm() 方

法检验令×××,如果检验通过,则把新添加的 confirmed 属性设为 True。



除了检验令×××,confirm() 方法还检查令×××中的 id 是否和存储在 current_user 中的已登录

用户匹配。如此一来,即使恶意用户知道如何生成签名令×××,也无法确认别人的账户。



8.6.2 发送确认邮件


当前的 /register 路由把新用户添加到数据库中后,会重定向到 /index。在重定向之前,这

个路由需要发送确认邮件。改动如示例 8-19 所示。

示例 8-19 app/auth/views.py:能发送确认邮件的注册路由

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,所以不能延后提交。

认证蓝本使用的电子邮件模板保存在 templates/auth/email 文件夹中,以便和 HTML 模板

区分开来。第 6 章介绍过,一个电子邮件需要两个模板,分别用于渲染纯文本正文和富

文本正文。


示例 8-20 app/templates/auth/email/confirm.txt:确认邮件的纯文本正文

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.confirm', token='abc') 返

回的字符串是 '/auth/confirm/abc'。这显然不是能够在电子邮件中发送的正确 URL。相

对 URL 在网页的上下文中可以正常使用,因为通过添加当前页面的主机名和端口号,浏

览器会将其转换成绝对 URL。但通过电子邮件发送 URL 时,并没有这种上下文。添加到

url_for() 函数中的 _external=True 参数要求程序生成完整的 URL,其中包含协议(http://

或 https://)、主机名和端口。


确认账户的视图函数如示例 8-21 所示。

示例 8-21 app/auth/views.py:确认用户的账户

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'))


Flask-Login 提供的 login_required 修饰器会保护这个路由,因此,用户点击确认邮件中的

链接后,要先登录,然后才能执行这个视图函数。


这个函数先检查已登录的用户是否已经确认过,如果确认过,则重定向到首页,因为很

显然此时不用做什么操作。这样处理可以避免用户不小心多次点击确认令×××带来的额外

工作。


由于令×××确认完全在 User 模型中完成,所以视图函数只需调用 confirm() 方法即可,然后

再根据确认结果显示不同的 Flash 消息。确认成功后,User 模型中 confirmed 属性的值会

被修改并添加到会话中,请求处理完后,这两个操作被提交到数据库。



每个程序都可以决定用户确认账户之前可以做哪些操作。比如,允许未确认的用户登录,

但只显示一个页面,这个页面要求用户在获取权限之前先确认账户。


这一步可使用 Flask 提供的 before_request 钩子完成,我们在第 2 章就已经简单介绍过钩

子的相关内容。对蓝本来说,before_request 钩子只能应用到属于蓝本的请求上。若想在

蓝本中使用针对程序全局请求的钩子,必须使用 before_app_request 修饰器。示例 8-22 展

示了如何实现这个处理程序。


示例 8-22 app/auth/views.py:在 before_app_request 处理程序中过滤未确认的账户

@auth.before_app_request
def before_request():
     if current_user.is_authenticated() \
           and not current_user.confirmed \
           and request.endpoint[:5] != 'auth.'\
           and request.endpoint != 'static':
     return redirect(url_for('auth.unconfirmed'))
@auth.route('/unconfirmed')
def unconfirmed():
     if current_user.is_anonymous() or current_user.confirmed:
         return redirect(url_for('main.index'))
     return render_template('auth/unconfirmed.html')

同时满足以下 3 个条件时,before_app_request 处理程序会拦截请求。


(1) 用户已登录(current_user.is_authenticated() 必须返回 True)。

(2) 用户的账户还未确认。

(3) 请求的端点(使用 request.endpoint 获取)不在认证蓝本中。访问认证路由要获取权

限,因为这些路由的作用是让用户确认账户或执行其他账户管理操作。


如果请求满足以上 3 个条件,则会被重定向到 /auth/unconfirmed 路由,显示一个确认账户

相关信息的页面。




#  如果 before_request 或 before_app_request 的回调返回响应或重定向,Flask

#  会直接将其发送至客户端,而不会调用请求的视图函数。因此,这些回调可

#   在必要时拦截请求。


显示给未确认用户的页面(如图 8-4 所示)只渲染一个模板,其中有如何确认账户的说明,

此外还提供了一个链接,用于请求发送新的确认邮件,以防之前的邮件丢失。重新发送确

认邮件的路由如示例 8-23 所示。


示例 8-23 app/auth/views.py:重新发送账户确认邮件.

@auth.route('/confirm')
@login_required
def resend_confirmation():
     token = current_user.generate_confirmation_token()
     send_email(current_user.email, 'Confirm Your Account',
                 'auth/email/confirm', user=current_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.7 管理账户

拥有程序账户的用户有时可能需要修改账户信息。下面这些操作可使用本章介绍的技术添

加到验证蓝本中。


修改密码


安全意识强的用户可能希望定期修改密码。这是一个很容易实现的功能,只要用户处于

登录状态,就可以放心显示一个表单,要求用户输入旧密码和替换的新密码。


重设密码


为避免用户忘记密码无法登入的情况,程序可以提供重设密码功能。安全起见,有必要

使用类似于确认账户时用到的令×××。用户请求重设密码后,程序会向用户注册时提供的

电子邮件地址发送一封包含重设令×××的邮件。用户点击邮件中的链接,令×××验证后,会

显示一个用于输入新密码的表单。


修改电子邮件地址


程序可以提供修改注册电子邮件地址的功能,不过接受新地址之前,必须使用确认邮件

进行验证。使用这个功能时,用户在表单中输入新的电子邮件地址。为了验证这个地

址,程序会发送一封包含令×××的邮件。服务器收到令×××后,再更新用户对象。服务器收

到令×××之前,可以把新电子邮件地址保存在一个新数据库字段中作为待定地址,或者将

其和 id 一起保存在令×××中。