Django session 字典,保存到数据库的时候是要先序列化的(session.encode方法), 读取的时候反序列化(session.decode),这样比较安全.
一 settings.py中间件
MIDDLEWARE_CLASSES = (
'django.contrib.sessions.middleware.SessionMiddleware', # ..
'django.contrib.auth.middleware.AuthenticationMiddleware', )
二 Django 中间件机制
request 来访时,按MIDDLEWARE_CLASSES 先后次序加载,返回response时加载顺序相反.
三 分述
(1) request 过程
1 django.contrib.sessions.middleware.SessionMiddleware
设定request.session:
读取SESSION_ENGINE, 读取request.COOKIES里面的session_key,用于初始化SessionStore.它是一个类似于字典的对象,其数据存储是由描述符_session代理的,为什么要代理? 因为要自动监测session数据是不是被读取(accessed)过.
首次对session操作时会触发它的load方法,该方法会设定并返回_session_cache.
class SessionBase(object): """ Base class for all Session classes. """ # ... 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)
load方法是由子类定义的:
class SessionStore(SessionBase): """ Implements database session store. """ def __init__(self, session_key=None): super(SessionStore, self).__init__(session_key) def load(self): try: s = Session.objects.get( session_key=self.session_key, expire_date__gt=timezone.now() ) return self.decode(s.session_data) except (Session.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 {}
2 django.contrib.auth.middleware.AuthenticationMiddleware
检查request是否有session属性
设定request.user:
这个代表请求对应的用户, 出于性能考虑, django采用的是proxy+lazy的模式. 这一步的user的实质是一个SimpleLazyObject, 它包装了auth.get_user, 是这个函数返回值的代理. 核心思路是用new_method_proxy代理绝大多数内部方法(诸如__getattr__,__getitem__等):
def new_method_proxy(func): def inner(self, *args): if self._wrapped is empty: self._setup() return func(self._wrapped, *args) return inner
比如你尝试访问request.user.some_attr, 就会触发SimpleLazyObject的__getattr__函数,它查看被代理的对象是否已经存在了,如果没有存在, 就用_setup设立.如果存在,就不重复调用了.
而_setup方法是调用auth.get_user(request),这个函数尝试从session里面获取user_id和backend_path, 然后遍历调用所有认证后端(settings.AUTHENTICATION_BACKENDS)的get_user方法, 如果数据库里面找不到user, 则返回AnonymousUser实例:
def get_user(request): """ Returns the user model instance associated with the given request session. If no user is retrieved an instance of `AnonymousUser` is returned. """ from .models import AnonymousUser user = None try: user_id = get_user_model()._meta.pk.to_python(request.session[SESSION_KEY]) backend_path = request.session[BACKEND_SESSION_KEY] except KeyError: pass else: if backend_path in settings.AUTHENTICATION_BACKENDS: backend = load_backend(backend_path) user = backend.get_user(user_id) # Verify the session if ('django.contrib.auth.middleware.SessionAuthenticationMiddleware' in settings.MIDDLEWARE_CLASSES and hasattr(user, 'get_session_auth_hash')): session_hash = request.session.get(HASH_SESSION_KEY) session_hash_verified = session_hash and constant_time_compare( session_hash, user.get_session_auth_hash() ) if not session_hash_verified: request.session.flush() user = None return user or AnonymousUser()
backend的get_user方法:
def get_user(self, user_id): UserModel = get_user_model() try: return UserModel._default_manager.get(pk=user_id) except UserModel.DoesNotExist: return None
(2)response过程.这一步只有SessionMiddleware. 主要是查看session是否需要保存或删除.
session其实是一个非常类似dict内建对象的东西, 只不过, 它可以觉察自身数据是否发生了变化.
class SessionMiddleware(object): def __init__(self): engine = import_module(settings.SESSION_ENGINE) self.SessionStore = engine.SessionStore def process_request(self, request): session_key = request.COOKIES.get(settings.SESSION_COOKIE_NAME, None) request.session = self.SessionStore(session_key) def process_response(self, request, response): """ If request.session was modified, or if the configuration is to save the session every time, save the changes and set a session cookie or delete the session cookie if the session has been emptied. """ 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, 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: request.session.save() 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
四 登录
用户输入账号和密码 ->
调用authenticate函数->
遍历settings的AUTHENTICATION_BACKENDS(默认就一个) ->
调用django.contrib.auth.backends.ModelBackend的authenticate方法, 得到user ->
class ModelBackend(object): """ Authenticates against settings.AUTH_USER_MODEL. """ def authenticate(self, username=None, password=None, **kwargs): UserModel = get_user_model() if username is None: username = kwargs.get(UserModel.USERNAME_FIELD) try: user = UserModel._default_manager.get_by_natural_key(username) if user.check_password(password): return user except UserModel.DoesNotExist: # Run the default password hasher once to reduce the timing # difference between an existing and a non-existing user (#20760). UserModel().set_password(password)
SESSION_KEY = '_auth_user_id' BACKEND_SESSION_KEY = '_auth_user_backend' HASH_SESSION_KEY = '_auth_user_hash'
def _get_user_session_key(request): # This value in the session is always serialized to a string, so we need # to convert it back to Python whenever we access it. return get_user_model()._meta.pk.to_python(request.session[SESSION_KEY])
调用auth.login函数:
def login(request, user): """ Persist a user id and a backend in the request. This way a user doesn't have to reauthenticate on every request. Note that data set during the anonymous session is retained when the user logs in. """ session_auth_hash = '' if user is None: user = request.user if hasattr(user, 'get_session_auth_hash'):# user is found session_auth_hash = user.get_session_auth_hash() if SESSION_KEY in request.session: if _get_user_session_key(request) != user.pk or ( session_auth_hash and request.session.get(HASH_SESSION_KEY) != session_auth_hash): # To avoid reusing another user's session, create a new, empty # session if the existing session corresponds to a different # authenticated user. request.session.flush() else: request.session.cycle_key() request.session[SESSION_KEY] = user._meta.pk.value_to_string(user) request.session[BACKEND_SESSION_KEY] = user.backend request.session[HASH_SESSION_KEY] = session_auth_hash if hasattr(request, 'user'): request.user = user rotate_token(request) user_logged_in.send(sender=user.__class__, request=request, user=user)
hash:
# from django.utils.crypto import get_random_string, salted_hmac def salted_hmac(key_salt, value, secret=None): """ Returns the HMAC-SHA1 of 'value', using a key generated from key_salt and a secret (which defaults to settings.SECRET_KEY). A different key_salt should be passed in for every application of HMAC. """ if secret is None: secret = settings.SECRET_KEY key_salt = force_bytes(key_salt) secret = force_bytes(secret) # We need to generate a derived key from our base key. We can do this by # passing the key_salt and our base key through a pseudo-random function and # SHA1 works nicely. key = hashlib.sha1(key_salt + secret).digest() # If len(key_salt + secret) > sha_constructor().block_size, the above # line is redundant and could be replaced by key = key_salt + secret, since # the hmac module does the same thing for keys longer than the block size. # However, we need to ensure that we *always* do this. return hmac.new(key, msg=force_bytes(value), digestmod=hashlib.sha1) # django.contrib.auth.models @python_2_unicode_compatible class AbstractBaseUser(models.Model): # ... def get_session_auth_hash(self): """ Returns an HMAC of the password field. """ key_salt = "django.contrib.auth.models.AbstractBaseUser.get_session_auth_hash" return salted_hmac(key_salt, self.password).hexdigest()
上面函数中的cycle_key()方法用于session不包含SESSION_KEY的时候:
class SessionBase(object): """ Base class for all Session classes. """ #.... def cycle_key(self): """ Creates a new session key, whilst retaining the current session data. """ data = self._session_cache key = self.session_key self.create() self._session_cache = data self.delete(key)
class SessionStore(SessionBase): """ Implements database session store. """ def __init__(self, session_key=None): super(SessionStore, self).__init__(session_key) def load(self): try: s = Session.objects.get( session_key=self.session_key, expire_date__gt=timezone.now() ) return self.decode(s.session_data) except (Session.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 {} def exists(self, session_key): return Session.objects.filter(session_key=session_key).exists() 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 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() obj = Session( session_key=self._get_or_create_session_key(), session_data=self.encode(self._get_session(no_load=must_create)), expire_date=self.get_expiry_date() ) using = router.db_for_write(Session, instance=obj) try: with transaction.atomic(using=using): obj.save(force_insert=must_create, using=using) except IntegrityError: if must_create: raise CreateError 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: Session.objects.get(session_key=session_key).delete() except Session.DoesNotExist: pass @classmethod def clear_expired(cls): Session.objects.filter(expire_date__lt=timezone.now()).delete()
五 登出.
Django清除request.session的所有数据,清除_session_cache,从数据库中删除session_key对应的条目,保留session里面的语言选项.
此外,由于session发生改变,session_key也设定为None了, session.save()的时候会重新创建一个会话.
# django.contrib.auth def logout(request): """ Removes the authenticated user's ID from the request and flushes their session data. """ # Dispatch the signal before the user is logged out so the receivers have a # chance to find out *who* logged out. user = getattr(request, 'user', None) if hasattr(user, 'is_authenticated') and not user.is_authenticated(): user = None user_logged_out.send(sender=user.__class__, request=request, user=user) # remember language choice saved to session language = request.session.get(LANGUAGE_SESSION_KEY) request.session.flush() if language is not None: request.session[LANGUAGE_SESSION_KEY] = language if hasattr(request, 'user'): from django.contrib.auth.models import AnonymousUser request.user = AnonymousUser() # django.contrib.sessions.backends.base class SessionBase(object): 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 flush(self): """ Removes the current session data from the database and regenerates the key. """ self.clear() self.delete() self._session_key = None # django.contrib.sessions.backends.db class SessionStore(SessionBase): def delete(self, session_key=None): if session_key is None: if self.session_key is None: return session_key = self.session_key try: Session.objects.get(session_key=session_key).delete() except Session.DoesNotExist: pass