第8章:用户认证-Flask Web 开发(第2版)

大多数应用程序需要都需要知道当前用户是谁。当用户与应用程序连接时,他们通过身份验证来标识身份。一旦应用程序知道用户是谁,就可以为其提供专属的服务。

最常用的身份验证方法是,让用户提供一份身份证明(通常是他们的电子邮件地址或用户名),以及只有他们知道的密码。本章将创建一个完整的 Flasky 用户认证系统。

Flask 的用户认证扩展

Python 有许多优秀软件包用来做认证,但它们都没有做任何事情。本章介绍的用户认证解决方案使用多个软件包,并将它们进行整合。下面列示了本章使用的软件包的名字及其作用:

  • Flask-Login:管理已登录用户的 Session
  • Werkzeug:加密并验证密码
  • itsdangerous:生成和验证安全令牌密文

除了特定于认证的软件包之外,还将使用以下通用扩展:

  • Flask-Mail:发送验证邮件
  • Flask-Bootstrap:HTML 模板
  • Flask-WTF:Web 表单

密码安全

为了保证存储在数据库里的用户信息安全,需要对保存在数据库中的用户密码进行加密存储。关键点在于,使用密码原文的哈希值加上一个随机盐值进行加密。

使用 Werkzeug 对密码进行加密

Werkzeug 的 security 模块提供了一个对密码进行安全加密的哈希实现。实现密码加密功能,只需要两个函数:生成密文及验证密码。

generate_password_hash(password, method='pbkdf2:sha256', salt_length=8) 函数用于生成密码的密文。它接收一个密码明文参数,返回加密后的密码的密文。

check_password_hash(hash, password) 函数用于验证密码。第一个参数是密码的密文,第二个参数是密码的明文。如果密码的明文和密文相匹配,返回 True

示例8-1对第5章的 User 模型进行修改,使其能保存用户密码。

# 示例 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.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,说明密码正确。

如果你从 Github 克隆了示例代码,可以使用 git checkout 8a 签出本示例。

密码加密功能至此已完成,现在可以在 Shell 中对其进行测试了:

(venv) $ flask shell
>>> u = User()
>>> u.password = 'cat'
>>> u.password
Traceback (most recent call last):
  File "<console>", line 1, in <module>
  File "/home/flask/flasky/app/models.py", line 24, in password
    raise AttributeError('密码是个只写属性')
AttributeError: 密码是个只写属性
>>> u.password_hash
'pbkdf2:sha256:50000$moHwFH1B$ef1574909f9c549285e8547cad181c5e0213cfa44a4aba4349fa830aa1fd227f'
>>> u.verify_password('cat')
True
>>> u.verify_password('dog')
False
>>> u2 = User()
>>> u2.password = 'cat'
>>> u2.password_hash
'pbkdf2:sha256:50000$Pfz0m0KU$27be930b7f0e0119d38e8d8a62f7f5e75c0a7db61ae16709bcaa6cfd60c44b74

虽然 uu2 的密码明文是一样的(都是"cat"),但由于盐值不同,生成的密码密文是不一样的。

示例8-2定义了三个测试,用于对 User 模型新加的功能进行测试:

# 示例 8-2. tests/test_user_model.py: 测试密码安全
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)

运行 flask test

(venv) $ flask test
test_app_exists (test_basics.BasicsTestCase) ... ok
test_app_is_testing (test_basics.BasicsTestCase) ... ok
test_no_password_getter (test_user_model.UserModelTestCase) ... ok
test_password_salts_are_random (test_user_model.UserModelTestCase) ... ok
test_password_setter (test_user_model.UserModelTestCase) ... ok
test_password_verification (test_user_model.UserModelTestCase) ... ok

.----------------------------------------------------------------------
Ran 6 tests in 0.379s

OK

创建用户认证蓝图

本节将为创建第二个蓝图,用于用户身份认证子系统,这个蓝图名为 auth。为不同的子系统创建单独的蓝图是保证项目良好组织的好方法。

auth 将保存在同名的 Python 包里。该包的构造器将创建一个新蓝图,并从同一个包中的 views 模块中导入路由。

# 示例 8-3. app/auth/__init__.py: 创建用户认证蓝图
from flask import Blueprint

auth = Blueprint('auth', __name__)

from . import views

