第八章 用户身份验证(一)

       优秀的Python身份验证包很多,但没有一个能实现所有功能。此部分介绍的身份验证方案将使用多个包,并编写胶水代码,让不同的包良好协作。此部分使用的包如下:

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

除了身份验证相关的包之外,此部分还将使用以下常规用途的扩展:

  • Flask-Mail:发送与身份验证相关的电子邮件;
  • Flask-Bootstrap:HTML模板;
  • Flask-WTF:web表单;

一. 密码安全性

        要保证数据库中用户密码的安全,关键在于不存储密码本身,而是存储密码的散列值。计算密码散列值的函数接受密码作为输入,输出一个和原始密码没有任何关系的字符序列。其中计算密码散列值的函数是可复现的,只要输入一样,结果就一样。

二. 使用Werkzeug计算密码散列值

Werkzeug中security模块能够很方便的实现密码散列值的计算:

  • generate_password_hash(password, method=pbkdf2:sha1, salt_length=8):将原始密码作为输入,以字符串形式输出密码的散列值;method和salt_length的默认值即可满足大多数需求;

  • check_password_hash(hash, password):比较散列值和用户输入的密码,返回值为True表示密码正确;

app/models.py:在User模型中加入密码散列:

from werkzeug.security import generate_password_hash, check_password_hash
from . import db


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

    def __repr__(self):
        return "" % self.username

        计算密码散列值的函数通过名为password的只写属性实现。设定这个属性的值时,赋值方法会调用Werkzeug 提供的 generate_password_hash() 函数,并将结果赋值给password_hash字段。如果试图读取password属性的值,则会返回错误。这样设计的原因很简单:因为生成散列值后将无法还原成原来的密码。

        verify_password方法接受一个参数(即密码),将 其 传 给 Werkzeug 提 供 的 check_ password_hash() 函数,和存储在 User 模型中的密码散列值进行比对。如果这个方法返回 True,就表明密码是正确的。

备注:

  • @property装饰器用来装饰类中的方法,使得方法可以向属性一样被调用,因此被装饰的方法只能有self一个参数。如,此处使用user.password既可以调用password()方法;
  • @xxx.setter是@property的副产品,其中xxx即为@property装饰的方法名。可以使用user.password=password来调用此方法为self.password_hash赋值;

下面是使用flask shell测试的过程:

第八章 用户身份验证(一)_第1张图片

注意:尝试访问password属性会返回AttributError。另外即使用户u1和u2使用了相同的密码,他们的密码散列值也完全不一样:

第八章 用户身份验证(一)_第2张图片

三. 密码散列单元测试

为了确保这个功能今后依然能使用,我们可以把上述手动测试的过程写成单元测试,以便重复执行:

tests/test_user_model.py:

import unittest
from app.models import User
from app import create_app, db


class UserModelTestCase(unittest.TestCase):
    def setUp(self):
        self.app = create_app('testing')
        self.app_context = self.app.app_context()
        self.app_context.push()
        db.create_all()

    def tearDown(self):
        db.session.remove()
        db.drop_all()
        self.app_context.pop()

    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):
        u1 = User(password='cat')
        u2 = User(password='cat')
        self.assertTrue(u1.password_hash != u2.password_hash)

运行我们之前注册的单元测试启动命令flask test:

第八章 用户身份验证(一)_第3张图片

四. 创建身份验证蓝本

        把创建应用实例的程序移入工厂函数,可以使用蓝本在全局作用域中定义路由。与用户认证相关的路由可在auth蓝本中定义。对于不同的程序功能,我们要使用不同的蓝本,这是保持代码整齐有序的好方法。

        我们使用蓝本的包构造文件创建蓝本,再从auth/views.py文件中引入路由,app/auth/__init__.py代码如下:

from flask import Blueprint

auth = Blueprint('auth', __name__)

from . import views

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

from flask import render_template
from . import auth


@auth.route('/login')
def login():
    return render_template('auth/login.html')

其中login()返回的模板文件在templates/auth/login.html中定义:

{% extends "base.html" %}

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

{% block page_content %}
    
{% endblock %}

注:Flask认为模板的路径是相对于程序模板文件而言。为避免与main蓝本和后续添加的蓝本发生模板命名冲突,可以把蓝本使用的模板保存在单独的文件夹中。

最后一步:记得在应用上注册auth蓝本,使路由生效。app/__init__.py:

def create_app(config_name):
    app = Flask(__name__)
    # 使用对象始化app.config
    app.config.from_object(config[config_name])
    config[config_name].init_app(app=app)

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

    # 注册蓝本:使蓝本中关联的路由和自定义错误界面生效
    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参数为可选参数,使用此参数,则注册后蓝本中定义的所有路由都会加上指定的前缀,如此处访问http://127.0.0.1:5000/auth/login 返回如下界面:

第八章 用户身份验证(一)_第4张图片

你可能感兴趣的:(Flask)