Django CSRF中间件解析

1. 什么是CSRF攻击

CSRF(Cross-Site Request Forgery)是一种Web安全漏洞,攻击者诱导用户在已登录的网站上执行非预期的操作。

1
2
3
4
5
6
<!-- 恶意网站的页面 -->
<form action="https://bank.com/transfer" method="POST" style="display:none;">
<input name="to" value="attacker@evil.com">
<input name="amount" value="10000">
</form>
<script>document.forms[0].submit();</script>

如果用户在银行网站已登录,这个表单会自动提交,转账给攻击者。

2. Django的CSRF保护机制

2.1 双重Token验证

1
2
3
4
5
6
7
8
# 1. 服务器生成密钥并存储
csrf_secret = _get_new_csrf_string() # 32位随机字符串
request.META["CSRF_COOKIE"] = _salt_cipher_secret(csrf_secret)

# 2. 每次表单请求都生成新的Token
def get_token(request):
csrf_secret = _unsalt_cipher_token(request.META["CSRF_COOKIE"])
return _salt_cipher_secret(csrf_secret) # 每次都不同

2.2 Salt加密机制

1
2
3
4
5
6
7
def _salt_cipher_secret(secret):
"""使用salt对密钥进行加密"""
salt = _get_new_csrf_string() # 生成新的salt
chars = CSRF_ALLOWED_CHARS
pairs = zip((chars.index(x) for x in secret), (chars.index(x) for x in salt))
cipher = ''.join(chars[(x + y) % len(chars)] for x, y in pairs)
return salt + cipher # salt + 加密后的密钥

加密过程

  • 生成32位随机salt
  • 将secret和salt对应字符的ASCII码相加
  • 结果对字符集长度取模,得到新字符
  • 返回 salt + 加密结果(总长64位)

3. 核心验证流程

3.1 process_view方法 - 主要验证逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def process_view(self, request, callback, callback_args, callback_kwargs):
# 1. 跳过已处理的请求
if getattr(request, 'csrf_processing_done', False):
return None

# 2. 获取CSRF token
csrf_token = self._get_token(request)

# 3. 跳过@csrf_exempt装饰的视图
if getattr(callback, 'csrf_exempt', False):
return None

# 4. 仅对不安全方法进行检查
if request.method not in ('GET', 'HEAD', 'OPTIONS', 'TRACE'):
# 执行CSRF验证

3.2 HTTPS下的Referer检查

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
if request.is_secure():  # HTTPS请求
referer = force_text(request.META.get('HTTP_REFERER'))

# 1. 必须有Referer头
if referer is None:
return self._reject(request, REASON_NO_REFERER)

# 2. Referer必须是有效URL
referer = urlparse(referer)
if '' in (referer.scheme, referer.netloc):
return self._reject(request, REASON_MALFORMED_REFERER)

# 3. Referer必须是HTTPS
if referer.scheme != 'https':
return self._reject(request, REASON_INSECURE_REFERER)

# 4. 域名必须在信任列表中
good_hosts = list(settings.CSRF_TRUSTED_ORIGINS)
good_hosts.append(request.get_host())

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

为什么HTTPS需要Referer检查?

在HTTPS环境下,攻击者可能通过中间人攻击绕过CSRF保护。Referer检查确保请求来源可信。

3.3 Token验证

1
2
3
4
5
6
7
8
9
10
11
# 1. 从POST数据获取token
request_csrf_token = request.POST.get('csrfmiddlewaretoken', '')

# 2. 如果没有,从HTTP头获取(用于AJAX)
if request_csrf_token == "":
request_csrf_token = request.META.get(settings.CSRF_HEADER_NAME, '')

# 3. 验证token
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)

4. Token比较机制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def _compare_salted_tokens(request_csrf_token, csrf_token):
"""比较两个加盐的token"""
return constant_time_compare(
_unsalt_cipher_token(request_csrf_token), # 解密用户提交的token
_unsalt_cipher_token(csrf_token), # 解密服务器的token
)

