1. 程式人生 > >詳解Django的CSRF認證

詳解Django的CSRF認證

1.csrf原理

csrf要求傳送post,put或delete請求的時候,是先以get方式傳送請求,服務端響應時會分配一個隨機字串給客戶端,客戶端第二次傳送post,put或delete請求時攜帶上次分配的隨機字串到服務端進行校驗

2.Django中的CSRF中介軟體

首先,我們知道Django中介軟體作用於整個專案。

在一個專案中,如果想對全域性所有檢視函式或檢視類起作用時,就可以在中介軟體中實現,比如想實現使用者登入判斷,基於使用者的許可權管理(RBAC)等都可以在Django中介軟體中來進行操作

Django內建了很多中介軟體,其中之一就是CSRF中介軟體

MIDDLEWARE_CLASSES = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

上面第四個就是Django內建的CSRF中介軟體

3.Django中介軟體的執行流程

Django中介軟體中最多可以定義5個方法

process_request
process_response
process_view
process_exception
process_template_response

Django中介軟體的執行順序

1.請求進入到Django後,會按中介軟體的註冊順序執行每個中介軟體中的process_request方法
    如果所有的中介軟體的process_request方法都沒有定義return語句,則進入路由對映,進行url匹配
    否則直接執行return語句,返回響應給客戶端
2.依次按順序執行中介軟體中的process_view方法
    如果某個中介軟體的process_view方法沒有return語句,則根據第1步中匹配到的URL執行對應的檢視函式或檢視類
    如果某個中介軟體的process_view方法中定義了return語句,則後面的檢視函式或檢視類不會執行,程式會直接返回
3.檢視函式或檢視類執行完成之後,會按照中介軟體的註冊順序逆序執行中介軟體中的process_response方法
    如果中介軟體中定義了return語句,程式會正常執行,把檢視函式或檢視類的執行結果返回給客戶端
    否則程式會丟擲異常
4.程式在檢視函式或檢視類的正常執行過程中
    如果出現異常,則會執行按順序執行中介軟體中的process_exception方法
    否則process_exception方法不會執行
    如果某個中介軟體的process_exception方法中定義了return語句,則後面的中介軟體中的process_exception方法不會繼續執行了
5.如果檢視函式或檢視類中使用render方法來向客戶端返回資料,則會觸發中介軟體中的process_template_response方法

4.Django CSRF中介軟體的原始碼解析

Django CSRF中介軟體的原始碼