示例8-4展示了 auth 蓝图的路由及视图函数所在 app/auth/views.py 模块的内容。

# 示例 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')

auth 蓝图需要在 create_app() 工厂函数里注册:

# 示例 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

register_blueprint()url_prefix 参数是可选的。如果设置了该参数,那么该蓝图里的所有路由都将加上该值作为前缀。本例中,使用 /auth,那么蓝图中的 /login 路由最终会变成:/auth/login

如果你从 Github 克隆了示例代码,可以使用 git checkout 8b 签出本示例。

使用 Flask-Login 认证用户

当用户登录到应用后,他的认证状态将保存到 Session 中,所以即使是在不同页面间跳转,认证状态都能得到维持。Flask-Login 是一个简单实用的用户认证管理系统。使用以下命令安装 Flask-Login:

(venv) $ pip install flask-login

为用户登录准备模型

Flask-Login 和应用程序的 User 对象紧密相连。要让 User 模型能和 Flask-Login 正常工作,需要对模型类进行扩展,使其实现 Flask-Login 要求的一些属性和方法。下表列示了 Flask-Login 需要的元素:

属性或方法 说明
is_authenticated 如果用户登录成功,必须返回 True;否则返回 False
is_active 如果允许用户登录,返回 True;否则返回 False
is_anonymous 对于常规用户(需要登录的用户)必须返回 False;对于匿名用户,返回 True
get_id() 必须返回一个唯一标识,用于标识用户。返回值必须是一个 Unicode 字符串

这些属性和方法可以直接在模型类中逐一手动实现。不过,借助 Flask-Login 提供的 UserMixin 类,可以简化操作。

# 示例 8-6. app/models.py: 更新User模型,以支持用户登录
from flask_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 视为用户名。

接下来,应该在 create_app() 工厂函数中,对 Flask-Login 进行初始化。

# 示例 8-7. app/__init__.py: Flask-Login 初始化
from flask_login import LoginManager

login_manager = LoginManager()
login_manager.login_view = 'auth.login'

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

login_manager.login_view 用来设置登录视图函数。当用户未登录而尝试访问一些需要登录之后才能访问的页面时,Flask-Login 会重定向到这个属性设置的视图函数。由于 login 视图函数是在 auth 蓝图中设置的,所以这里要加上蓝图名。

最后,Flask-Login 要求应用程序定义一个函数,用于 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))

login_manager.user_loader 装饰器用于将函数注册为 Flask-Login 的用户加载器。它将在需要获取已登录用户的信息时,由 Flask-Login 自动调用。其参数是一个字符串类型的用户ID,由于我们的数据库中将用户ID设计成了整数(db.Integer),所以需要在通过 Flask-SQLAlcemy 查询用户数据时,将该参数转换成整数。这个函数必须返回一个 User 模型类的实例,如果该ID的用户不存在,则应返回 None

保护路由

为了限制某些页面只允许已登录的用户访问,Flask-Login 提供了 login_required 装饰器。通过该装饰器可以对只允许已登录用户访问的路由进行保护。

from flask_login import login_required

@app.route('/secret')
@login_required
def secret():
    return '只允许已登录用户访问!'

注意此例中的 secret() 函数使用了多个装饰器,这称为装饰器链。其执行顺序由靠着函数名最近的装饰器开始,逐一执行。本例中,装饰器链的执行顺序是: login_required -- app.route。必须保证 login_required 第一执行,否则会出现错误。

添加登录表单

登录表单由 Email 文本框、密码框、“记住我” 复选框和提交按钮组成。示例8-9是是该表单的表单类:

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

from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, BooleanField, SubmitField
from wtforms.validators import DataRequired, Length, Email

class LoginForm(FlaskForm):
    email = StringField('Email', validators=[DataRequired(), Length(1, 64),
                                             Email()])
    password = PasswordField('密码', validators=[DataRequired()])
    remember_me = BooleanField('让我保持登录')
    submit = SubmitField('登录')

定义好表单类之后,可以在其模板中显示了。通过 Flask-Bootstrap 的 wtf.quick_form() 宏,可以快速的在模板 app/templates/auth/login.html 中显示该表单。

基础模板 base.html 的导航栏,也要加上“登录”和“注销”两个链接。如示例8-10所示:

