写在前面
本文学习来源The-Flask-Mega-Tutorial-zh,在此表达对作者和译者的感谢,正是因为他们,才能学习到这么好的免费教程。本机环境:选用的Makedown编辑器为Atom,实验环境为Ubuntu18.04,Python版本为 3.7.1本篇仅作为自己Flask入门的记录,想通过此来记录代码和自己不懂的概念。下面通过之前学习到的用户登录表单和数据库来构建用户登录子系统。
密码哈希
在此先让我们讨论一下密码哈希,在数据库的学习过程中,用户模型设置了一个password——hash
字段,用于保存用户密码的hash值,并在登录过程中进行验证,网络安全一直是需要注意的问题(举个栗子,由于个人搭建的wordpress被黑了,访问就会进行恶意跳转,现在考虑重新搭建,就很心塞),在Flask
中可以通过Werkzeug
来实现密码哈希,下面请看这段demo
:
>>> from werkzeug.security import generate_password_hash
>>> hash = generate_password_hash('evil')
>>> hash
'pbkdf2:sha256:150000$mb3k5sJ8$5e705d22055e739afef0318113ee239f7af11709ae493c660910c4abdc8bfe24'
通过一系列操作,将密码evil
变成了长编码字串,这样就算获得密码哈希也无法逆推原始密码。
验证过程可以通过check_password_hash
来完成:
>>> from werkzeug.security import check_password_hash
>>> check_password_hash(hash, 'evil')
True
>>> check_password_hash(hash, 'Devil')
False
可以看到之前生成了evil
的hash,检查中对hash和evil
执行hash过程进行了匹配,返回了True
而Devil
没有与之对应的hash,则返回了False
。
在用户模型中实现如下:
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 = 'evil', email = '[email protected]')
>>> u.set_password('evilpassword')
>>> u.check_password('evilpassword')
True
>>> u.check_password('Devilpassword')
False
下面来看一下一个非常受欢迎的插件--->Flask-Login
Flask-Login
Flask-Login为Flask提供会话管理,处理长时间登录,它还提供了“记住我”的功能,允许用户在关闭浏览器窗口后再次访问应用时保持登录状态。先在虚拟环境中进行安装。
pip3 install flask-login
然后需要在app/__init__.py
中初始化:
# ...
from flask_login import LoginManager
my_app = Flask(__name__)
# ...
login = LoginManager(app = my_app)
# ...
准备用户模型
Flask-login文档中说明,用户类中需要完成以下四个属性:
-
is_authenticated
:若用户通过登录验证,则返回True
,否则返回Flase
-
is_active
:如果是活跃用户,则属性为True
,否则为Flase
-
is_anonmous
:实际用户返回Flase
,匿名用户返回True
-
get_id()
:返回用户的唯一id的方法,返回值类型是字符串.
Flask-Login
提供了UserMixin
的mixin类来实现它们,下面将Mixin类添加到模型中:
from flask_login import UserMixin
class User(UserMixin, my_db.Model):
#...
用户加载函数
用户会话是Flask分配给每个连接到应用的用户的存储空间,Flask-Login通过在用户会话中存储其唯一标识符来跟踪登录用户。每当已登录的用户导航到新页面时,Flask-Login将从会话中检索用户的ID,然后将该用户实例加载到内存中。
因为数据库对Flask-Login透明,插件期望应用配置一个用户加载函数,可以调用该函数来加载给定ID的用户。 该功能可以添加到app/models.py
模块中:
from app import login
#...
@login.user_loader
def load_user(id):
return(User.query.get(int(id)))
@login.user_loader
装饰器为用户加载功能注册函数。Flask-Login将字符串类型的参数id
传入用户加载函数,因此使用数字ID的数据库需转换成整数。
用户登入
在登录视图函数中,实现了模拟登陆,发出了一个flash()
消息。现在,让我们完善验证工作:
from flask_login import current_user, login_user
from app.models import User
#...
@my_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)
从代码中看到,如果用户是登陆状态,但仍在登录界面,应返回index
,先前使用flash()
模拟登陆,现在进行真实登录,先从数据库加载用户,在SQLAlchemy
中通过filter_by()
方法来查询,它返回一个只包含具有匹配用户名的对象的查询结果集,当只需要一个结果时,通常使用first()
方法。如果用户名成功匹配数据库,通过`check_password()``方法来检查表单中随附的密码,密码验证通过比对hash值完成,可能出现的错误包括:用户名可能是无效的,或者用户密码是错误的。 在这两种情况下,我都会闪现一条消息,然后重定向到登录页面,以便用户可以再次尝试。
如果两者均正确,则调用login_user()
函数。该函数将用户状态注册为已登录,然后将页面重定向到主页,完成整个登陆过程。
用户登出
用户登出功能可以通过logout_user()
函数来实现,视图函数代码如下:
#...
from flask_login import logout_user
#...
@my_app.route('/logout')
def logout():
logout_user()
return redirect(url_for('index'))
而登出链接的实现可以通过在导航栏当用户实现登陆后,登录链接自动转换为登出链接,对base.html
进行修改如下:
在用户未登录时current_user.is_anonymous
仅当用户未登录时值为True
要求用户登录
Flask-Login可以实现在查看特定页面之前登录,如果未登录用户想要查看受到保护的内容,Flask-Login会重定向到登录界面.
实现该功能前,Flask-Login需要知道哪个函数处理登录认证。在app/__init__.py
中添加代码:
login = LoginManager(my_app)
login.login_view = 'login'
上面的login
的值是登录函数视图名,该名称可用于url_for()
函数的参数并返回对应的URL。
FLask-Login使用@login_required
来进行页面保护,禁止未登录的用户查看,使用方法如下:
from flask_login import login_required
@my_app.route('/')
@my_app.route('/index')
@login_required
def index():
...
我们还要实现登录完成的重定向,首先,一个匿名用户想查看受保护页面时,会定向到登录界面,然而通常在我们登录过后会进行跳转,这是因为next
参数的作用。
from flask import request
from werkzeug.urls import url_parse
@my_app.route('/login', methods=['GET', 'POST'])
def login():
#...
if form.validate_on_submit():
user = User.query.filter_by(username.data).first()
if user is None or not user.check_possword(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)
#...
在用户调用了login_user()
函数登录后,应用获取了next查询字串的值。Flask提供一个request变量,其中包含客户端随请求发送的所有信息。 特别是request.args属性,可用友好的字典格式暴露查询字符串的内容。 实际上有三种可能的情况需要考虑,以确定成功登录后重定向的位置:
- 如果登录URL中不含next参数,那么将会重定向到本应用的主页。
- 如果登录URL中包含next参数,其值是一个相对路径(换句话说,该URL不含域名信息),那么将会重定向到本应用的这个相对路径。
- 如果登录URL中包含next参数,其值是一个包含域名的完整URL,那么重定向到本应用的主页。
第三种让人摸不着头脑,它主要是为了保证站点安全。攻击者可以在next参数中插入一个指向恶意站点的URL,因此应用仅在重定向URL是相对路径时才执行重定向,这可确保重定向与应用保持在同一站点中。
显示已登录用户
现在可以删除模拟用户,而使用真正的用户
{% extends "base.html" %}
{% block content %}
Hi, {{ current_user.username }}!
{% for post in posts %}
{{ post.author.username }} says: {{ post.body }}
{% endblock %}
并且可以routes.py
中删除user
了:
@my_app.route('/')
@my_app.route('/index')
def index():
#...
return render_template("index.html", title='Home Page', posts=posts)
到目前还没有实现用户注册功能,下面让我们实现用户通过Web表单进行注册功能。
用户注册
在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.')
从代码中看到,对于email字段,后多加了一个验证器Email
,它可以确保用户输入的结构符合电子邮件的结构;还可以看到具有俩个Password字段,第二个字段使用EqualTo验证器,用来保证两次输入的密码一致。
后面的两个新方法是用来确保用户输入的username和email不会与数据库中存在的冲突。
现在创建HTNL模板使得其在网页上显示表单,新建app/templates/register.html文件:
{% extends "base.html" %}
{% block Devil %}
Register
{% endblock %}
同时需要在登录表单模板下添加一个链接来使未注册用户注册
New User? Click to Register!
现在到了构建处理用户注册的视图函数了:
from app import my_db
from app.forms import RegistrationForm
#...
@my_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)
my_db.session.add(user)
my_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)
从视图函数中可以看到,首先确保路由用户没有登录,然后注册时获取表单中的各字段数据,创建一个新用户,并写入数据库,在注册成功后会重定向到登录界面。运行结果如下: