Djangoのセッションモジュール(1)モデルとストレージ

Djangoセッションモデル

Djangoのセッションは非常に単純なモデルであり、セッションの抽象的なモデルは次のように定義されていdjango/contrib/sessions/base_sessiion.pyます。

from django.db import models
from django.utils.translation import gettext_lazy as _


class BaseSessionManager(models.Manager):
    def encode(self, session_dict):
        """
        Return the given session dictionary serialized and encoded as a string.
        """
        session_store_class = self.model.get_session_store_class()
        return session_store_class().encode(session_dict)

    def save(self, session_key, session_dict, expire_date):
        s = self.model(session_key, self.encode(session_dict), expire_date)
        if session_dict:
            s.save()
        else:
            s.delete()  # Clear sessions with no data.
        return s


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)

セッションの抽象モデルとモデルオブジェクト(オブジェクト)の管理方法は、それぞれ上記で定義されています。モデルが含まれsession_keysession_dataそしてexpire_data三つのフィールド。管理メソッドにencodeメソッドが追加され、saveメソッドが再定義されます。encodeメソッドは、実際のセッションモデルクラスのget_session_store_classメソッドを呼び出して、オブジェクトのencodeメソッドを返します。受信したセッションディクショナリシーケンスは文字列になります。このメソッドは抽象クラスでは定義されておらず、実際のモデルクラスで定義されます。saveメソッドは、モデルが一時オブジェクトを形成するために必要なフィールドを受け入れます。session_dictフィールドが空でない場合にのみ、モデルのsave()メソッドが呼び出されてデータベースに保存されます。それ以外の場合、一時オブジェクトは削除されます。
実際のセッションモデルクラスは次のように定義されていdjango/contrib/sessions/models.pyます。

from django.contrib.sessions.base_session import (
    AbstractBaseSession, BaseSessionManager,
)


class SessionManager(BaseSessionManager):
    use_in_migrations = True


class Session(AbstractBaseSession):
    """
    Django provides full support for anonymous sessions. The session
    framework lets you store and retrieve arbitrary data on a
    per-site-visitor basis. It stores data on the server side and
    abstracts the sending and receiving of cookies. Cookies contain a
    session ID -- not the data itself.

    The Django sessions framework is entirely cookie-based. It does
    not fall back to putting session IDs in URLs. This is an intentional
    design decision. Not only does that behavior make URLs ugly, it makes
    your site vulnerable to session-ID theft via the "Referer" header.

    For complete documentation on using Sessions in your code, consult
    the sessions documentation that is shipped with Django (also available
    on the Django Web site).
    """
    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'

実際のセッションモデルのフィールドは、上記で定義した抽象モデルから継承されます。モデル管理オブジェクトのuse_in_migrations属性はTrueに変更されます。この属性はdjango/db/models/manage.pyDjangoのデフォルトモデル管理クラスの基本クラスBaseManageで定義され、現在のモデルかどうかを示します。データベース移行操作の影響になります。そして、抽象クラスのget_session_store_classメソッドがカバーDjango/Contrib/session/backends/dbされていSessionStoreます。このメソッドはで定義されたクラスを返します。セッションデータテーブルの名前も表示されますdjango_session
get_session_store_classセッションストアクラスからメソッドを継承によって返さdjango/contrib/sessions/backends/base.pyれ、SessionBase次のように定義されます:

import base64
import logging
import string
import warnings
from datetime import datetime, timedelta

from django.conf import settings
from django.contrib.sessions.exceptions import SuspiciousSession
from django.core.exceptions import SuspiciousOperation
from django.utils import timezone
from django.utils.crypto import (
    constant_time_compare, get_random_string, salted_hmac,
)
from django.utils.deprecation import RemovedInDjango40Warning
from django.utils.module_loading import import_string
from django.utils.translation import LANGUAGE_SESSION_KEY

# session_key should not be case sensitive because some backends can store it
# on case insensitive file systems.
VALID_KEY_CHARS = string.ascii_lowercase + string.digits


