我们知道在SessionMiddlewareprocess_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_keyexpire_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)
posted on 2018-04-08 21:19  Py行僧  阅读(140)  评论(0编辑  收藏  举报