学会了如何创建用户登录表单及运用数据库。现在教你如何结合这两章的主题来创建一个简单的用户登录系统。
一、User模型中的密码哈希值
用户模型设置了一个password_hash字段, 这个字段的目的是保存用户密码的哈希值,并用于验证用户在登录过程中输入的密码。 密码哈希的实现是一个复杂的话题,应该由安全专家来搞定,不过,已经有数个现成的简单易用且功能完备加密库存在了。其中一个实现密码哈希的包是Werkzeug,当安装Flask时,你可能会在pip的输出中看到这个包,因为它是Flask的一个核心依赖项。 所以,Werkzeug已经安装在你的虚拟环境中。
验证一下:
from werkzeug.security import generate_password_hash
hash=generate_password_hash('hash_worth')
hash
'pbkdf2:sha256:260000$42OZBYPD3gasP8Dp$b322f885bef0e6a16d92763441361238d99e8d2dfe03f926455915fccb6dda62'
from werkzeug.security import check_password_hash
check_password_hash(hash,'password')
False
check_password_hash(hash,'hash_worth')
True
可以在用户模型中实现为两个新的方法:
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@mail.com')
>>> u.set_password('password')
>>> u.check_password('password')
True
>>> u.check_password('pass')
False
>>>
二、Flask_login扩展的应用
该插件管理用户登录状态,以便用户可以登录到应用,然后用户在导航到该应用的其他页面时,应用会“记得”该用户已经登录。它还提供了“记住我”的功能,允许用户在关闭浏览器窗口后再次访问应用时保持登录状态。可以先在你的虚拟环境中安装Flask-Login来做好准备工作:
(microblog) D:\pythonProgram\PycharmProjects\microblog>pip install flask-login
Collecting flask-login
Using cached Flask_Login-0.5.0-py2.py3-none-any.whl (16 kB)
Requirement already satisfied: Flask in d:\pycharmprojects\flask\microblog\lib\site-packages (from flask-login) (2.0.1)
Requirement already satisfied: Jinja2>=3.0 in d:\pycharmprojects\flask\microblog\lib\site-packages (from Flask->flask-login) (3.0.1)
Requirement already satisfied: Werkzeug>=2.0 in d:\pycharmprojects\flask\microblog\lib\site-packages (from Flask->flask-login) (2.0.1)
Requirement already satisfied: click>=7.1.2 in d:\pycharmprojects\flask\microblog\lib\site-packages (from Flask->flask-login) (8.0.1)
Requirement already satisfied: itsdangerous>=2.0 in d:\pycharmprojects\flask\microblog\lib\site-packages (from Flask->flask-login) (2.0.1)
Requirement already satisfied: colorama in d:\pycharmprojects\flask\microblog\lib\site-packages (from click>=7.1.2->Flask->flask-login) (0.4.4)
Requirement already satisfied: MarkupSafe>=2.0 in d:\pycharmprojects\flask\microblog\lib\site-packages (from Jinja2>=3.0->Flask->flask-login) (2.0.1)
Installing collected packages: flask-login
Successfully installed flask-login-0.5.0
和其他插件一样,Flask-Login需要在app/__init__py中的应用实例之后被创建和初始化。
# ...
from flask_login import LoginManager
app = Flask(__name__)
# ...
login = LoginManager(app)
# ...
准备用户模型
Flask-Login插件需要在用户模型上实现某些属性和方法。必须的四项如下:
- is_authenticated: 一个用来表示用户是否通过登录认证的属性,用True和False表示。
- is_active: 如果用户账户是活跃的,那么这个属性是True,否则就是False(译者注:活跃用户的定义是该用户的登录状态是否通过用户名密码登录,通过“记住我”功能保持登录状态的用户是非活跃的)。
- is_anonymous: 常规用户的该属性是False,对特定的匿名用户是True。
- get_id():返回用户的唯一id的方法,返回值类型是字符串(Python 2下返回unicode字符串).
Flask-Login提供了一个叫做UserMixin的mixin类来将它们归纳其中。 下面演示了如何将mixin类添加到模型中:
# ...
from flask_login import UserMixin
class User(UserMixin, db.Model):
# ...
用户加载函数
flask-login通过调用session内存储的用户ID来识别用户,基于此,应用需配置一个用户加载函数,可以调用该函数来加载给定ID的用户。 该功能可以添加到app/models.py模块中:
from app import login
# ...
@login.user_loader
def load_user(id):
return User.query.get(int(id))
三、用户登入及登出函数
用户登入视图函数
# ...
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)
用户登出视图函数
# ...
from flask_login import logout_user
# ...
@app.route('/logout')
def logout():
logout_user()
return redirect(url_for('index'))
导航栏上实现当用户登录之后,登录链接自动转换成登出链接。修改base.html模板的导航栏部分后,代码如下:
<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>
要求用户登录
Flask-Login提供了一个非常有用的功能——强制用户在查看应用的特定页面之前登录。 如果未登录的用户尝试查看受保护的页面,Flask-Login将自动将用户重定向到登录表单,并且只有在登录成功后才重定向到用户想查看的页面。
为了实现这个功能,Flask-Login需要知道哪个视图函数用于处理登录认证。在app/init.py中添加代码如下:
# ...
login = LoginManager(app)
login.login_view = 'login' #'login'值是登录视图函数(endpoint)名
Flask-Login使用名为@login_required的装饰器来拒绝匿名用户的访问以保护某个视图函数。 当你将此装饰器添加到位于@app.route装饰器下面的视图函数上时,该函数将受到保护,不允许未经身份验证的用户访问。 以下是该装饰器如何应用于应用的主页视图函数的案例:
from flask_login import login_required
@app.route('/')
@app.route('/index')
@login_required
def index():
# ...
剩下的就是实现登录成功之后自定重定向回到用户之前想要访问的页面。 当一个没有登录的用户访问被@login_required装饰器保护的视图函数时,装饰器将重定向到登录页面,不过,它将在这个重定向中包含一些额外的信息以便登录后的回转。 例如,如果用户导航到/index,那么@login_required装饰器将拦截请求并以重定向到/login来响应,但是它会添加一个查询字符串参数来丰富这个URL,如/login?next=/index。 原始URL设置了next查询字符串参数后,应用就可以在登录后使用它来重定向。
下面是一段代码,展示了如何读取和处理next查询字符串参数:
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)
# ...
在模板中显示已登录的用户
{% 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.route('/')
@app.route('/index')
def index():
# ...
return render_template("index.html", title='Home Page', posts=posts)
添加用户到数据库的唯一方法是通过Python shell执行,所以运行flask shell并输入以下命令来注册用户:
(microblog) D:\pythonProgram\PycharmProjects\microblog>flask shell
Python 3.8.5 (tags/v3.8.5:580fbb0, Jul 20 2020, 15:57:54) [MSC v.1924 64 bit (AMD64)] on win32
App: app [production]
Instance: D:\pythonProgram\PycharmProjects\microblog\instance
>>> u=User(username='susan',email='susan@mail.com')
>>> u.set_password('pass')
>>> db.session.add(u)
>>> db.session.commit()
启动应用并尝试访问 http://localhost:5000/ 或 http://localhost:5000/index ,会立即重定向到登录页面。在使用之前添加到数据库的凭据登录后,就会跳转回到之前访问的页面,并看到其中的个性化欢迎。
四、用户注册
在app/forms.py中创建Web表单类:
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.')
需要一个
HTML模板
以便在网页上显示这个表单,存储在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 %}
登录表单模板需要在其表单之下添加一个链接来将未注册的用户引导到注册页面:
<p>New User? <a href="{{ url_for('register') }}">Click to Register!</a></p>
实现处理
用户注册的视图函数
,存储在app/routes.py中,代码如下:
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)
精雕细琢之后,用户已经能够在此应用上注册帐户,并进行登录和注销。