Django中CSRF防御全过程解析以及中间件作用机制

版权声明:本文为博主原创文章,未经博主允许不得转载。转载请Email我....... https://blog.csdn.net/Deft_MKJing/article/details/90348835

前言

XSS和CSRF攻击的基础原理这里就不介绍了,之前写了一篇文章单独介绍的很详细了,传送门,这里我们直接以Django为分析对象,分析中间件csrf生成原理以及防范Token如何运作的。

Settings文件

Setting.py中有茫茫多的配置选项。传送门

CSRF中间件

官方文档介绍的也是表面,本文通过源码层面直接分析流程
官方文档针对CSRF的介绍以及参数配置 传送门

Django全流程中间件参与过程

  • 是一个轻量级,底层的插件系统,可以介入Django的请求和响应处理过程,修改Django的输入和输出
  • 激活:添加到Django配置文件的MIDDLEWARE
MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

上面的第四个就是我们这里要详细分析的CSRF中间件。

Django中中间件最多可以定义五个方法:

process_request(self,request)
process_view(self, request, view_func, view_args, view_kwargs)
process_template_response(self,request,response)
process_exception(self, request, exception)
process_response(self, request, response)

Django工作流程中中间件执行顺序:

1.请求进入到Django后,Django接收到第一个请求会调用中间件的初始化方法__init__,处理请求之前会按中间件的注册顺序执行每个中间件中的process_request方法。如果所有的中间件的process_request方法return None,则进入路由映射,进行url匹配,通过规则确认请求由哪个视图处理,如果是return HTTPResponse,则调用process_response返回到客户端

2.依次按顺序执行中间件中的process_view方法
如果某个中间件的process_view方法没有return HttpResponse,则根据第1步中匹配到的URL执行对应的视图函数或视图类(process_view)在views.py执行之前
如果某个中间件的process_view方法中返回了return HttpRespons,则后面的视图函数或视图类不会执行,程序会执行process_response返回客户端

3.视图函数或者视图类接收Request,通过模型Model与数据库交互,获取数据交给模板引擎进行渲染,如果视图函数或视图类中使用render()方法来向客户端返回数据,则会触发中间件中的process_template_response方法,response函数都是通过注册的逆序进行调用,必须返回response才能继续转发,否则程序排除异常

4.随后视图函数或视图类执行计算渲染,如果没有任何异常,会按照中间件的注册顺序逆序执行中间件中的process_response方法
如果中间件中定义了return response,程序会正常执行,把视图函数或视图类的执行结果返回给客户端,否则程序会抛出异常

5.在第三步,程序在视图函数或视图类的正常执行过程中
如果出现异常,则会执行按顺序执行中间件中的process_exception方法,该方法也是逆序执行的,如果某个中间件的process_exception方法中定义了return语句,则终端传递中间件中的process_exception函数,转发给process_response处理后返回给客户端

单个CSRF中间件详细流程图如下
在这里插入图片描述
根据上面的步骤,验证了一下多个中间件组合调用的顺序

# 两个中间件代码
from django.http import HttpResponse
from django.utils.deprecation import MiddlewareMixin


class Exp1(MiddlewareMixin):
    def process_request(self,request):
        print('Exp1 ---> precess_request %s'%id(request))

    def process_response(self, request, response):
        print('Exp1 ---> process_response')
        return response

    def process_view(self, request, view_func, view_args, view_kwargs):

        print('Exp1 ---> process_view')
        print('Exp1',view_func, view_func.__name__)

    def process_exception(self, request, exception):
        print('Exp1',exception)
        print('Exp1','process_exception')
        # return HttpResponse('卧槽,挂了啊')

    def process_template_response(self, request, response):
        print('Exp1','process_template_response')
        return response



class Exp2(MiddlewareMixin):
    def process_request(self, request):
        print('Exp2 ---> precess_request %s'%id(request))
        print()


    def process_response(self, request, response):
        print('Exp2 ---> process_response')
        return response

    def process_view(self, request, view_func, view_args, view_kwargs):

        print('Exp2 ---> process_view')
        print('Exp2', view_func, view_func.__name__)
        print()

    def process_exception(self, request, exception):
        print('Exp2', exception)
        print('Exp2', 'process_exception')
        return HttpResponse('enenenen???')

    def process_template_response(self, request, response):
        print('Exp2','process_template_response')
        return response
# 视图代码
def exrp(request):
    # a = [1, 2, 3]
    # a[100]
    print('views.py-----视图中的代码被执行了')
    # return render(request, 'books/cook2.html', {'x':'渲染一下'})
    def render():
        print("触发 Template render()方法")
        return HttpResponse("反悔了啊")

    rep = HttpResponse("11123232")
    rep.render = render
    return rep
    
