小引
- 完成两个需求:用户的登录(
login
)及新用户的注册(register
) - 为完成 login,引入Flask-Login
- Flask-Login可通过存储用户的 unique identifier 来 keep tracking
- Flask-Login 提供多种 features,如
UserMixin, @login.user_loader, current_user, login_user, logout_user, login_required ( with login.login_view),request
等
Pashword Hashing
常规的login,需要验证 username 及 password。
Model User 中之所以设置 password_hash 而非直接的 password,是为安全起见,db 不直接存储 password,而是存储 password 的 hash 值。
Werkzeug,作为 Flask 的 core dependency之一,其中 security 模块有两个函数用于hashing: generate_password_hash
, check_password_hash
- app / models.py: Password hashing and verification
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)
Intro to Flask-Login
(venv) $ pip install flask-login
- This extension manages the user logged-in state, so that for example users can log in to the application and then navigate to different pages while the application “remembers” that the user is logged in.
- It also provides the “remember me” functionality that allows users to remain logged in even after closing the browser window.
初始化:每次引入新的扩展,往往需要初始化 (app/__init__.py
),从 flask_login
引入 LoginManager
,对 app instance 进行 login 的初始化。
# ...
from flask_login import LoginManager
app = Flask(__name__)
# ...
login = LoginManager(app)
# ...
Prepare The User Model for Flask-Login
Requirements: Flask-Login works with app’s User Model, and expects certain properties / methods to be implemented in it.
Four required items:
is_authenticated
: property; True if user has valid credentials, False otherwise.is_active
: property; True if user’s account is active, False otherwise.is_anonymous
: property; False for regular users, True for a special, anonymous user.get_id()
: method; returns a unique identifier for the user as a string (unicode for Python 2).
UserMixin:Since these implementations are rather generic, we can use a mixin class from Flask-Login
app / models.py:
Flask-Login user mixin class
# ...
from flask_login import UserMixin
class User(UserMixin, db.Model):
# ...
User Loader Function
原理:任何一个 user 连接到 application 时,Flask 都会给其分配一个 user session (storage space),Flask-Login 会将每个 user 的 unique identifier 存储到相应的 user session 中。每当 logged-in user 浏览至新的网页时,Flask-Login 就会从其对应的 user session 中提取此 user 的 ID, 并将此 user 载入memory。
Flask-Login keeps track of the logged in user by storing its unique identifier in Flask’s user session, a storage space assigned to each user who connects to the application. Each time the logged-in user navigates to a new page, Flask-Login retrieves the ID of the user from the session, and then loads that user into memory.
由于 Flask-Login 不涉及数据库,在载入 user 的时候需要 application 的辅助 ,为此配置一个 user loader function, 在其被调用时能够基于给定的 ID 来载入 user。
Because Flask-Login knows nothing about databases, it needs the application’s help in loading a user. For that reason, the extension expects that the application will configure a user loader function, that can be called to load a user given the ID. This function can be added in the app/models.py module:
app/models.py
: Flask-Login user loader function
from app import login
# ...
@login.user_loader
def load_user(id):
return User.query.get(int(id))
注: The id that Flask-Login passes to the function as an argument is going to be a string, so databases that use numeric IDs need to convert the string to integer as above.
Logging Users In
- 回顾:之前 login view function 试过 fake login 并 flash 出一条信息 (
Chapter 3: Web Forms
):
@app.route('/login', methods=['GET', 'POST'])
def login():
form = LoginForm()
if form.validate_on_submit():
flash('Login requested for user {}, remember_me={}'.format(
form.username.data, form.remember_me.data))
return redirect(url_for('index'))
return render_template('login.html', title='Sign In', form=form)
现在 application 已可以连接 db,且知道如何生成及验证 password_hash,时机成熟,可以将此 login view func 完善。
准备工作:初始化 Flask-Login, 更改 User Model(添加 UserMixin),配置 user_loader (
app / models.py
中) 。继续完善
app /routes.py
: Login view function logic
# ...
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)
current_user
系Flask-Login自带变量 ,任何时候想获得请求客户端(client of the request)的 用户(user object)时,都可调用此变量。
此变量值分两种:如果已登录(logged-in),则为Flask-Login 通过 user loader 返回获得的 db 中的 一个 user object;如未登录,则为一个特殊的匿名用户(a special anonymous user object)。The current_user variable comes from Flask-Login and can be used at any time during the handling to obtain the user object that represents the client of the request. The value of this variable can be a user object from the database (which Flask-Login reads through the user loader callback I provided above), or a special anonymous user object if the user did not log in yet.
一旦 user (尚未logged-in)通过 username / password 的双重验证,则调用
login_user()
函数进行logging。此后,无论此用户浏览任何网页,网页都会将current_user
变量设定为此 user。If the username and password are both correct, then I call the login_user() function, which comes from Flask-Login. This function will register the user as logged in, so that means that any future pages the user navigates to will have the current_user variable set to that user.
最后,所有登录成功的(已登录 / 重新登录),均被 redirect 到 ‘index’ 。
Logging Users Out
app / routes.py
: Logout view function
# ...
from flask_login import logout_user
# ...
@app.route('/logout')
def logout():
logout_user()
return redirect(url_for('index'))
注:一旦 logout,则跳转到 ‘index’。但由于 ‘index’ 设定了 login_required(见下文),所以会重新跳转到 ‘login’(通过__init__.py
中的 login.login_view='login'
设定)。
- 对登录后的用户(logged-in user),希望页面上方的导航条中的 Login 改为 Logout:
app / templates/base.html
: Conditional login and logout links
<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>
反向判断:未登录(anonymous),则显示 Login;其他,则显示 Logout。
(is_anonymous,见 User Model 中的 UserMixin )
Requiring Users To Login
- 应用场景:如果某网页设置为保户状态(login_required),则未登录用户无法浏览,此时会被 redirect 至 ‘login’;用户进行登录验证后,会被 redirect 至 之前尚无权限浏览的原网页。
- Requirements: Flask-Login needs to know what is the view function that handles logins.
1.app/__init__.py
: 定义 login.login_view
# ...
login = LoginManager(app)
login.login_view = 'login'
这里的 ‘login’,类似url_for(view_function_name)的用法,指的是 view function.
当未登录用户访问某路由时,如果此路由被 @login_required 修饰过,则会被重定向至 login.login_view = 'login'
定义的 ‘login’ ,以便进行登录验证。
2.app/routes.py
: @login_required decorator 修饰需要被保护的 view_func。
from flask_login import login_required
@app.route('/')
@app.route('/index')
@login_required
def index():
# ...
用 @login_required
修饰 ‘index’,即 home page。
未登录用户访问 /index 时,其request 会被 @login_required
截获,并重定向至 /login;同时,原 URL 被重定向为 /login?next=/index
。
此 next 请求字符参数,被设定为 源 URL, 所以application 可在用户登录后据此重定向回来。
If the user navigates to /index, for example, the @login_required decorator will intercept the request and respond with a redirect to /login, but it will add a query string argument to this URL, making the complete redirect URL /login?next=/index.
The next query string argument is set to the original URL, so the application can use that to redirect back after login.
3.app / routes.py
: Redirect to “next” page
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)
# ...
login_user() 登录用户后,通过 flask 中的 request 变量 来获取 next
参数,即next_page = request.args.get('next')
。
flask 的 request 变量,包含客户端发起的请求中的所有信息,并通过 request.args 属性以 dict 方式展现请求字符串的内容 (contents of the query string),所以可以采用 request.args.get('next')
的方式获取 next
参数。
Flask provides a request variable that contains all the information that the client sent with the request. In particular, the request.args attribute exposes the contents of the query string in a friendly dictionary format.
获得的 next_page,分三种情况:
(1) 不存在,则直接重定向至主页的 ‘index’ (通过url_for())。
(2) 存在,但为绝对路径(包含域名),则为安全起见,直接定向为 ‘index’。
(3) 存在,且为 相对路径(不含域名),形如 ‘/index’,则重定向至 next_page。注意,直接用 redirect(next_page), 而不涉及 url_for(),因 next_page 是完备的 相对路径,且尚不知对应的 view func。
注:To determine if the URL is relative or absolute, we parse it with Werkzeug’s url_parse()
function and then check if the netloc
component is set or not.
Showing The Logged In User in Templates
- 之前 chapter 2 中,写 home page (index.html)时,由于 用户系统(user subsystem)尚未搭建,所以采用了 fake user,如下:
@app.route('/')
@app.route('/index')
def index():
user = {'username': 'Miguel'}
posts = [
{
'author': {'username': 'John'},
'body': 'Beautiful day in Portland!'
},
{
'author': {'username': 'Susan'},
'body': 'The Avengers movie was so cool!'
}
]
return render_template('index.html', title='Home', user=user, posts=posts)
对应的 index. html
{% extends "base.html" %}
{% block content %}
<h1>Hi, {{ user.username }}!</h1>
{% for post in posts %}
<div><p>{{ post.author.username }} says: <b>{{ post.body }}</b></p></div>
{% endfor %}
{% endblock %}
- 现在已经搭建 用户系统 (User Model),可以去掉 fake user,如下:
app / templates / index.html
: Pass current user to template
{% 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 调为 current_user。
app / routes.py
: not pass user to template anymore
@app.route('/')
@app.route('/index')
def index():
# ...
return render_template("index.html", title='Home Page', posts=posts)
- 测试
因为尚无注册用户,所以只能通过 shell 环境创建用户,然后写入 db。
>>> u = User(username='susan', email='susan@example.com')
>>> u.set_password('cat')
>>> db.session.add(u)
>>> db.session.commit()
启动 app,输入 http://localhost:5000/
或者 http://localhost:5000/index
, 将被直接重定向至 login 页面。
用刚刚创建的上述用户名及密码登录后,将被定向至原网页,即 /index 页。
User Registration
- 创建注册样表
app / forms.py
: User registration form
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.')
RegistrationForm 中涉及 邮箱、二次密码等,故自 wtforms.validators 中新引入验证函数: Email (用户验证邮箱)、EqualTo(二次验证password)以及 ValidationError(验证报错)。
采用 validate_<field_name>
模式自行设置两个 method。
WTForms would invoke the custom validators in addition to the stock validators, in case of ValidationException.
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 %}
email 的form 字段长度设置为 64,大于其他的 32。
app / templates / login.html
: login页面 添加 register 链接
<form>
...
</form>
<p>New User? <a href="{{ url_for('register') }}">Click to Register!</a></p>
app / routes.py
: register 路由
因为注册后的用户信息,须写入 db,所以引入 db。(上文测试时,手动在shell中添加用户并写入db)
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)
(1) 已登录用户,定向至 /index。
(2) 初次 GET 请求,返回 register.html。
(3) 填写完毕并POST,利用 user.set_password(form.password.data)
设置 self.password_hash
。
(4) 提交修改,写入 db。
(5) flash 成功消息,并定向至 ‘login’。
小结
- 引入 Flask-Login —-> 初始化 login —->更改 User Model (UserMixin) —-> 设置 user loader —-> 更新 login 路由(最重要的 login_user())—-> logout 路由
如果页面有 login_required 需求 —-> 初始化设置 login.login_view —-> 涉及两次重定向 (如 /login?next=/index) —-> 重新更新 login 路由,以便登录后重定向源网页 (–> 更新 index.html —-> 更新 index 路由)
路由中,往往需要涉及判断是否是登录用户(
current_user.is_authenticated
)