Flask 教程 第五章:用户登陆

本文翻译自The Flask Mega-Tutorial Part V: User Loginshtml

这是Flask Mega-Tutorial系列的第五部分,我将告诉你如何建立一个用户登陆子系统。git

你在第三章中学会了如何建立用户登陆表单,在第四章中学会了运用数据库。本章将教你如何结合这两章的主题来建立一个简单的用户登陆系统。github

本章的GitHub连接为:BrowseZipDiff.shell

密码哈希

第四章中,用户模型设置了一个password_hash字段,到目前为止尚未被使用到。 这个字段的目的是保存用户密码的哈希值,并用于验证用户在登陆过程当中输入的密码。 密码哈希的实现是一个复杂的话题,应该由安全专家来搞定,不过,已经有数个现成的简单易用且功能完备加密库存在了。数据库

其中一个实现密码哈希的包是Werkzeug,当安装Flask时,你可能会在pip的输出中看到这个包,由于它是Flask的一个核心依赖项。 因此,Werkzeug已经安装在你的虚拟环境中。 如下Python shell会话演示了如何哈希密码:flask

1 >>> from werkzeug.security import generate_password_hash 2 >>> hash = generate_password_hash('foobar') 3 >>> hash 4 'pbkdf2:sha256:50000$vT9fkZM8$04dfa35c6476acf7e788a1b5b3c35e217c78dc04539d295f011f01f18cd2175f'

在这个例子中,经过一系列已知没有反向操做的加密操做,将密码foobar转换成一个长编码字符串,这意味着得到密码哈希值的人将没法使用它逆推出原始密码。 做为一个附加手段,屡次哈希相同的密码,你将获得不一样的结果,因此这使得没法经过查看它们的哈希值来肯定两个用户是否具备相同的密码。浏览器

验证过程使用Werkzeug的第二个函数来完成,以下所示:安全

1 >>> from werkzeug.security import check_password_hash 2 >>> check_password_hash(hash, 'foobar') 3 True 4 >>> check_password_hash(hash, 'barfoo') 5 False
向验证函数传入以前生成的密码哈希值以及用户在登陆时输入的密码,若是用户提供的密码执行哈希过程后与存储的哈希值匹配,则返回,不然返回TrueFalse

整个密码哈希逻辑能够在用户模型中实现为两个新的方法:session

 1 from werkzeug.security import generate_password_hash, check_password_hash  2 
 3 # ...
 4 
 5 class User(db.Model):  6     # ...
 7 
 8     def set_password(self, password):  9         self.password_hash = generate_password_hash(password) 10 
11     def check_password(self, password): 12         return check_password_hash(self.password_hash, password)

使用这两种方法,用户对象如今能够在无需持久化存储原始密码的条件下执行安全的密码验证。 如下是这些新方法的示例用法:app

1 >>> u = User(username='susan', email='susan@example.com') 2 >>> u.set_password('mypassword') 3 >>> u.check_password('anotherpassword') 4 False 5 >>> u.check_password('mypassword') 6 True

Flask-Login简介

在本章中,我将向你介绍一个很是受欢迎的Flask插件Flask-Login。 该插件管理用户登陆状态,以便用户能够登陆到应用,而后用户在导航到该应用的其余页面时,应用会“记得”该用户已经登陆。它还提供了“记住我”的功能,容许用户在关闭浏览器窗口后再次访问应用时保持登陆状态。能够先在你的虚拟环境中安装Flask-Login来作好准备工做:

(venv) $ pip install flask-login

和其余插件同样,Flask-Login须要在app/__init__py中的应用实例以后被建立和初始化。 该插件初始化代码以下:

1 # ...
2 from flask_login import LoginManager 3 
4 app = Flask(__name__) 5 # ...
6 login = LoginManager(app) 7 
8 # ...

为Flask-Login准备用户模型