# 最后记得在配置文件下写入对应的中间件

访问对应的路径,模拟正常流程下render()渲染的结果

Exp1 ---> precess_request 4385889584
Exp2 ---> precess_request 4385889584

Exp1 ---> process_view
Exp1 <function exrp at 0x1052e2400> exrp
Exp2 ---> process_view
Exp2 <function exrp at 0x1052e2400> exrp

views.py-----视图中的代码被执行了
Exp2 process_template_response
Exp1 process_template_response
触发 Template render()方法
Exp2 ---> process_response
Exp1 ---> process_response
[22/May/2019 16:44:32] "GET /books/exrp/ HTTP/1.1" 200 12

再看一下如果视图渲染的时候错误打印

Exp1 ---> precess_request 4370030264
Exp2 ---> precess_request 4370030264

Exp1 ---> process_view
Exp1 <function exrp at 0x104640400> exrp
Exp2 ---> process_view
Exp2 <function exrp at 0x104640400> exrp

Exp2 list index out of range
Exp2 process_exception
Exp2 ---> process_response
Exp1 ---> process_response
[22/May/2019 16:50:29] "GET /books/exrp/ HTTP/1.1" 200 11

总结:

  1. 中间件的process_requestprocess_view这两个函数是在视图函数之前执行的
  2. 多个中间件,会按照配置文件的顺序执行,process_requestprocess_view是顺序执行,其他三个是逆序执行
  3. 中间件传递的都是同一个对象
  4. 如果视图正常渲染,不会执行process_exception方法,如果不正常,执行顺序逆序,有一个返回HTTPResponse则终端传递,直接传给process_response返回给客户端
  5. 当我们通过render()渲染的时候,会触发process_template_response函数,很少用这个方法

两张图详细描述了多个中间件执行顺序,可以配合上面的总概览图看
在这里插入图片描述
在这里插入图片描述

CSRF中间件原理解析

首先,我们假设settings中的中间件只有一个
'django.middleware.csrf.CsrfViewMiddleware',根据上面的流程,分析下csrf.py中间件的源码

1.process_request

Django的第一个请求过来,会首先分发到CsrfViewMiddleware中间件模块

class CsrfViewMiddleware(MiddlewareMixin):
    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
    
    ......

无论是GET还是POST,都会进入该流程,假定我们是一个未登录的时候的GET请求,会首先调用__get_token方法(注意:这里调用的私有方法,不是外部的get_token方法,这两个有很大的区别)。可以看到会先在settings中拿CSRF_USE_SESSIONS参数,这里用到了懒加载的机制,该机制会单独开一片文章介绍TODO一下。文章上面放出了官方文档settings的所有参数介绍,这里的两个分支获取token是根据以下两个值配置的。

CSRF_USE_SESSIONS or CSRF_COOKIE_HTTPONLY
这两个值是配套出现的,默认不出现,也就是都是False。那么HTTP报文是这样的
在这里插入图片描述
除了sessionid之外,还把csrftoken作为key,也存储在cookies中一并返回给客户端。
前者代表是否把token存入session,如果存入,那么cookies就不在存放csrftoken这个key,而是直接存储sessionid,通过sessionid查出来在从session中获取csrf_token。那么配套的就是把CSRF_COOKIE_HTTPONLY设置成YES,告诉浏览器Cookies只能浏览器默认行为获取,脚本是无法获取的。默认值之外,两者都是True的话,我们可以看Cookies中的报文就只剩sessiondid了。

流程继续,还是按默认值走,我们会从request.COOKIES[settings.CSRF_COOKIE_NAME]中获取,其中CSRF_COOKIE_NAME默认是csrftoken。一开始肯定是None。这里没有return _reject,因此进入URLConf进行路径匹配

from django.contrib import admin
from django.urls import path, include, re_path

urlpatterns = [
    path('admin/', admin.site.urls),
    path('polls/', include('polls.urls')),
    re_path(r'^books/', include('books.urls', namespace='books'))
]

一层层匹配后会有一个对应的视图函数接收,接收之前,来到了第二步。