class SessionBase:
    """
    Base class for all Session classes.
    """
    TEST_COOKIE_NAME = 'testcookie'
    TEST_COOKIE_VALUE = 'worked'

    __not_given = object()

    def __init__(self, session_key=None):
        self._session_key = session_key
        # 标记是否获取过session数据
        self.accessed = False
        # 标记是否修改过session数据
        self.modified = False
        # 默认的序列器是'django.contrib.sessions.serializers.JSONSerializer'
        self.serializer = import_string(settings.SESSION_SERIALIZER)

    def __contains__(self, key):
        return key in self._session

    def __getitem__(self, key):
    	"""实现对实例对象的字典式取值操作"""
        if key == LANGUAGE_SESSION_KEY:
            warnings.warn(
                'The user language will no longer be stored in '
                'request.session in Django 4.0. Read it from '
                'request.COOKIES[settings.LANGUAGE_COOKIE_NAME] instead.',
                RemovedInDjango40Warning, stacklevel=2,
            )
        return self._session[key]

    def __setitem__(self, key, value):
    	"""实现对实例对象的字典式赋值操作"""
        self._session[key] = value
        self.modified = True

    def __delitem__(self, key):
    	"""实现del方法"""
        del self._session[key]
        self.modified = True

    def get(self, key, default=None):
        return self._session.get(key, default)

    def pop(self, key, default=__not_given):
        self.modified = self.modified or key in self._session
        args = () if default is self.__not_given else (default,)
        return self._session.pop(key, *args)

    def setdefault(self, key, value):
    	"""给实例对象的_session属性赋值,如果键不存在则标记对象被修改"""
        if key in self._session:
            return self._session[key]
        else:
            self.modified = True
            self._session[key] = value
            return value

    def set_test_cookie(self):
        self[self.TEST_COOKIE_NAME] = self.TEST_COOKIE_VALUE

    def test_cookie_worked(self):
        return self.get(self.TEST_COOKIE_NAME) == self.TEST_COOKIE_VALUE

    def delete_test_cookie(self):
        del self[self.TEST_COOKIE_NAME]

    def _hash(self, value):
    	"""接受一个值,并构造盐,对收到的值加盐序列化,返回序列化结果"""
    	# 构造hash时的盐,django.contrib.sessionssession
        key_salt = "django.contrib.sessions" + self.__class__.__name__
        #  salted_hmac方法将传入的key_salt以及settings中的SECRET_KEY以'utf8'编码为bytes格式,
        #  首先用sha1算法散列bytes格式的key_salt + SECRET_KEY,作为key_salt
        #  再将value作为值,key_salt作为盐,使用sha1算法散列value
        return salted_hmac(key_salt, value).hexdigest()

    def encode(self, session_dict):
        "将传入的session字典序列化为字符串"
        #  默认的JsonSerializer将会话字典转为json对象, 以'latin-1'编码
        serialized = self.serializer().dumps(session_dict)
        #  将json对象散列,作为签名
        hash = self._hash(serialized)
        # 将签名和三列对象用':'拼接,并编码为'ascii'格式
        return base64.b64encode(hash.encode() + b":" + serialized).decode('ascii')

    def decode(self, session_data):
    	#  对收到的会话数据做'ascii'解码
        encoded_data = base64.b64decode(session_data.encode('ascii'))
        try:
            #  以':'分割会话字符串, 如果解码出的对象没有b':',会抛出ValueError
            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:
            # 如果解析会话数据中出错,记入日志,并返回空字典
            if isinstance(e, SuspiciousOperation):
                logger = logging.getLogger('django.security.%s' % e.__class__.__name__)
                logger.warning(str(e))
            return {}

    def update(self, dict_):
    	"""
    	接受一个字典更新入类实例的_session中,并标记修改
    	"""
        self._session.update(dict_)
        self.modified = True

    def has_key(self, key):
    	"""判断实例的_session中是否有某个键"""
        return key in self._session

    def keys(self):
    	"""返回实例_session属性中的所有键"""
        return self._session.keys()

    def values(self):
    	"""返回实例_session属性中的所有值"""
        return self._session.values()

    def items(self):
    	"""返回实例_session属性中所有键值对"""
        return self._session.items()

    def clear(self):
        # To avoid unnecessary persistent storage accesses, we set up the
        # internals directly (loading data wastes time, since we are going to
        # set it to an empty dict anyway).
        self._session_cache = {}
        self.accessed = True
        self.modified = True

    def is_empty(self):
        "Return True when there is no session_key and the session is empty."
        try:
            return not self._session_key and not self._session_cache
        except AttributeError:
            return True

    def _get_new_session_key(self):
        "返回没被使用过的会话id"
        while True:
        	#  随机生成一个字符串
            session_key = get_random_string(32, VALID_KEY_CHARS)
            #  如果这个字符串不在已有的会话id中, 则返回
            if not self.exists(session_key):
                return session_key

    def _get_or_create_session_key(self):
    	"""如果当前实例没有sessionid则生成一个新的sessionid"""
        if self._session_key is None:
            self._session_key = self._get_new_session_key()
        return self._session_key

    def _validate_session_key(self, key):
        """
        判断传入的值是否不少于8位, 8位是安全性的必需下限
        """
        return key and len(key) >= 8

    def _get_session_key(self):
    	"""返回会话实例的sessionid"""
        return self.__session_key

    def _set_session_key(self, value):
        """
        判断传入的值是否不少于8位,是则作为当前实例的会话id
        """
        if self._validate_session_key(value):
            self.__session_key = value
        else:
            self.__session_key = None

	#  将session_key作为类属性,用于获取实例的会话id
    session_key = property(_get_session_key)
    #  将_session_key作为类属性,可以用于获取或修改会话id
    _session_key = property(_get_session_key, _set_session_key)

    def _get_session(self, no_load=False):
        """
        从内存中懒加载session (如果no_load为True,则设置一个空字典) 并设置给当前实例
        """
        #  标记会话数据已被访问
        self.accessed = True
        #  返回实例的_session_cache属性
        try:
            return self._session_cache
        #  如果没有_session_cache属性
        except AttributeError:
            if self.session_key is None or no_load:
                self._session_cache = {}
            else:
            	#  调用load()方法给_session_cache属性赋值,
            	#  该方法在中django/contrib/sessions/backends/db.py SessionStore中实现
                self._session_cache = self.load()
        return self._session_cache

    #  将_session作为类属性
    _session = property(_get_session)

    def get_session_cookie_age(self):
    	#  返回设置中的会话存储时间
        return settings.SESSION_COOKIE_AGE

    def get_expiry_age(self, **kwargs):
        """
        返回会话还剩多少秒过期
        可以接受 `modification` 和 `expiry` 两个关键字标记会话修改时间和剩余存储秒数
        """
        #  获取modification关键字,没有则设为当前时间
        try:
            modification = kwargs['modification']
        except KeyError:
            modification = timezone.now()
        # expiry=None和没有收到expiry两种情况分开处理,以保证有expity时不会触发load()方法
 		#  不能获取expiry参数时,获取settings中的SESSION_COOKIE_AGE 
        try:
            expiry = kwargs['expiry']
        except KeyError:
            expiry = self.get('_session_expiry')

		#  当expiry为0或None时,返回SESSION_COOKIE_AGE
        if not expiry:
            return settings.SESSION_COOKIE_AGE
        #  如果expiry不是datetime对象, 直接返回
        if not isinstance(expiry, datetime):
            return expiry
        #  计算expiry和modification的差值,返回差值总秒数
        delta = expiry - modification
        return delta.days * 86400 + delta.seconds

    def get_expiry_date(self, **kwargs):
        """
        计算会话过期日期(返回datetime对象).
		可以接受 `modification` 和 `expiry` 两个关键字标记会话修改时间和剩余存储秒数
        """
        try:
            modification = kwargs['modification']
        except KeyError:
            modification = timezone.now()
        # Same comment as in get_expiry_age
        try:
            expiry = kwargs['expiry']
        except KeyError:
            expiry = self.get('_session_expiry')

        if isinstance(expiry, datetime):
            return expiry
        expiry = expiry or self.get_session_cookie_age()
        return modification + timedelta(seconds=expiry)

    def set_expiry(self, value):
        """
        设置会话的过期时间 ``value``可以是整数对象,datetime对象,timedelta对象或None
        如果''value'', 会话经过该整数秒后过期. 如果设为0,则在关闭浏览器后过期。
        如果 ``value`` 是``datetime`` 或 ``timedelta`` 对象,则会话在指定的时间后过期。
        如果``value`` 是 ``None``, 则使用全局过期策略。
        """
        if value is None:
            # Remove any custom expiration for this session.
            try:
                del self['_session_expiry']
            except KeyError:
                pass
            return
        if isinstance(value, timedelta):
            value = timezone.now() + value
        self['_session_expiry'] = value

    def get_expire_at_browser_close(self):
        """
        如果会话设为关闭浏览器时过期则返回 ``True``, 否则返回 ``False`` 。
        如果设置了过期策略,使用  ``get_expiry_date()`` 或 ``get_expiry_age()`` 获取过期时间。
        """
        if self.get('_session_expiry') is None:
            return settings.SESSION_EXPIRE_AT_BROWSER_CLOSE
        return self.get('_session_expiry') == 0

    def flush(self):
        """
        从数据库删除会话,并生成新的会话id
        """
        self.clear()
        self.delete()
        self._session_key = None

    def cycle_key(self):
        """
        生成新的会话id,但不改变会哈u数据
        """
        data = self._session
        key = self.session_key
        self.create()
        self._session_cache = data
        if key:
            self.delete(key)

    # Methods that child classes must implement.

    def exists(self, session_key):
        """
        如果会话已过期,则返回True
        """
        raise NotImplementedError('subclasses of SessionBase must provide an exists() method')

    def create(self):
        """
        生成新的会话实例。 必须保证会话id的唯一性并且在本方法返回前携带空值存储一次
        """
        raise NotImplementedError('subclasses of SessionBase must provide a create() method')

    def save(self, must_create=False):
        """
        存储会话数据。 如果 'must_create' 为 True, 生成新的会话对象或抛出CreateError。
         否则只更新已有的会话对象        
         """
        raise NotImplementedError('subclasses of SessionBase must provide a save() method')

    def delete(self, session_key=None):
        """
        删除session_key对应的会话对象。
        如果没有传入session_key,则使用当前实例的session_key
        """
        raise NotImplementedError('subclasses of SessionBase must provide a delete() method')

    def load(self):
        """
        加载会话数据以字典结构返回
        """
        raise NotImplementedError('subclasses of SessionBase must provide a load() method')

    @classmethod
    def clear_expired(cls):
        """
        从和会话存储中删除过期会话对象。
       如果引用的会话后台没有实现这一方法,会抛出 NotImplementedError。 
        如果引用的后台有内建的过期处理机制,则此方法因设为no-op。
        """
        raise NotImplementedError('This backend does not support clear_expired().')