class CsrfViewMiddleware(MiddlewareMixin):

    def _accept(self, request):
        request.csrf_processing_done = True
        return None

    def _reject(self, request, reason):
        logger.warning(
            'Forbidden (%s): %s', reason, request.path,
            extra={
                'status_code': 403,
                'request': request,
            }
        )
        return _get_failure_view()(request, reason=reason)

    def _get_token(self, request):
        if settings.CSRF_USE_SESSIONS:
            try:
                return request.session.get(CSRF_SESSION_KEY)
            except AttributeError:
                raise ImproperlyConfigured(
                    'CSRF_USE_SESSIONS is enabled, but request.session is not '
                    'set. SessionMiddleware must appear before CsrfViewMiddleware '
                    'in MIDDLEWARE%s.' % ('_CLASSES' if settings.MIDDLEWARE is None else '')
                )
        else:
            try:
                cookie_token = request.COOKIES[settings.CSRF_COOKIE_NAME]
            except KeyError:
                return None

            csrf_token = _sanitize_token(cookie_token)
            if csrf_token != cookie_token:
                # Cookie token needed to be replaced;
                # the cookie needs to be reset.
                request.csrf_cookie_needs_reset = True
            return csrf_token

    def _set_token(self, request, response):
        if settings.CSRF_USE_SESSIONS:
            request.session[CSRF_SESSION_KEY] = request.META['CSRF_COOKIE']
        else:
            response.set_cookie(
                settings.CSRF_COOKIE_NAME,
                request.META['CSRF_COOKIE'],
                max_age=settings.CSRF_COOKIE_AGE,
                domain=settings.CSRF_COOKIE_DOMAIN,
                path=settings.CSRF_COOKIE_PATH,
                secure=settings.CSRF_COOKIE_SECURE,
                httponly=settings.CSRF_COOKIE_HTTPONLY,
            )
            patch_vary_headers(response, ('Cookie',))

    def process_request(self, request):
        csrf_token = self._get_token(request)
        if csrf_token is not None:
            # Use same token next time.
            request.META['CSRF_COOKIE'] = csrf_token

    def process_view(self, request, callback, callback_args, callback_kwargs):
        if getattr(request, 'csrf_processing_done', False):
            return None

        if getattr(callback, 'csrf_exempt', False):
            return None

        if request.method not in ('GET', 'HEAD', 'OPTIONS', 'TRACE'):
            if getattr(request, '_dont_enforce_csrf_checks', False):
                return self._accept(request)

            if request.is_secure():
                referer = force_text(
                    request.META.get('HTTP_REFERER'),
                    strings_only=True,
                    errors='replace'
                )
                if referer is None:
                    return self._reject(request, REASON_NO_REFERER)

                referer = urlparse(referer)

                if '' in (referer.scheme, referer.netloc):
                    return self._reject(request, REASON_MALFORMED_REFERER)

                if referer.scheme != 'https':
                    return self._reject(request, REASON_INSECURE_REFERER)

                good_referer = (
                    settings.SESSION_COOKIE_DOMAIN
                    if settings.CSRF_USE_SESSIONS
                    else settings.CSRF_COOKIE_DOMAIN
                )
                if good_referer is not None:
                    server_port = request.get_port()
                    if server_port not in ('443', '80'):
                        good_referer = '%s:%s' % (good_referer, server_port)
                else:
                    good_referer = request.get_host()

                good_hosts = list(settings.CSRF_TRUSTED_ORIGINS)
                good_hosts.append(good_referer)

                if not any(is_same_domain(referer.netloc, host) for host in good_hosts):
                    reason = REASON_BAD_REFERER % referer.geturl()
                    return self._reject(request, reason)

            csrf_token = request.META.get('CSRF_COOKIE')
            if csrf_token is None:
                return self._reject(request, REASON_NO_CSRF_COOKIE)

            request_csrf_token = ""
            if request.method == "POST":
                try:
                    request_csrf_token = request.POST.get('csrfmiddlewaretoken', '')
                except IOError:
                    pass

            if request_csrf_token == "":
                request_csrf_token = request.META.get(settings.CSRF_HEADER_NAME, '')

            request_csrf_token = _sanitize_token(request_csrf_token)
            if not _compare_salted_tokens(request_csrf_token, csrf_token):
                return self._reject(request, REASON_BAD_TOKEN)

        return self._accept(request)

    def process_response(self, request, response):
        if not getattr(request, 'csrf_cookie_needs_reset', False):
            if getattr(response, 'csrf_cookie_set', False):
                return response

        if not request.META.get("CSRF_COOKIE_USED", False):
            return response

        self._set_token(request, response)
        response.csrf_cookie_set = True
        return response

從上面的原始碼中可以看到,CsrfViewMiddleware中介軟體中定義了process_request,process_view和process_response三個方法

先來看process_request方法

def _get_token(self, request):  
    if settings.CSRF_USE_SESSIONS:  
        try:  
            return request.session.get(CSRF_SESSION_KEY)  
        except AttributeError:  
            raise ImproperlyConfigured(  
                'CSRF_USE_SESSIONS is enabled, but request.session is not '  
 'set. SessionMiddleware must appear before CsrfViewMiddleware ' 'in MIDDLEWARE%s.' % ('_CLASSES' if settings.MIDDLEWARE is None else '')  
            )  
    else:  
        try:  
            cookie_token = request.COOKIES[settings.CSRF_COOKIE_NAME]  
        except KeyError:  
            return None  
  
  csrf_token = _sanitize_token(cookie_token)  
        if csrf_token != cookie_token:  
            # Cookie token needed to be replaced;  
 # the cookie needs to be reset.  request.csrf_cookie_needs_reset = True  
 return csrf_token

def process_request(self, request):  
        csrf_token = self._get_token(request)  
        if csrf_token is not None:  
            # Use same token next time.  
      request.META['CSRF_COOKIE'] = csrf_token