<!-- 示例 8-10. app/templates/base.html: 导航栏中的“登录”和“注销”两个链接-->
<ul class="nav navbar-nav navbar-right">
    {% if current_user.is_authenticated %}
    <li><a href="{{ url_for('auth.logout') }}">注销</a></li>
    {% else %}
    <li><a href="{{ url_for('auth.login') }}">登录</a></li>
    {% endif %}
</ul>

current_user 由 Flask-Login 提供。它会自动将该变量的值设置为当前已登录的用户。这个变量在模板和视图函数中都可以直接使用。它的 is_authenticated 用于标识当前用户是已登录用户(True)或是未登录的匿名用户(False)。

用户登录

示例 8-11 展示了用户登录视图函数 login() 的实现:

# 示例 8-11. app/auth/views.py: 用户登录路由
from flask import render_template, redirect, request, url_for, flash
from flask_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)
            next = request.args.get('next')
            if next is None or not next.startswith('/'):
                next = url_for('main.index')
            return redirect(next)
        flash('错误的用户名或密码.')
    return render_template('auth/login.html', form=form)

Flask-Login 的 login_user() 函数用于将当前已登录的用户记录到 Session 中。第一个参数是当前用户的实例对象,第二个参数是可选的,用于标识是否记住该用户。如果第二个参数为 True,则即使关闭浏览器,该用户也保持登录状态——即通常说的“记住我”或“自动登录”功能。默认,登录状态会保持一年,可以通过 REMEMBER_COOKIE_DURATION 配置设置其它时间值。

警告:在生产环境中应该启用安全的 HTTP(即 HTTPS)。因为在 HTTP 中,包括用户密码在内的所有数据都是明文传输的,敏感数据可以被轻易截获;而 HTTPS 中则以加密形式进行传输。有关配置 HTTPS 的内容,请关注本站推出的视频教程。

登录模板需要进行修改,如示例8-12所示。

<!-- 示例 8-12. app/templates/auth/login.html: 登录表单的模板 -->
{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}

{% block title %}Flasky - 登录{% endblock %}

{% block page_content %}
<div class="page-header">
    <h1>登录</h1>
</div>
<div class="col-md-4">
    {{ wtf.quick_form(form) }}
</div>
{% endblock %}

用户注销登录

示例8-13展示用户注销登录的实现。

# 示例 8-13. app/auth/views.py: 注销登录路由
from flask_login import logout_user, login_required

@auth.route('/logout')
@login_required
def logout():
    logout_user()
    flash('你已成功登出.')
    return redirect(url_for('main.index'))

Flask-Login 的 logout_user() 函数将 Session 进行删除和重置,以实现用户注销登录的功能。

如果你从 Github 克隆了示例代码,可以使用 git checkout 8c 签出本示例。

测试登录

为了验证登录功能是否正常工作,首页可以修改为通过用户的名字输出问候信息。

<!-- 示例 8-14. app/templates/index.html: 问候已登录用户 -->
你好,
{% if current_user.is_authenticated %}
    {{ current_user.username }}
{% else %}
    陌生人
{% endif %}!

该模板再次使用了 current_user.is_authenticated 来判断用户是否登录。

由于还没有用户注册功能,所以暂时只能在 Shell 里创建一个新用户来测试登录功能。

(venv) $ $ flask shell
>>> u = User(email='[email protected]', username='john', password='cat')
>>> db.session.add(u)
>>> db.session.commit()

用户注册

当一个新用户要成为应用的新成员时,需要进行注册并能够登录。登录页面增加一个指向注册页面的链接,以方便用户注册。

增加用户注册表单

# 示例 8-15. app/auth/forms.py: 用户注册表单
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, BooleanField, SubmitField
from wtforms.validators import DataRequired, Length, Email, Regexp, EqualTo
from wtforms import ValidationError
from ..models import User

class RegistrationForm(FlaskForm):
    email = StringField('Email', validators=[DataRequired(), Length(1, 64),
                                             Email()])
    username = StringField('用户名', validators=[
        DataRequired(), Length(1, 64),
        Regexp('^[A-Za-z][A-Za-z0-9_.]*$', 0,
               '用户名只能包含字母、数字、点或下划线')])
    password = PasswordField('密码', validators=[
        DataRequired(), EqualTo('password2', message='两次输入的密码不一致')])
    password2 = PasswordField('重复密码', validators=[DataRequired()])
    submit = SubmitField('注册')

    def validate_email(self, field):
        if User.query.filter_by(email=field.data).first():
            raise ValidationError('Email已经被注册了')

    def validate_username(self, field):
        if User.query.filter_by(username=field.data).first():
            raise ValidationError('用户名已经被注册了')

