Flask-Login详解
Flask-Login详解
关于Flask登录认证的详细过程请参见拙作<<使用Flask实现用户登陆认证的详细过程>>一文,而本文则偏重于详细介绍Flask-Login的原理,代码的解析。
首次登陆
我们首先来看一下首次登录验证的流程图:

Flask-Login在登录过程中主要负责:
- 将用户对象存入request context中
- 将用户ID,Session ID等信息存入Session中
在<<使用Flask实现用户登陆认证的详细过程>>中我们已经介绍过如何通过Flask-Login来实现登录的过程,其中最重要的代码就是login_user,如下:
login_user(user, remember=remember_me)
那么login_user具体做了什么呢?我们来看下源码
def login_user(user, remember=False, force=False, fresh=True):
if not force and not user.is_active:
return False
user_id = getattr(user, current_app.login_manager.id_attribute)()
session['user_id'] = user_id
session['_fresh'] = fresh
session['_id'] = current_app.login_manager._session_identifier_generator()
if remember:
session['remember'] = 'set'
_request_ctx_stack.top.user = user
user_logged_in.send(current_app._get_current_object(), user=_get_user())
return True
getattr(user, current_app.login_manager.id_attribute)()这里login_manager.id_attribute是一个字符串'get_id'。因此这句的意思是获取User对象的get_id method,然后执行,从而获取到用户的ID- 通过
session['user_id'] = user_id来将用户的ID存储进Session当中,后面紧跟着将fresh信息,session id信息,remember信息存储进session。
注意:Flask的session是以cookie为基础,但是是在Server端使用secret key并使用AES之类的对称加密算法进行加密的,然后将加密后的cookie发送给客户端。由于是加密后的数据,客户端无法篡改数据,也无法获知session中的信息,只能保存该session信息,在之后的请求中携带该session信息
_request_ctx_stack.top.user = user这里是将user对象存储进当前的request context中,_request_ctx_stack是一个LocalStack对象,top属性指向的就是当前的request context。关于LocalStack及相关技术,请参考拙作<<Werkzeug(Flask)之Local、LocalStack和LocalProxy>>user_logged_in.send(current_app._get_current_object(), user=_get_user())此句中user_logged_in是Flask-Login定义的signal,此处通过send来发射此signal,当注册监听此signal的回调函数收到此signal之后就会执行函数。这里send有两个参数,第一个参数是sender对象,此处通过current_app._get_current_object()来获取当前的app对象,即此signal的sender设为当前的应用;第二个参数是该signal携带的数据,此处将user对象做为signal的数据传递给相应的回调函数。关于signal的详细解释请参考拙作<<Flask Signals详解>>
非首次登陆
非首次登陆流程图如下:

在这个流程图中,Flask-Login主要起如下作用:
- 从session中获取用户ID
- 当用户的请求访问的是受登录保护的路由时,就要通过用户ID重新load user,如果load user失败则进入鉴权失败处理流程,如果成功,则允许正常处理请求
那么Flask-Login究竟是如何保护路由的呢?我们来看个例子:
@app.route('/')
@app.route('/main')
@login_required
def main():
return render_template(
'main.html', username=current_user.username)
我们看到只要给路由函数加一个@login_required装饰器就可以了,那么这个装饰器究竟是怎么做到的呢?来看下源码:
# flask_login/utils.py
def login_required(func):
@wraps(func)
def decorated_view(*args, **kwargs):
# 如果request method为例外method,即在EXEMPT_METHODS中的method,可以不必鉴权
if request.method in EXEMPT_METHODS:
return func(*args, **kwargs)
# 如果_login_disabled为True则不必鉴权
elif current_app.login_manager._login_disabled:
return func(*args, **kwargs)
# 正常鉴权
elif not current_user.is_authenticated:
return current_app.login_manager.unauthorized()
return func(*args, **kwargs)
return decorated_view
- 默认情况下只有OPTIONS method在EXEMPT_METHODS set中,而GET、PUT、POST等常见的methods都需要鉴权
_login_disabled默认为False- 正常鉴权的关键在于
current_user.is_authenticated是否为True,为True则正常处理请求,为False则进入unauthorized处理流程。那么这个current_user到底怎么就能鉴权了?它是怎么来的呢?来看下定义:
# flask_login/utils.py
current_user = LocalProxy(lambda: _get_user())
原来current_user是一个LocalProxy对象,其代理的对象需要通过_get_user()来获取,简单来说_get_user()会返回两种用户,一种是正常的用户对象(鉴权成功),一种是anonymous用户对象(鉴权失败)。而正常的用户对象其is_authenticated属性总是为True,相对的anonymous用户对象的is_authenticated属性总是为False
LocalProxy对象每次操作都会重新获取代理的对象从而实现动态更新,关于LocalProxy的详细说明请参考拙作<<Werkzeug(Flask)之Local、LocalStack和LocalProxy>>
而要实现动态更新的关键就在于_get_user函数,接下来我们看下_get_user函数是如何获取user对象的:
# flask_login/utils.py
def _get_user():
if has_request_context() and not hasattr(_request_ctx_stack.top, 'user'):
current_app.login_manager._load_user()
return getattr(_request_ctx_stack.top, 'user', None)
在之前的首次登陆那小节中,我们已经知道用户鉴权成功后,会将User对象保存在当前的request context当中,这时我们调用_get_user函数时就会直接从request context中获取user对象return getattr(_request_ctx_stack.top, 'user', None)
但如果是非首次登陆,当前request context中并没有保存user对象,就需要调用current_app.login_manager._load_user()来去load user对象,接下来再看看如何去load:
# flask_login/login_manager.py
def _load_user(self):
'''Loads user from session or remember_me cookie as applicable'''
user_accessed.send(current_app._get_current_object())
# first check SESSION_PROTECTION
config = current_app.config
if config.get('SESSION_PROTECTION', self.session_protection):
deleted = self._session_protection()
if deleted:
return self.reload_user()
# If a remember cookie is set, and the session is not, move the
# cookie user ID to the session.
#
# However, the session may have been set if the user has been
# logged out on this request, 'remember' would be set to clear,
# so we should check for that and not restore the session.
is_missing_user_id = 'user_id' not in session
if is_missing_user_id:
cookie_name = config.get('REMEMBER_COOKIE_NAME', COOKIE_NAME)
header_name = config.get('AUTH_HEADER_NAME', AUTH_HEADER_NAME)
has_cookie = (cookie_name in request.cookies and
session.get('remember') != 'clear')
if has_cookie:
return self._load_from_cookie(request.cookies[cookie_name])
elif self.request_callback:
return self._load_from_request(request)
elif header_name in request.headers:
return self._load_from_header(request.headers[header_name])
return self.reload_user()
_load_user大体的过程是首先检查SESSION_PROTECTION设置,如果SESSION_PROTECTION 为strong或者basic类型,那么就会执行_session_protection()动作,否则不执行此操作。_session_protection在session_id不一致的时候(比如IP变化会导致session id的变化)才真正有用,这时,如果为basic类型或者session permanent为True时,只标注session为非新鲜的(not fresh);而如果为strong,则会删除session中的用户信息,并重新load user,即调用reload_user。
session permanent为True时,用户退出浏览器不会删除session,其会保留permanent_session_lifetime s(默认是31天),但是当其为False且SESSION_PROTECTION 设为strong时,用户的session就会被删除。
- 接下来的代码是说当session中没有用户信息时(这里通过是否能获取到
user_id来判断),如果有则直接reload_user,如果没有,则有三种方式来load user,一种是通过remember cookie,一种通过request,一种是通过request header,依次尝试。
remember cookie是指,当用户勾选'remember me'复选框时,Flask-Login会将用户信息放入到指定的cookie当中,同样也是加密的。这就是为什么当session中没有携带用户信息时,我们可以通过remember cookie来获取用户的信息
而reload_user是如何获取用户的呢,来看下源代码:
# flask_login/login_manager.py
def reload_user(self, user=None):
ctx = _request_ctx_stack.top
if user is None:
user_id = session.get('user_id')
if user_id is None:
# 当无法获取到有效的用户id时,就认为是anonymous user
ctx.user = self.anonymous_user()
else:
# user callback就是我们通过@login_manager.user_loader装饰的函数,用于获取user object
if self.user_callback is None:
raise Exception(
"No user_loader has been installed for this "
"LoginManager. Add one with the "
"'LoginManager.user_loader' decorator.")
user = self.user_callback(user_id)
if user is None:
ctx.user = self.anonymous_user()
else:
ctx.user = user
else:
ctx.user = user
- 首先获取user id,如果获取不到有效的id,就将user设为anonymous user
- 获取到id后,再通过@login_manager.user_loader装饰的函数获取到user对象,如果没有获取到有效的user对象,就认为是anonymous user
- 最后将user保存于request context中(无论是正常的用户还是anonymous用户)
至此,我们已经将Flask-Login的核心代码剖析了一遍,如果你有收获,不妨点个赞鼓励一下吧!
flask_login模块中user_loader装饰器引发的思考
今天看书遇到了flask login模块中的信号机制,看到user_loader这个装饰器时有些疑惑,为什么需要这个装饰器呢,先看一下源码:
def user_loader(self, callback):
'''
This sets the callback for reloading a user from the session. The
function you set should take a user ID (a ``unicode``) and return a
user object, or ``None`` if the user does not exist.
:param callback: The callback for retrieving a user object.
:type callback: callable
'''
self.user_callback = callback
return callback
看到这不禁疑惑,它的作用只是将被它包装的函数存到self.user_callback这个属性中去
那么在哪里用到了它呢,我在login_manager.py中查找,发现只有一个方法使用到了这个熟悉,这个方法是reload_user():
它先从请求上下文中取出最新的请求,如果没有传入user,那么会从session中试图取出对应的user_id,这是一种保护机制,不使用cookie,而使用session,user_id在login时会写入session,如果登陆时remember参数传入了True,那么关闭浏览器重新打开后session['user_id']将不会被清除,这时候也就可以获取到了,如果登陆时没有设置remember为True,那么关闭浏览器后user_id会被设为None,则ctx.user = self.anonymous_user(),栈顶的用户为匿名用户,也就需要重新登陆了;取出了user_id,并且self.user_callback不为空,则会调用被user_loader装饰的函数,并传入user_id,在被装饰的函数中我们要根据这个user_id来查找并返回对应的用户实例,如果成功返回,那么当前请求上下文栈顶的用户就设置为返回的用户。
你可能会问,为什么要重载用户呢?因为http协议是无状态的,每次都会发送一个新的请求,请求上下文的栈顶会被新的请求覆盖,对应的user属性也就没了,所以需要通过reload_user重载上一次记录在session中并且未被清除的用户,重载失败则需要重新登陆,这也就是这个装饰器的作用了。
最后我们看下logout_user()这个方法:
def logout_user():
'''
Logs a user out. (You do not need to pass the actual user.) This will
also clean up the remember me cookie if it exists.
'''
user = _get_user()
if 'user_id' in session:
session.pop('user_id')
if '_fresh' in session:
session.pop('_fresh')
cookie_name = current_app.config.get('REMEMBER_COOKIE_NAME', COOKIE_NAME)
if cookie_name in request.cookies:
session['remember'] = 'clear'
if 'remember_seconds' in session:
session.pop('remember_seconds')
user_logged_out.send(current_app._get_current_object(), user=user)
current_app.login_manager.reload_user()
return True
logout主要是清除了session和cookie中的关键参数,比如login时设置的user_id以及remember等,清除后又调用了reload_user(),根据之前的逻辑,当然不可能重载成功,因为user_id已经为None了,执行到ctx.user = self.anonymous_user()就已经结束了,其实reload_user算是这个模块中很关键的一个函数,login_manager这个类也是这个模块的核心所在,以后有时间继续研究。
关于Flask-Login中session失效时间的处理
最近需要使用Python开发web系统,主要用到的框架就是Flask,前端使用Jinja2模板引擎和Bootstrap,web容器使用Cherrypy,其中关于Login管理的使用了Flask-Login插件。
基本上也是从零学起,前前后后花了有好几个月的时间,还是在借鉴了已有的一些项目基础上。在开发的过程中有很多的想法和体会,记录下来,有不对的地方欢迎大家指正。
在处理登录管理的部分,在 Flask-Login 中,如果你不特殊处理的话,session 是在你关闭浏览器之后失效的,而如果不关闭浏览器的话,失效的时间据说是1年,还是1个月,这个地方没看到官方说法,总之是很长,在某些业务场景下这样的处理方式是不能接受的。由于系统的使用者提出了新的需求类似平时的SSO处理机制,大概无任何操作一二十分钟就提示需要再次登录,这样的要求是合理的,之前也没有太注意这个方面的时间,所以就需要回过头来研究Flask-Login的session失效时间和设置问题。以前的登录部分代码:
1 @app.route('/login', methods=['GET', 'POST'])
2 def login():
3 if request.method == 'GET':
4 return render_template('login.html')
5
6 username = request.form['username']
7 password = request.form['password']
8 user = User.get_user(username, password)
9 if not user:
10 flash('Username or Password is invalid.', 'error')
11 return redirect(url_for('.login'))
12 login_user(user)
13
14 # flash('Logged in successfully', "info")
15
16 return render_template('index.html')
其中的User是models.py中定义的class,可以用于验证用户的合法性。在views部分通过@login_required控制,这样可以成功验证用户并跳转到相应页面,但是之前提到的session失效问题默认是很长时间而且一旦关闭浏览器即失效。那么如何设置能够实现制定时间内session失效,比如10minutes,查了一下文档竟然没发现说到这个问题,比如这个网站:http://www.pythondoc.com/flask-login/应该是正规的文档吧,接着查一些论坛,很多人讨论这个问题,有说设置REMEMBER_COOKIE_DURATION这个参数的,但是仔细看了这个参数和要解决的问题不是一个问题,接着就想到去stackoverflow上查一下吧,就搜索了一下关键词,找了好几个问答,其中有说到参数permanent_session_lifetime,
我觉得应该是这个差不多了,接着就这个关键词再次搜索,果然发现了一些很有参考价值问答,其中还提到session.permanent = True这个参数,于是在代码里进行设置:
from flask import session from datetime import timedelta session.permanent = True app.permanent_session_lifetime = timedelta(minutes=10)
其中两个参数意义也比较好理解,第一个为设置session为永久的,第二个再来制定具体时间
但是,关键是但是测试了半天貌似不起作用,又查了半天中文的一些论坛,不知原因何在,然后又是在stackoverflow上有人说这个设置不能放在request外面,要放在request里面才能生效,比如这样:
1 from flask import session
2 from datetime import timedelta
3
4 @app.route('/login', methods=['GET', 'POST'])
5 def login():
6 if request.method == 'GET':
7 return render_template('login.html')
8
9 username = request.form['username']
10 password = request.form['password']
11 user = User.get_user(username, password)
12 if not user:
13 flash('Username or Password is invalid.', 'error')
14 return redirect(url_for('.login'))
15 login_user(user)
16 session.permanent = True
17 app.permanent_session_lifetime = timedelta(minutes=10)
18 # flash('Logged in successfully', "info")
19
20 return render_template('index.html')
再去测试,已经可以成功按设置的时间来提示登录了。
这个问题也许对于一些人来不算问题,但是从我自己解决这个问题来看,目前我们的一些文档,问答还是有很多欠缺的地方,论坛上往往对一个问题互相转载,很少有独立的思考和实践的代码sample让初学者参考,在此再次推荐推荐大家一个问答网站:stackoverflow,相信很多人也都在用这个,能够得到很多有价值的问答,尤其是在国内还不是用的特别广泛的技术。
对于这个问题,目前也是能使用的阶段,也没有再去研究Flask-Login的一些原理,对于技术只有实践了你才有可能真正掌握,或者只能停留在理论阶段,希望以后有机会再深入研究实践一下。
浙公网安备 33010602011771号