flask二十三: 用户登录认证
一.密码存储和登录接口
M:
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)
generate_password_hash:生成密码散列值
check_password_hash:接收用户输入的密码,和存储在 User 模型中的密码散列值进行比对。如果这个方法返回True,就表明密码是正确的。
注意,即使用户 u 和 u2 使用了相同的密码,它们的密码散列值也完全不一样。为了确保 这个功能今后可持续使用
V:
from flask import Blueprint auth = Blueprint('auth', __name__) from . import views
from flask import render_template from . import auth @auth.route('/login') def login(): return render_template('auth/login.html')
def create_app(config_name): # ... # 原先的其他代码 from .auth import auth as auth_blueprint app.register_blueprint(auth_blueprint, url_prefix='/auth') return app
二.使用Flask-Login认证用户
用户登录程序后,他们的认证状态要被记录下来,这样浏览不同的页面时才能记住这个状
态。Flask-Login 是个非常有用的小型扩展,专门用来管理用户认证系统中的认证状态,且
不依赖特定的认证机制。
使用之前,我们要在虚拟环境中安装这个扩展:
(venv) $ pip install flask-login
1.准备
要想使用 Flask-Login 扩展,程序的 User 模型必须实现几个方法:
方 法 说 明
is_authenticated() 如果用户已经登录,必须返回 True,否则返回 False
is_active() 如果允许用户登录,必须返回 True,否则返回 False。如果要禁用账户,可以返回 False
is_anonymous() 对普通用户必须返回 False
get_id() 必须返回用户的唯一标识符,使用 Unicode 编码字符串
这 4 个方法可以在模型类中作为方法直接实现,不过还有一种更简单的替代方案。FlaskLogin
提供了一个 UserMixin 类,其中包含这些方法的默认实现,且能满足大多数需求
2.修改M:
from flask.ext.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')) @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)
3.注册flask-login
from flask.ext.login import LoginManager login_manager = LoginManager() """ LoginManager 对象的session_protection 属性可以设为None、'basic' 或'strong',以提 供不同的安全等级防止用户会话遭篡改。设为'strong' 时,Flask-Login 会记录客户端IP 地址和浏览器的用户代理信息,如果发现异动就登出用户. """ login_manager.session_protection = 'strong' # login_view 属性设置登录页面的端点。 login_manager.login_view = 'auth.login' def create_app(config_name): # ... login_manager.init_app(app)
4.增加回调函数:使用指定的标识符加载用户
Flask-Login 要求程序实现一个回调函数,使用指定的标识符加载用户。
model.py
from . import login_manager @login_manager.user_loader def load_user(user_id): return User.query.get(int(user_id))
加载用户的回调函数接收以 Unicode 字符串形式表示的用户标识符。如果能找到用户,这
个函数必须返回用户对象;否则应该返回 None。
5.保护路由:只让认证用户访问
为了保护路由只让认证用户访问,Flask-Login 提供了一个 login_required 修饰器。
from flask.ext.login import login_required @app.route('/secret') @login_required def secret(): return 'Only authenticated users are allowed!'
如果未认证的用户访问这个路由,Flask-Login 会拦截请求,把用户发往登录页面。
6.添加登录表单
呈现给用户的登录表单中包含一个用于输入电子邮件地址的文本字段、一个密码字段、一
个“记住我”复选框和提交按钮。这个表单使用的 Flask-WTF 类,app/auth/forms.py:登录表单
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')
电子邮件字段用到了 WTForms 提供的 Length() 和 Email() 验证函数。PasswordField 类表
示属性为 type="password" 的 <input> 元素。BooleanField 类表示复选框。
7.判断用户是否认证成功
current_user.is_authenticated() # 判断用户是否认证成功
<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>
判断条件中的变量 current_user 由 Flask-Login 定义,且在视图函数和模板中自动可用。
这个变量的值是当前登录的用户,如果用户尚未登录,则是一个匿名用户代理对象。如果
是匿名用户,is_authenticated() 方法返回 False。所以这个方法可用来判断当前用户是否
已经登录
三.登录用户示例
V:
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)
当请求类型是 GET 时,视图函数直接渲染模板,即显示表单。
当表单在 POST 请求中提交时,Flask-WTF 中的 validate_on_submit() 函数会验证表单数据,然后尝试登入用户。
为了登入用户,视图函数首先使用表单中填写的 email 从数据库中加载用户。
如果电子邮件地址对应的用户存在,再调用用户对象的 verify_password() 方法,其参数是表单中填
写的密码。
如果密码正确,则调用 Flask-Login 中的 login_user() 函数,在用户会话中把
用户标记为已登录。
login_user() 函数的参数是要登录的用户,以及可选的“记住我”布
尔值,“记住我”也在表单中填写。
如果值为 False,那么关闭浏览器后用户会话就过期了,所以下次用户访问时要重新登录。
如果值为 True,那么会在用户浏览器中写入一个长期有效的 cookie,使用这个 cookie 可以复现用户会话。
“Post/ 重定向 /Get 模式”,提交登录密令的 POST 请求最后也做了重定向,不过目标 URL 有两种可能。
用户访问未授权的 URL 时会显示登录表单,Flask-Login会把原地址保存在查询字符串的 next 参数中,这个参数可从 request.args 字典中读取。
如果查询字符串中没有 next 参数,则重定向到首页。
如果用户输入的电子邮件或密码不正确,程序会设定一个 Flash 消息,再次渲染表单,让用户重试登录。
T:
app/templates/auth/login.html:渲染登录表单
{% extends "base.html" %} {% import "bootstrap/wtf.html" as wtf %} 86 | 第 8 章 {% 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 %}
四.登出用户
app/auth/views.py:退出路由
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'))
为了登出用户,这个视图函数调用 Flask-Login 中的 logout_user() 函数,删除并重设用户
会话。
随后会显示一个 Flash 消息,确认这次操作,再重定向到首页,这样登出就完成了。
五.注册用户
T:
app/auth/forms.py:用户注册表单
from flask.ext.wtf import Form 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(Form): 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, 'Usernames 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() 渲染表单。
V:
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)
五.确认账户
使用itsdangerous生成确认令×××
from itsdangerous import TimedJSONWebSignatureSerializer as Serializer s = Serializer(app.config['SECRET_KEY'], expires_in=3600) token = s.dumps({'confirm': 23}) # 'eyJhbGciOiJIUzI1NiIsImV4cCI6MTM4MTcxODU1OCwiaWF0IjoxMzgxNzE0OTU4fQ.ey ...' data = s.loads(token) # {u'confirm': 23}
itsdangerous 提供了多种生成令×××的方法。其中,TimedJSONWebSignatureSerializer 类生成
具有过期时间的 JSON Web 签名(JSON Web Signatures,JWS)。这个类的构造函数接收
的参数是一个密钥,在 Flask 程序中可使用 SECRET_KEY 设置。
dumps() 方法为指定的数据生成一个加密签名,然后再对数据和签名进行序列化,生成令
×××字符串。expires_in 参数设置令×××的过期时间,单位为秒。
为了解码令×××,序列化对象提供了 loads() 方法,其唯一的参数是令×××字符串。这个方法
会检验签名和过期时间,如果通过,返回原始数据。如果提供给 loads() 方法的令×××不正
确或过期了,则抛出异常.
我们可以将这种生成和检验令×××的功能可添加到 User 模型中
app/models.py:确认用户账户
from itsdangerous import TimedJSONWebSignatureSerializer as Serializer from flask import current_app from . import db class User(UserMixin, db.Model): # ... confirmed = db.Column(db.Boolean, default=False) def generate_confirmation_token(self, expiration=3600): s = Serializer(current_app.config['SECRET_KEY'], expiration) return s.dumps({'confirm': self.id}) def confirm(self, token): s = Serializer(current_app.config['SECRET_KEY']) try: data = s.loads(token) 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 中的已登录
用户匹配。如此一来,即使恶意用户知道如何生成签名令×××,也无法确认别人的账户。
确认邮件:
from ..email import send_email @auth.route('/register', methods = ['GET', 'POST']) def register(): form = RegistrationForm() if form.validate_on_submit(): # ... 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('main.index')) return render_template('auth/register.html', form=form)
posted on 2020-06-28 16:44 myworldworld 阅读(342) 评论(0) 收藏 举报