Flask-Login插件须要在用户模型上实现某些属性和方法。这种作法很棒,由于只要将这些必需项添加到模型中,Flask-Login就没有其余依赖了,它就能够与基于任何数据库系统的用户模型一块儿工做。

必须的四项以下:

  • is_authenticated: 一个用来表示用户是否经过登陆认证的属性,用TrueFalse表示。
  • is_active: 若是用户帐户是活跃的,那么这个属性是True,不然就是False(译者注:活跃用户的定义是该用户的登陆状态是否经过用户名密码登陆,经过“记住我”功能保持登陆状态的用户是非活跃的)。
  • is_anonymous: 常规用户的该属性是False,对特定的匿名用户是True
  • get_id(): 返回用户的惟一id的方法,返回值类型是字符串(Python 2下返回unicode字符串).

我能够很容易地实现这四个属性或方法,可是因为它们是至关通用的,所以Flask-Login提供了一个叫作UserMixinmixin类来将它们概括其中。 下面演示了如何将mixin类添加到模型中:

1 # ...
2 from flask_login import UserMixin 3 
4 class User(UserMixin, db.Model): 5     # ...

用户加载函数

用户会话是Flask分配给每一个链接到应用的用户的存储空间,Flask-Login经过在用户会话中存储其惟一标识符来跟踪登陆用户。每当已登陆的用户导航到新页面时,Flask-Login将从会话中检索用户的ID,而后将该用户实例加载到内存中。

由于数据库对Flask-Login透明,因此须要应用来辅助加载用户。 基于此,插件指望应用配置一个用户加载函数,能够调用该函数来加载给定ID的用户。 该功能能够添加到app/models.py模块中:

1 from app import login 2 # ...
3 
4 @login.user_loader 5 def load_user(id): 6     return User.query.get(int(id))

使用Flask-Login的@login.user_loader装饰器来为用户加载功能注册函数。 Flask-Login将字符串类型的参数id传入用户加载函数,所以使用数字ID的数据库须要如上所示地将字符串转换为整数。

用户登入

让咱们回顾一下登陆视图函数,它实现了一个模拟登陆,只发出一个flash()消息。 如今,应用能够访问用户数据,并知道如何生成和验证密码哈希值,该视图函数就能够完工了。

 1 # ...
 2 from flask_login import current_user, login_user  3 from app.models import User  4 
 5 # ...
 6 
 7 @app.route('/login', methods=['GET', 'POST'])  8 def login():  9     if current_user.is_authenticated: 10         return redirect(url_for('index')) 11     form = LoginForm() 12     if form.validate_on_submit(): 13         user = User.query.filter_by(username=form.username.data).first() 14         if user is None or not user.check_password(form.password.data): 15             flash('Invalid username or password') 16             return redirect(url_for('login')) 17         login_user(user, remember=form.remember_me.data) 18         return redirect(url_for('index')) 19     return render_template('login.html', title='Sign In', form=form)

login()函数中的前两行处理一个非预期的状况:假设用户已经登陆,却导航到应用的/login URL。 显然这是一个不可能容许的错误场景。 current_user变量来自Flask-Login,能够在处理过程当中的任什么时候候调用以获取用户对象。 这个变量的值能够是数据库中的一个用户对象(Flask-Login经过我上面提供的用户加载函数回调读取),或者若是用户尚未登陆,则是一个特殊的匿名用户对象。 还记得那些Flask-Login必须的用户对象属性? 其中之一是is_authenticated,它能够方便地检查用户是否登陆。 当用户已经登陆,我只须要重定向到主页。

相比以前的调用flash()显示消息模拟登陆,如今我能够真实地登陆用户。 第一步是从数据库加载用户。 利用表单提交的username,我能够查询数据库以找到用户。 为此,我使用了SQLAlchemy查询对象的filter_by()方法。 filter_by()的结果是一个只包含具备匹配用户名的对象的查询结果集。 由于我知道查询用户的结果只多是有或者没有,因此我经过调用first()来完成查询,若是存在则返回用户对象;若是不存在则返回None。 在第四章中,你已经看到当你在查询中调用all()方法时, 将执行该查询并得到与该查询匹配的全部结果的列表。 当你只须要一个结果时,一般使用first()方法。