Djangoのデフォルトのセッションストレージの背景は次のとおりです。

import logging

from django.contrib.sessions.backends.base import (
    CreateError, SessionBase, UpdateError,
)
from django.core.exceptions import SuspiciousOperation
from django.db import DatabaseError, IntegrityError, router, transaction
from django.utils import timezone
from django.utils.functional import cached_property


class SessionStore(SessionBase):
    """
    使用数据库存储会话数据
    """
    def __init__(self, session_key=None):
        super().__init__(session_key)

    @classmethod
    def get_model_class(cls):
    	"""返回Session模型类"""
        # 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

    @cached_property
    def model(self):
        return self.get_model_class()

    def _get_session_from_db(self):
    	"""
    	从数据库中获取指定会话id且过期时间晚于当前时间的session对象
		不存在则返回None
		"""
        try:
            return self.model.objects.get(
                session_key=self.session_key,
                expire_date__gt=timezone.now()
            )
        except (self.model.DoesNotExist, SuspiciousOperation) as e:
            if isinstance(e, SuspiciousOperation):
                logger = logging.getLogger('django.security.%s' % e.__class__.__name__)
                logger.warning(str(e))
            self._session_key = None

    def load(self):
    	"""从数据库中获取有效的会话对象,并加载为字典结构  """
        s = self._get_session_from_db()
        return self.decode(s.session_data) if s else {}

    def exists(self, session_key):
    	"""判断会话id对应的会话对象是否存在"""
        return self.model.objects.filter(session_key=session_key).exists()

    def create(self):
    	"""创建会话id并存入数据库,记录当前实例已修改"""
        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

    def create_model_instance(self, data):
        """
        返回新的session模型实例,表示当前会话状态。用于数据库存储
        """
        return self.model(
            session_key=self._get_or_create_session_key(),  # 调用BaseSession的_get_or_create_session_key方法,
            session_data=self.encode(data),  # 将传入的会话数据编码为会话数据
            expire_date=self.get_expiry_date(),  # 计算过期时间
        )

    def save(self, must_create=False):
        """
        将当前会话数据存入数据库。 如果 'must_create' 为True而存储操作没有生成新的实例则抛出数据库异常。
        """
        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

    def delete(self, session_key=None):
        if session_key is None:
            if self.session_key is None:
                return
            session_key = self.session_key
        try:
            self.model.objects.get(session_key=session_key).delete()
        except self.model.DoesNotExist:
            pass

    @classmethod
    def clear_expired(cls):
    	"""从数据库中筛选出过期会话并删除"""
        cls.get_model_class().objects.filter(expire_date__lt=timezone.now()).delete()

おすすめ

転載: blog.csdn.net/JosephThatwho/article/details/105804115