2.process_view

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

        # 如果装饰器 @csrf_exempt生效,则不处理
        if getattr(callback, 'csrf_exempt', False):
            return None

        # Assume that anything not defined as 'safe' by RFC7231 needs protection
        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():
                # 发出的是HTTPS请求,确保我们的url在Refer中
                referer = request.META.get('HTTP_REFERER')
                if referer is None:
                    return self._reject(request, REASON_NO_REFERER)

                referer = urlparse(referer)

                # Make sure we have a valid URL for Referer.
                if '' in (referer.scheme, referer.netloc):
                    return self._reject(request, REASON_MALFORMED_REFERER)

                # Ensure that our Referer is also secure.
                if referer.scheme != 'https':
                    return self._reject(request, REASON_INSECURE_REFERER)

                # If there isn't a CSRF_COOKIE_DOMAIN, require an exact match
                # match on host:port. If not, obey the cookie rules (or those
                # for the session cookie, if CSRF_USE_SESSIONS).
                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:
                    try:
                        # request.get_host() includes the port.
                        good_referer = request.get_host()
                    except DisallowedHost:
                        pass

               # HTTP白名单,可信任来源
                good_hosts = list(settings.CSRF_TRUSTED_ORIGINS)
                if good_referer is not None:
                    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:
                # POST 一定要有Cookies存储CSRFToken,避免CSRF攻击
                return self._reject(request, REASON_NO_CSRF_COOKIE)

            # Check non-cookie token for match.
            request_csrf_token = ""
            if request.method == "POST":
                try:
                	# request.POST.get() 相当于获取request.POST['csrfmiddlewaretoken']的值,
                	# 若果出错就返回 ''.这里的csrfmiddlewaretoken是提交的表单中的值,在
                	# 模板中用{% csrf_token %} 生成
                    request_csrf_token = request.POST.get('csrfmiddlewaretoken', '')
                except IOError:
                    # 在我们完成读取POST数据之前处理断开的连接。  
                    # process_view不应该引发任何exception,因此我们将忽略并返回403
                    # 假设他们仍在监听,他们可能不是因为错误
                    pass

            if request_csrf_token == "":
                # ajax中适用'X-CSRFToken'
                # CSRF_HEADER_NAME = 'HTTP_X_CSRFTOKEN'
                request_csrf_token = request.META.get(settings.CSRF_HEADER_NAME, '')

            request_csrf_token = _sanitize_token(request_csrf_token)
            # 对比两个csrf_token,一个是表单里隐藏的csrfmiddlewaretoken
            # 或者ajax的hearder: X_CSRFTOKEN),另一个是自带的cookies里的csrf_token
            if not _compare_salted_tokens(request_csrf_token, csrf_token):
                return self._reject(request, REASON_BAD_TOKEN)

        return self._accept(request)

源码已经加了注释,如果请求是GET,HEAD,OPTIONS,TRACE中的,返回None,直接渲染模板。如果不是这几个里面的,比如POST,首先判断是否已经禁用CSRF防御,是的话也是直接渲染模板,不是的话判断是否是HTTPS进入,进行域名白名单匹配。如果是POST,会先从Cookies中取csrf_token,如果取不到,直接拒绝403,如果取到了,表单提交,从csrfmiddlewaretoken中取request_csrf_token,如果是Ajax,从X-CSRFToken取。然后进行这两个值的清洗,去除csrf_secret进行对比,相等就渲染模板,反之直接403。这里的两个Token匹配规则后面会详细介绍

3.Views模块中执行render渲染

到这里,肯定会有人好奇,我一个网站下,一直普通Get请求是没有Cookies的,如果添加了模板的{% csrf_token%}就会触发Cookies和csrfmiddleware这两个值,而且上面提到的数据清洗,这些Token是在什么时候赋值上去的,Token生成规则,匹配规则等,我们从render函数切入。

以下是上面两个执行完之后,没有问题,就会渲染模板

return render(request, 'books/test1.html', {'a' : 111111,'b' : request.GET.get('b'), 'c' : request.GET.get('c')})

# render是便利函数,以下是源码
def render(request, template_name, context=None, content_type=None, status=None, using=None):
    content = loader.render_to_string(template_name, context, request, using=using)
    return HttpResponse(content, content_type, status)
# 继续往下render_to_string, loader是模块名,
def render_to_string(template_name, context=None, request=None, using=None):
    if isinstance(template_name, (list, tuple)):
        template = select_template(template_name, using=using)
    else:
        template = get_template(template_name, using=using)
    return template.render(context, request)

最后的render必然是模板渲染的,模板也是一个类,在/django/template/backends/jinja2.py

