我们知道在SessionMiddleware
中process_response
方法下,下面一行代码才是对session进行数据库持久化操作
request.session.save()
那么到底是如何进行数据库持久化的呢?我们来简单看下SessionStore
对象下的save
方法是如何操作的
def save(self, must_create=False):
if self.session_key is None:
return self.create()
data = self._get_session(no_load=must_create)
obj = self.create_model_instance(data)
using = router.db_for_write(self.model, instance=obj)
try:
with transaction.atomic(using=using):
obj.save(force_insert=must_create, force_update=not must_create, using=using)
except IntegrityError:
if must_create:
raise CreateError
raise
except DatabaseError:
if not must_create:
raise UpdateError
raise
当第一次访问时候,浏览器虽然不会携带网站写入的sessionid
键值对的,但是在process_request
方法中已经生成并且存放到缓存中,例如{"username":"safly"}
我在上篇博客中简单跟过源码,但是此时self.session_key is None
会执行如下代码,生成session_key
return self.create()
我们跟进代码看看在create
做了哪些操作?
def create(self):
while True:
self._session_key = self._get_new_session_key()
try:
# Save immediately to ensure we have a unique entry in the
# database.
self.save(must_create=True)
except CreateError:
# Key wasn't unique. Try again.
continue
self.modified = True
return
在上面代码self._get_new_session_key()
中,又去执行了如下操作
def _get_new_session_key(self):
"Returns session key that isn't being used."
while True:
session_key = get_random_string(32, VALID_KEY_CHARS)
if not self.exists(session_key):
break
return session_key
def get_random_string(length=12,
allowed_chars='abcdefghijklmnopqrstuvwxyz'
'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'):
"""
Returns a securely generated random string.
The default length of 12 with the a-z, A-Z, 0-9 character set returns
a 71-bit value. log_2((26+26+10)^12) =~ 71 bits
"""
if not using_sysrandom:
# This is ugly, and a hack, but it makes things better than
# the alternative of predictability. This re-seeds the PRNG
# using a value that is hard for an attacker to predict, every
# time a random string is required. This may change the
# properties of the chosen random sequence slightly, but this
# is better than absolute predictability.
random.seed(
hashlib.sha256(
("%s%s%s" % (
random.getstate(),
time.time(),
settings.SECRET_KEY)).encode('utf-8')
).digest())
return ''.join(random.choice(allowed_chars) for i in range(length))
我们看到get_random_string
是django框架封装的一个工具类,生成一串字符串,然后利用hashlib方法进行加密,然后_get_new_session_key
方法返回生成的加密后的随机字符串,交给create
方法中的 self._session_key = self._get_new_session_key()
进行赋值操作,然后self.save(must_create=True)
进行进一步操作,我们看下在save
带参数must_create=True
的情况下进行什么操作?
这里我再次贴出上面已经贴出过的save
方法,方便查阅
def save(self, must_create=False):
"""
Saves the current session data to the database. If 'must_create' is
True, a database error will be raised if the saving operation doesn't
create a *new* entry (as opposed to possibly updating an existing
entry).
"""
if self.session_key is None:
return self.create()
data = self._get_session(no_load=must_create)
obj = self.create_model_instance(data)
using = router.db_for_write(self.model, instance=obj)
try:
with transaction.atomic(using=using):
obj.save(force_insert=must_create, force_update=not must_create, using=using)
except IntegrityError:
if must_create:
raise CreateError
raise
except DatabaseError:
if not must_create:
raise UpdateError
raise
这次在判断self.session_key
就不为None
了,进而执行余下的代码data = self._get_session(no_load=must_create)
我们看下_get_session
是如何执行的?
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
因为no_load=True
,我们上篇博客分析过了,在process_request
中是进行了session缓存操作,如果有缓存的化,就直接返回缓存数据,在某种意外的前提下,或者缓存数据丢失,就执行self._session_cache = {}
构建一个字典数据类型,进行接下来的操作
那么接下来进行哪些操作呢?看如下一行代码
obj = self.create_model_instance(data)
def create_model_instance(self, data):
"""
Return a new instance of the session model object, which represents the
current session state. Intended to be used for saving the session data
to the database.
"""
return self.model(
session_key=self._get_or_create_session_key(),
session_data=self.encode(data),
expire_date=self.get_expiry_date(),
)
通过self.model()
定位到如下代码:
def model(self):
return self.get_model_class()
@cached_property是一个装饰器类,
将单个参数转换为缓存在实例上的属性。可选的“名称”参数允许您生成其他缓存属性。方法.(例如URL = cached_property(get_absolute_url,名称= ‘url ‘)) 这里的意思我也不太懂,大概就是先将数据置于缓存中,然后在赋值接下来的代码返回的session model
对象,有待于测试那个装饰器类,我们继续看self.get_model_class()
如下代码
@classmethod
def get_model_class(cls):
# Avoids a circular import and allows importing SessionStore when
# django.contrib.sessions is not in INSTALLED_APPS.
from django.contrib.sessions.models import Session
return Session
上面代码返回一个session对象模型类
class Session(AbstractBaseSession):
objects = SessionManager()
@classmethod
def get_session_store_class(cls):
from django.contrib.sessions.backends.db import SessionStore
return SessionStore
class Meta(AbstractBaseSession.Meta):
db_table = 'django_session'
我们看到了django_session
联想到django下的django_session
表
继续看下父类AbstractBaseSession
代码如下:
@python_2_unicode_compatible
class AbstractBaseSession(models.Model):
session_key = models.CharField(_('session key'), max_length=40, primary_key=True)
session_data = models.TextField(_('session data'))
expire_date = models.DateTimeField(_('expire date'), db_index=True)
objects = BaseSessionManager()
class Meta:
abstract = True
verbose_name = _('session')
verbose_name_plural = _('sessions')
def __str__(self):
return self.session_key
@classmethod
def get_session_store_class(cls):
raise NotImplementedError
def get_decoded(self):
session_store_class = self.get_session_store_class()
return session_store_class().decode(self.session_data)
父类表就定义了django_session
表的一些字段,如 session_key
session_data
expire_date
我们在将眼光拉回db下的如下代码,我再次贴出来
def create_model_instance(self, data):
"""
Return a new instance of the session model object, which represents the
current session state. Intended to be used for saving the session data
to the database.
"""
return self.model(
session_key=self._get_or_create_session_key(),
session_data=self.encode(data),
expire_date=self.get_expiry_date(),
)
所以该方法会把self.model()
里面的参数赋值给session
对象模型类,这里面data进行了encode
序列化操作,然后在进行base64
编码处理
def encode(self, session_dict):
"Returns the given session dictionary serialized and encoded as a string."
serialized = self.serializer().dumps(session_dict)
hash = self._hash(serialized)
return base64.b64encode(hash.encode() + b":" + serialized).decode('ascii')
然后在定位到save
方法中obj = self.create_model_instance(data)
赋值给一个变量obj,接下来我们继续看save
余下的代码,看看如下代码进行了什么操作
using = router.db_for_write(self.model, instance=obj)
继续定位代码如下:
db_for_write = _router_func('db_for_write')
def _router_func(action):
def _route_db(self, model, **hints):
chosen_db = None
for router in self.routers:
try:
method = getattr(router, action)
except AttributeError:
# If the router doesn't have a method, skip to the next one.
pass
else:
chosen_db = method(model, **hints)
if chosen_db:
return chosen_db
instance = hints.get('instance')
if instance is not None and instance._state.db:
return instance._state.db
return DEFAULT_DB_ALIAS
return _route_db
_router_func
大概意思就是找到可以使用的db数据库,比如代码里面的settings.DATABASE_ROUTERS
去项目下的settings.py
配置文件去找,我看代码是通过反射调用db_for_write
进行查找的,然后返回进行下一步的写入操作,我们继续回来看save
方法,看余下的方法
try:
with transaction.atomic(using=using):
obj.save(force_insert=must_create, force_update=not must_create, using=using)
以上代码是开启数据库事务,进行写入数据库数据操作,具体如何写入数据库的,逻辑也很复杂,这里就不去研究了,有兴趣的去了解吧,然后在写入过程中,捕获2个异常IntegrityError
和DatabaseError
以上代码就是如何对session进行数据库持久化的,当写入浏览器cookie返回,下次浏览器就会带着sessionid
键值对进行服务器访问,我们来看下在访问时候,是如何加载load
数据的
在SessionBase模块中,上篇博文我们分析过流程,我直接贴出代码
def __setitem__(self, key, value):
self._session[key] = value
self.modified = True
def __getitem__(self, key):
return self._session[key]
通过上面的代码,我们2次定位到如下代码:
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
_session = property(_get_session)
这个_get_session
我们在session数据库持久化操作,简单看过,这里的逻辑就不在赘述,我们主要看下第二次执行的self.load
方法,看下是如何加载的
跟进代码在SessionBase
中,该load
方法需要让子类强制实现
def load(self):
"""
Loads the session data and returns a dictionary.
"""
raise NotImplementedError('subclasses of SessionBase must provide a load() method')
于是我们在db
模块下的SessionStore
类中找到该方法的实现
def load(self):
try:
s = self.model.objects.get(
session_key=self.session_key,
expire_date__gt=timezone.now()
)
return self.decode(s.session_data)
except (self.model.DoesNotExist, SuspiciousOperation) as e:
if isinstance(e, SuspiciousOperation):
logger = logging.getLogger('django.security.%s' % e.__class__.__name__)
logger.warning(force_text(e))
self._session_key = None
return {}
是通过self.model.objects.get
进行数据库获取的,通过session_key
和expire_date__gt
过期大于当前时间,否则session是处于失效状态,最后通过self.decode(s.session_data)
进行反序列化,如下是反序列化的代码操作:
def decode(self, session_data):
encoded_data = base64.b64decode(force_bytes(session_data))
try:
# could produce ValueError if there is no ':'
hash, serialized = encoded_data.split(b':', 1)
expected_hash = self._hash(serialized)
if not constant_time_compare(hash.decode(), expected_hash):
raise SuspiciousSession("Session data corrupted")
else:
return self.serializer().loads(serialized)
except Exception as e:
# ValueError, SuspiciousOperation, unpickling exceptions. If any of
# these happen, just return an empty dictionary (an empty session).
if isinstance(e, SuspiciousOperation):
logger = logging.getLogger('django.security.%s' % e.__class__.__name__)
logger.warning(force_text(e))
return {}
在数据库持久化我们分析过,是进行了base64为编码,这里也需要进行base64.b64decode(force_bytes(session_data))
解码,然后返回即可,如果有异常抛出self.model.DoesNotExist, SuspiciousOperation
则返回None,然后进行self._session_cache = self.load()
赋值操作
如果切换用户登陆会是什么样的情况呢?
访问服务器时候,带来第一个用户的sessionid
中的键值session_key
session_key
依然从缓存拿,我们着重看下
obj.save(force_insert=must_create, force_update=not must_create, using=using)