最近在Flask Web Development作者博客看到第二版Flask Mega-Tutorial已在2017年底更新,现翻译给大家参考,希望帮助大家学习flask。
这是Flask Mega-Tutorial系列的第五章,其中我将告诉您如何创建用户登录子系统。
供您参考,以下是本系列文章的列表。
- 第1章:Hello, World!
- 第2章:模板
- 第3章:Web表单
- 第4章:数据库
- 第5章:用户登录(本文)
- 第6章:配置文件页面和头像
- 第7章:错误处理
- 第8章:关注与被关注
- 第9章:分页
- 第10章:电子邮件支持
- 第11章:整容
- 第12章:日期和时间
- 第13章:I18n和L10n
- 第14章:Ajax
- 第15章:大型应用程序结构
- 第16章:全文搜索
- 第17章:在Linux上部署
- 第18章:在Heroku上部署
- 第19章:Docker容器上的部署
- 第20章:一些JavaScript Magic
- 第21章:用户通知
- 第22章:后台工作
- 第23章:应用程序编程接口(API)
在第3章中,您学习了如何创建用户登录表单,在第4章中,您学习了如何使用数据库。本章将教您如何结合这两章的主题来创建一个简单的用户登录系统。
密码hashing
在第4章中,给用户模型定义了一个password_hash
字段,到目前为止尚未使用。此字段的用途是保存用户密码的哈希值,该密码将用于验证用户在登录过程中输入的密码。密码如何散列加密是一个复杂的主题,就留给安全专家。但有几个易于使用的库,可以让应用程序方便调用来实现实现所有这些加密。
其中一个实现密码hashing的软件包叫Werkzeug,当你安装Flask时,你可能已经在pip的输出中看到了它,因为它是flask的核心依赖之一。由于它是依赖项,Werkzeug已经安装在您的虚拟环境中。
以下Python shell session演示了如何散列hashing:
>> from werkzeug.security import generate_password_hash
>>> hash = generate_password_hash('foobar')
>>> hash
'pbkdf2:sha256:50000$vT9fkZM8$04dfa35c6476acf7e788a1b5b3c35e217c78dc04539d295f011f01f18cd2175f'
在此例中,密码
foobar
通过一系列没有已知反向操作的加密操作转换为长编码字符串,这意味着获得散列密码的人将无法使用它来获取原始明文密码。作为一项额外措施,如果您多次hash相同的密码,您将得到不同的结果,因此这使得无法通过查看其哈希值来确定两个用户是否具有相同的密码。
验证使用Werkzeug的第二个功能过程,如下所示:
>>> from werkzeug.security import check_password_hash
>>> check_password_hash(hash, 'foobar')
True
>>> check_password_hash(hash, 'barfoo')
False
验证函数使用先前生成的密码哈希值和用户在登录时输入的密码。如果用户提供的密码与哈希值匹配,则返回
True,否则返回
False
。
整个密码哈希逻辑可以在用户模型中使用两个新方法实现:
# app/models.py: Password hashing and verification
from werkzeug.security import generate_password_hash, check_password_hash
# ...
class User(db.Model):
# ...
def set_password(self, password):
self.password_hash = generate_password_hash(password)
def check_password(self, password):
return check_password_hash(self.password_hash, password)
有了这两种方法,用户对象现在就可以进行安全的密码验证,而无需存储原始密码。
以下是这些新方法的示例用法:
>>> u = User(username='susan', email='susan@example.com')
>>> u.set_password('mypassword')
>>> u.check_password('anotherpassword')
False
>>> u.check_password('mypassword')
True
Flask-Login简介
在本章中,我将向您介绍一个非常流行的Flask扩展Flask-Login。Flask-Login为Flask提供用户session的管理机制。它可以处理Login、Logout和session等服务。作用:
- 将用户的 id 储存在 session 中,方便用于 Login/Logout 等流程。
- 让你能够约束用户 Login/Logout 的视图
- 提供 remember me 功能
- 保护 cookies 不被篡改
首先在虚拟环境中安装Flask-Login:
(venv) $ pip install flask-login
与其他扩展一样,需要在app /__init__.py中的app实例之后立即创建和初始化Flask-Login 。
这是初始化方式:
# app/__init__.py: Flask-Login initialization
# ...
from flask_login import LoginManager
app = Flask(__name__)
# ...
login = LoginManager(app)
# ...
为Flask-Login准备用户模型
Flask-Login扩展与用户模型一起使用,并可以在其中实现某些属性和方法。这种方法很好,因为只要将这些必需的项添加到模型中,Flask-Login就没有任何其他要求。因此,它可以与基于任何数据库系统的用户模型一起使用。
下面列出了四个必需项目:
is_authenticated
:检验User的实例化对象是否登录,登录的话则为True。is_active
:检验用户是否通过某些验证,如果为True,则表示此用户已激活,反之,未激活。is_anonymous
:检验用户是否为匿名用户,如果为False,适用于普通用户,反之则为特殊匿名用户。get_id()
:以字符串形式返回User实例化对象的唯一标识(如果使用Python 2,则返回unicode)。
我本可以轻松实现这四个功能,但由于这种功能相当通用,所以Flask-Login提供了UserMixin
类,它包含适用于大多数用户模型类的通用功能。
以下是将UserMixin
类添加到模型中的方法:
# app/models.py: Flask-Login user mixin class
# ...
from flask_login import UserMixin
class User(UserMixin, db.Model):
# ...
User_loader 功能
Flask-Login通过在Flask的用户会话(session)中存储其唯一标识符来跟踪登录用户,该用户会话(session)是分配给连接到应用程序的每个用户的存储空间。每次登录用户导航到新页面时,Flask-Login都会从会话(session)中检索用户的ID,然后将该用户加载到内存中。
因为Flask-Login对数据库一无所知,所以在加载用户时需要应用程序的帮助。因此,需要在应用中配置用户加载程序函数,并调用该函数来加载给定ID的用户。
此功能可以添加到app/models.py模块中:
# app/models.py: Flask-Login user loader function
from app import login
# ...
@login.user_loader
def load_user(id):
return User.query.get(int(id))
使用
@login.user_loader
装饰器向Flask-Login注册 '用户加载程序' 。flask-login会将参数id
作为一个字符串传递给函数,而数据库只接受数字类型的ID值,所以需要将字符串转换为整数,请看上面。
登录视图
让我们重新打开登录视图功能,您可以回忆一下,我们刚刚实现了发出flash()
消息的虚假登录。现在既然可以访问用户数据库,又知道如何生成和验证密码哈希,那么可以完善登录视图功能。
# app/routes.py: Login view function logic
# ...
from flask_login import current_user, login_user
from app.models import User
# ...
@app.route('/login', methods=['GET', 'POST'])
def login():
if current_user.is_authenticated:
return redirect(url_for('index'))
form = LoginForm()
if form.validate_on_submit():
user = User.query.filter_by(username=form.username.data).first()
if user is None or not user.check_password(form.password.data):
flash('Invalid username or password')
return redirect(url_for('login'))
login_user(user, remember=form.remember_me.data)
return redirect(url_for('index'))
return render_template('login.html', title='Sign In', form=form)
login()
函数中的前两行处理了一个奇怪的情况。想象一下,您有一个登录的用户,用户被导航到URL /login 。显然这是一个错误,我们应该避免它。
Flask-Login提供一个代理对象
current_user
,在视图和模板中都可以访问它,以获取当前访问或登录的用户对象。此变量的值可以是数据库中的用户对象(Flask-Login通过上面提供的用户加载器回调读取)。如果用户尚未登录,则可以是特殊的匿名用户对象。还记得用户对象中需要Flask-Login的那些属性吗?其中一个是is_authenticated
,它可以方便地检查用户是否登录。当用户已经登录时,我只是重定向到首页。
替代我之前使用flash()
,现在我可以使用真实用户登录。
- 第一步是从数据库加载用户。username来自表单提交,我可以通过检索username来查询用户。因此,我将使用SQLAlchemy查询对象的方法
filter_by()
。filter_by()的结果
是只包含具有匹配用户名的对象的查询。因为我知道结果只有一个或零,所以我通过调用first()来完成查询,如果存在,first()则返回用户对象,如果不存在,first()则返回None
。在第4章中,您已经看到,当您all()
在查询中调用该方法时,查询将执行,您将获得与该查询匹配的所有结果的列表。当您只需要一个结果时,first()
方法是执行查询的另一种常用方法。 - 如果我得到了所提供用户名的匹配项,我接下来可以检查该表单附带的密码是否有效。这通过调用上面定义的方法
check_password()
来完成的。将在表单中获取输入的密码password,与用户存储在数据库的password_hash字段进行核对,并确定它们是否哈希匹配。所以有两个可能的错误条件:用户名可能无效,或者用户的密码可能不正确。在任何一种情况下,我都会闪烁一条消息,并重定向回登录提示,以便用户可以再次尝试。 - 如果用户名和密码都正确,那么我调用Flask-Login 的函数
login_user()
。此函数将用户标记为登录,这意味着无论用户导航到什么页面,都会将current_user
变量设置为此用户。 - 要完成登录过程,我只需将新登录的用户重定向到首页。
登出视图
我们还需要为用户提供退出登录的功能。这可以使用Flask-Login的logout_user()
功能完成。这是注销视图功能:
# app/routes.py: Logout view function
# ...
from flask_login import logout_user
# ...
@app.route('/logout')
def logout():
logout_user()
return redirect(url_for('index'))
要向用户展示此URL,我可以在用户登录后使导航栏中的“登录”URL自动切换到“注销”URL。
这可以通过base.html模板中的条件来完成:
app/templates/base.html: Conditional login and logout links
<div>
Microblog:
<a href="{{ url_for('index') }}">Home</a>
{% if current_user.is_anonymous %}
<a href="{{ url_for('login') }}">Login</a>
{% else %}
<a href="{{ url_for('logout') }}">Logout</a>
{% endif %}
</div>
is_anonymous
属性是Flask-Login通过UserMixin
类添加到用户对象的属性之一。current_user.is_anonymous仅当
用户没登录时为True,否则为False。
@login_required
Flask-Login提供了一个非常有用的功能,强制用户在查看网站的某些页面之前登录。如果未登录的用户尝试查看受保护的页面,Flask-Login将自动将用户重定向到登录页面,并且仅在登录过程完成后重定向回用户想要查看的页面。
要实现此功能,Flask-Login需要知道处理登录的视图函数是什么。
这可以在app/__ init__.py中添加:
# ...
login = LoginManager(app)
login.login_view = 'login'
上面的值
'login'
是登录视图的函数(或端点)名称。换句话说,是您在使用url_for()
获取URL调用使用的名称。
Flask-Login为匿名用户提供保护视图另一个方式是使用一个
@login_required
装饰器。当您将此装饰器添加到视图函数Flask 的装饰器@app.route
下面时,该函数将受到保护,并且不允许未经过身份验证的用户访问。
以下是装饰器如何应用于网站的首页视图功能:
# app/routes.py: @login_required decorator
from flask_login import login_required
@app.route('/')
@app.route('/index')
@login_required
def index():
# ...
剩下的工作是实现从成功登录到用户想要访问的页面的重定向。
当未登录的用户访问受
@login_required
保护的视图时,装饰器将重定向到登录页面,但它将在此重定向中包含一些额外信息,以便网站可以返回到之前的页面。例如,如果用户导航到/index,@login_required
将拦截请求并使用重定向响应/ login,但它会向此URL添加一个查询字符串参数,从而形成完整的重定向URL/login?next=/index。next
查询字符串参数设置为之前URL,因此网站可以使用该参数在登录后重定向回之前页面。
下面是一段代码,展示了如何读取和处理next
查询字符串参数:
# app/routes.py: Redirect to "next" page
from flask import request
from werkzeug.urls import url_parse
@app.route('/login', methods=['GET', 'POST'])
def login():
# ...
if form.validate_on_submit():
user = User.query.filter_by(username=form.username.data).first()
if user is None or not user.check_password(form.password.data):
flash('Invalid username or password')
return redirect(url_for('login'))
login_user(user, remember=form.remember_me.data)
next_page = request.args.get('next')
if not next_page or url_parse(next_page).netloc != '':
next_page = url_for('index')
return redirect(next_page)
# ...
在用户通过调用Flask-Login的
login_user()
函数登录后,获取next
查询字符串参数的值。Flask提供了一个request
变量,其中包含客户端随请求发送的所有信息。特别是,request.args
属性以友好的字典格式公开查询字符串的内容。实际上,在成功登录后,我们需要考虑三种可能的情况来确定重定向的位置:
- 如果登录URL没有
next
参数,则将用户重定向到索引页面。- 如果登录URL包含
next
设置为相对路径的参数(换句话说,没有域名的URL,如/index),则将用户重定向到该URL。- 如果登录URL包含
next
设置为包含域名的完整URL 的参数,则会将用户重定向到首页。
第一和第二个情况是不言自明的。第三种情况是为了使应用程序更安全。攻击者可以在next
参数中插入恶意站点的URL ,因此应用程序仅在URL为相对路径时重定向,这可确保重定向与应用程序保持在同一站点内。要确定URL是相对的还是绝对的,使用Werkzeug的url_parse()
函数解析它,然后检查netloc
组件是否已设置。
在模板中显示登录用户
您是否记得在用户子系统到位之前,我们在第2章中创建了一个假用户来帮助设计应用程序的主页?嗯,应用程序现在有真正的用户,所以我现在可以删除假用户并开始与真实用户交互。我可以在模板中使用Flask-Login的current_user
代替假用户:
app/templates/index.html: Pass current user to template
{% extends "base.html" %}
{% block content %}
<h1>Hi, {{ current_user.username }}!</h1>
{% for post in posts %}
<div><p>{{ post.author.username }} says: <b>{{ post.body }}</b></p></div>
{% endfor %}
{% endblock %}
我可以在视图函数中删除模板user
参数:
# app/routes.py: Do not pass user to template anymore
@app.route('/')
@app.route('/index')
def index():
# ...
return render_template("index.html", title='Home Page', posts=posts)
现在是测试登录和注销功能如何工作的好时机。由于仍然没有用户注册功能,将用户添加到数据库的唯一方法,是通过Python shell执行此操作。
请运行flask shell
并输入以下命令来注册用户:
>>> u = User(username='susan', email='susan@example.com')
>>> u.set_password('cat')
>>> db.session.add(u)
>>> db.session.commit()
如果您启动应用程序并尝试访问 http://localhost:5000/ 或 http://localhost:5000/index,您将立即重定向到登录页面,并在使用您添加到数据库后的账号密码登录后,将返回到首页,您将在其中看到个性化问候语。
用户注册
我将在本章中构建的最后一项功能是注册表单,以便用户可以通过Web表单进行注册。我们首先在app/forms.py中创建Web表单类:
# app/forms.py: User registration form
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, BooleanField, SubmitField
from wtforms.validators import ValidationError, DataRequired, Email, EqualTo
from app.models import User
# ...
class RegistrationForm(FlaskForm):
username = StringField('Username', validators=[DataRequired()])
email = StringField('Email', validators=[DataRequired(), Email()])
password = PasswordField('Password', validators=[DataRequired()])
password2 = PasswordField(
'Repeat Password', validators=[DataRequired(), EqualTo('password')])
submit = SubmitField('Register')
def validate_username(self, username):
user = User.query.filter_by(username=username.data).first()
if user is not None:
raise ValidationError('Please use a different username.')
def validate_email(self, email):
user = User.query.filter_by(email=email.data).first()
if user is not None:
raise ValidationError('Please use a different email address.')
在这个新表格中与验证相关的有趣内容。首先,对于email
字段,我在DataRequired
之后添加了第二个称为Email
验证器。这是WTForms附带的另一个常用验证器,它将确保用户在此字段中键入的内容与电子邮件地址的结构相匹配。
要使用WTForms的Email()验证器还需要另外安装外部依赖项。
(venv) $ pip install email-validator
由于这是注册表单,因此通常要求用户输入两次密码以减少拼写错误的风险。出于这个原因,我有password
和password2两个字段
。第二个密码字段使用另一个被调用的常用验证器EqualTo
,这将确保其值与第一个密码字段的值相同。
我还为这个类添加了两个名为validate_username()和validate_email()的
方法。当您添加任何与模式validate_<field_name>
匹配的方法时,WTForms会将这些方法作为自定义验证器,并在常用验证器之外调用它们。在本例中,我想确保用户输入的用户名和电子邮件地址不在数据库中,因此这两种方法会发出数据库查询以保证他们不会重复。如果存在结果,则弹出ValidationError来触发验证错误。异常ValidationError中参数所包含的消息将显示在字段旁边,以供用户查看。
要在网页上显示此表单,我需要一个HTML模板,我将它存储在文件 app/templates/register.html中。此模板的构造类似于登录表单:
app/templates/register.html: Registration template
{% extends "base.html" %}
{% block content %}
<h1>Register</h1>
<form action="" method="post">
{{ form.hidden_tag() }}
<p>
{{ form.username.label }}<br>
{{ form.username(size=32) }}<br>
{% for error in form.username.errors %}
<span style="color: red;">[{{ error }}]</span>
{% endfor %}
</p>
<p>
{{ form.email.label }}<br>
{{ form.email(size=64) }}<br>
{% for error in form.email.errors %}
<span style="color: red;">[{{ error }}]</span>
{% endfor %}
</p>
<p>
{{ form.password.label }}<br>
{{ form.password(size=32) }}<br>
{% for error in form.password.errors %}
<span style="color: red;">[{{ error }}]</span>
{% endfor %}
</p>
<p>
{{ form.password2.label }}<br>
{{ form.password2(size=32) }}<br>
{% for error in form.password2.errors %}
<span style="color: red;">[{{ error }}]</span>
{% endfor %}
</p>
<p>{{ form.submit() }}</p>
</form>
{% endblock %}
登录表单模板需要增加一个新用户注册URL,就像下面的表单:
app/templates/login.html: Link to registration page
<p>New User? <a href="{{ url_for('register') }}">Click to Register!</a></p>
最后,我需要在app/routes.py中编写处理用户注册的视图函数:
# app/routes.py: User registration view function
from app import db
from app.forms import RegistrationForm
# ...
@app.route('/register', methods=['GET', 'POST'])
def register():
if current_user.is_authenticated:
return redirect(url_for('index'))
form = RegistrationForm()
if form.validate_on_submit():
user = User(username=form.username.data, email=form.email.data)
user.set_password(form.password.data)
db.session.add(user)
db.session.commit()
flash('Congratulations, you are now a registered user!')
return redirect(url_for('login'))
return render_template('register.html', title='Register', form=form)
这种视图函数的功能不言自明。首先确保调用此路由的用户未登录。表单的处理方式与登录时相同。在if validate_on_submit()
为Ture时,创建了一个新用户,其中包含了username,email 和 password ,将其写入数据库,然后重定向到登录页面,以便用户可以登录。
通过这些更改,用户应该能够在网站上创建帐户,登录和注销。请务必尝试我在注册表单中添加的所有验证功能,以便更好地了解它们的工作原理。我将在以后的章节中重写用户身份验证子系统,以添加其他功能,例如允许用户在忘记密码时重置密码。但就目前而言,这足以继续构建应用程序的其他功能。
原文链接:https://blog.miguelgrinberg.com/post/the-flask-mega-tutorial-part-v-user-logins