一, 背景
云计算之VUE开发【上】结尾的配置有些问题,此处继续优化改造。
CSRF(Cross Site Request Forgery protection),中文简称跨站请求伪造。
Vue-Django csrftoken这个问题卡了我好久,今天终于有结果了,整理一下,供大家参考,如果文档有什么地方描述不清楚了,望指正。
看源码这里还是有些乱的,大家可以参考着 https://blog.youkuaiyun.com/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.youkuaiyun.com/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开发【下】
最新推荐文章于 2025-05-22 21:17:05 发布
771

被折叠的 条评论
为什么被折叠?