def _unsalt_cipher_token(token):
"""解密token获取原始secret"""
salt = token[:CSRF_SECRET_LENGTH] # 前32位是salt
token = token[CSRF_SECRET_LENGTH:] # 后32位是加密的secret
chars = CSRF_ALLOWED_CHARS
pairs = zip((chars.index(x) for x in token), (chars.index(x) for x in salt))
secret = ''.join(chars[x - y] for x, y in pairs) # 解密
return secret

5. Cookie管理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def _set_token(self, request, response):
if settings.CSRF_USE_SESSIONS:
# 存储在session中
request.session[CSRF_SESSION_KEY] = request.META['CSRF_COOKIE']
else:
# 设置Cookie
response.set_cookie(
settings.CSRF_COOKIE_NAME, # 默认: 'csrftoken'
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, # 仅HTTPS
httponly=settings.CSRF_COOKIE_HTTPONLY, # 防XSS
)

5.2 process_response - Cookie更新

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def process_response(self, request, response):
# 1. 跳过不需要重置cookie的情况
if not getattr(request, 'csrf_cookie_needs_reset', False):
if getattr(response, 'csrf_cookie_set', False):
return response

# 2. 跳过未使用CSRF的请求
if not request.META.get("CSRF_COOKIE_USED", False):
return response

# 3. 设置或更新CSRF cookie
self._set_token(request, response)
response.csrf_cookie_set = True
return response

6. 实际使用示例

6.1 模板中使用

1
2
3
4
5
6
7
8
9
10
11
12
13
<form method="post">
{% csrf_token %}
<input name="username" value="john">
<button type="submit">提交</button>
</form>

<!-- 渲染后 -->
<form method="post">
<input type="hidden" name="csrfmiddlewaretoken"
value="abc123...xyz789">
<input name="username" value="john">
<button type="submit">提交</button>
</form>

6.2 AJAX请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 获取CSRF token
function getCSRFToken() {
return document.querySelector('[name=csrfmiddlewaretoken]').value;
}

// AJAX请求
fetch('/api/data/', {
method: 'POST',
headers: {
'X-CSRFToken': getCSRFToken(),
'Content-Type': 'application/json',
},
body: JSON.stringify({data: 'value'})
});

7. 配置选项

1
2
3
4
5
6
7
8
9
10
11
12
13
# settings.py
CSRF_COOKIE_NAME = 'csrftoken' # Cookie名称
CSRF_COOKIE_AGE = None # Cookie过期时间(秒)
CSRF_COOKIE_DOMAIN = None # Cookie域名
CSRF_COOKIE_PATH = '/' # Cookie路径
CSRF_COOKIE_SECURE = False # 仅HTTPS传输
CSRF_COOKIE_HTTPONLY = False # 防止JS访问
CSRF_COOKIE_SAMESITE = 'Lax' # SameSite策略

CSRF_USE_SESSIONS = False # 使用session存储token
CSRF_TRUSTED_ORIGINS = [] # 信任的来源域名
CSRF_HEADER_NAME = 'HTTP_X_CSRFTOKEN' # AJAX请求头名称
CSRF_FAILURE_VIEW = 'django.views.csrf.csrf_failure' # 失败页面

8. 安全特性总结

  1. 双重验证:Cookie + 表单Token
  2. Salt加密:每次生成不同的Token
  3. Referer检查:HTTPS下验证请求来源
  4. 时序攻击防护:使用constant_time_compare
  5. Session隔离:每个用户独立的CSRF密钥
  6. 域名限制:通过配置控制可信来源

这个中间件提供了完整的CSRF保护,有效防止跨站请求伪造攻击,是Django安全体系的重要组成部分。


Django CSRF中间件解析
https://luffy997.github.io/2025/07/16/Django-CSRF中间件解析/
作者
Luffy997
发布于
2025年7月16日
许可协议