django Admin登陆认证流程源码分析

首先要明确,登陆认证也是自己定义的对url的处理,如果一个项目调用了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()函数就好了,后面两个是url函数的参数。get_urls函数的源代码如下:

    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'),

这里我们直接关注最后一句 url(r'^login/$', self.login, name='login'),也就是self.login函数,他的源码如下:

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

这个函数前面是做一些上下文环境的检测和准备,最后真正进入django/contrib/auth/views.py中的login函数,这个函数的源码如下:

@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(): #重点关注这里
            auth_login(request, form.get_user())  #重点关注这里
            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)

在这函数里面,我们重点关注form.is_valid()函数,form=authentication_form,authentication_form就是login函数的参数AuthenticationForm,他继承自forms.Form,forms.Form继承BaseForm。所以,直接看form.is_valid函数做了什么就好了。
在django/form/forms.py中,is_valid函数的源码如下:

    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中调用了self.errors,那么继续看errors函数具体做了什么呢?,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_函数中有self._errors = None # Stores the errors after clean() has been called,而且我们在整个源码中也可以看到,在调用self.full_clean()之前self._errors = None一直为真,那么就调用self.full_clean函数。继续看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() #重点关注
        self._post_clean()

这里重点关注_clean_form函数,每个网页的登陆都是通过提交form表单,然后验证用户名和密码的,django也不例外。self._clean_form()中调用了self.clean函数,也就是AuthenticationForm的clean函数,直接看AuthenticationForm的clean函数源码:

    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)# 重点关注
            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函数里面终于见到了我们最想看到的逻辑,也就是从POST中获取用户名和密码,然后执行authenticate进行登录认证。(这里有个小疑问,self.cleaned_data虽然在self.field_clean函数中有初始化,但是self.fields = copy.deepcopy(self.base_fields)中的self.base_fields一直没找到来源)
在 if form.is_valid()函数执行完成后,就到了 auth_login(request, form.get_user()) ,这里的form.get_user()函数返回的就是authenticate返回的user,最后auth_login函数将authenticate返回的user赋值给request.user,并写到session中,也就是这次请求站点的admin时就有了身份标签。下次判断这个请求是否需要登陆的时候直接就是看request.user是否存在,并且这个user是合法的被允许登陆的就ok了。具体体现在,django/contrib/admin/sites.py中的login函数,

    @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方法,所以只需要关注self.has_permission(request):这个判断,关于这个函数的源码如下:

    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框架在正式环境中的请求流程分析》一文了//www.greatytc.com/writer#/notebooks/14133407/notes/14917548
request对象是在调用wsgi应用的时候创建的一个WSGIRequest对象,一开始这个对象是对http请求信息,以及上下文环境的封装,然后作为参数传递给django的middleware去处理,对于需要认证的project,一定要安装django.contrib.auth.middleware.AuthenticationMiddleware,也正是因为这个middleware,使得request对象有了user属性。这个可以从django/contrib.auth.middleware.py中的def process_request函数说明也可以看出,

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

现在我们正式的去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)) #重点关注

看到了么,最后一句就是创建request.user属性。所以说,request.user是django权限验证系统的基础。那创建这个user对象的依据又是什么呢,怎样创建的user他是is_active而且是is_staff呢?继续看get_user函数:

def get_user(request):
    if not hasattr(request, '_cached_user'):
        request._cached_user = auth.get_user(request) #重点关注
    return request._cached_user

这里重点关注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()

这里先重点关注user_id = _get_user_session_key(request),backend_path = request.session[BACKEND_SESSION_KEY]两个函数,这两个函数都要用到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做了什么呢?我们依据从源码中看起:

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数据库中寻找对应的session对象,赋值给request.session,这个seesion对象保存着这个session对应的登陆用户id,所以最后get_user函数通过处理request.session获得用户id,进而从对应的AUTH_USER_MODEL指定的数据库中获取对应的user对象,赋值给request.user进行后续的权限验证。(关于session和cookies,参见:
https://github.com/alsotang/node-lessons/tree/master/lesson16
http://mertensming.github.io/2016/10/19/cookie-session/

现在,很有可能现在又冒出一个疑问,像AuthenticationMiddleware这些middleware的process_request是什么时候调用的呢?不要忘记django处理请求的流程,django的请求处理流程是先通过middleware处理,middleware如果返回了response,那么就不会走到我们定义的url->view处理流程的。其实,每个middleware中都有固定的一类方法,并且每个请求的处理流程都会经过Basehandler类的load_middleware函数,这个函数将middleware中定义的对应的函数装载到一个固定的函数集合,他们分别是

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

然后按顺序对请求做处理。更详细的参见django/core/handlers/base.py中的load_middleware函数,起源码如下:

    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在请求进来的时候调用process_request时候创建一个空的session,然后到AuthorizationMiddleware去根据seesion获取用户,如果获取失败,就创建一个匿名用户,然后在返回response流的时候调用SessionMiddleware的process_response,此时会根据request.user的认证情况去保存或者销毁session。 但是这里好像出现了一个死循环。如果是这个流程,那不得一直是匿名用户?第一个有效的用户验证是放在哪儿的呢? 个人理解,这个得看项目的具体部署。session的创建和保存,是根据request.user去做的,而在整个请求流程中,我们可以在很多地方去替换request.user,不过大多数替换工作都是在我们自己定义url对应的的View中,比如django自带的认证登录界面流程中,在登陆后调用auth_login(request, form.get_user())函数去替换;或者我们在基于django的rest-framework定义自己的APIview的时候去调用框架的Authorization_class去做用户认证,然后替换request.user。

总结:
当用户第一次登陆Admin的时候,request.COOKIES中没有sessionid,所以auth/init.py中的get_user函数返回的是一个AnonymousUser()对象,他的is_staff和is_active都是False,所以访问admin就被重定向到了login,也就是登陆界面,然后提交用户名和密码登陆,在form.is_valid()函数里面对提交的用户名和密码在settings中指定的AUTHENTICATION_BACKENDS中验证,如果成功则重定向到admin的index.html界面。当下次再一次访问admin界面的时候,因为request中的cookies中有sessionid, SessionMiddleware根据这个sessionid从session数据库中拿到对应的session赋值给request.session,AuthenticationMiddleware处理request.session获取对应user,也就是此次访问的user,并赋值给request.user供下次判断和调用。在login函数里面对request.user进行is_staff和is_active判断,最后决定是重定向到admin的index.html还是登陆界面。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,029评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,395评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,570评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,535评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,650评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,850评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,006评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,747评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,207评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,536评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,683评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,342评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,964评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,772评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,004评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,401评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,566评论 2 349

推荐阅读更多精彩内容