云计算之VUE开发【下】

一, 背景

    云计算之VUE开发【上】结尾的配置有些问题,此处继续优化改造。
    CSRF(Cross Site Request Forgery protection),中文简称跨站请求伪造。
    Vue-Django csrftoken这个问题卡了我好久,今天终于有结果了,整理一下,供大家参考,如果文档有什么地方描述不清楚了,望指正。
    看源码这里还是有些乱的,大家可以参考着 https://blog.csdn.net/qq_27952549/article/details/82392790 配合源码文件 csrf.py 一起看,流程推动这块不止这一个文件完成的,牵扯比较多,此处没有过多描述。

二, 挖源码 csrf.py

1. user -> process_request[ 用户访问Django,进入csrf.py ]
    def process_request(self, request):
        csrf_token = self._get_token(request)
        # 进入 _get_token
        [ def _get_token(self, request):
            if settings.CSRF_USE_SESSIONS:
            # 第一次在settings里没有CSRF_USE_SESSIONS,所以不走if,直接进else
                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]
                    # 到请求COOKIES中找值:csrftoken,要在settings中设置CSRF_COOKIE_NAME为csrftoken,
                    # 因为settings里没有设置,并且COOKIES里也没有值,则返回None
                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
        ]
        因此,csrf_token = self._get_token(request) 的值为None
            if csrf_token is not None:
                # Use same token next time.
                request.META['CSRF_COOKIE'] = csrf_token
            因为csrf_token值为None,进不去这个if 判断语句
            则此处运行完这个函数后得到csrf_token = None