class Template:

    def __init__(self, template, backend):
        self.template = template
        self.backend = backend
        self.origin = Origin(
            name=template.filename, template_name=template.name,
        )

    def render(self, context=None, request=None):
        from .utils import csrf_input_lazy, csrf_token_lazy
        if context is None:
            context = {}
        if request is not None:
        	# 以下就是我们要找的核心的代码
            context['request'] = request
            context['csrf_input'] = csrf_input_lazy(request)
            context['csrf_token'] = csrf_token_lazy(request)
            for context_processor in self.backend.template_context_processors:
                context.update(context_processor(request))
        return self.template.render(context)

这里的context就是我们的参数字典,最终模板渲染的时候会赋值多几个key和value,这里我们就能找到csrf_token,这就是填充到{% csrf_token %}的值,再往下看

from django.middleware.csrf import get_token
from django.utils.functional import lazy
from django.utils.html import format_html
from django.utils.safestring import SafeText
def csrf_input(request):
    return format_html(
        '<input type="hidden" name="csrfmiddlewaretoken" value="{}">',
        get_token(request))


csrf_input_lazy = lazy(csrf_input, SafeText, str)
csrf_token_lazy = lazy(get_token, str)

又是懒加载,这里直接用他的原理,后续再开文章介绍,记住,只有当值真正被用的时候才会执行,函数调用是不会执行的。比如csrf_token_lay(request)。注意看这里的核心函数get_token,引用来自django.middleware.csrf,这下就又回到中间件的核心函数了,这就能解释为什么普通的GET请求不会生成Token,只有加载了{% csrf_token %}的模板,在赋值的时候,去中间件获取token值,代码如下

def get_token(request):
    if "CSRF_COOKIE" not in request.META:
    	# 如果request中不存在csrf,先生成一个新的secret,加密赋值到META["CSRF_COOKIE"] 中,	
		# 后面用来放到set_cookie之中
        csrf_secret = _get_new_csrf_string()
        request.META["CSRF_COOKIE"] = _salt_cipher_secret(csrf_secret)
    else:
    	# 如果request的cookie中存在了csrf_token,冲洗解密,取出secret        csrf_secret = _unsalt_cipher_token(request.META["CSRF_COOKIE"])
    	
        csrf_secret = _unsalt_cipher_token(request.META["CSRF_COOKIE"])
    request.META["CSRF_COOKIE_USED"] = True
    # 返回另外一个加密生成的secret, 由于加密是随机的,所以与上面的META["CSRF_COOKIE"]不一样
    return _salt_cipher_secret(csrf_secret)

这里就不贴_get_new_csrf_string,_salt_cipher_secret_unsalt_cipher_token了,代码太多影像阅读,首先看个图,为什么Cookies中的值csrfmiddleware中的值不同,而且刷新的时候Cookies一直不变,csrfmiddleware会一直变,而且还能匹配上?
在这里插入图片描述
在这里插入图片描述
这个函数会在渲染模板的时候调用,具体来说是由csrf context processor调用。
如果request.META["CRSF_COOKIE"]不存在,就调用_get_new_csrf_string()函数来生成一串随机字符(32个字符,大小写字母和数字),赋给csrf_secret,再调用_salt_cipher_secret(scrf_secret)和随机生成的32位salt一起生成64个字符的字符串赋给request.META[“CSRF_COOKIE”],而这个request.META["CSRF_COOKIE"]之后用来设置COOKIE 的csrf_token
最后的返回值_salt_cipher_secret(csrf_secret)就渲染到POST表单的csrfmiddlewaretoken。值得一提的是_salt_cipher_secret(csrf_secret)每次的返回值都不一样,而csrf_secret == _unsalt_cipher_token(_salt_cipher_secret(csrf_secret))
​总的来说,涉及到三个值,csrf_tokencsrfmiddlewaretokencsrf_secret,还有两个函数,_unsalt_cipher_token(token)_salt_cipher_secret(token)

这就解释了为什么两个值不同,但是这两个值传入的csrf_secret是一样的,只是salt不同,_unsalt_cipher_token解析出来的值就是一样的,这就是核心比较的东西。

When validating the ‘csrfmiddlewaretoken’ field value, only the secret, not the full token, is compared with the secret in the cookie value. This allows the use of ever-changing tokens. While each request may use its own token, the secret remains common to all.
官方文档也说了,我们比较的不是token,而是计算出来的secret,由于salt的存在,每次的csrfmiddlewaretoken是不同的,但是和cookies中的比较secret是一样的。

用图来说明下这两个过程:

  1. 生成csrf_token和csrfmiddlewaretoken。
  2. 验证csrf_token和csrfmiddlewaretoken是否一致。
    在这里插入图片描述

_unsalt_cipher_token_salt_cipher_secret函数实现图解。而且两次返回的Token不同如何计算出同一个csrf_secret
在这里插入图片描述

