- 用户登录功能是 Web 系统一个基本功能,是为用户提供更好服务的基础,在 Flask 框架中怎么做用户登录功能呢?今天学习一下 Flask 的用户登录组件
Flask-Login - Python 之所以如此强大和流行,除了本身易于学习和功能丰富之外,最重要的是因为各种类库和组件,可以说没有 Python 做不了的事情,只有不知道的组件。
- 之所以选择
Flask-Login,是因为它基于Session,适合做有 UI 交互的用户登录,用我们学习了的 Flask 表单做演示,更容易理清用户登录的流程 -
用户登录说明
Flask-Login和其他 Flask 组件并没有太大区别,有必要开始之前了解下用户登录的步骤:-
- 登录:用户提供登录凭证(如用户名和密码)提交给服务器
- 建立会话:服务器验证用户提供的凭证,如果通过验证,则建立会话(
Session),并返回给用户一个会话号(Session id) - 验证:用户在后续的交互中提供会话号,服务器将根据会话号(
Session id)确定用户是否有效 - 登出:当用户不再与服务器交互时,注销与服务器建立的会话
-
依据以上步骤,我们设计一个应用场景,作为实现:
- 提供一个主页,需要登录才能访问
- 如果没有登录,跳转到登录页面,登录成功再跳回
- 登录成功后,可以点击登出退出登录
- 在登录页面提供注册连接,点击后跳转到注册页面
- 注册完成后,跳转到登录页面
-
初始化
- 先实例化
login_manager对象,然后用它来初始化应用: -
from flask import Flask from flask_login import LoginManager # ... app = Flask(__name__) # 创建 Flask 应用 app.secret_key = 'abc' # 设置表单交互密钥 login_manager = LoginManager() # 实例化登录管理对象 login_manager.init_app(app) # 初始化应用 login_manager.login_view = 'login' # 设置用户登录视图函数 endpoint
-
- 表单交互时,所以要设置
secret_key,以防跨域攻击( CSRF ) - 登录管理对象
login_manager的login_view属性,指定登录页面的视图函数 (登录页面的endpoint),即验证失败时要跳转的页面,这里设置为登录页
- 表单交互时,所以要设置
-
用户模块
-
用户数据
- 要做用户验证,需要维护用户记录,为了方便演示,使用一个全局列表
USERS来记录用户信息,并且初始化了两个用户信息: -
from werkzeug.security import generate_password_hash # ... USERS = [ { "id": 1, "name": 'lily', "password": generate_password_hash('123') }, { "id": 2, "name": 'tom', "password": generate_password_hash('123') } ]
-
用户信息只包含最基本的信息:
name为登录用户名password为登录密码,切忌:无论如何不要在系统中存放用户密码的明文,幸运的是模块werkzeug.security提供了generate_password_hash方法,使用 sha256 加密算法将字符串变为密文id为用户识别码,相当于主键
基于用户信息,定义两方法,用来创建(
create_user)和获取(get_user)用户信息: -
from werkzeug.security import generate_password_hash import uuid # ... def create_user(user_name, password): """创建一个用户""" user = { "name": user_name, "password": generate_password_hash(password), "id": uuid.uuid4() } USERS.append(user) def get_user(user_name): """根据用户名获得用户记录""" for user in USERS: if user.get("name") == user_name: return user return None
-
create_user接受用户名和密码,创建用户记录,对密码明文进行加密,并添加用户ID(使用uuid模板的uuid4方法生成一个全球唯一码),存储到USERS列表中get_user接受用户名,从USERS列表中查找用户记录,没有返回空
-
用户类
- 下面创建一个用户类,类维护用户的登录状态,是生成
Session的基础,Flask-Login提供了用户基类UserMixin,方便定义自己的用户类,我们定义一个User: -
from flask_login import UserMixin # 引入用户基类 from werkzeug.security import check_password_hash # ... class User(UserMixin): """用户类""" def __init__(self, user): self.username = user.get("name") self.password_hash = user.get("password") self.id = user.get("id") def verify_password(self, password): """密码验证""" if self.password_hash is None: return False return check_password_hash(self.password_hash, password) def get_id(self): """获取用户ID""" return self.id @staticmethod def get(user_id): """根据用户ID获取用户实体,为 login_user 方法提供支持""" if not user_id: return None for user in USERS: if user.get('id') == user_id: return User(user) return None
-
- 实例化方法接受一个用户记录,即
USERS列表中的一个元素,用来初始化成员变量 get_id方法返回用户实例的ID,这是必须实现的,不然Flask-Login将无法判断用户是否被验证get是个静态方法,即可以通过类之间调用,是为了在获取验证后的用户实例时用的,必须接受参数ID,返回ID所以对应的用户实例verify_password方法接受一个明文密码,与用户实例中的密码做校验,将被用在用户验证的判断逻辑中
- 实例化方法接受一个用户记录,即
-
加载登录用户
- 有了用户类,并且实现了
get方法,就可以实现login_manager的user_loader回调函数了,user_loader的作用是根据Session信息加载登录用户,它根据用户ID,返回一个用户实例: -
@login_manager.user_loader # 定义获取登录用户的方法 def load_user(user_id): return User.get(user_id)
-
登录页面
页面包括后台和展现(可以理解成前台)两部分
-
from wtforms import StringField, PasswordField from wtforms.validators import DataRequired, EqualTo # ... class LoginForm(FlaskForm): """登录表单类""" username = StringField('用户名', validators=[DataRequired()]) password = PasswordField('密码', validators=[DataRequired()])
-
- 定义用户名和密码两个字段,分别是字符类型字段和密码类型字段,密码类型字段会在页面上显示为密码形式,以提高安全性
- 为两个字段设置必填规则
然后定义一个用户登录的视图函数
login: -
from flask import render_template, redirect, url_for, request from flask_login import login_user # ... @app.route('/login/', methods=('GET', 'POST')) # 登录 def login(): form = LoginForm() emsg = None if form.validate_on_submit(): user_name = form.username.data password = form.password.data user_info = get_user(user_name) # 从用户数据中查找用户记录 if user_info is None: emsg = "用户名或密码密码有误" else: user = User(user_info) # 创建用户实体 if user.verify_password(password): # 校验密码 login_user(user) # 创建用户 Session return redirect(request.args.get('next') or url_for('index')) else: emsg = "用户名或密码密码有误" return render_template('login.html', form=form, emsg=emsg)
-
分析下视图函数的逻辑:
- 视图函数同时支持
GET和POST方法 form.validate_on_submit()可以判断用户是否完整的提交了表单,只对POST有效,所以可以用来判断请求方式- 如果是
POST请求,获取提交数据,通过get_user方法查找是否存在该用户 - 如果用户存在,则创建用户实体,并校验登录密码
- 校验通过后,调用
login_user方法创建用户Session,然后跳转到请求参数中next所指定的地址或者首页 (不用担心如何设置next,还记得上面设置的login_manager.login_view = 'login'吗?对,未登录访问时,会跳转到login,并且带上next查询参数) - 非
POST请求,或者未经过验证,会显示login.html模板渲染后的结果
- 视图函数同时支持
-
前台
在
templates模板下创建登录页面的模板login.html: -
{% macro render_field(field) %} <!-- 定义字段宏 --> <dt>{{ field.label }}: <dd>{{ field(**kwargs)|safe }} {% if field.errors %} <ul class=errors> {% for error in field.errors %} <li>{{ error }}</li> {% endfor %} </ul> {% endif %} </dd> {% endmacro %} <!-- 登录表单 --> <form method="POST"> {{ form.csrf_token }} {{ render_field(form.username) }} {{ render_field(form.password) }} {% if emsg %} <!-- 如果有错误信息 则显示 --> <h3> {{ emsg }}</h3> {% endif %} <input type="submit" value="登录"> </form> -
render_field是 Jinja2 模板引擎的宏,接受表单字段将其渲染成 Html 代码,并格式化错误信息emsg错误信息单独做了处理,如果存在会显示出来form中并没有action属性,默认为当前路径
-
需要验证的页面
为了方便演示,将首页作为需要验证的页面,通过验证将看到登录者欢迎信息,页面上还有个登出链接
首页视图函数
index: -
from flask import render_template, url_for from flask_login import current_user, login_required # ... @app.route('/') # 首页 @login_required # 需要登录才能访问 def index(): return render_template('index.html', username=current_user.username)
-
- 注解
@login_required会做用户登录检测,如果没有登录要方法此视图函数,就被跳转到login接入点(endpoint) current_user是当前登录者,是User的实例,是Flask-Login提供全局变量( 类似于全局变量g)username是模板中的变量,可以将当前登录者的用户名传入index.html模板
首页模板
index.html: - 注解
-
<h1>欢迎 {{ username }}!</h1> <a href='{{ url_for('logout')}}'>登出</a> - 登出视图函数
logout: -
from flask import redirect, url_for from flask_login import logout_user # ... @app.route('/logout') # 登出 @login_required def logout(): logout_user() return redirect(url_for('login'))
-
- 只有登录了才有必要登出,所以加上注解
@login_required logout_user方法和login_user相反,由于注销用户的Session- 登出视图不需要模板,直接跳转到登录页,实际项目中可以增加一个登出页,展示些有趣的东西
- 只有登录了才有必要登出,所以加上注解
-
小试牛刀
终于可以试试了,加上启动代码:
-
if __name__ == '__main__': app.run(debug=True)
- 启动项目,如果一切正常将看到类似的反馈:
-
python app.py * Serving Flask app "app" (lazy loading) * Environment: production WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. * Debug mode: on * Restarting with stat * Debugger is active! * Debugger PIN: 176-611-251 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
- 访问 localhost:5000,将看到登录页,主要浏览器地址上的
next查询参数: - 填写正确的用户名和密码,点击登录,将进入首页:
-
用户注册
上面的演示了,已存在用户登录的情况,不存在用户需要完成注册才能登录。
注册功能和登录很类似,页面上多了密码确认字段,并且需要验证两次输入的密码是否一致,后台逻辑是:如果用户不存在,且通过检验,将用户数据保存到
USERS列表中,跳转到login页面。关于具体实现这里不做详细讲解了,本节代码示例中有实现,可以参考。
-
Flask-Login 其他特性
上面的实例中使用了一些
Flask-Login的基本特性,Flask-Login还提供了一些其他重要特性记住我
记住我,并不是用户登出之后,再次登录时自动填写用户名和密码(这是浏览器的功能),而是在用户意外退出后(比如关闭浏览器)不用再次登录。
如果用户本地的
cookie失效了,Flask-Login会自动将用户Session放入cookie中。开启方法是将
login_user方法的命名参数remember设置为True,此功能默认是关闭的Session 防护
Session信息一般存放在cookie中,但是cookie不够安全,容易被窃取其中Session信息,伪造用户登录系统,幸运的是Flask-Login提供了Session防护机制,提供有basic和strong两种保护等级,通过login_manager.session_protection来开关和设置等级,默认等级为basic,如果设置为None将关闭Session防护机制。在保护机制开启的情况下,每次请求会根据用户的特征(一般指有用户IP、浏览器类型生成的哈希码)与
Session中的对比,如果无法匹配则要求用户重新登录,在强模式下(strong)一旦匹配失败会删除登录者Session,以消除攻击者重构cookie的可能Request Loader
有时候因为一些原因不想或者无法使用
cookie,可以将Session记录在其他地方,比如Header中或者请求参数中,那么构造用户Session时就需要将user_loader替换为request_loader,request_loader将request作为参数,这样就可以从请求的任何数据中获取Session信息了
