优秀的Python身份验证包很多,但没有一个能实现所有功能。此部分介绍的身份验证方案将使用多个包,并编写胶水代码,让不同的包良好协作。此部分使用的包如下:
除了身份验证相关的包之外,此部分还将使用以下常规用途的扩展:
要保证数据库中用户密码的安全,关键在于不存储密码本身,而是存储密码的散列值。计算密码散列值的函数接受密码作为输入,输出一个和原始密码没有任何关系的字符序列。其中计算密码散列值的函数是可复现的,只要输入一样,结果就一样。
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,就表明密码是正确的。
备注:
下面是使用flask shell测试的过程:
注意:尝试访问password属性会返回AttributError。另外即使用户u1和u2使用了相同的密码,他们的密码散列值也完全不一样:
为了确保这个功能今后依然能使用,我们可以把上述手动测试的过程写成单元测试,以便重复执行:
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:
把创建应用实例的程序移入工厂函数,可以使用蓝本在全局作用域中定义路由。与用户认证相关的路由可在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 %}
Login
{% 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 返回如下界面: