学习Flask之八、用户授权
大部分的应用都要跟踪它的用户是谁。当用户连接应用时,通常需要通过应用授权,这是一个身份识别的过程。一旦应用知道用户是谁,就可以提供定制化的体验。授权最常见的方法是用户提供标识(Email或用户名)和密码。本章,我们创建完全的授权系统。
Flask的授权扩展
有许多优秀的Python授权包,但是它们都不是万能的。本章呈现的用户授权方案使用多个包来提供粘合剂使它们一起工作。下面是需要使用的包的列表:
• Flask-Login: 管理登入的用户的用户会话
• Werkzeug: 密码加密和确认
• itsdangerous: 产生加密的安全标识并确认。
除了授权特定的包,如下的通用扩展会使用:
• Flask-Mail: 发送授权相关的邮件
• Flask-Bootstrap: HTML模板
• Flask-WTF: 网页表单
密码安全
在开发网络应用时存贮于数据库的用户安全通常被忽略。如果攻击者能进入你的服务器并访问你的用户数据库,那么你有使你的用户安全受到威协的风险,这个风险远大于你的想像。已知的事实是许多用户在多个网站使用相同的密码,所以即便你不存贮任何敏感信息,访问存贮于你的数据库里的密码会使攻击者访问你的用户在其它网站的帐户成为可能。安全地在数据库里存贮用户密码不只取决于密码本身还需要对它进行加密。密码加密函数取用户密码为参数并对它进行一个或多个加密转换。结果是一个新的字符串它不体现原始的密码。密码加密可以用真实的密码进行确认,因为加密函数是可重复的:给定相同的输入,结果总是相同的。密码加密是复杂的工作,很难做好。建议你不要实施自已的解决方案而是使用知名的被社区审核过的库。 如果你对安全密码的产生感兴趣你可以看一下 Salted Password Hashing - Doing it Right这篇文章。
用Werkzeug进行密码加密
Werkzeug的安全模块通常进行安全密码加密。这个功能只提供两个函数,用于注册和确认阶段,它们分别是:
• generate_password_hash(password, method=pbkdf2:sha1, salt_length=8):
这个函数取明文密码并返回加密字符,这个字符可以存贮在数据库里。对于大部分使用情况,黙认的method和salt_length已经足够了。
• check_password_hash(hash, password): 这个函数取存贮于数据库的加密字符以及用户输入的密码,返回True表明密码是正确的。
Example 8-1 展示更新的用户模型以适应密码加密。
Example 8-1. app/models.py: Password hashing in User model
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)
密码加密函数的通过称为密码的只写属性实施。当这个属性设置,setter方法会调用Werkzeug的generate_password_hash()函数并将结果写入password_hash域。试图读取密码属性会返回错误,很明显一旦加密原始的密码不能还原。verify_password方法取取密码并把它传给 Werkzeug的check_password_hash()函数以确认存贮于用户模型中的加密版本。 如果方法返回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会有完全不同的密码加密,即使它们使用相同的密码。要确何这个功能能在以后持续工作,上面的测试可以写作单元测试以便重复。
Example 8-2测试包里的一个新的模块展示三个新的测试对应用户模型的当前更前。
Example 8-2. tests/test_user_model.py: Password hashing tests
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)
创建一个授权Blueprint
第七章介绍了将Blueprints作为一种将应用创建移到工厂函数之后在全局范围内定义路由的方法。与用户授权系统相关的路由可以添加到auth blueprint。为不同的应用功能使用不同的blueprints是保持代码整洁的很好的方法。auth blueprint将存在于同名的python包。这个blueprint包构造函数创建blueprint对象并从views.py模块导入路由。这展示于 Example 8-3。
Example 8-3. app/auth/__init__.py: Blueprint creation
from flask import Blueprint
auth = Blueprint('auth', __name__)
from . import views
Example 8-4里展示的app/auth/views.py模块导入blueprint 并定义与授权相关的路由,使用路由装饰器。现在添加一个/login路由,它宣染同名模板的占位符。
Example 8-4. app/auth/views.py: Blueprint routes and view functions
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 希望模板与应用的模板文件夹相关。通过将blueprint模板放在它们自已的文件夹,不会与主blueprint 或其它未加添加的blueprints有名字冲突。Blueprints也可以配置到它们自已独立的文件夹。当配置多个模板文件夹时,render_template()函数会先查找应用的模板文件夹配置,然后是blueprints的模板文件夹配置。auth blueprint需要附着于create_app()工厂函数里的应用,见Example 8-5。
Example 8-5. app/__init__.py: Blueprint attachment
def create_app(config_name): </