该表单类使用 WTForms 的 Regexp(正则表达式)验证器来确保 username 只能以字母、数字、点或下划线开头。

WTForms 的 EqualTo 验证器用来确保两次输入的密码是相同的。

该表单类还有两个方法,用于实现自定义验证。自定义验证总是以 validate_<字段名> 的格式命名。当验证失败时,自定义验证应该抛出 ValidationError 异常。

用户注册的模板位于 app/templates/auth/register.html,和登录页面类似,该模板只需要简单的使用 Flask-Bootstrap 提供的 wtf.quick_form() 即可快速完成表单的渲染。

用户登录页面需要加上用户注册链接。

<!-- 示例 8-16. app/templates/auth/login.html: 链接到注册页面 -->
<p>
    新用户?
    <a href="{{ url_for('auth.register') }}">
        点击这里进行注册
    </a>
</p>

新用户注册

新用户注册功能实现起来并不难。当用户提交表单后,将用户从注册表单提交的信息保存到数据库中即可。示例 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)
        db.session.commit()
        flash('注册成功。你现在可以登录了')
        return redirect(url_for('auth.login'))
    return render_template('auth/register.html', form=form)

如果你从 Github 克隆了示例代码,可以使用 git checkout 8d 签出本示例。

用户确认

为了保证注册用户的信息有效,许多应用都会对用户进行确认。其中常用的方式之一就是发送一封带有确认链接的邮件到用户填写的Email中。只有用户点击了该链接,用户才能完成注册并进行登录。

使用 itsdangerous 生成确认令牌

最简单的确认链接的格式是 http://www.example.com/auth/confirm/<id>,其中的 <id> 是该用户保存在数据库中的主键值。当确认链接的视图函数接收到该 id 参数后,可以对数据库进行更新,使该用户的状态更新为“已确认”。

但这种方式明显不安全,用户完全可以通过该 URL 猜测到实现原理,进而通过使用随机数作为 id 值来进行批量确认。

回顾第4章所讨论的用户 Session,Flask 使用加密的已签名的 Cookie 来保护 Session 数据,防止数据被篡改。Cookie 包含一个生成签名密文的 itsdangerous 包。如果 Session 的内容被篡改,这个签名将会验证失败,那么 Flask 将丢失这个 Session,并生成一个新的 Session。这一概念同样适用于确认令牌。

下面的 Shell 会话展示了,如何使用 itsdangerous 来生成带有用户 ID 的、已签名的确认令牌。

(venv) $ flask shell
>>> 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
{'confirm': 23}

itsdangerous 包提供多种令牌生成器。其中的 TimedJSONWebSignatureSerializer 类用于生成带过期时间的 JSON Web 签名(JWSs)。这个类的构造函数需要一个密钥,在 Flask 应用中,可以使用配置中的 SECRET_KEYexpires_in 参数指定令牌的过期时间,单位是秒。

dump() 方法,将接收到的参数作为原始数据,并为其生成加密签名,然后将数据和签名序列化方便使用的令牌字符串。

通过 loads() 方法,可对令牌进行解码,它所需要的唯一参数就是令牌字符串。该方法会对签名及过期时间进行验证,只有两者都通过验证,才会返回原始数据;否则抛出异常。

确认令牌的生成和验证功能,可以添加到 User 模型类中。

# Example 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}).decode('utf-8')

    def confirm(self, token):
        s = Serializer(current_app.config['SECRET_KEY'])
        try:
            data = s.loads(token.encode('utf-8'))
        except:
            return False
        if data.get('confirm') != self.id:
            return False
        self.confirmed = True
        db.session.add(self)
        return True

generate_confirmation_token() 方法根据用户 ID 生成有效期为 1 小时的确认令牌;confirm() 方法用于验证确认令牌的有效性。同时,该方法还会对确认令牌里的用户 ID 和存储在 current_user 中的当前登录系统的用户 ID 进行比对,只有两个 ID 一致,才会对数据库进行更新。这样,能确保验证令牌不能用于其它用户。

