此篇博客是分析django SessionMiddleware
源码执行流程 SessionMiddleware
是django
框架的一个中间件,关于中间件的自定义创建、执行流程,这篇博客不去说明讲解,我们只是来了解下SessionMiddleware
的源码
当我们创建一个django
项目,会默认在项目目录下settings.py
中会设置中间件,这篇笔记就来简单了解下SessionMiddleware
中间件的执行流程
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
那么在上面代码中,我们该如何去浏览SessionMiddleware
的源码呢?我们看python安装包的目录:
找到我们pip安装的django库,在django中间件的配置中有如下一行代码
'django.contrib.sessions.middleware.SessionMiddleware',
我们通过这个字符串可以定位到该中间件的代码目录,从而进行查看其源码
我们来看下SessionMiddleware在django库的什么位置?
django在加载中间件配置时,会在conf下去加载配置,这个加载过程,我们接下来的代码会简单分析
所以我们可以通过,进行点击源码查看
from django.contrib.sessions.middleware import SessionMiddleware
ok我们现在定位到middleware
模块,就能查看SessionMiddleware
类及其类方法了
我们看到该中间件代码如下,这也是其他中间件的基本代码组成
class SessionMiddleware(MiddlewareMixin):
def __init__(self, get_response=None):
..省去一些代码..
def process_request(self, request):
..省去一些代码..
def process_response(self, request, response):
..省去一些代码..
毋庸置疑,我们先看__init__
方法,在类初始化时,进行调用的,我们看下做了什么操作?
贴出__init__
代码如下:
def __init__(self, get_response=None):
self.get_response = get_response
engine = import_module(settings.SESSION_ENGINE)
self.SessionStore = engine.SessionStore
第一行不解释,直接赋值了一个None值
第二行代码engine = import_module(settings.SESSION_ENGINE)
看看做了什么?
该方法 import_module
是利用了importlib
模块下的方法,意思就是通过字符串标识的路径,去导入该模块或者该模块下的方法或属性,我们来简单看下即可,最后定位到如下代码:
def _find_and_load(name, import_):
"""Find and load the module."""
with _ModuleLockManager(name):
module = sys.modules.get(name, _NEEDS_LOADING)
if module is _NEEDS_LOADING:
return _find_and_load_unlocked(name, import_)
if module is None:
message = ('import of {} halted; '
'None in sys.modules'.format(name))
raise ModuleNotFoundError(message, name=name)
_lock_unlock_module(name)
return module
那么settings.SESSION_ENGINE
来看下到底是什么字符串,来代表哪个模块或者属性方法
当我们看settings时候,去发现SESSION_ENGINE
定位到了conf\_init_
模块,进而定位到了如下属性:
settings = LazySettings()
但是该对象下没有SESSION_ENGINE
此属性,我们可能想到该类是继承了LazyObject
,难道在LazyObject下面,我们跟随代码查看,结果依然没有,那我们只能在LazySettings
类中进行查找了,看看能不能发现线索,我们看LazySettings类下的_setup方法代码:
def _setup(self, name=None):
settings_module = os.environ.get(ENVIRONMENT_VARIABLE)
if not settings_module:
desc = ("setting %s" % name) if name else "settings"
raise ImproperlyConfigured(
"Requested %s, but settings are not configured. "
"You must either define the environment variable %s "
"or call settings.configure() before accessing settings."
% (desc, ENVIRONMENT_VARIABLE))
self._wrapped = Settings(settings_module)
在Settings(settings_module)
中跟随代码进行追踪查看,会看到如下代码
def __init__(self, settings_module):
for setting in dir(global_settings):
if setting.isupper():
setattr(self, setting, getattr(global_settings, setting))
self.SETTINGS_MODULE = settings_module
mod = importlib.import_module(self.SETTINGS_MODULE)
tuple_settings = (
"INSTALLED_APPS",
"TEMPLATE_DIRS",
"LOCALE_PATHS",
)
self._explicit_settings = set()
for setting in dir(mod):
if setting.isupper():
setting_value = getattr(mod, setting)
if (setting in tuple_settings and
not isinstance(setting_value, (list, tuple))):
raise ImproperlyConfigured("The %s setting must be a list or a tuple. " % setting)
setattr(self, setting, setting_value)
self._explicit_settings.add(setting)
我们能通过代码大致了解到,django会先去循环遍历global_settings
中的配置,然后在去循环settings_module中的配置,也就是我们项目下的setting配置,里面有诸多我们自己的配置
比如如下的
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'rbac.apps.RbacConfig',
'app.apps.AppConfig',
]
好把,所以settings.SESSION_ENGINE
我们从global_settings
找到如下配置
SESSION_ENGINE = 'django.contrib.sessions.backends.db'
所有 engine = import_module(settings.SESSION_ENGINE)
获取到的engine 是一个db模块
接下来的代码self.SessionStore = engine.SessionStore
是拿到了一个db模块下的SessionStore
对象
该模块是对数据库的一些操作,后续我们在看这块的源码
以上就把SessionMiddleware中间件中的__init__
方法分析完毕,接下来我们看下process_request方法
def process_request(self, request):
session_key = request.COOKIES.get(settings.SESSION_COOKIE_NAME)
request.session = self.SessionStore(session_key)
我们在global_settings
中拿到SESSION_COOKIE_NAME = 'sessionid'
,request.COOKIES.get(settings.SESSION_COOKIE_NAME)
意思就是获取我们设置的sessionid值,
接下来request.session = self.SessionStore(session_key)
我们就看db模块做了什么操作?
首先看对象初始化方法
def __init__(self, session_key=None):
super(SessionStore, self).__init__(session_key)
我们看看父类SessionBase
做了什么操作
def __init__(self, session_key=None):
self._session_key = session_key
self.accessed = False
self.modified = False
self.serializer = import_string(settings.SESSION_SERIALIZER)
这里通过代码意思也能猜到是赋值session_key
以及accessed
是标识是否获取过session,以及modified
是否进行对session进行修改操作,serializer
就是对session进行序列化,然后存入数据库级别
我们在设置session时候通常进行request.session["user_id"] = user.pk
类似的操作
在python标准库中,对象利用字典式的添加属性,会调用__setitem__
方法,获取操作调用__getitem__
方法
所以我们看下在SessionBase
进行了如何操作? request.session["user_id"]
会触发如下操作
def __getitem__(self, key):
return self._session[key]
继而跟踪如下代码:
def _get_session(self, no_load=False):
"""
Lazily loads session from storage (unless "no_load" is True, when only
an empty dict is stored) and stores it in the current instance.
"""
self.accessed = True
try:
return self._session_cache
except AttributeError:
if self.session_key is None or no_load:
self._session_cache = {}
else:
self._session_cache = self.load()
return self._session_cache
当第一访问服务器时,self._session_cache
是空的,所有会执行AttributeError
异常代码,no_load默认是False,所以会执行异常代码块的如下部分,返回一个空字典
if self.session_key is None or no_load:
self._session_cache = {}
request.session["user_id"] = user.pk
赋值操作,会执行如下的代码:
def __setitem__(self, key, value):
self._session[key] = value
self.modified = True
self._session
看看做了什么操作呢?
_session = property(_get_session)
property
调用会再次执行_get_session
方法,这是标准库的内置函数,这里就不赘述了,看下_get_session
代码
def _get_session(self, no_load=False):
self.accessed = True
try:
return self._session_cache
except AttributeError:
if self.session_key is None or no_load:
self._session_cache = {}
else:
self._session_cache = self.load()
return self._session_cache
这里我们看到将accessed 修改为True
,标识获取了session,然后去_session_cache
缓存中获取,如果缓存中不存在,那么就去self.load()
加载,所以这次会获取到self._session_cache
缓存值
以上就是process_request
的执行流程,我们注意一点在process_request
还没有保存到数据库中,只是在缓存级别进行操作
接下来会执行url
路由,然后执行views
视图,这里是中间件的执行流程,不在赘述,然后在执行process_response
,我们来看下process_response
代码进行了哪些操作?
def process_response(self, request, response):
try:
accessed = request.session.accessed
modified = request.session.modified
empty = request.session.is_empty()
except AttributeError:
pass
else:
# First check if we need to delete this cookie.
# The session should be deleted only if the session is entirely empty
if settings.SESSION_COOKIE_NAME in request.COOKIES and empty:
response.delete_cookie(
settings.SESSION_COOKIE_NAME,
path=settings.SESSION_COOKIE_PATH,
domain=settings.SESSION_COOKIE_DOMAIN,
)
else:
if accessed:
patch_vary_headers(response, ('Cookie',))
if (modified or settings.SESSION_SAVE_EVERY_REQUEST) and not empty:
if request.session.get_expire_at_browser_close():
max_age = None
expires = None
else:
max_age = request.session.get_expiry_age()
expires_time = time.time() + max_age
expires = cookie_date(expires_time)
# Save the session data and refresh the client cookie.
# Skip session save for 500 responses, refs #3881.
if response.status_code != 500:
try:
request.session.save()
except UpdateError:
raise SuspiciousOperation(
"The request's session was deleted before the "
"request completed. The user may have logged "
"out in a concurrent request, for example."
)
response.set_cookie(
settings.SESSION_COOKIE_NAME,
request.session.session_key, max_age=max_age,
expires=expires, domain=settings.SESSION_COOKIE_DOMAIN,
path=settings.SESSION_COOKIE_PATH,
secure=settings.SESSION_COOKIE_SECURE or None,
httponly=settings.SESSION_COOKIE_HTTPONLY or None,
)
return response
我们看以上代码的最开始的3个属性值
accessed = request.session.accessed
modified = request.session.modified
empty = request.session.is_empty()
accessed
,modified
这2个值是由初始化的False
,修改为True
前面代码有说明
我们着重看下empty
值
def is_empty(self):
"Returns True when there is no session_key and the session is empty"
try:
return not bool(self._session_key) and not self._session_cache
except AttributeError:
return True
意思就是当没有session_key
且session
是空时候为True,我们之前的代码可以分析出self._session_cache
为True,所以最后返回False
try
里面如果没有异常,会执行else
部分代码,由于empty
为False
,会执行如下代码
patch_vary_headers(response, ('Cookie',))
Vary
头部定义了缓存机制,在构建其缓存键值时应当将哪个请求头标考虑在内。 例如,如果网页的内容取决于用户的语言偏好,该页面被称为根据语言而不同。看如下代码
def patch_vary_headers(response, newheaders):
if response.has_header('Vary'):
vary_headers = cc_delim_re.split(response['Vary'])
else:
vary_headers = []
# Use .lower() here so we treat headers as case-insensitive.
existing_headers = set(header.lower() for header in vary_headers)
additional_headers = [newheader for newheader in newheaders
if newheader.lower() not in existing_headers]
response['Vary'] = ', '.join(vary_headers + additional_headers)
大概意思就是django中间件对response进行了修改,然后将Cookie
添加进去,构建心的headers
接下来我们分析process_response
其他的代码段
if request.session.get_expire_at_browser_close():
max_age = None
expires = None
def get_expire_at_browser_close(self):
"""
Returns ``True`` if the session is set to expire when the browser
closes, and ``False`` if there's an expiry date. Use
``get_expiry_date()`` or ``get_expiry_age()`` to find the actual expiry
date/age, if there is one.
"""
if self.get('_session_expiry') is None:
return settings.SESSION_EXPIRE_AT_BROWSER_CLOSE
return self.get('_session_expiry') == 0
在session设置中有如下几种情况
# request.session.set_expiry(value)
# * 如果value是个整数,session会在些秒数后失效。
# * 如果value是个datatime或timedelta,session就会在这个时间后失效。
# * 如果value是0,用户关闭浏览器session就会失效。
# * 如果value是None,session会依赖全局session失效策略。
如果全局session
失效,或者self.get('_session_expiry') == 0
或者浏览器关闭,就将其更新为如下,让其session
失效
max_age = None
expires = None
然后在接下来的代码中,获取设置的session时效
max_age = request.session.get_expiry_age()
expires_time = time.time() + max_age
expires = cookie_date(expires_time)
继续看如下的代码
request.session.save()
在save
方法中很关键,这里才是进行数据库更新操作,在前面代码分析中,process_request
中只是进行了缓存处理,在缓存级别,并没有进行数据库持久化
最后一步就是如下的代码,我们来看下
response.set_cookie(
settings.SESSION_COOKIE_NAME,
request.session.session_key, max_age=max_age,
expires=expires, domain=settings.SESSION_COOKIE_DOMAIN,
path=settings.SESSION_COOKIE_PATH,
secure=settings.SESSION_COOKIE_SECURE or None,
httponly=settings.SESSION_COOKIE_HTTPONLY or None,
)
很简单就是写给浏览器set_cookie
进行cookie写操作
以上就是简单流程,接下来我会继续分析session的重复更新操作、以及配合django下的auth
认证模块来分析session中间件、以及数据库是如何进行持久化的