• Django 是如何实现用户登录和登出机制的(默认版本-数据库版本)


    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
  • 相关阅读:
    基于 HTML5 WebGL 构建智能数字化城市 3D 全景
    基于 H5 + WebGL 实现 3D 可视化地铁系统
    基于 HTML5 WebGL 的 3D 科幻风机
    基于 HTML5 + WebGL 的太阳系 3D 展示系统
    HT Vue 集成
    基于 HTML5 + WebGL 的地铁 3D 可视化系统
    基于 HTML5 WebGL 和 VR 技术的 3D 机房数据中心可视化
    String、StringBuffer和StringBuilder的区别
    Python--Numpy基础
    python中的next()以及iter()函数
  • 原文地址:https://www.cnblogs.com/xiangnan/p/5136428.html
Copyright © 2020-2023  润新知