10、密码扩展,使用Flask-Login认证用户
密码扩展
1、使用Werkzeug实现密码散列
在User模型中加入密码散列
app/models.py
计算密码散列值的函数通过名为password的只写属性实现,设定这个属性的值时,赋值方法会调用Werkzeug提供的generate_password_hash()函数,并把得到的结果赋值给password_hash字段。
如果试图读取password属性的值,则会返回错误,原因很明显,因为生成散列值后就无法还原成原来的密码了
from . import db from werkzeug.security import generate_password_hash, check_password_hash #定义数据库模型 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') 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')) password_hash = db.Column(db.String(128)) #在User模型中加入密码散列 @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 '<User %r>' %self.username
注:
把password方法变为属性只需要加上@property装饰器即可,此时@property本身又会创建另外一个装饰器@password.setter,负责把password方法变成给属性赋值
u.password = 'cat' 实际转化成 u.password('cat')
u.password 实际转化成 u.password()
所以当读取password值时,会调用 u.password(),会抛出异常
在shell中验证加入的密码散列功能
注:u1,u2即使使用了相同的密码,它们的密码散列值也完全不一样
把上述测试写成单元测试,以便于重复执行,我们在test包中新建一个模块,编写3个新测试,测试最近对User模型所做的修改
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)
2、创建认证蓝本
创建蓝本
app/auth/__init__.py
app/auth/views.py模块引入蓝本,然后使用蓝本的route修饰器定义与认证相关的路由
from flask import Blueprint #创建认证蓝本 auth = Blueprint('auth', __name__) from . import views
蓝本中的路由和视图函数
app/auth/views.py
添加一个/login路由,渲染同名占位模板
render_template()指定的模板文件保存在auth文件夹中,这个文件夹必须在app/templates中创建,因为Flask认为模板的路径是相对于程序模板文件夹而言的。
为避免与main蓝本和后序添加的蓝本发生模板命名冲突,可以把蓝本使用的模板保存在单独的文件夹中
from flask import render_template from . import auth #蓝本中的路由和视图函数 @auth.route('/login') def login(): return render_template('auth/login.html')
附加蓝本
app/__init__.py
url_prefix是可选参数,使用这个参数后,注册蓝本中定义的所有路由都会加上指定的前缀,本例中,/login路由会注册成/auth/login,在web服务器中,完整的URL就变成了http://locahost:5000/auth/login
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['default']) config['default'].init_app(app) bootstrap.init_app(app) mail.init_app(app) moment.init_app(app) db.init_app(app) #认证函数的附加蓝本 from .auth import auth as auth_blueprint app.register_blueprint(auth_blueprint, url_prefix='/auth') from .main import main as main_blueprint app.register_blueprint(main_blueprint) #附加路由和自定义的错误页面 return app
使用Flask-login认证用户
1、安装flask-login
2、准备用于登录的用户模型
要想使用Flask-login扩展,程序的User模型必须实现几个方法,这四个方法可以直接实现。另一种简单的替代方案,是使用Flask-Login提供的UserMixin类,其中包含这些方法的默认实现,并能满足大多数需求
2、修改User,支持用户登录
app/models.py
示例中还添加了email字段,在这个程序中,用户使用电子邮件地址登录,因为对于用户而言,用户更不容易忘记自己的电子邮件地址
from . import db from werkzeug.security import generate_password_hash, check_password_hash from flask_login import UserMixin #定义数据库模型 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') def __repr__(self): return '<Role %r>' %self.name class User(UserMixin, 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)) email = db.Column(db.String(64), unique=True, index=True) #在User模型中加入密码散列 @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 '<User %r>' %self.username
3、初始化Flask-Login
app/__init__.py
LoginManager对象的session_protection属性可以设置为None、‘basic’、‘strong’,以提供不同的安全等级防止用户会话遭篡改
设为‘strong’时,Flask-Login会记录客户端IP地址和浏览器的用户代理信息,如果发现异常就登出用户
login_view属性设置登录页面的端点
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
from flask_login import LoginManager
bootstrap = Bootstrap()
mail = Mail()
moment = Moment()
db = SQLAlchemy()
# 初始化Flask-Login
login_manager = LoginManager()
login_manager.session_protection = 'strong'
login_manager.login_view = 'auth.login'
def create_app(config_name):
app = Flask(__name__)
app.config.from_object(config['default'])
config['default'].init_app(app)
login_manager.init_app(app)
bootstrap.init_app(app)
mail.init_app(app)
moment.init_app(app)
db.init_app(app)
#认证函数的附加蓝本
from .auth import auth as auth_blueprint
app.register_blueprint(auth_blueprint, url_prefix='/auth')
from .main import main as main_blueprint
app.register_blueprint(main_blueprint)
#附加路由和自定义的错误页面
return app
4、加载用户的回调函数
Flask-Login要求程序实现一个回调函数,使用指定的标识符加载这个用户,这个函数定义如下
app/models.py
加载用户的回调函数接受Uicode字符串形式表示的用户标识符。如果能找到用户,这个函数必须返回用户对象,否则应该返回None
#加载用户回调函数 from . import login_manager @login_manager.user_loader def load_user(user_id): return User.query.get(int(user_id))
5、保护路由
如果未认证的用户访问这个路由, Flask-Login 会拦截请求,把用户发往登录页面。
from flask.ext.login import login_required @app.route('/secret') @login_required def secret(): return 'Only authenticated users are allowed!'
6、添加登录表单
app/auth/forms.py: 登录表单
包含一个用于输入电子邮件地址的文本字段、一个密码字段、一个“记住我”复选框和提交按钮
电子邮件字段用到了 WTForms 提供的 Length() 和 Email() 验证函数。 PasswordField 类表示属性为 type="password" 的 <input> 元素。 BooleanField 类表示复选框
from flask.ext.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')
7、导航条中的 Sign In 和 Sign Out 链接
app/templates/base.html
判断条件中的变量 current_user 由 Flask-Login 定义,且在视图函数和模板中自动可用。这个变量的值是当前登录的用户, 如果用户尚未登录,则是一个匿名用户代理对象。如果是匿名用户, is_authenticated() 方法返回 False。所以这个方法可用来判断当前用户是否已经登录
<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 %} </ul>
8、登入用户
视图函数 login() 的实现如下
app/auth/views.py 登录路由
这个视图函数创建了一个 LoginForm 对象 ,当请求类型是 GET 时,视图函数直接渲染模板,即显示表单。当表单在 POST 请求中提交时,Flask-WTF 中的 validate_on_submit() 函数会验证表单数据,然后尝试登入用户
from flask import render_template, redirect, request, url_for, flash from flask.ext.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) return redirect(request.args.get('next') or url_for('main.index')) flash('Invalid username or password.') return render_template('auth/login.html', form=form)
9、更新登录模板以渲染表单
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 %}
10、登出用户
退出路由的实现如下
app/auth/views.py 退出路由
为了登出用户,这个视图函数调用 Flask-Login 中的 logout_user() 函数,删除并重设用户会话。随后会显示一个 Flash 消息,确认这次操作,再重定向到首页,这样登出就完成了
from flask.ext.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'))
11、 测试登录
为验证登录功能可用,可以更新首页,使用已登录用户的名字显示一个欢迎消息
app/templates/index.html:为已登录的用户显示一个欢迎消息
Hello, {% if current_user.is_authenticated() %} {{ current_user.username }} {% else %} Stranger {% endif %}!
在这个模板中再次使用 current_user.is_authenticated() 判断用户是否已经登录