由于新版的 User 模型添加了 confirmed 字段,所以需要使用 Flask-Migrate 做一次数据库结构更新

发送确认邮件

当前的 /auth/register 在把新用户添加到数据库后,会重定向到 /index。现在,该路由在重定向之前,需要先执行发送确认邮件的操作。

# 示例 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, '确认你的账户',
                   'auth/email/confirm', user=user, token=token)
        flash('一封确认邮件已经发送到你的邮箱中。')
        return redirect(url_for('main.index'))
    return render_template('auth/register.html', form=form)

原书正文描述的是重定向到 /index,但书中之前列出的用户注册视图函数里,重定向的却是 /auth/login。同时,示例8-19代码中写的是又重定向到 /index。本站对此作出提醒,并沿用原书中的描述,重定向到 /index ,即 main.index

注意,在生成确认令牌前,就需要将数据保存到数据库中。不然,无法获取到用户的 ID。

邮件模板存储在 auth 蓝图子目录下。为了和普通模板作区分,特将其放在 email 子目录下,即 app/templates/auth/email/

# 示例 8-20. app/templates/auth/email/confirm.txt: 确认邮件的正文
亲爱的 {{ user.username }},

欢迎来到 Flasky!

请点击下面的链接来确认你的账号:

{{ url_for('auth.confirm', token=token, _external=True) }}

此致,

Flasky 团队

提示: 请不要直接回复本邮件.

默认情况下,url_for() 生成的是相对URL,比如:url_for('auth.confirm', token='abc') 生成的是 /auth/confirm/abc。这种相对 URL 发送到邮箱里,是无效的。我们需要生成带有域名和协议的绝对 URL。此时,需要给 url_for() 传入关键字参数 _external=True

# 示例 8-21. app/auth/views.py: 确认用户账号
from flask_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):
        db.session.commit()
        flash('你的账号确认成功,谢谢!')
    else:
        flash('确认链接无效或已过期.')
    return redirect(url_for('main.index'))

这个路由通过 login_required 装饰器进行了保护,所以需要用户先进行登录。

该功能首先检查登录用户是否已经确认,并在这种情况下重定向到主页,显然没有什么可做的。 如果用户错误地点击确认令牌多次,这可以防止不必要的工作。

由于令牌的验证实际上是在 User 模型中完成的,视图函数 confirm() 只需要调用模型中的相关方法,并根据返回的结果来设置不同的闪现消息即可。一旦确认成功,User 模型中的 confirmed 属性将被设置为 True,并将其更新到 Session 中,同时保存到数据库里。

应用程序可以按需决定未确认的用户可以执行哪些操作。一种可能性是允许未经确认的用户登录,但只向他们显示一个页面,要求他们确认他们的账户,然后才能进一步操作。

这个功能可以借助第2章讨论的 Flask 的 before_request 钩子来实现。在蓝图中,before_request 只对该蓝图有效,要使整个应用都有效,需要使用 before_app_request 钩子。

# 示例 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.blueprint != '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')

before_app_request 处理会拦截满足以下条件之一的请求:

  • 用户已登录 current_user.is_authenticated is True
  • 用户账号未确认
  • 请求的 URL 不属于 auth 蓝图并且请求的 URL 不是一个静态文件
# 示例 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, '确认你的账号',
               'auth/email/confirm', user=current_user, token=token)
    flash('一封新的确认邮件已发送到你的邮箱.')
    return redirect(url_for('main.index'))

如果你从 Github 克隆了示例代码,可以使用 git checkout 8e 签出本示例。

账号管理

用户可能需要不时地更改账号信息。可以使用本章介绍的技术将以下任务添加到身份验证蓝图中:

  • 修改密码
  • 重置密码
  • 修改 Email 地址

本文并非《Flask Web Development(2nd edition)》的全文翻译。而只是在阅读该书时,对自认为重点的内容进行记录以及思考。由于水平有限,本文所述内容难免出现不足或错误。请通过购买正版图书来进行更系统地学习,原书地址。本站将依照本系列文章录制视频教程,届时本文未作记录的部分以及项目实际开发中所需要的知识都会在视频教程中进行补充。

flask Flask Web Development(2nd edition) 2018-03-27 14:15 3820283