若是使用提供的用户名执行查询并成功匹配,我能够接下来经过调用上面定义的check_password()方法来检查表单中随附的密码是否有效。 密码验证时,将验证存储在数据库中的密码哈希值与表单中输入的密码的哈希值是否匹配。 因此,如今我有两个可能的错误状况:用户名多是无效的,或者用户密码是错误的。 在这两种状况下,我都会闪现一条消息,而后重定向到登陆页面,以便用户能够再次尝试。

若是用户名和密码都是正确的,那么我调用来自Flask-Login的login_user()函数。 该函数会将用户登陆状态注册为已登陆,这意味着用户导航到任何将来的页面时,应用都会将用户实例赋值给current_user变量。

而后,只需将新登陆的用户重定向到主页,我就完成了整个登陆过程。

用户登出

提供一个用户登出的途径也是必须的,我将会经过Flask-Login的logout_user()函数来实现。其视图函数代码以下:

1 # ...
2 from flask_login import logout_user 3 
4 # ...
5 
6 @app.route('/logout') 7 def logout(): 8  logout_user() 9     return redirect(url_for('index'))

为了给用户暴露登出连接,我会在导航栏上实现当用户登陆以后,登陆连接自动转换成登出连接。修改base.html模板的导航栏部分后,代码以下:

1     <div>
2  Microblog: 3         <a href="{{ url_for('index') }}">Home</a>
4         {% if current_user.is_anonymous %} 5         <a href="{{ url_for('login') }}">Login</a>
6         {% else %} 7         <a href="{{ url_for('logout') }}">Logout</a>
8         {% endif %} 9     </div>

用户实例的is_anonymous属性是在其模型继承UserMixin类后Flask-Login添加的,表达式current_user.is_anonymous仅当用户未登陆时的值是True

要求用户登陆

Flask-Login提供了一个很是有用的功能——强制用户在查看应用的特定页面以前登陆。 若是未登陆的用户尝试查看受保护的页面,Flask-Login将自动将用户重定向到登陆表单,而且只有在登陆成功后才重定向到用户想查看的页面。

为了实现这个功能,Flask-Login须要知道哪一个视图函数用于处理登陆认证。在app/__init__.py中添加代码以下:

1 # ...
2 login = LoginManager(app) 3 login.login_view = 'login'

上面的'login'值是登陆视图函数(endpoint)名,换句话说该名称可用于url_for()函数的参数并返回对应的URL。

Flask-Login使用名为@login_required的装饰器来拒绝匿名用户的访问以保护某个视图函数。 当你将此装饰器添加到位于@app.route装饰器下面的视图函数上时,该函数将受到保护,不容许未经身份验证的用户访问。 如下是该装饰器如何应用于应用的主页视图函数的案例:

1 from flask_login import login_required 2 
3 @app.route('/') 4 @app.route('/index') 5 @login_required 6 def index(): 7     # ...

剩下的就是实现登陆成功以后自定重定向回到用户以前想要访问的页面。 当一个没有登陆的用户访问被@login_required装饰器保护的视图函数时,装饰器将重定向到登陆页面,不过,它将在这个重定向中包含一些额外的信息以便登陆后的回转。 例如,若是用户导航到/index,那么@login_required装饰器将拦截请求并以重定向到/login来响应,可是它会添加一个查询字符串参数来丰富这个URL,如/login?next=/index。 原始URL设置了next查询字符串参数后,应用就能够在登陆后使用它来重定向。

下面是一段代码,展现了如何读取和处理next查询字符串参数:

 1 from flask import request  2 from werkzeug.urls import url_parse  3 
 4 @app.route('/login', methods=['GET', 'POST'])  5 def login():  6     # ...
 7     if form.validate_on_submit():  8         user = User.query.filter_by(username=form.username.data).first()  9         if user is None or not user.check_password(form.password.data): 10             flash('Invalid username or password') 11             return redirect(url_for('login')) 12         login_user(user, remember=form.remember_me.data) 13         next_page = request.args.get('next') 14         if not next_page or url_parse(next_page).netloc != '': 15             next_page = url_for('index') 16         return redirect(next_page) 17     # ...

 

在用户经过调用Flask-Login的login_user()函数登陆后,应用获取了next查询字符串参数的值。 Flask提供一个request变量,其中包含客户端随请求发送的全部信息。 特别是request.args属性,可用友好的字典格式暴露查询字符串的内容。 实际上有三种可能的状况须要考虑,以肯定成功登陆后重定向的位置:

  • 若是登陆URL中不含next参数,那么将会重定向到本应用的主页。
  • 若是登陆URL中包含next参数,其值是一个相对路径(换句话说,该URL不含域名信息),那么将会重定向到本应用的这个相对路径。
  • 若是登陆URL中包含next参数,其值是一个包含域名的完整URL,那么重定向到本应用的主页。

前两种状况很好理解,第三种状况是为了使应用更安全。 攻击者能够在next参数中插入一个指向恶意站点的URL,所以应用仅在重定向URL是相对路径时才执行重定向,这可确保重定向与应用保持在同一站点中。 为了肯定URL是相对的仍是绝对的,我使用Werkzeug的url_parse()函数解析,而后检查netloc属性是否被设置。

在模板中显示已登陆的用户

你还记得在实现用户子系统以前的第二章中,我建立了一个模拟的用户来帮助我设计主页的事情吗? 如今,应用实现了真正的用户,我就能够删除模拟用户了。 取而代之,我会在模板中使用Flask-Login的current_user

