用户认证

8.1 Flask的认证扩展

  • Flask-Login:管理已登录用户的用户会话。
  • Werkzeug:计算密码散列值并进行核对。
  • itsdangerous:生成并核对加密安全令牌。

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

  • Flask-Mail:发送与认证相关的电子邮件。
  • Flask-Bootstrap:HTML模板。
  • Flask-WTF:Web表单

8.2 密码安全性

使用Werkzeug实现密码散列

Werkzeug中的security模块能够很方便地实现密码散列值的计算。这一功能的实现只需要两个函数,分别用在注册用户和验证用户阶段。

  • generate_password_hash(password,method=pbkdf2:sha1,salt_length=8):这个函数将原始密码作为输入,以字符串的形式输出密码的散列值,输出的值可保存在用户数据库中。method和salt_length的默认值就能够满足大多数需求。
  • 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
from app import db

class Role(db.Model):
    __tablename__ = 'roles'
    id = db.Column(db.Integer,primary_key=True)
    name = db.Column(db.String(64),unique=True)
    users = db.relationship('User',backref='role',lazy='dynamic')

    def __repr__(self):
        return '<Role %r>' % self.name

class User(db.Model):
    __tablename__ = 'users'
    id = db.Column(db.Integer,primary_key=True)
    username = db.Column(db.String(64),unique=True,index=True)
    role_id = db.Column(db.Integer,db.ForeignKey('roles.id'))

    def __repr__(self):
        return '<User %r>' % self.username

    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中进行测试:


注意,即使用户u和u2使用了相同的密码,它们的密码散列值也完全不一样。为了确保这个功能今后可持续使用,可以把上述测试写成单元测试,以便于重复执行。要在tests包中新建一个模块,编写3个新测试,测试最近对User模型所做的修改,如示例8-2所示。

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

8.3 创建认证蓝本

把创建程序的过程移入工厂函数中,可以使用蓝本在全局作用域中定义路由。与用户认证系统相关的路由可在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蓝本和后续添加的蓝本发生模板命名冲突,可以把蓝本使用的模板保存在单独的文件夹中。

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

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

from flask import Flask,render_template
from flask_bootstrap import Bootstrap
from flask_mail import Mail
from flask_moment import Moment
from flask_sqlalchemy import SQLAlchemy
from config import config

bootstrap = Bootstrap()
mail = Mail()
moment = Moment()
db = SQLAlchemy()

def create_app(config_name):
    app = Flask(__name__)
    app.config.from_object(config[config_name])
    config[config_name].init_app(app)

    bootstrap.init_app(app)
    mail.init_app(app)
    moment.init_app(app)
    db.init_app(app)

    # attach routes and custom error pages here
    from .main import main as main_blueprint
    app.register_blueprint(main_blueprint)

    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认证用户

pip install flask-login

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

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

    这4个方法可以在模型类中作为方法直接实现,不过还有一种更简单的替代方案。Flask-Login提供了一个UserMixin类,其中包括这些方法的默认实现,且能满足大多数需求。修改后的User模型如示例8-6所示。

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

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

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

from flask_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)
    # ...
    LoginManager对象的session_protection属性可以设为None、'basic'或'strong',以提供不同的安全等级防止用户会话遭篡改。设为'strong'时,Flask-Login会记录客户端IP地址和浏览器的用户代理信息,如果发现异动就登出用户。login_view属性设置登录页面的端点。

    最后,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_login import login_required
@app.route('/secret')
@login_required
def secret():
    return 'Only authenticated users are allowed!'
如果未认证的用户访问这个路由,Flask-Login会拦截请求,把用户发往登录页面。

8.4.3 添加登录表单

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

from flask_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()宏渲染表单即可。

base.html模板中的导航条使用Jinja2条件语句,并根据当前用户的登录状态分别显示"Sign In"或"Sign Out"链接。这个条件语句如示例8-10所示。

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

{% extends "bootstrap/base.html" %}

{% block title %}Flasky{% endblock %}


{% block navbar %}
<div class="navbar navbar-inverse" role="navigation">
  <div class="container">
    <div class="navbar-header">
      <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
        <span class="sr-only">Toggle navigation</span>
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
      </button>
      <a class="navbar-brand" href="/">Flasky</a>
    </div>
    <div class="navbar-collapse collapse">
      <ul class="nav navbar-nav">
        <li><a href="/">Home</a></li>
      </ul>
      <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 %}
    </div>
  </div>
</div>
{% endblock %}

{% block content %}
<div class="container">
{% for message in get_flashed_messages() %}
<div class="alert alert-warning">
<button type="button" class="close" data-dismiss="alert">&times;</button>
{{ message }}
</div>
{% endfor %}
{% block page_content %}{% endblock %}
</div>
{% endblock %}


    判断条件中的变量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 . import auth
