深入理解Flask-Login原理

​ 使用Flask作为工作的主要框架也快两年了,对于Flask-Login插件一直是似懂非懂,今天决定参考一些文章资料深入理解下原理。

Flask-Login官方文档:英文版, 中文版

在深入学习原理前我们需要知道如何在我们的Flask项目中集成Flask-Login插件,具体可以参考官方文档或者之前的一篇笔记Flask基础学习-flask-login篇

flask-login一些知识点:

login_user 函数

我们在使用Flask-Login插件的时候,登录使用的函数是login_user函数。我们先看下这个函数的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
def login_user(user, remember=False, duration=None, 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'
if duration is not None:
try:
# equal to timedelta.total_seconds() but works with Python 2.6
session['remember_seconds'] = (duration.microseconds +
(duration.seconds +
duration.days * 24 * 3600) *
10**6) / 10.0**6
except AttributeError:
raise Exception('duration must be a datetime.timedelta, '
'instead got: {0}'.format(duration))

_request_ctx_stack.top.user = user
user_logged_in.send(current_app._get_current_object(), user=_get_user())
return True

我们分析一下这个函数具体做了什么。

函数的第一个参数是用户模型User的实例。

之前在配置使用Flask-Login的时候我们在用户模型上面继承了UserMixin

这样我们可以通过get_id方法获得到user实例的id

然后将获得user_id保存到session中,除此之外将更新_fresh重新生成会话id

如果我们设置了rememberduration则会进行相应的更新。

然后将当前用户推入到一个线程隔离的请求栈的栈顶,这样使用login_required获得的就是这个栈顶的user

最后发出登录信号,调用了_get_user

1
2
3
4
5
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)

在这个函数中判断_request_ctx_stack中是否存在user对象

如果没有则调用LoginManager_load_user方法。

如果有则结束登录流程,然后进入到_update_remember_cookie函数中。

为什么是这个函数呢?我看了下LoginManager的源码中的初始化方法中有指定请求钩子。

1
2
3
4
5
6
7
8
def init_app(self, app, add_context_processor=True):

app.login_manager = self
app.after_request(self._update_remember_cookie)

self._login_disabled = app.config.get('LOGIN_DISABLED', False)
if add_context_processor:
app.context_processor(_user_context_processor)
1
2
3
4
5
6
7
8
9
10
11
12

def _update_remember_cookie(self, response):
# Don't modify the session unless there's something to do.
if 'remember' not in session and \
current_app.config.get('REMEMBER_COOKIE_REFRESH_EACH_REQUEST'):
session['remember'] = 'set'
if 'remember' in session:
operation = session.pop('remember', None)
if operation == 'set' and 'user_id' in session:
self._set_cookie(response)
elif operation == 'clear':
self._clear_cookie(response)

_update_remember_cookie函数中,如果设置了remember则会写入cookie否则不处理。

注意:通过上面函数我们看到即使没有设置remember,浏览器也会被写入一段cookie。这个cookie其实是flasksessionflasksession实现很特殊,它不保存在服务器,它保存在浏览器的cookie中。

关于 current_user的原理
1
current_user = LocalProxy(lambda: _get_user())

current_user是一个local proxy,被代理的函数是_get_user。当我们调用current_user的时候,会实时的调用_get_user方法。这个函数源码在上面,在方法内部会去查看_request_ctx_stack中是否有user对象,如果有则返回,如果没有则调用_load_user方法,这个返回回去cookie或者session中加载user对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
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

def _load_user(self):
'''Loads user from session or remember_me cookie as applicable'''
return self.reload_user()


def reload_user(self, user=None):
'''
This set the ctx.user with the user object loaded by your customized
user_loader callback function, which should retrieved the user object
with the user_id got from session.
Syntax example:
from flask_login import LoginManager
@login_manager.user_loader
def any_valid_func_name(user_id):
# get your user object using the given user_id,
# if you use SQLAlchemy, for example:
user_obj = User.query.get(int(user_id))
return user_obj
Reason to let YOU define this self.user_callback:
Because we won't know how/where you will load you user object.
'''
ctx = _request_ctx_stack.top
if user is None:
user_id = session.get('user_id')
if user_id is None:
ctx.user = self.anonymous_user()
else:
if self.user_callback is None:
raise Exception(
"No user_loader has been installed for this "
"LoginManager. Refer to"
"https://flask-login.readthedocs.io/"
"en/latest/#how-it-works for more info.")
user = self.user_callback(user_id)
if user is None:
ctx.user = self.anonymous_user()
else:
ctx.user = user

我们看到_load_user最终调用的是reload_user的返回。这个函数才是真正获得用户对象的函数。我们一般重写这个函数,在我们集成Flask-Login的时候。

1
2
3
@login_manager.user_loader
def load_user(user_id):
return User.query.get(int(user_id))

完成这个配置之后,源码里所有的self.user_callback都是指代我们配置的这个函数。

关于装饰器 login_require的实现

我们看下这个装饰器的源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
def login_required(func):
'''
If you decorate a view with this, it will ensure that the current user is
logged in and authenticated before calling the actual view. (If they are
not, it calls the :attr:`LoginManager.unauthorized` callback.) For
example::

@app.route('/post')
@login_required
def post():
pass

If there are only certain times you need to require that your user is
logged in, you can do so with::

if not current_user.is_authenticated:
return current_app.login_manager.unauthorized()

...which is essentially the code that this function adds to your views.

It can be convenient to globally turn off authentication when unit testing.
To enable this, if the application configuration variable `LOGIN_DISABLED`
is set to `True`, this decorator will be ignored.


:param func: The view function to decorate.
:type func: function
'''
@wraps(func)
def decorated_view(*args, **kwargs):
if request.method in EXEMPT_METHODS:
return func(*args, **kwargs)
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

我们看到装饰器中使用了current_user,一旦使用了current_user,就会触发_get_user方法,一旦触发这个方法后续流程就会和current_user的原理分析一样了。

最后看下 logout_user 函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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_user主要是清除了sessioncookie中的关键参数。比如login时设置的user_id以及remember等,清除后又调用了reload_user(),根据之前的逻辑,当然不可能重载成功,因为user_id已经为None了,执行到ctx.user = self.anonymous_user()就已经结束了

关于session的问题

flasksession默认是client side的。也就是说flasksession是写入到浏览器cookie中的。当浏览器关闭后,默认情况下,这个cookie会丢失。

flask-login 的 remember me 功能

我们可以通过将flasksession设置为长期session来实现记住用户的功能。但是这样一来所有的session都将被设置为长期sessionflask-login通过设置一个单独的cookie来实现记住我。但是这个单独的cookie并不会直接被使用,而是同样要搭配flasksession来实现。如果session中没有userid,那么flask会从flask-login设置的cookie中恢复userid并存储到session中。

参考文章:

flask_login模块中user_loader装饰器引发的思考

Flask-Login详解

flask中的session设置

Flask扩展系列(八)–用户会话管理

知识就是财富
如果您觉得文章对您有帮助, 欢迎请我喝杯水!