4、CSRF防护:CSRF攻击原理、Token机制实现、SameSite Cookie属性配置
CSRF,全称叫跨站请求伪造。这玩意儿我早些年刚做后端时,觉得它就是个「理论漏洞」,直到有一次在内部渗透测试中,亲眼看着测试人员用一个简单的图片标签,就绕过了我们当时的所有权限校验……嗯,从那以后,我再也不敢小看它了。
4.1 CSRF攻击原理:到底是怎么发生的?
说白了,CSRF就是利用了你对某个网站的「登录态」,让你在不知情的情况下,替攻击者执行了恶意操作。
举个例子你就明白了:
- 你登录了银行网站A,浏览器里留下了A的Cookie。
- 你没退出A,又去访问了一个恶意网站B。
- 网站B里藏了个请求,比如
<img src="https://bank.com/transfer?to=attacker&amount=10000" />。 - 浏览器带着你的Cookie,向银行A发起了转账请求。
- 银行A一看,Cookie有效,用户已登录,转账成功!
为什么会这样?因为浏览器在发送请求时,会自动带上目标站点的Cookie。服务器只认Cookie,不认请求是从哪里发出来的。这就是CSRF的核心问题——服务器无法区分请求是用户主动发起的,还是被恶意网站伪造的。
关键点:CSRF攻击利用的是「身份认证」的漏洞,而不是「加密」的漏洞。你的HTTPS、密码强度再高,也防不住CSRF。
4.2 Token机制实现:最经典的防御方案
CSRF Token是目前最主流的防御手段。我个人习惯把它叫做「一次性暗号」——服务器给每个合法用户发一个随机字符串,每次提交表单时都必须带上这个暗号,服务器验证通过才放行。
4.2.1 基本原理
- 服务器生成一个随机的Token,存入Session或Cookie中。
- 在渲染表单时,把Token作为隐藏字段嵌入。
- 用户提交表单时,Token随请求一起发送。
- 服务器比对请求中的Token和Session中的Token是否一致。
攻击者无法获取到你的Token,所以伪造的请求自然就通不过验证。
4.2.2 Python实现示例(Flask)
import secrets
from flask import Flask, session, request, render_template_string
app = Flask(__name__)
app.secret_key = 'your-secret-key-here'
def generate_csrf_token():
if '_csrf_token' not in session:
session['_csrf_token'] = secrets.token_hex(32)
return session['_csrf_token']
@app.before_request
def csrf_protect():
if request.method == 'POST':
token = session.get('_csrf_token')
request_token = request.form.get('_csrf_token')
if not token or token != request_token:
return 'CSRF验证失败', 403
@app.route('/transfer', methods=['GET', 'POST'])
def transfer():
if request.method == 'POST':
# 执行转账逻辑
return '转账成功'
return render_template_string('''
<form method="post">
<input type="hidden" name="_csrf_token" value="{{ csrf_token }}">
金额:<input type="text" name="amount">
<button type="submit">转账</button>
</form>
''', csrf_token=generate_csrf_token())
我的经验:Token一定要用密码学安全的随机数生成器,比如Python的secrets模块。别用random模块,那个是伪随机,理论上可预测。我在一个老项目里就见过用random.randint()生成Token的……嗯,后来全改了。
4.2.3 Django中的CSRF防护
如果你用Django,那更简单了。框架自带CSRF中间件,开箱即用:
# settings.py
MIDDLEWARE = [
'django.middleware.csrf.CsrfViewMiddleware', # 默认已启用
# ...
]
# 模板中使用
<form method="post">
{% csrf_token %}
<input type="text" name="amount">
<button type="submit">转账</button>
</form>
Django的CSRF Token是绑定用户Session的,而且每次刷新页面都会重新生成。不过要注意,如果你用了缓存或分布式Session,要确保Token的同步。
4.3 SameSite Cookie属性配置:现代浏览器的原生防御
SameSite是浏览器提供的一个原生安全机制。你想想看,如果浏览器自己就能判断「这个请求是不是跨站的」,那很多问题不就迎刃而解了吗?
4.3.1 三个取值
| 属性值 | 行为 | 适用场景 |
|---|---|---|
Strict |
完全禁止跨站携带Cookie | 银行、支付等敏感操作 |
Lax |
允许部分安全请求(GET、导航)携带Cookie | 大多数Web应用(默认值) |
None |
允许所有跨站请求携带Cookie(需配合Secure) | 第三方嵌入场景(如支付回调) |
4.3.2 Python中的配置方式
# Flask中设置SameSite
from flask import make_response
@app.route('/login', methods=['POST'])
def login():
resp = make_response('登录成功')
resp.set_cookie(
'session_id',
'your-session-value',
samesite='Lax', # 可选 Strict / Lax / None
secure=True, # 生产环境必须开启
httponly=True # 防止XSS窃取Cookie
)
return resp
# Django中设置(settings.py)
SESSION_COOKIE_SAMESITE = 'Lax'
SESSION_COOKIE_SECURE = True
SESSION_COOKIE_HTTPONLY = True
注意:如果你把SameSite设为None,必须同时设置Secure=True(即仅HTTPS下生效)。否则浏览器会直接拒绝这个Cookie。我曾经在调试一个第三方支付回调时踩过这个坑,排查了半天才发现是少了Secure标记。
4.4 综合防御策略:别只依赖一种方案
我个人建议,不要只依赖Token或SameSite中的某一个。安全是分层防御的,多一层就多一分保障。
- 第一层:SameSite=Lax —— 浏览器原生拦截大部分跨站请求。
- 第二层:CSRF Token —— 对关键操作(转账、改密、删除)强制校验。
- 第三层:二次确认 —— 敏感操作要求用户输入密码或验证码。
- 第四层:Referer/Origin校验 —— 检查请求来源是否合法。
我曾经在一个金融项目中,把SameSite设为Strict,同时所有POST请求都校验Token,转账操作还要输入短信验证码。虽然用户体验上多了一步,但安全团队审计时直接给了满分。
4.5 避坑指南
- 不要用GET请求做状态修改操作。 这是最基本的原则,但很多新手会犯。GET请求可以被图片、脚本、iframe等任意触发。
- Token不要放在URL参数中。 URL可能被记录在浏览器历史、服务器日志、Referer头里,容易泄露。
- SameSite不是银弹。 老版本浏览器(比如Safari 12之前)不支持SameSite,所以Token机制还是得保留。
- API接口也要防CSRF。 别以为只有表单需要,REST API同样可能被CSRF攻击。建议使用自定义Header(如
X-CSRF-Token)来传递Token。
总结一下:CSRF防御的核心思路就是「让服务器能确认请求是用户主动发起的」。Token机制靠的是「只有用户页面才有的暗号」,SameSite靠的是「浏览器帮你拦截跨站请求」。两者结合,基本就能挡住99%的CSRF攻击了。