from ..models import User
from .forms import LoginForm
from flask_login import login_required,login_user

@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从数据库中加载用户。如果电子邮件地址对应的用户存在,在调用用户对象的verify_password()方法,其参数是表单中填写的密码。如果密码正确,则调用Flask-Login中的login_user函数,在用户会话中把用户标记为已登录。login_user()函数的参数是要登录的用户,以及可选的"记住我"布尔值,"记住我"也在表单中填写。如果值为False,那么关闭浏览器后用户会话就过期了,所以下次用户访问时要重新登录。如果值为True,那么会在用户浏览器中写入一个长期有效的cookie,使用这个cookie可以复现用户会话。

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

{% 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.4.5 登出用户

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

from flask_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'))
8.4.6 测试登录

示例8-14 app/templates/index.html: 为已登录的用户显示一个欢迎消息

{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}
{% block title %}Flasky{% endblock %}
{% block page_content %}
<div class="page-header">
<h1>Hello,
{% if current_user.is_authenticated() %}
  {{ current_user.username }}
{% else %}
  Stranger
{% endif %}!
</h1>
</div>
{% endblock %}


点击Sign Out退出到登陆页面.


8.5 注册新用户

8.5.1 添加用户注册表单

from flask_wtf import FlaskForm
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(FlaskForm):
    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,'Userna               mes 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()渲染表单。

{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}

{% block title %}Flasky - Register{% endblock %}

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


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

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

{% 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) }}
     <br>
     <p>
         New user?
         <a href="{{ url_for('auth.register') }}">
             Click here to register
         </a>
     </p>
</div>
{% endblock %}
8.5.2 注册新用户
示例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换成将相同信息安全加密后得到的令牌。

    Flask使用加密的签名cookie保护用户会话,防止被篡改。这种安全的cookie使用itsdangerous包签名。同样的方法也可用于确认令牌上。

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


    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):
    __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'))
    password_hash=db.Column(db.String(128))
    confirmed=db.Column(db.Boolean,default=False)

    def __repr__(self):
        return '<User %r>' % self.username

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

    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()方法生成一个令牌,有效期默认为一小时。confirm()方法检验令牌,如果检验通过,则把新添加的confirmed属性设为True。除了检验令牌,confirm()方法还检查令牌中的id是否和存储在current_user中的已登录用户匹配。如此一来,即使恶意用户知道如何生成签名令牌,也无法确认别人的账户。

执行数据库迁移。

python manage.py db migrate -m "initial migration1"
python manage.py db upgrade
很容易进行单元测试。

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():
        user = User(email=form.email.data,
                    username=form.username.data,
                    password=form.password.data)
        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('auth.login'))
    return render_template('auth/register.html', form=form)
注意,即便通过配置,程序已经可以在请求末尾自动提交数据库变化,这里也要添加db.session.commit()调用。问题在于,提交数据库之后才能赋予新用户id值,而确认令牌需要用到id,所以不能延后提交。

    认证蓝本使用的电子邮件模板保存在templates/auth/email文件夹中,以便和HTML模板区分开来。第6章介绍过,一个电子邮件需要两个模板,分别用于渲染纯文本正文和富文本正文。举个例子,示例8-20是确认邮件模板的纯文本版本。

示例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.
HTML版本:

<p>Dear {{ user.username }},</p>
<p>Welcome to <b>Flasky</b>!</p>
<p>To confirm your account please <a href="{{ url_for('auth.confirm', token=token, _external=True) }}">click here</a>.</p>
<p>Alternatively, you can paste the following link in your browser's address bar:</p>
<p>{{ url_for('auth.confirm', token=token, _external=True) }}</p>
<p>Sincerely,</p>
<p>The Flasky Team</p>
<p><small>Note: replies to this email address are not monitored.</small></p>
    默认情况下,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_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('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钩子完成,对蓝本来说,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:
        current_user.ping()
        if not current_user.confirmed \
                and request.endpoint \
                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')
同时满足以下3个条件时,before_app_request处理程序会拦截请求。

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

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

(3)请求的端点(使用request.endpoint获取)不在认证蓝本中。访问认证路由要获取权限,因为这些路由的作用是让用户确认账户或执行其他账户管理操作。

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

    如果before_request或before_app_request的回调返回响应或重定向,Flask会直接将其发送至客户端,而不会调用请求的视图函数。因此,这些回调可在必要时拦截请求。

显示给未确认用户的页面只渲染一个模板,其中又如何确认账户的说明,此外还提供了一个链接,用于请求发送新的确认邮件,以防之前的邮件丢失。重新发送确认邮件的路由如示例8-23所示。

示例8-23 app/auth/view.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保护,确保访问时程序知道请求再次发送邮件的是那个用户。

















评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值