本文主要通过源码分析Flask-Login插件,并详述其使用方法
关于Flask登录认证的详细过程请参见拙作<<使用Flask实现用户登陆认证的详细过程>>一文,而本文则偏重于详细介绍Flask-Login的原理,代码的解析。
首次登陆
我们首先来看一下首次登录验证的流程图:
Flask-Login在登录过程中主要负责:
- 将用户对象存入request context中
- 将用户ID,Session ID等信息存入Session中
在<<使用Flask实现用户登陆认证的详细过程>>中我们已经介绍过如何通过Flask-Login来实现登录的过程,其中最重要的代码就是login_user,如下:
1
login_user(user, remember=remember_me)
那么login_user具体做了什么呢?我们来看下源码
1 | def login_user(user, remember=False, force=False, fresh=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究竟是如何保护路由的呢?我们来看个例子:
1
2
3
4
5
6@app.route('/')
@app.route('/main')
@login_required
def main():
return render_template(
'main.html', username=current_user.username)
我们看到只要给路由函数加一个@login_required
装饰器就可以了,那么这个装饰器究竟是怎么做到的呢?来看下源码:
1 |
|
- 默认情况下只有OPTIONS method在EXEMPT_METHODS set中,而GET、PUT、POST等常见的methods都需要鉴权
_login_disabled
默认为False- 正常鉴权的关键在于
current_user.is_authenticated
是否为True,为True则正常处理请求,为False则进入unauthorized处理流程。那么这个current_user到底怎么就能鉴权了?它是怎么来的呢?来看下定义:1
2# 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对象的:
1 | # flask_login/utils.py |
在之前的首次登陆那小节中,我们已经知道用户鉴权成功后,会将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:
1 | # flask_login/login_manager.py |
_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
是如何获取用户的呢,来看下源代码:
1 | # flask_login/login_manager.py |
- 首先获取user id,如果获取不到有效的id,就将user设为anonymous user
- 获取到id后,再通过@login_manager.user_loader装饰的函数获取到user对象,如果没有获取到有效的user对象,就认为是anonymous user
- 最后将user保存于request context中(无论是正常的用户还是anonymous用户)
至此,我们已经将Flask-Login的核心代码剖析了一遍,如果你有收获,不妨点个赞鼓励一下吧!
v1.5.2