2. process_request -> process_view [ 请求函数进入具体分类函数process_view ]
    一进函数,就有几个 if 判断
        if getattr(request, 'csrf_processing_done', False):
            return None
        给 getattr传了三个参数: request , "csrf_processing_done" , False
        当getattr返回 True 时, process_view 返回None并退出该函数
        而 getattr是一个内建函数,在 builtins.py 中
        def getattr(object, name, default=None):
            # known special case of getattr
            """
            getattr(object, name[, default]) -> value

            #从一个对象中获取一个命名属性;GATTARC(x,‘y’)相当于x.y。当给定一个默认参数时,它在属性不存在时返回;如果没有它,在这种情况下会引发异常。
            """
            pass
            根据注释说明可知,getattr(request, 'csrf_processing_done', False),就相当于request.csrf_processing_done不存在时返回 False;因此,这个 if 就相当于: request.csrf_processing_done 不存在
            if  False:
                return None
            没传default时,getattr为None,传了,则不是,可能不会进入这个 if 语句
            客户端发送请求的时候,request中没发现有csrf_processing_done字段
        # 等到request.META[“CSRF_COOKIE”]被操纵后再释放,这样get_token仍然有效
        if getattr(callback, 'csrf_exempt', False):
            return None
        当一个函数前一行写着 @csrf_exempt 这个值代表跳过 csrftoken 设置

        # 假设RFC7231没有定义为“安全”的任何东西都需要保护
        if request.method not in ('GET', 'HEAD', 'OPTIONS', 'TRACE'):
            # 请求方法不是这些时进入
            进入这个分支后又有好几个 if 判断
            if getattr(request, '_dont_enforce_csrf_checks', False):
                # 关闭测试套件的CSRF检查的机制。它在创建CSRF cookies之后,以便其他所有内容继续完全相同的工作(例如,发送cookies等),但在调用reject()的任何分支之前。 => 先不做CSRF检查,在CSRF cookies发送完,以及reject()之间调用CSRF检查机制
                return self._accept(request)
                [
                    调用了_accept函数
                    # 当前 _accept 和 _reject 方法只存在于 requires_csrf_token 装饰器中。
                    def _accept(self, request):
                        # 通过向请求添加自定义属性,避免检查请求两次。当同时使用decorator和中间件时,这将是相关的
                        request.csrf_processing_done = True
                        return None
                ]
            # 判断 request.is_secure()函数返回结果,如果为True,则进入这个分支。
            if request.is_secure():
                [
                def is_secure(self):
                    return self.scheme == 'https'
                ] => 根据这个函数可知,当用户以 https 方式请求时,进入这个分支
            # 用户请求并非 https 方式,则程序往下走
            csrf_token = request.META.get('CSRF_COOKIE')
            # 从请求头元数据中获取 CSRF_COOKIE,因为请求头中没有这个CSRF_COOKIE值,则 csrf_token 为 None
            if csrf_token is None:
                # 上面分析,csrf_token为None,会进入这个分支
                # 没有CSRF cookie。对于POST请求,我们坚持使用CSRF cookie,这样就可以避免所有CSRF攻击,包括登录CSRF
                return self._reject(request, REASON_NO_CSRF_COOKIE)
                # REASON_NO_CSRF_COOKIE = "CSRF cookie not set."
                [
                def _reject(self, request, reason):
                    response = _get_failure_view()(request, reason=reason)
                    [
                    def _get_failure_view():
                        """Return the view to be used for CSRF rejections."""
                        return get_callable(settings.CSRF_FAILURE_VIEW)
                    ]
                    log_response(
                        'Forbidden (%s): %s', reason, request.path,
                        response=response,
                        request=request,
                        logger=logger,
                    )
                    return response
                ]
            request_csrf_token = ""
            # POST请求进入
            if request.method == "POST":
                try:
                    request_csrf_token = request.POST.get('csrfmiddlewaretoken', '')
                    # 获取请求表单中的字段 csrfmiddlewaretoken, 如果客户端没传,则初始化为空值
                except IOError:
                    # 在我们完成读取POST数据之前处理断开的连接。process_view 不应该引发任何异常,因此我们将忽略并为用户提供403服务(假设他们仍在监听,这可能不是因为错误)。
                    pass
            if request_csrf_token == "":
                # 当request_csrf_token为空值后进入
                # 回到X-CSRFToken,使AJAX变得更容易,并使PUT/DELETE成为可能。
                request_csrf_token = request.META.get(settings.CSRF_HEADER_NAME, '')
            到目前为止,request_csrf_token为""
            request_csrf_token = _sanitize_token(request_csrf_token)
            进入另一个函数_sanitize_token取值
            [
                def _sanitize_token(token):  # sanitize: 净化
                    # 只允许数字字母
                    if re.search('[^a-zA-Z0-9]', token):
                        return _get_new_csrf_token()
                    elif len(token) == CSRF_TOKEN_LENGTH:
                        return token
                    elif len(token) == CSRF_SECRET_LENGTH:
                        # Older Django versions set cookies to values of CSRF_SECRET_LENGTH
                        # alphanumeric characters. For backwards compatibility, accept
                        # such values as unsalted secrets.
                        # It's easier to salt here and be consistent later, rather than add
                        # different code paths in the checks, although that might be a tad more
                        # efficient.
                        return _salt_cipher_secret(token)
                    # 因为传过来的token为"",没有值,直接进_get_new_csrf_token函数
                    return _get_new_csrf_token()
                    [
                        def _get_new_csrf_token():
                            # 因为没有token,直接返回另外一个函数,来生成相应的值
                            return _salt_cipher_secret(_get_new_csrf_string())
                            [
                                def _get_new_csrf_string():
                                    return get_random_string(CSRF_SECRET_LENGTH, allowed_chars=CSRF_ALLOWED_CHARS)
                                    # return get_random_string(32,allowed_chars=大小写字母+数字)
                                    # 这个函数返回 32位数字字母随机组合的字符串
                                # return _salt_cipher_secret(_get_new_csrf_string())
                                # return _salt_cipher_secret(32位数字字母随机组合的字符串)
                                [
                                    def _salt_cipher_secret(secret): # secret也是 32位数字字母组成的随机字符串
                                        """
                                        给定一个 secret(假设是一个CSRF允许的字符串),
                                        通过添加salt并使用它加密secret来生成一个token。
                                        """
                                        salt = _get_new_csrf_string()
                                        # salt = 32位数字字母组成的随机字符串
                                        chars = CSRF_ALLOWED_CHARS
                                        # chars = 大小写字母+数字
                                        pairs = zip((chars.index(x) for x in secret), (chars.index(x) for x in salt))
                                        # secret 后面接 salt
                                        [
                                            (chars.index(x) for x in secret)
                                                这个 for 循环每次获取secret当前字符x对应在chars的下标
                                                for x in secret:
                                                    print(chars.index(x))
                                            (chars.index(x) for x in salt)
                                                这个 for 循环每次获取salt当前字符x对应在chars的下标
                                                for x in salt:
                                                    print(chars.index(x))
                                            所以这两个参数都是对应字符在对应字符串中的下标(数字数组)
                                            zip() 函数用于将可迭代的对象作为参数,将对象中对应的元素打包成一个个元组,然后返回由这些元组组成的列表。
                                            zip 函数将前后两个数字数组取出,两个数字组成一个新的元组
                                        ]
                                        # pairs = [(11,2),(5,2),(10,0),(42,21)...] 类似与这样的格式(32组,每组两个数字)
                                        cipher = ''.join(chars[(x + y) % len(chars)] for x, y in pairs)
                                        [
                                            (x + y) % len(chars)
                                            # 两个下标和 % chars字符串的长度 (11+2) % 58 = 13
                                            chars[(x + y) % len(chars)]
                                            # 取出对应数字下标chars元素的值
                                            ''.join(chars[(x + y) % len(chars)]
                                            # 将取出的元素拼接成字符串, 上述命令可转换为
                                            cipher = ""
                                            for x,y in pairs:
                                                # x: secret对应下标; y: salt对应下标
                                                cipher += "".join(chars[两个下标和 % chars字符串的长度])
                                            cipher: 32位长度的数字字母组成的随机字符串
                                        ]
                                        return salt + cipher
                                        # 将salt 和 cipher 拼接成64位字符串返回
                                ]
                                # return _salt_cipher_secret(32位数字字母随机组合的字符串) 返回的是一个64位长度的字符串
                            ]
                            # return _salt_cipher_secret(_get_new_csrf_string()) 返回一个64位长度的字符串
                    ]
                    # return _get_new_csrf_token() 返回经过加密后的一个64位长度的字符串
            ]
            # request_csrf_token = _sanitize_token(request_csrf_token) request_csrf_token = 经过加密后的一个64位长度的字符串
            if not _compare_salted_tokens(request_csrf_token, csrf_token):
                return self._reject(request, REASON_BAD_TOKEN)
                # return self._reject(request, "CSRF token missing or incorrect.")
                # 如果_compare_salted_tokens(request_csrf_token, csrf_token)校验不对,则返回给客户端403报错,并提示:"CSRF token missing or incorrect."
                [
                    def _compare_salted_tokens(request_csrf_token, csrf_token):
                        # request_csrf_token:服务端自己生成的, 经过加密后的一个64位长度的字符串
                        # csrf_token:客户端传过来的,由上述步骤分析,csrf_token=None
                        # 假设这两个参数都经过了清理——也就是说,长度为 CSRF_TOKEN_LENGTH、所有 CSRF_ALLOWED_CHARS 的字符串。
                        return constant_time_compare(
                            _unsalt_cipher_token(request_csrf_token),
                            _unsalt_cipher_token(csrf_token),
                            [
                                _unsalt_cipher_token(request_csrf_token), # 根据函数名,猜测为将加密后的 request_csrf_token 重新解密
                            ]

                        )
                        [
                            def constant_time_compare(val1, val2):
                                """如果两个字符串相同,返回True,否则返回False."""
                                return hmac.compare_digest(force_bytes(val1), force_bytes(val2))
                        ]
                ]
                # _compare_salted_tokens(request_csrf_token, csrf_token) 返回false,提示 "CSRF token missing or incorrect." 错误
        return self._accept(request) # 如果是'GET', 'HEAD', 'OPTIONS', 'TRACE'这些请求方式
        [
            # 当前接受和拒绝方法只存在于requires_csrf_token装饰器中。
            def _accept(self, request):
                # 通过向请求添加自定义属性,避免检查请求两次。当同时使用decorator和中间件时,这将是相关的。
                request.csrf_processing_done = True
                return None
        ]
    # 客户端首次访问,process_request 的返回值应该是None, 继续走 process_view 函数 , process_view 也返回None
    # https://blog.csdn.net/qq_27952549/article/details/82392790
    # 根据这个流程图,当不是post请求的时候,进入系统路由,View {%csrf_token%},将csrf_token值设置到隐藏的csrfmiddleware中
3. process_view --(服务端生成token,传给csrfmiddleware中)--> process_response
    # 服务端生成token[只要调用get_token(request)方法即可],通过客户端初次GET请求将token传给csrfmiddleware?

    def process_response(self, request, response):
        接收request , response两个参数, 返回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
        # 即使CSRF cookie已经存在,也要设置它,然后更新到期计时器
        self._set_token(request, response)
        [
            def _set_token(self, request, response):
                if settings.CSRF_USE_SESSIONS: # 如果 settings 配置中有 CSRF_USE_SESSIONS 属性配置
                    if request.session.get(CSRF_SESSION_KEY) != request.META['CSRF_COOKIE']:
                        # CSRF_SESSION_KEY = '_csrftoken'
                        # request.META['CSRF_COOKIE'] 和 requests.session中
                        request.session[CSRF_SESSION_KEY] = request.META['CSRF_COOKIE']
                        # request.session['_csrftoken'] = request.META['CSRF_COOKIE']
                else:
                    # 如果 settigns 配置中没有CSRF_USE_SESSIONS属性配置,则开始设置cookies值
                    response.set_cookie(
                        settings.CSRF_COOKIE_NAME, # None (应该是csrftoken)
                        request.META['CSRF_COOKIE'], # csrftoken 的值
                        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_cookie in response.py
                        def set_cookie(self, key, value='', max_age=None, expires=None, path='/',
                            domain=None, secure=False, httponly=False, samesite=None):
                            ...
                    ]
                    # 设置Vary头,因为内容随CSRF cookie的变化而变化。
                    patch_vary_headers(response, ('Cookie',)) # 添加响应头,Vary: Cookie
        ]
        # 将 token 值设置到响应头中
        response.csrf_cookie_set = True
        # 给 response 响应头设置属性 csrf_cookie_set 为 True
        return response
        # 将 response 响应头信息返回给客户端
        [
            客户端可以获取到带有 token 值的响应头消息
        ]
4. process_response --(带着token[csrftoken]值)--> user
    user 获取到响应消息,将响应消息中的cookies部分值
        [
            Set-Cookie: csrftoken=QDEmMHDN5hyzq20LAGuyGlGyjae76qvb0kD05yDGn2Wlk9m93bgegTCuh0zNXfPu; expires=Sun, 10 Jan 2021 10:02:36 GMT; Max-Age=31449600; Path=/; SameSite=Lax
        ]
    设置到浏览器 Application/Cookies中
        [
            Name        Value   Domain      Path    Expires / Max-Age   Size    HttpOnly    Secure  SameSite
            csrftoken  QDEmMHDN5hyzq20LAGuyGlGyjae76qvb0kD05yDGn2Wlk9m93bgegTCuh0zNXfPu   172.16.3.100   /  2021-01-11T03:00:03.772Z   73       Lax
        ]
    疑问: Django 如何设置将响应头中的cookie设置到浏览器的Cookies中的?

5. 源码分析完成,着手刚刚的疑问: Django 如何设置将响应头中的cookie设置到浏览器的Cookies中的?
    参考文档:
        https://www.cnblogs.com/linxizhifeng/p/8995077.html
        [这个文档也有些地方可以改进的,不过它也给了我一个灵感]
    Django后端:
        1) views.py
            def check_user(request):
                if request.method == "GET":
                    response = HttpResponse()
                    get_token(request)
                    return response
                if request.method == "POST":
                        uname = request.POST.get('username')
                        upass = request.POST.get('password')
                        all_user = USerProfile.objects.all().order_by("-username")
                        for user in all_user:
                            if uname == user.username:
                                remote_pass = user.password
                                if check_password(upass,remote_pass):
                                    return JsonResponse({"result":"success","code":1})
                                else:
                                    return JsonResponse({"result":"error","code":0})
                            else:
                                return JsonResponse({"result":"error","code":0})
                        else:
                            return JsonResponse({"result":"error","code":0})
                # 当客户端发起get访问时,这种方式,客户端请求头里的cookies和application/cookies里一致。
                # 注意点:Vue-Django开启cors和csrf认证时,必须先发送GET请求获取对应的cookies值,然后才能正常使用
        2)  主urls.py
                ...
                path('api/', include('baseline.urls')),
                ...
            辅urls.py
                ...
                path("check_user",views.check_user, name="check_user"),
                ...
        3)  settings.py
            ...
            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',
            ]
            ...
        python mange.py runserver 0.0.0.0:10082

    Vue前端:
        # 因为有跨域要求,以及开启csrftoken,所以牵扯到的配置如下
        src/components/Login.vue
            ...
            <script>
                import {getCookie,setCookie} from  '../assets/js/cookie'
                export default {
                  name: "Login",
                  data:function(){
                    return {
                      form: {
                        csrfmiddlewaretoken: '', // 这个值需要随表单传给 Django
                        username: '',
                        password: ''
                      }
                    }
                  },
                  beforeMount:function(){
                    $.ajax({
                      method: 'get',
                      url: '/api/check_user',
                      success: function (data) {
                        console.log(data)
                      },
                      error: function (err) {
                        console.log(err)
                      }
                    })
                  },
                  methods:{
                    login:function () {
                      var vm = this;
                      vm.form.csrfmiddlewaretoken = getCookie('csrftoken');
                      $.ajax({
                        method: 'post',
                        url: '/api/check_user',
                        data: vm.form,
                        success: function (data) {
                          if(data.code === 1){
                            setCookie('username',vm.form.username);
                            vm.$router.push({path:'/base'})
                          }else{
                            alert('用户名或密码不正确');

                          }
                        },
                        error: function (err) {
                          console.log(err)
                        }
                      })
                    }
                  }
                }
            </script>
            ...
        src/assets/js/cookie.js
            function getCookie(name)
                {
                    var arr,reg=new RegExp("(^| )"+name+"=([^;]*)(;|$)");
                    if(arr=document.cookie.match(reg))
                        return unescape(arr[2]);
                    else
                        return null;
                }

            function setCookie(name,value, days)
                {
                    var exp = new Date();
                    exp.setTime(exp.getTime() + days*24*60*60*1000);
                    document.cookie = name + "="+ escape (value) + ";expires=" + exp.toGMTString();
                }

            export {
                    getCookie,
                    setCookie
                }
        config/index.js
            proxyTable: {
              '/api': {
                target: 'http://192.168.89.133:10082/',
                changeOrigin: true,
                pathRewrite: {
                  '^/api': '/api'
                }
              }
            },
            # 这地方配置还是稍微有些绕脑的,稍作说明:
            '/api': 第一行这个/api,就是 http://192.168.89.133:10082/
            target: Django后端服务器接口
            changeOrigin: 是否允许跨域
            pathRewrite: {
                "^/api": "/api" : 路径重写,为了区分前后端接口,后端接口可以在前面加个/api以作区分
            }
        cnpm run dev
            Your application is running here: http://192.168.89.133:81

三, 结果验证:

    浏览器打开 http://192.168.89.133:81
    输入用户名密码登录成功,Application/Cookies 中有 csrftoken 和 username两个key,验证成功,内部功能,比如退出删除cookies,可以由公司自己的业务决定。我这块就是通过验证是否有username: 用户名,如果没有,则跳转到登录页,如果点击退出,也将这个cookie删除。

四, 总结:

    这个坑也是爬了近一个星期了,各种文档,各种参考,感觉他们都是互相抄袭,不管对错,比如,Vue本身不渲染html,那么还有人说要在html里写 <% csrf_token %> 这个东西,还有的只是简单的把Django settings.py中的'django.middleware.csrf.CsrfViewMiddleware'注释掉,很不安全。就算开启了,网上的大部分文档资料也没有说到点子上,这个并非是只要配置csrfmiddlewaretoken就可以了。这件事也告诫一下自己,对于知识,首先要求真务实,不要因为追求数量而忽略了质量。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值