安装: pip install flask-login
1、密码哈希
一个实现密码哈希的包是Werkzeug,当安装Flask时,你可能会在pip的输出中看到这个包,因为它是Flask的一个核心依赖项。
整个密码哈希逻辑可以在用户模型中实现为两个新的方法:
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)
使用这两种方法,用户对象现在可以在无需持久化存储原始密码的条件下执行安全的密码验证。
2、flask-login插件安装及初始化
管理用户登录状态,以便用户可以登录到应用,用户在导航到该应用的其他页面时,应用会“记得”该用户已经登录。它还提供了“记住我”的功能,允许用户在关闭浏览器窗口后再次访问应用时保持登录状态。
安装: pip install flask-login
和其他插件一样,Flask-Login需要在app/__init__py中的应用实例之后被创建和初始化:
# ...
from flask_login import LoginManager
app = Flask(__name__)
# ...
login = LoginManager(app)
# ...
3、为flask-login准备用户模型
Flask-Login插件需要在用户模型上实现某些属性和方法。只要将这些必需项添加到模型中,Flask-Login就可以与基于任何数据库系统的用户模型一起工作。
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):
# ...
4、用户加载函数
用户会话是Flask分配给每个连接到应用的用户的存储空间,Flask-Login通过在用户会话中存储其唯一标识符(ID)来跟踪登录用户。每当已登录的用户导航到新页面时,Flask-Login将从会话中检索用户的ID,然后将该用户实例加载到内存中。此时,相当于Login插件已知用户ID,需要返回具体用户,因此插件期望应用配置一个用户加载函数,可以调用该函数来加载给定ID的用户。
该功能可以添加到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装饰器来为用户加载功能注册函数。
5、用户登入
# ...
from flask_login import current_user, login_user
from app.models import User
# ...
@app.route('/login', methods=['GET', 'POST'])
def login():
#处理一个非预期的情况:假设用户已经登录,却导航到应用的*/login* URL。需要导航到/index URL
if current_user.is_authenticated:
return redirect(url_for('index'))
form = LoginForm()
if form.validate_on_submit():
#利用form表单提交的username,从数据库加载用户。SQLAlchemy查询对象的filter_by()方法,结果是一个只包含具有匹配用户名的对象的查询“结果集”。因为我知道查询用户的结果只可能是有或者没有,所以我通过调用first()来完成查询,如果存在则返回用户对象;如果不存在则返回None。
user = User.query.filter_by(username=form.username.data).first()
#如果使用提供的用户名执行查询并成功匹配,我可以接下来通过调用上面定义的check_password()方法来检查表单中随附的密码是否有效。
if user is None or not user.check_password(form.password.data):
#在这两种情况下,都会闪现一条消息,然后重定向到登录页面,以便用户可以再次尝试。
flash('Invalid username or password')
return redirect(url_for('login'))
#如果用户名和密码都是正确的,那么调用来自Flask-Login的login_user()函数。该函数会将用户登录状态注册为已登录,这意味着用户导航到任何未来的页面时,应用都会将用户实例赋值给current_user变量。
login_user(user, remember=form.remember_me.data)
#然后,只需将新登录的用户重定向到主页,就完成了整个登录过程。
return redirect(url_for('index'))
return render_template('login.html', title='Sign In', form=form)
current_user变量来自Flask-Login,可以在处理过程中的任何时候调用以获取用户对象。这个变量的值可以是数据库中的一个用户对象(Flask-Login通过我上面提供的用户加载函数回调读取),或者如果用户还没有登录,则是一个特殊的匿名用户对象。
6、用户登出
提供一个用户登出的途径也是必须的,我将会通过Flask-Login的logout_user()函数来实现。
在routes.py里定义:
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>
#current_user.is_anonymous仅当用户未登录时的值是True。当未登录时,导航栏是login,登录后,导航栏是logout。
{% if current_user.is_anonymous %}
<a href="{{ url_for('login') }}">Login</a>
{% else %}
<a href="{{ url_for('logout') }}">Logout</a>
{% endif %}
</div>
用户实例的is_anonymous属性是在其模型继承UserMixin类后Flask-Login添加的,表达式current_user.is_anonymous仅当用户未登录时的值是True。
7、特定页面强制用户登录
Flask-Login提供了一个非常有用的功能——强制用户在查看应用的特定页面之前登录。 如果未登录的用户尝试查看受保护的页面,Flask-Login将自动将用户重定向到登录表单,并且只有在登录成功后才重定向到用户想查看的页面。
为了实现这个功能,Flask-Login需要知道哪个视图函数用于处理登录认证。在app/init.py中添加代码如下:
# ...
login = LoginManager(app)
login.login_view = 'login'
#上面的'login'值是(route.py脚本的)登录视图函数(endpoint)名,换句话说该名称可用于url_for()函数的参数并返回对应的URL。
Flask-Login使用名为@login_required的装饰器来拒绝匿名用户的访问以保护某个视图函数。例如,用该装饰器保护主视图函数(index):
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')#用户登录之后,应用获取next查询字符串参数的值
#实际上有三种可能的情况需要考虑,以确定成功登录后重定向的位置:
#如果登录URL中不含next参数,那么将会'重定向到本应用的主页'。
#如果登录URL中包含next参数,其值是一个相对路径(换句话说,该URL不含域名信息),那么将会重定向到本应用的这个相对路径。
#如果登录URL中包含next参数,其值是一个包含域名的完整URL,那么'重定向到本应用的主页'。
if not next_page or url_parse(next_page).netloc != '':
next_page = url_for('index')
#第三种情况是为了使应用更安全。 攻击者可以在next参数中插入一个指向恶意站点的URL,因此应用仅在重定向URL是相对路径时才执行重定向,这可确保重定向与应用保持在同一站点中。 为了确定URL是相对的还是绝对的,我使用Werkzeug的url_parse()函数解析,然后检查netloc属性是否被设置。
return redirect(next_page)
Flask提供一个request变量,其中包含客户端随请求发送的所有信息, 特别是request.args属性,可用友好的字典格式暴露查询字符串的内容。
8、利用flask shell注册用户并在页面显示已登录的用户
在index.html模板中使用Flask-Login的current_user
,同时在视图函数传入渲染模板函数的参数中删除user。
注册用户:
由于仍然没有用户注册功能,所以添加用户到数据库的唯一方法是通过Python shell执行,所以运行flask shell并输入以下命令来注册用户:
>>> u = User(username='susan', email='susan@example.com')
>>> u.set_password('cat')
>>> db.session.add(u)
>>> db.session.commit()
9、通过Web表单进行用户注册
开发注册表单,以便用户可以通过Web表单进行注册。
在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字段,我在DataRequired之后添加了第二个验证器,名为Email。 这个来自WTForms的另一个验证器将确保用户在此字段中键入的内容与电子邮件地址的结构相匹配。
email = StringField('Email', validators=[DataRequired(), Email()])
#由于这是一个注册表单,习惯上要求用户输入密码两次,以减少输入错误的风险。 出于这个原因,我提供了password和password2字段。
password = PasswordField('Password', validators=[DataRequired()])
#EqualTo验证器,它将确保其值与第一个password字段的值相同。
password2 = PasswordField(
'Repeat Password', validators=[DataRequired(), EqualTo('password')])
submit = SubmitField('Register')
#当添加任何匹配模式validate_ <field_name>的方法时,WTForms将这些方法作为自定义验证器,并在已设置验证器之后调用它们。这两个方法执行数据库查询,并期望结果集为空。否则,则通过ValidationError触发验证错误。
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 %}
来实现处理用户注册的视图函数,存储在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 validate_on_submit()条件块下,完成的逻辑如下:使用获取自表单的username、email和password创建一个新用户,将其写入数据库,然后重定向到登录页面以便用户登录。
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)
此时,还需要在login.html文件最下方添加“注册”导航窗口:
<p>New User? <a href="{{ url_for('register') }}">Click to Register!</a></p>
flask run:
click to register
注册后login:
最后logout: