前面两章节我们学习了使用表单和数据库,本章节我们把两者结合起来,来创建一个简单的用户登录系统。
密码哈希
在第四章中,用户模型设置了一个 password_hash
字段,到目前为止还没有被使用到。 这个字段的目的是保存用户密码的哈希值,并用于验证用户在登录过程中输入的密码。密码哈希的原理我们暂不讨论,我们只要使用第三方库来实现它就行了。
我们用来实现密码哈希的包是 Werkzeug
,当安装 Flask 时,你可能会在 pip
的输出中看到这个包,因为它是 Flask 的一个核心依赖项。 所以,Werkzeug
已经安装在你的虚拟环境中。
下面在 Python shell
演示如何实现哈希密码:
>>> from werkzeug.security import generate_password_hash
>>> hash = generate_password_hash('foobar')
>>> hash
'pbkdf2:sha256:260000$eIYxDpg9aHu8HlyF$257c12ba1109dfc38651e6be0ed4e29d2db4d8e15b0b89430516c1064d045eb3'
这个例子中,我们将密码 foobar
转换成一个长编码字符串,请记住这个过程是没有办法逆向操作的,无法从加密后的长编码字符倒推出原始密码。
作为一个附加手段,多次哈希相同的密码,你将得到不同的结果,所以这使得无法通过查看它们的哈希值来确定两个用户是否具有相同的密码。
要验证密码是否正确,我们使用 Werkzeug
库的 check_password_hash
方法:
>>> from werkzeug.security import generate_password_hash, check_password_hash
>>> hash = generate_password_hash('foobar')
>>> check_password_hash(hash, 'foobar')
True
>>> check_password_hash(hash, 'barfoo')
False
它会把用户提供的密码执行哈希过程后与存储的哈希值匹配,通过校验则返回 True
,否则返回 False
。
现在我们在数据库用户模型中实现密码哈希的逻辑:
# app\models.py
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
来做好准备工作:
(venv) $ pip install flask-login
和其他插件一样,Flask-Login
需要在 app/__init__py
中的工厂函数中初始化和实例化。
# app/__init__py
# ...
from flask_login import LoginManager
login = LoginManager()
def create_app():
app = Flask(__name__)
# ...
login.init_app(app)
# ...
# ...
扩展用户模型
现在我们把 Flask-Login
插件的某些属性和方法扩展到我们的用户模型上。
必须的四项如下:
-
is_authenticated
:一个用来表示用户是否通过登录认证的属性,用True
和False
表示。 -
is_active
:如果用户账户是活跃的,那么这个属性是True
,否则就是False
(活跃用户的定义是该用户的登录状态是否通过用户名密码登录,通过“记住我”功能保持登录状态的用户是非活跃的)。 -
is_anonymous
:常规用户的该属性是False
,对特定的匿名用户是True
。 -
get_id()
: 返回用户的唯一id的方法,返回值类型是字符串。
要实现只需把 Flask-Login
提供的一个叫做 UserMixin
的 mixin
类让用户模型一起继承就完成了。
# app\models.py
# ...
from flask_login import UserMixin
class User(UserMixin, db.Model):
# ...
用户加载函数
我们还需要在 app/models.py模块
中添加一个通过主键(对于用户就是 id
)来获取用户实例的函数才能让 Flask-Login
运行。因为数据库对 Flask-Login
透明,所以需要应用来辅助加载用户。
# app\models.py
from app import login
# ...
@login.user_loader
def load_user(id):
return User.query.get(int(id))
使用 Flask-Login
的 @login.user_loader
装饰器来为用户加载功能注册函数。Flask-Login
将字符串类型的参数 id
传入用户加载函数,因此使用数字 ID
的数据库需要将字符串转换为整数。
用户登入
现在我们在视图函数内实现登录的逻辑:
# app\routes.py
# ...
from flask_login import current_user, login_user
from app.models import User
# ...
@main_routes.route('/login', methods=['GET', 'POST'])
def login():
if current_user.is_authenticated:
return redirect(url_for('main.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('main.login'))
login_user(user, remember=form.remember_me.data)
return redirect(url_for('main.index'))
return render_template('login.html', title='Sign In', form=form)
login()
函数中的前两行处理一个非预期的情况:假如用户已经登录,我们应该让其跳转到 /index
路由。
current_user
变量来自 Flask-Login
,可以在任何时候获取当前浏览器的用户对象。这个用户对象的获取是由我们刚才在 models.py
中定义的 load_user()
方法来执行的。如果用户还没有登录,current_user
变量则是一个特殊的匿名用户对象。
is_authenticated
属性,正如我们上面提到的,它可以方便地检查用户是否
登录。当用户已经登录,我只需要重定向到主页。
相比之前的模拟登录,现在是真实地地登录用户。第一步是从数据库根据表单提交的 username
,查找到数据库中对应的用户记录。为此,我们使用 SQLAlchemy
查询对象的 filter_by()
方法。
filter_by()
的结果是一个个包含匹配 username
的所有对象的查询结果集,结果集可以包含一个或多个对象,因为我们知道我们的数据库内暂时不会有重名的用户,所以我们调研 first()
方法来获得查询集里的第一个用户对象即可。当然,查询了错误的用户名是不会返回用户对象的,这时候返回 None
。
如果浏览器输入的用户名能成功匹配,接下来就调用 check_password()
方法来检查密码是否有效。现在有两个可能的错误情况:用户名是无效的,或者用户密码是错误的。在这两种情况下,我都会闪现(flash
)一条消息,然后重定向到登录页面,以便用户可以再次尝试。
如果用户名和密码都是正确的,那么我调用来自 Flask-Login
的 login_user()
函数。 该函数会将用户登录状态注册为已登录,这意味着用户导航到任何未来的页面时,应用都会将用户实例赋值给 current_user
变量。
然后,只需将新登录的用户重定向到主页,就完成了整个登录过程。
用户登出
提供一个用户登出的途径也是必须的,我们使用 Flask-Login
的 logout_user()
函数来实现它。其视图函数代码如下:
# app\routes.py
# ...
from flask_login import logout_user
# ...
@main_routes.route('/logout')
def logout():
logout_user()
return redirect(url_for('main.index'))
我们还需要在导航栏给用户一个登出的链接,当用户登录之后,登录链接自动转换成登出链接。修改 base.html
模板的导航栏部分:
# app\templates\base.html
<div>
Microblog:
<a href="{{ url_for('main.index') }}">Home</a>
{% if current_user.is_anonymous %}
<a href="{{ url_for('main.login') }}">Login</a>
{% else %}
<a href="{{ url_for('main.logout') }}">Logout</a>
{% endif %}
</div>
现在模板会检查当前用户的登录状态来决定渲染 login
或者 logout
链接。
在模板中显示已登录的用户
在第二章中,我们在视图函数和模板文件中模拟了一下用户界面,现在我们要显示真实存在于数据库的用户信息,先把之前的模拟代码删除再修改。
# app\templates\index.html
{% extends "base.html" %}
{% block content %}
<h1>Hello, {{ current_user.username }}!</h1>
{% for post in posts %}
<div>
<p>{{ post.author.username }} says: <b>{{ post.body }}</b></p>
</div>
{% endfor %}
{% endblock %}
模板变量中我们加载 Flask-Login
的 current_user
变量来获得当前用户的属性。
再修改视图函数:
# app\routes.py
# ...
@main_routes.route('/')
@main_routes.route('/index')
def index():
posts = Post.query.all()
return render_template(
'index.html',
title='Home Page',
posts=posts
)
# ...
现在我们通过 Flask Shell
创建一些用户,然后我们的登录系统就能在浏览器正常使用了。
用户注册
最后我们来实现网站的用户注册功能,这一部分是前面所讲的各个知识点的再次运用,所以不会用很详细叙述。
在 app/forms.py
中创建注册表单类:
# app/forms.py
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
的验证器将确保用户在此字段中键入的内容与电子邮件地址的结构相匹配。
这个验证器是需要先安装再使用的:
(venv) $ pip install email_validator
由于这是一个注册表单,习惯上要求用户输入密码两次,以减少输入错误的风险。出于这个原因,我提供了 password
和 password2
字段。 第二个 password
字段使用另一个名为 EqualTo
的验证器,它将确保其值与第一个 password
字段的值相同。
我们还添加了两个自定义的验证方法,名为 validate_username()
和 validate_email()
。 当添加任何函数名为 validate_ <field_name>
的方法时,WTForms
将这些方法作为自定义验证器,并在已设置验证器之后调用它们。这两个验证器会检查用户输入的 usernam
和 email
两个字段是否和已存在于数据库的记录有重复,从而保证 usernam
和 email
的唯一性。注意:这里我们使用 ValidationError
触发验证错误, 异常中作为参数的消息将会在对应字段旁边显示,以供用户查看。
我们在编写一个名为 register.html
的 HTML 模板以便在网页上显示这个表单,依旧是存储在 app/templatesl
文件夹中。 这个模板的构造与登录表单类似:
# app\templates\register.html
{% 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 %}
还需要在登录表单模板之下添加一个链接,来将未注册的用户引导到注册页面:
# app\templates\login.html
<p>
New User? <a href="{{ url_for('main.register') }}">Click to Register!</a>
</p>
最后,在 app/routes.py
编写相应视图函数:
# app\routes.py
from app import db
from app.forms import RegistrationForm
# ...
@main_routes.route('/register', methods=['GET', 'POST'])
def register():
if current_user.is_authenticated:
return redirect(url_for('main.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('main.login'))
return render_template('register.html', title='Register', form=form)
这个视图函数的逻辑也是一目了然,首先确保调用这个路由的用户没有登录。表单的处理方式和登录的方式一样。在 if validate_on_submit()
条件块下,获取来自表单的 username
、email
和 password
创建一个新用户,将其写入数据库,然后重定向到登录页面。
到这里,我们网站的注册登录功能就已经完成。
本文源码:https://github.com/SingleDiego/Flask-Tutorial-Source-Code/tree/SingleDiego-patch-05
网友评论