{% 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了:

1 @app.route('/') 2 @app.route('/index') 3 def index(): 4     # ...
5     return render_template("index.html", title='Home Page', posts=posts)

这正是测试登陆和注销功能运做机制的好时机。 因为仍然没有用户注册功能,因此添加用户到数据库的惟一方法是经过Python shell执行,因此运行flask shell并输入如下命令来注册用户:

1 >>> u = User(username='susan', email='susan@example.com') 2 >>> u.set_password('cat') 3 >>> db.session.add(u) 4 >>> db.session.commit()

若是启动应用并尝试访问http://localhost:5000/http://localhost:5000/index,会当即重定向到登陆页面。在使用以前添加到数据库的凭据登陆后,就会跳转回到以前访问的页面,并看到其中的个性化欢迎。

用户注册

本章要构建的最后一项功能是注册表单,以便用户能够经过Web表单进行注册。 让咱们在app/forms.py中建立Web表单类来开始吧:

 1 from flask_wtf import FlaskForm  2 from wtforms import StringField, PasswordField, BooleanField, SubmitField  3 from wtforms.validators import ValidationError, DataRequired, Email, EqualTo  4 from app.models import User  5 
 6 # ...
 7 
 8 class RegistrationForm(FlaskForm):  9     username = StringField('Username', validators=[DataRequired()]) 10     email = StringField('Email', validators=[DataRequired(), Email()]) 11     password = PasswordField('Password', validators=[DataRequired()]) 12     password2 = PasswordField( 13         'Repeat Password', validators=[DataRequired(), EqualTo('password')]) 14     submit = SubmitField('Register') 15 
16     def validate_username(self, username): 17         user = User.query.filter_by(username=username.data).first() 18         if user is not None: 19             raise ValidationError('Please use a different username.') 20 
21     def validate_email(self, email): 22         user = User.query.filter_by(email=email.data).first() 23         if user is not None: 24             raise ValidationError('Please use a different email address.')

代码中与验证相关的几处至关有趣。首先,对于email字段,我在DataRequired以后添加了第二个验证器,名为Email。 这个来自WTForms的另外一个验证器将确保用户在此字段中键入的内容与电子邮件地址的结构相匹配。

因为这是一个注册表单,习惯上要求用户输入密码两次,以减小输入错误的风险。 出于这个缘由,我提供了passwordpassword2字段。 第二个password字段使用另外一个名为EqualTo的验证器,它将确保其值与第一个password字段的值相同。

我还为这个类添加了两个方法,名为validate_username()validate_email()。 当添加任何匹配模式validate_ <field_name>的方法时,WTForms将这些方法做为自定义验证器,并在已设置验证器以后调用它们。 本处,我想确保用户输入的username和email不会与数据库中已存在的数据冲突,因此这两个方法执行数据库查询,并指望结果集为空。 不然,则经过ValidationError触发验证错误。 异常中做为参数的消息将会在对应字段旁边显示,以供用户查看。

我须要一个HTML模板以便在网页上显示这个表单,我其存储在app/templates/register.html文件中。 这个模板的构造与登陆表单相似:

 1 {% extends "base.html" %}  2 
 3 {% block content %}  4     <h1>Register</h1>
 5     <form action="" method="post">
 6  {{ form.hidden_tag() }}  7         <p>
 8             {{ form.username.label }}<br>
 9             {{ form.username(size=32) }}<br>
10             {% for error in form.username.errors %} 11             <span style="color: red;">[{{ error }}]</span>
12             {% endfor %} 13         </p>
14         <p>
15             {{ form.email.label }}<br>
16             {{ form.email(size=64) }}<br>
17             {% for error in form.email.errors %} 18             <span style="color: red;">[{{ error }}]</span>
19             {% endfor %} 20         </p>
21         <p>
22             {{ form.password.label }}<br>
23             {{ form.password(size=32) }}<br>
24             {% for error in form.password.errors %} 25             <span style="color: red;">[{{ error }}]</span>
26             {% endfor %} 27         </p>
28         <p>
29             {{ form.password2.label }}<br>
30             {{ form.password2(size=32) }}<br>
31             {% for error in form.password2.errors %} 32             <span style="color: red;">[{{ error }}]</span>
33             {% endfor %} 34         </p>
35         <p>{{ form.submit() }}</p>
36     </form>
37 {% endblock %}

登陆表单模板须要在其表单之下添加一个连接来将未注册的用户引导到注册页面:

1     <p>New User? <a href="{{ url_for('register') }}">Click to Register!</a></p>

最后,我来实现处理用户注册的视图函数,存储在app/routes.py中,代码以下:

 1 from app import db  2 from app.forms import RegistrationForm  3 
 4 # ...
 5 
 6 @app.route('/register', methods=['GET', 'POST'])  7 def register():  8     if current_user.is_authenticated:  9         return redirect(url_for('index')) 10     form = RegistrationForm() 11     if form.validate_on_submit(): 12         user = User(username=form.username.data, email=form.email.data) 13  user.set_password(form.password.data) 14  db.session.add(user) 15  db.session.commit() 16         flash('Congratulations, you are now a registered user!') 17         return redirect(url_for('login')) 18     return render_template('register.html', title='Register', form=form)

这个视图函数的逻辑也是一目了然,我首先确保调用这个路由的用户没有登陆。表单的处理方式和登陆的方式同样。在if validate_on_submit()条件块下,完成的逻辑以下:使用获取自表单的username、email和password建立一个新用户,将其写入数据库,而后重定向到登陆页面以便用户登陆。

注册表单

精雕细琢以后,用户已经可以在此应用上注册账户,并进行登陆和注销。 请确保你尝试了我在注册表单中添加的全部验证功能,以便更好地了解其工做原理。 我将在将来的章节中再次更新用户认证子系统,以增长额外的功能,好比容许用户在忘记密码的状况下重置密码。 不过对于目前的应用来说,这已经无碍于继续构建了。

相关文章
相关标签/搜索