一, 背景 云计算之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就可以了。这件事也告诫一下自己,对于知识,首先要求真务实,不要因为追求数量而忽略了质量。
云计算之VUE开发【下】
最新推荐文章于 2024-04-13 23:17:13 发布