4.process_response

模板渲染完之后会触发`process_response``

 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

        # Set the CSRF cookie even if it's already set, so we renew
        # the expiry timer.
        self._set_token(request, response)
        response.csrf_cookie_set = True
        return response

    def _set_token(self, request, response):
        if settings.CSRF_USE_SESSIONS:
            if request.session.get(CSRF_SESSION_KEY) != request.META['CSRF_COOKIE']:
                request.session[CSRF_SESSION_KEY] = request.META['CSRF_COOKIE']
        else:
            response.set_cookie(
                settings.CSRF_COOKIE_NAME,
                # request.META['CSRF_COOKIE']就是在上面赋值的
                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,
                samesite=settings.CSRF_COOKIE_SAMESITE,
            )
            # Set the Vary header since content varies with the CSRF cookie.
            patch_vary_headers(response, ('Cookie',))

组装response,设置Cookies,返回response,客户端浏览器完成渲染,这样一次请求就结束了。

5.Django csrf防御过程解析

前几步已经把每一步具体的实现和原理用图和文字都介绍清楚了,现在带着问题,Django是如何验证一个请求不是CSRF的?
当我们普通的访问一个GET页面的时候,没有任何{%csrf_token%},渲染的时候,context['csrf_token']不会被填充,就是不会去触发get_token方法,所以你请求几次都不会有Cookies。当我们开发一个网站的时候,如果启用CSRF防御,用户登录之前需要提交表单,表单的这个页面会嵌入{%csrf_token%},会先触发当前登录页面的GET请求,页面render的时候,根据上面第三步,会触发lazy的get_token函数,这个函数会自动生成csrf_token,如果cookies中有的话就不生成,用csrf_token清洗出后32位,再解析出秘钥,返回给表单一个新的csrfmiddlewaretoken,至此登录页面的Cookies也已经生成并返回了,而且表单动态生成了一个csrfmiddlewaretoken
当正式登录的时候,发送POST请求,同源策略浏览器会携带对应的Cookies,而且会携带表单生成的csrfmiddlewaretoken发送给服务端,当服务器接收到的时候转发给Django,又会来到中间层1-4的步骤。这个时候第一步process_request能获取到值,给request.META["CRSF_COOKIE"]设置值,进入URLConfig路径匹配,匹配到进去视图函数之前进入process_view,如果是GET,直接渲染页面,如果是POST,判断是否禁用防御,判断是否HTTPS安全,判断csrf_token是否为空,这些判断有一个失败,就能看到熟悉的403页面。这个时候就进入最核心的CSRF防御的Token验证过程了,首先会去Cookies中拿到csrf_token,然后如果是表单,直接拿出csrfmiddlewaretoken如果是Ajax,就从header里面拿X-CSRF-Token,根据上面的Token算法,这不是比较这两个token,因为肯定不相同,我们需要反向解析清洗出真正的csrf_secret值,对比如果相同,就是正常访问,如果不同,依然是熟悉的403页面,禁止访问。通过之后渲染页面,然后继续执行precess_response,返回给客户端。

下图就是画了半天的流程图,这算是整个框架流程的大致过程了
在这里插入图片描述

其他

1.django/middleware/csrf.py文件里有个函数:rotate_token(request),这个函数用来改变csrf_token这个COOKIE。 在用户登录后(是指 django.contrib.auth这个组件的登录)调用,主要从安全考虑,避免这个COOKIE跟登录前的一样。 如果自己实现的登录逻辑,可以调用这个函数提高点安全性。
一般csrf_token这个COOKIE是不会变的,除了第一点说的登录,和不存在时重新生成一个。有时候会出现登录后csrftoken失效的情况。

2.为什么Django要把csrf_tokencsrfmiddlewaretoken设置成不相等,直接生成的时候让它们相等,验证的时候判断是否相等不就好了?个人觉得这样做有个好处,有时候csrftoken这个COOKIE前端不需要获取,可以设置成HTTP ONLY,提高点安全性。 纯属个人理解
3.从上面分析的算法来看,csrf_tokencsrfmiddlewaretoken相同也可以通过CSRF验证。所以在AJAX请求中,直接取csrf_token值加到请求中就好了,当然和表单一样单独再动态下发也行,问题是要改太多地方了,Ajax可以统一获取。

参考文章:
官方文档
官方文档配置文件
Token验证原理简单篇
Django cookies和session
CSRF验证原理中间件核心
CSRF防御原理解析
中间件源码解析
懒加载原理
Django工作流转

展开阅读全文

没有更多推荐了,返回首页