django Admin登陸認證流程源碼分析

首先要明確,登陸認證也是自己定義的對url的處理谱俭,如果一個項目調(diào)用了django的Admin,那么在project的urls.py中一定會有這樣一段代碼:

urlpatterns = [
    url(r'^admin/', admin.site.urls),

直接到admin.site.urls去看看宵蛀,其源代碼如下:

    @property
    def urls(self):
        return self.get_urls(), 'admin', self.name

這里直接看self.get_urls()函數(shù)就好了昆著,后面兩個是url函數(shù)的參數(shù)。get_urls函數(shù)的源代碼如下:

    def get_urls(self):
        from django.conf.urls import url, include
        #中間省略
        urlpatterns = [
            url(r'^$', wrap(self.index), name='index'),
            url(r'^login/$', self.login, name='login'),

這里我們直接關(guān)注最后一句 url(r'^login/$', self.login, name='login')术陶,也就是self.login函數(shù)凑懂,他的源碼如下:

    @never_cache
    def login(self, request, extra_context=None):
        """
        Displays the login form for the given HttpRequest.
        """
        if request.method == 'GET' and self.has_permission(request):
            # Already logged-in, redirect to admin index
            index_path = reverse('admin:index', current_app=self.name)
            return HttpResponseRedirect(index_path)

        from django.contrib.auth.views import login
        # Since this module gets imported in the application's root package,
        # it cannot import models from other applications at the module level,
        # and django.contrib.admin.forms eventually imports User.
        from django.contrib.admin.forms import AdminAuthenticationForm
        context = dict(
            self.each_context(request),
            title=_('Log in'),
            app_path=request.get_full_path(),
            username=request.user.get_username(),
        )
        if (REDIRECT_FIELD_NAME not in request.GET and
                REDIRECT_FIELD_NAME not in request.POST):
            context[REDIRECT_FIELD_NAME] = reverse('admin:index', current_app=self.name)
        context.update(extra_context or {})

        defaults = {
            'extra_context': context,
            'authentication_form': self.login_form or AdminAuthenticationForm,
            'template_name': self.login_template or 'admin/login.html',
        }
        request.current_app = self.name
        return login(request, **defaults)

這個函數(shù)前面是做一些上下文環(huán)境的檢測和準備,最后真正進入django/contrib/auth/views.py中的login函數(shù)梧宫,這個函數(shù)的源碼如下:

@deprecate_current_app
@sensitive_post_parameters()
@csrf_protect
@never_cache
def login(request, template_name='registration/login.html',
          redirect_field_name=REDIRECT_FIELD_NAME,
          authentication_form=AuthenticationForm,
          extra_context=None, redirect_authenticated_user=False):
    """
    Displays the login form and handles the login action.
    """
    redirect_to = request.POST.get(redirect_field_name, request.GET.get(redirect_field_name, ''))

    if redirect_authenticated_user and request.user.is_authenticated:
        redirect_to = _get_login_redirect_url(request, redirect_to)
        if redirect_to == request.path:
            raise ValueError(
                "Redirection loop for authenticated user detected. Check that "
                "your LOGIN_REDIRECT_URL doesn't point to a login page."
            )
        return HttpResponseRedirect(redirect_to)
    elif request.method == "POST":
        form = authentication_form(request, data=request.POST)
        if form.is_valid(): #重點關(guān)注這里
            auth_login(request, form.get_user())  #重點關(guān)注這里
            return HttpResponseRedirect(_get_login_redirect_url(request, redirect_to))
    else:
        form = authentication_form(request)

    current_site = get_current_site(request)

    context = {
        'form': form,
        redirect_field_name: redirect_to,
        'site': current_site,
        'site_name': current_site.name,
    }
    if extra_context is not None:
        context.update(extra_context)

    return TemplateResponse(request, template_name, context)

在這函數(shù)里面接谨,我們重點關(guān)注form.is_valid()函數(shù)摆碉,form=authentication_form,authentication_form就是login函數(shù)的參數(shù)AuthenticationForm,他繼承自forms.Form脓豪,forms.Form繼承BaseForm巷帝。所以,直接看form.is_valid函數(shù)做了什么就好了扫夜。
在django/form/forms.py中楞泼,is_valid函數(shù)的源碼如下:

    def is_valid(self):
        """
        Returns True if the form has no errors. Otherwise, False. If errors are
        being ignored, returns False.
        """
        return self.is_bound and not self.errors

看到最后return中調(diào)用了self.errors,那么繼續(xù)看errors函數(shù)具體做了什么呢笤闯?堕阔,errors源碼如下:

    @property
    def errors(self):
        "Returns an ErrorDict for the data provided for the form"
        if self._errors is None:
            self.full_clean()
        return self._errors

這里先看一下self._errors,在AuthenticationForm的_init_函數(shù)中有self._errors = None # Stores the errors after clean() has been called颗味,而且我們在整個源碼中也可以看到超陆,在調(diào)用self.full_clean()之前self._errors = None一直為真,那么就調(diào)用self.full_clean函數(shù)浦马。繼續(xù)看self.full_clean的源碼:

    def full_clean(self):
        """
        Cleans all of self.data and populates self._errors and
        self.cleaned_data.
        """
        self._errors = ErrorDict()
        if not self.is_bound:  # Stop further processing.
            return
        self.cleaned_data = {}
        # If the form is permitted to be empty, and none of the form data has
        # changed from the initial data, short circuit any validation.
        if self.empty_permitted and not self.has_changed():
            return

        self._clean_fields()
        self._clean_form() #重點關(guān)注
        self._post_clean()

這里重點關(guān)注_clean_form函數(shù)侥猬,每個網(wǎng)頁的登陸都是通過提交form表單,然后驗證用戶名和密碼的捐韩,django也不例外。self._clean_form()中調(diào)用了self.clean函數(shù)鹃锈,也就是AuthenticationForm的clean函數(shù)荤胁,直接看AuthenticationForm的clean函數(shù)源碼:

    def clean(self):
        username = self.cleaned_data.get('username')
        password = self.cleaned_data.get('password')

        if username and password:
            self.user_cache = authenticate(username=username, password=password)# 重點關(guān)注
            if self.user_cache is None:
                raise forms.ValidationError(
                    self.error_messages['invalid_login'],
                    code='invalid_login',
                    params={'username': self.username_field.verbose_name},
                )
            else:
                self.confirm_login_allowed(self.user_cache)

        return self.cleaned_data

在clean函數(shù)里面終于見到了我們最想看到的邏輯,也就是從POST中獲取用戶名和密碼,然后執(zhí)行authenticate進行登錄認證屎债。(這里有個小疑問仅政,self.cleaned_data雖然在self.field_clean函數(shù)中有初始化,但是self.fields = copy.deepcopy(self.base_fields)中的self.base_fields一直沒找到來源)
在 if form.is_valid()函數(shù)執(zhí)行完成后盆驹,就到了 auth_login(request, form.get_user()) 圆丹,這里的form.get_user()函數(shù)返回的就是authenticate返回的user,最后auth_login函數(shù)將authenticate返回的user賦值給request.user,并寫到session中躯喇,也就是這次請求站點的admin時就有了身份標簽辫封。下次判斷這個請求是否需要登陸的時候直接就是看request.user是否存在,并且這個user是合法的被允許登陸的就ok了廉丽。具體體現(xiàn)在倦微,django/contrib/admin/sites.py中的login函數(shù),

    @never_cache
    def login(self, request, extra_context=None):
        """
        Displays the login form for the given HttpRequest.
        """
        if request.method == 'GET' and self.has_permission(request):
            # Already logged-in, redirect to admin index
            index_path = reverse('admin:index', current_app=self.name)
            return HttpResponseRedirect(index_path)
     后面的省略正压。欣福。。

瀏覽器頁面若果沒有表單焦履,所有的請求一般都是get拓劝,我們直接請求我們的admin站點的時候就是用的get方法雏逾,所以只需要關(guān)注self.has_permission(request):這個判斷,關(guān)于這個函數(shù)的源碼如下:

    def has_permission(self, request):
        """
        Returns True if the given HttpRequest has permission to view
        *at least one* page in the admin site.
        """
        return request.user.is_active and request.user.is_staff

所以說郑临,只要request.user對象的is_active而且is_staff栖博,那么就直接返回到了admin的index.html界面

那么又有一個疑問了,request.user到底是什么時候得來的牧抵,這里就要參照《django框架在正式環(huán)境中的請求流程分析》一文了http://www.reibang.com/writer#/notebooks/14133407/notes/14917548
request對象是在調(diào)用wsgi應用的時候創(chuàng)建的一個WSGIRequest對象笛匙,一開始這個對象是對http請求信息,以及上下文環(huán)境的封裝犀变,然后作為參數(shù)傳遞給django的middleware去處理妹孙,對于需要認證的project,一定要安裝django.contrib.auth.middleware.AuthenticationMiddleware获枝,也正是因為這個middleware,使得request對象有了user屬性蠢正。這個可以從django/contrib.auth.middleware.py中的def process_request函數(shù)說明也可以看出,

    def process_request(self, request):
        # AuthenticationMiddleware is required so that request.user exists.
        if not hasattr(request, 'user'):

現(xiàn)在我們正式的去AuthenticationMiddleware里面找一下request.user,AuthenticationMiddleware的源碼如下:

class AuthenticationMiddleware(MiddlewareMixin):
    def process_request(self, request):
        assert hasattr(request, 'session'), (
            "The Django authentication middleware requires session middleware "
            "to be installed. Edit your MIDDLEWARE%s setting to insert "
            "'django.contrib.sessions.middleware.SessionMiddleware' before "
            "'django.contrib.auth.middleware.AuthenticationMiddleware'."
        ) % ("_CLASSES" if settings.MIDDLEWARE is None else "")
        request.user = SimpleLazyObject(lambda: get_user(request)) #重點關(guān)注

看到了么省店,最后一句就是創(chuàng)建request.user屬性嚣崭。所以說,request.user是django權(quán)限驗證系統(tǒng)的基礎(chǔ)懦傍。那創(chuàng)建這個user對象的依據(jù)又是什么呢雹舀,怎樣創(chuàng)建的user他是is_active而且是is_staff呢?繼續(xù)看get_user函數(shù):

def get_user(request):
    if not hasattr(request, '_cached_user'):
        request._cached_user = auth.get_user(request) #重點關(guān)注
    return request._cached_user

這里重點關(guān)注auth.get_user(request)粗俱,直接看源碼:django/contrib/auth/init.py

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_session_key(request)  #重點注意
        backend_path = request.session[BACKEND_SESSION_KEY] #重點注意
        print "user_id: %s, backend_path: %s" %(user_id, backend_path)
    except KeyError:
        pass
    else:
        if backend_path in settings.AUTHENTICATION_BACKENDS:
            backend = load_backend(backend_path)
            user = backend.get_user(user_id) #重點注意
            print "user attr: ", dir(user)
            # Verify the session
            if 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()

這里先重點關(guān)注user_id = _get_user_session_key(request)说榆,backend_path = request.session[BACKEND_SESSION_KEY]兩個函數(shù),這兩個函數(shù)都要用到request.session寸认,那么request.session從哪兒來的呢签财?從中間件django.contrib.sessions.middleware.SessionMiddleware,而且在AuthenticationMiddleware中很明顯的說明了AuthenticationMiddleware依賴于SessionMiddleware偏塞,而且在settings的配置中INSTALL_APPS中唱蒸,可以看到django.contrib.sessions.middleware.SessionMiddleware是在django.contrib.auth.middleware.AuthenticationMiddleware,同樣灸叼,再次看一下源碼神汹,重點注意assert hasattr(request, 'session'),也能看到說明怜姿。

class AuthenticationMiddleware(MiddlewareMixin):
    def process_request(self, request):
        assert hasattr(request, 'session'), (
            "The Django authentication middleware requires session middleware "
            "to be installed. Edit your MIDDLEWARE%s setting to insert "
            "'django.contrib.sessions.middleware.SessionMiddleware' before "
            "'django.contrib.auth.middleware.AuthenticationMiddleware'."
        ) % ("_CLASSES" if settings.MIDDLEWARE is None else "")
        request.user = SimpleLazyObject(lambda: get_user(request))

那具體SessionMiddleware做了什么呢慎冤?我們依據(jù)從源碼中看起:

class SessionMiddleware(MiddlewareMixin):
    def __init__(self, get_response=None):
        self.get_response = get_response
        engine = import_module(settings.SESSION_ENGINE)
        self.SessionStore = engine.SessionStore

    def process_request(self, request):
        session_key = request.COOKIES.get(settings.SESSION_COOKIE_NAME)
        request.session = self.SessionStore(session_key)
        print "request.session._session: ", request.session._session
        print "_session_cache: ", request.session._session_cache

他通過獲取COOKIES中的session_key,然后從django的session數(shù)據(jù)庫中尋找對應的session對象沧卢,賦值給request.session蚁堤,這個seesion對象保存著這個session對應的登陸用戶id,所以最后get_user函數(shù)通過處理request.session獲得用戶id,進而從對應的AUTH_USER_MODEL指定的數(shù)據(jù)庫中獲取對應的user對象披诗,賦值給request.user進行后續(xù)的權(quán)限驗證撬即。(關(guān)于session和cookies,參見:
https://github.com/alsotang/node-lessons/tree/master/lesson16
http://mertensming.github.io/2016/10/19/cookie-session/

現(xiàn)在,很有可能現(xiàn)在又冒出一個疑問呈队,像AuthenticationMiddleware這些middleware的process_request是什么時候調(diào)用的呢剥槐?不要忘記django處理請求的流程,django的請求處理流程是先通過middleware處理宪摧,middleware如果返回了response粒竖,那么就不會走到我們定義的url->view處理流程的。其實几于,每個middleware中都有固定的一類方法蕊苗,并且每個請求的處理流程都會經(jīng)過Basehandler類的load_middleware函數(shù),這個函數(shù)將middleware中定義的對應的函數(shù)裝載到一個固定的函數(shù)集合沿彭,他們分別是

        self._request_middleware = []
        self._view_middleware = []
        self._template_response_middleware = []
        self._response_middleware = []
        self._exception_middleware = []

然后按順序?qū)φ埱笞鎏幚硇嗯椤8敿毜膮⒁奷jango/core/handlers/base.py中的load_middleware函數(shù),起源碼如下:

    def load_middleware(self):
        """
        Populate middleware lists from settings.MIDDLEWARE (or the deprecated
        MIDDLEWARE_CLASSES).

        Must be called after the environment is fixed (see __call__ in subclasses).
        """
        self._request_middleware = []
        self._view_middleware = []
        self._template_response_middleware = []
        self._response_middleware = []
        self._exception_middleware = []

        if settings.MIDDLEWARE is None:
            warnings.warn(
                "Old-style middleware using settings.MIDDLEWARE_CLASSES is "
                "deprecated. Update your middleware and use settings.MIDDLEWARE "
                "instead.", RemovedInDjango20Warning
            )
            handler = convert_exception_to_response(self._legacy_get_response)
            for middleware_path in settings.MIDDLEWARE_CLASSES:
                mw_class = import_string(middleware_path)
                try:
                    mw_instance = mw_class()
                except MiddlewareNotUsed as exc:
                    if settings.DEBUG:
                        if six.text_type(exc):
                            logger.debug('MiddlewareNotUsed(%r): %s', middleware_path, exc)
                        else:
                            logger.debug('MiddlewareNotUsed: %r', middleware_path)
                    continue

                if hasattr(mw_instance, 'process_request'):
                    self._request_middleware.append(mw_instance.process_request)
                if hasattr(mw_instance, 'process_view'):
                    self._view_middleware.append(mw_instance.process_view)
                if hasattr(mw_instance, 'process_template_response'):
                    self._template_response_middleware.insert(0, mw_instance.process_template_response)
                if hasattr(mw_instance, 'process_response'):
                    self._response_middleware.insert(0, mw_instance.process_response)
                if hasattr(mw_instance, 'process_exception'):
                    self._exception_middleware.insert(0, mw_instance.process_exception)
        else:
            handler = convert_exception_to_response(self._get_response)
            for middleware_path in reversed(settings.MIDDLEWARE):
                middleware = import_string(middleware_path)
                try:
                    mw_instance = middleware(handler)
                except MiddlewareNotUsed as exc:
                    if settings.DEBUG:
                        if six.text_type(exc):
                            logger.debug('MiddlewareNotUsed(%r): %s', middleware_path, exc)
                        else:
                            logger.debug('MiddlewareNotUsed: %r', middleware_path)
                    continue

                if mw_instance is None:
                    raise ImproperlyConfigured(
                        'Middleware factory %s returned None.' % middleware_path
                    )

                if hasattr(mw_instance, 'process_view'):
                    self._view_middleware.insert(0, mw_instance.process_view)
                if hasattr(mw_instance, 'process_template_response'):
                    self._template_response_middleware.append(mw_instance.process_template_response)
                if hasattr(mw_instance, 'process_exception'):
                    self._exception_middleware.append(mw_instance.process_exception)

                handler = convert_exception_to_response(mw_instance)

        # We only assign to this when initialization is complete as it is used
        # as a flag for initialization being complete.
        self._middleware_chain = handler

SessionMiddleware在請求進來的時候調(diào)用process_request時候創(chuàng)建一個空的session喉刘,然后到AuthorizationMiddleware去根據(jù)seesion獲取用戶瞧柔,如果獲取失敗,就創(chuàng)建一個匿名用戶睦裳,然后在返回response流的時候調(diào)用SessionMiddleware的process_response造锅,此時會根據(jù)request.user的認證情況去保存或者銷毀session。 但是這里好像出現(xiàn)了一個死循環(huán)廉邑。如果是這個流程备绽,那不得一直是匿名用戶?第一個有效的用戶驗證是放在哪兒的呢鬓催? 個人理解,這個得看項目的具體部署恨锚。session的創(chuàng)建和保存宇驾,是根據(jù)request.user去做的,而在整個請求流程中猴伶,我們可以在很多地方去替換request.user课舍,不過大多數(shù)替換工作都是在我們自己定義url對應的的View中,比如django自帶的認證登錄界面流程中他挎,在登陸后調(diào)用auth_login(request, form.get_user())函數(shù)去替換筝尾;或者我們在基于django的rest-framework定義自己的APIview的時候去調(diào)用框架的Authorization_class去做用戶認證,然后替換request.user办桨。

總結(jié):
當用戶第一次登陸Admin的時候筹淫,request.COOKIES中沒有sessionid,所以auth/init.py中的get_user函數(shù)返回的是一個AnonymousUser()對象呢撞,他的is_staff和is_active都是False,所以訪問admin就被重定向到了login损姜,也就是登陸界面饰剥,然后提交用戶名和密碼登陸,在form.is_valid()函數(shù)里面對提交的用戶名和密碼在settings中指定的AUTHENTICATION_BACKENDS中驗證摧阅,如果成功則重定向到admin的index.html界面汰蓉。當下次再一次訪問admin界面的時候,因為request中的cookies中有sessionid, SessionMiddleware根據(jù)這個sessionid從session數(shù)據(jù)庫中拿到對應的session賦值給request.session棒卷,AuthenticationMiddleware處理request.session獲取對應user顾孽,也就是此次訪問的user,并賦值給request.user供下次判斷和調(diào)用。在login函數(shù)里面對request.user進行is_staff和is_active判斷比规,最后決定是重定向到admin的index.html還是登陸界面若厚。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市苞俘,隨后出現(xiàn)的幾起案子盹沈,更是在濱河造成了極大的恐慌,老刑警劉巖吃谣,帶你破解...
    沈念sama閱讀 210,914評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件乞封,死亡現(xiàn)場離奇詭異,居然都是意外死亡岗憋,警方通過查閱死者的電腦和手機肃晚,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,935評論 2 383
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來仔戈,“玉大人关串,你說我怎么就攤上這事〖嗯牵” “怎么了晋修?”我有些...
    開封第一講書人閱讀 156,531評論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長凰盔。 經(jīng)常有香客問我墓卦,道長,這世上最難降的妖魔是什么户敬? 我笑而不...
    開封第一講書人閱讀 56,309評論 1 282
  • 正文 為了忘掉前任落剪,我火速辦了婚禮仁锯,結(jié)果婚禮上福也,老公的妹妹穿的比我還像新娘。我一直安慰自己旨剥,他們只是感情好抄瑟,可當我...
    茶點故事閱讀 65,381評論 5 384
  • 文/花漫 我一把揭開白布凡泣。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪问麸。 梳的紋絲不亂的頭發(fā)上往衷,一...
    開封第一講書人閱讀 49,730評論 1 289
  • 那天,我揣著相機與錄音严卖,去河邊找鬼席舍。 笑死,一個胖子當著我的面吹牛哮笆,可吹牛的內(nèi)容都是我干的来颤。 我是一名探鬼主播,決...
    沈念sama閱讀 38,882評論 3 404
  • 文/蒼蘭香墨 我猛地睜開眼稠肘,長吁一口氣:“原來是場噩夢啊……” “哼福铅!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起项阴,我...
    開封第一講書人閱讀 37,643評論 0 266
  • 序言:老撾萬榮一對情侶失蹤滑黔,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后环揽,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體略荡,經(jīng)...
    沈念sama閱讀 44,095評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,448評論 2 325
  • 正文 我和宋清朗相戀三年歉胶,在試婚紗的時候發(fā)現(xiàn)自己被綠了汛兜。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,566評論 1 339
  • 序言:一個原本活蹦亂跳的男人離奇死亡通今,死狀恐怖粥谬,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情辫塌,我是刑警寧澤漏策,帶...
    沈念sama閱讀 34,253評論 4 328
  • 正文 年R本政府宣布,位于F島的核電站臼氨,受9級特大地震影響哟玷,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜一也,卻給世界環(huán)境...
    茶點故事閱讀 39,829評論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望喉脖。 院中可真熱鬧椰苟,春花似錦、人聲如沸树叽。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,715評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至洁仗,卻和暖如春层皱,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背赠潦。 一陣腳步聲響...
    開封第一講書人閱讀 31,945評論 1 264
  • 我被黑心中介騙來泰國打工叫胖, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人她奥。 一個月前我還...
    沈念sama閱讀 46,248評論 2 360
  • 正文 我出身青樓瓮增,卻偏偏與公主長得像,于是被迫代替她去往敵國和親哩俭。 傳聞我的和親對象是個殘疾皇子绷跑,可洞房花燭夜當晚...
    茶點故事閱讀 43,440評論 2 348

推薦閱讀更多精彩內(nèi)容