Django原始碼分析之許可權系統_擒賊先擒王

靈吾發表於2016-05-13

乍見

Django內建的許可權系統已經很完善了,加上django-guardian提供的功能,基本上能滿足大部分的許可權需求。暫且不說django-guardian,我們先來看下Django內建的許可權系統:django.contrib.auth 包。

相識

一般許可權系統分為全域性許可權和物件許可權。Django只提供了一個物件許可權的框架,具體實現由第三方庫django-gardian完成。我們只看全域性許可權。

先來看auth包暴露出哪些介面。

django.contrib.auth.__init__.py

def load_backend(path):
    return import_string(path)()


def _get_backends(return_tuples=False):
    backends = []
    for backend_path in settings.AUTHENTICATION_BACKENDS:
        backend = load_backend(backend_path)
        backends.append((backend, backend_path) if return_tuples else backend)
    if not backends:
        raise ImproperlyConfigured(
            `No authentication backends have been defined. Does `
            `AUTHENTICATION_BACKENDS contain anything?`
        )
    return backends


def get_backends():
    return _get_backends(return_tuples=False)

前三個方法都是為了載入backends。一個backend其實就是一個class,必須實現authenticate和get_user兩個方法。每當我們這樣驗證使用者時

authenticate(username=`username`, password=`password`)

django就會去呼叫這些backend class,用其提供的方法去驗證使用者許可權。那django是如何知道要呼叫哪些backend class呢?答案就在settings.py中,預設為

AUTHENTICATION_BACKENDS = [`django.contrib.auth.backends.ModelBackend`]

那Django是如何呼叫這些backend class的呢?

def authenticate(**credentials):
    """
    If the given credentials are valid, return a User object.
    """
    for backend, backend_path in _get_backends(return_tuples=True):
        try:
            inspect.getcallargs(backend.authenticate, **credentials)
        except TypeError:
            # This backend doesn`t accept these credentials as arguments. Try the next one.
            continue

        try:
            user = backend.authenticate(**credentials)
        except PermissionDenied:
            # This backend says to stop in our tracks - this user should not be allowed in at all.
            return None
        if user is None:
            continue
        # Annotate the user object with the path of the backend.
        user.backend = backend_path
        return user

    # The credentials supplied are invalid to all backends, fire signal
    user_login_failed.send(sender=__name__,
            credentials=_clean_credentials(credentials))

由此可見,Django會在第一個驗證正確的backend class呼叫完成後停止,或者碰到PermissionDenied異常也會停止,所以backend class的順序也很重要。可以新增自定義的backend class。

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`):
        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)

login方法,顧名思義,登入使用者,同時設定好session,最後傳送登入成功通知

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()

相對的,logout方法,負責登出使用者,清理session,最後設定當前使用者為匿名使用者

def get_user_model():
    """
    Returns the User model that is active in this project.
    """
    try:
        return django_apps.get_model(settings.AUTH_USER_MODEL)
    except ValueError:
        raise ImproperlyConfigured("AUTH_USER_MODEL must be of the form `app_label.model_name`")
    except LookupError:
        raise ImproperlyConfigured(
            "AUTH_USER_MODEL refers to model `%s` that has not been installed" % settings.AUTH_USER_MODEL
        )

Django不推薦直接使用User class,而是通知get_user_model方法獲取當前的使用者class(或者使用settins.AUTH_USER_MODEL)。這是為了防止因為開發者使用了自定義使用者class而導致的資訊錯誤。

def update_session_auth_hash(request, user):
    """
    Updating a user`s password logs out all sessions for the user if
    django.contrib.auth.middleware.SessionAuthenticationMiddleware is enabled.

    This function takes the current request and the updated user object from
    which the new session hash will be derived and updates the session hash
    appropriately to prevent a password change from logging out the session
    from which the password was changed.
    """
    if hasattr(user, `get_session_auth_hash`) and request.user == user:
        request.session[HASH_SESSION_KEY] = user.get_session_auth_hash()

最後這個方法的使用場景很少。一般我們更新使用者密碼時,會在session中清除使用者登入資訊,導致使用者需要重新登入。而使用update_session_auth_hash我們就可以在更新使用者密碼的同時更新使用者的session資訊,這樣,使用者就不需要重新登入了。

回想

擒賊先擒王,以上都是django.contrib.auth包中的__init__.py入口檔案中的內容,背後還有很多“能工巧匠”,否則怎麼支撐起auth整套許可權系統?後續文章會一一介紹。


相關文章