從Django專案配置資料夾中讀取CSRF_USE_SESSIONS的值,如果獲取成功,則從session中讀取CSRF_SESSION_KEY的值,預設為'_csrftoken',如果沒有獲取到CSRF_USE_SESSIONS的值,則從傳送過來的請求中獲取CSRF_COOKIE_NAME的值,如果沒有定義則返回None。

再來看process_view方法

在process_view方法中,先檢查檢視函式是否被csrf_exempt裝飾器裝飾,如果檢視函式沒有被csrf_exempt裝飾器裝飾,則程式繼續執行,否則返回None。接著從request請求頭中或者cookie中獲取攜帶的token並進行驗證,驗證通過才會繼續執行與URL匹配的檢視函式,否則就返回403 Forbidden錯誤。

實際專案中,會在傳送POST,PUT,DELETE,PATCH請求時,在提交的form表單中新增

{% csrf_token %}

即可,否則會出現403的錯誤

5.csrf_exempt裝飾器和csrf_protect裝飾器

5.1 基於Django FBV

在一個專案中,如果註冊起用了CsrfViewMiddleware中介軟體,則專案中所有的檢視函式和檢視類在執行過程中都要進行CSRF驗證。

此時想使某個檢視函式或檢視類不進行CSRF驗證,則可以使用csrf_exempt裝飾器裝飾不想進行CSRF驗證的檢視函式

from django.views.decorators.csrf import csrf_exempt

@csrf_exempt  
def index(request):  
    pass

也可以把csrf_exempt裝飾器直接加在URL路由對映中,使某個檢視函式不經過CSRF驗證

from django.views.decorators.csrf import csrf_exempt  
  
from users import views  
 
urlpatterns = [  
    url(r'^admin/', admin.site.urls),  
    url(r'^index/',csrf_exempt(views.index)),  
]

同樣的,如果在一個Django專案中,沒有註冊起用CsrfViewMiddleware中介軟體,但是想讓某個檢視函式進行CSRF驗證,則可以使用csrf_protect裝飾器

csrf_protect裝飾器的用法跟csrf_exempt裝飾器用法相同,都可以加上檢視函式上方裝飾檢視函式或者在URL路由對映中直接裝飾檢視函式

from django.views.decorators.csrf import csrf_exempt  

@csrf_protect  
def index(request):  
    pass

或者

from django.views.decorators.csrf import csrf_protect  
  
from users import views  
 
urlpatterns = [  
    url(r'^admin/', admin.site.urls),  
    url(r'^index/',csrf_protect(views.index)),  
]

5.1 基於Django CBV

上面的情況是基於Django FBV的,如果是基於Django CBV,則不可以直接加在檢視類的檢視函式中了

此時有三種方式來對Django CBV進行CSRF驗證或者不進行CSRF驗證

方法一,在檢視類中定義dispatch方法,為dispatch方法加csrf_exempt裝飾器

from django.views.decorators.csrf import csrf_exempt
from django.utils.decorators import method_decorator

class UserAuthView(View):

    @method_decorator(csrf_exempt)
    def dispatch(self, request, *args, **kwargs):
        return super(StudentsView,self).dispatch(request,*args,**kwargs)

    def get(self,request,*args,**kwargs):
        pass

    def post(self,request,*args,**kwargs):
        pass

    def put(self,request,*args,**kwargs):
        pass

    def delete(self,request,*args,**kwargs):
        pass

方法二:為檢視類上方新增裝飾器

@method_decorator(csrf_exempt,name='dispatch')
class UserAuthView(View):
    def get(self,request,*args,**kwargs):
        pass

    def post(self,request,*args,**kwargs):
        pass

    def put(self,request,*args,**kwargs):
        pass

    def delete(self,request,*args,**kwargs):
        pass

方式三:在url.py中為類新增裝飾器

from django.views.decorators.csrf import csrf_exempt

urlpatterns = [
    url(r'^admin/', admin.site.urls),
    url(r'^auth/', csrf_exempt(views.UserAuthView.as_view())),
]

csrf_protect裝飾器的用法跟上面一樣