在 Web 安全领域,CSRF(跨站请求伪造)始终是 OWASP Top 10 的常客。它的核心逻辑并不复杂:攻击者诱导已登录的用户在不知情的情况下,向目标站点发起一个带有身份凭证的请求。浏览器会自动携带 Cookie,服务器无法区分这个请求是用户主动发起的,还是被第三方页面伪造的。防御 CSRF 的手段有很多,其中同步令牌模式(Synchronizer Token Pattern) 是最经典、也最可靠的方案之一。本文将从原理出发,给出可落地的实现代码,并重点梳理实际开发中容易踩的坑。
一、为什么 Cookie 防不住 CSRF
要理解同步令牌的价值,先要认清 Cookie 的“原罪”。浏览器遵循同源策略,但 <img>、<form>、<script> 等标签发起的请求并不受同源策略限制,而 Cookie 的发送只取决于目标域名,与请求来源页面无关。这意味着:
- 用户在
bank.com登录后,Cookie 被浏览器保存; - 用户访问了恶意站点
evil.com; evil.com页面里放一个<form action="https://bank.com/transfer" method="POST">,自动提交;- 浏览器带着
bank.com的 Cookie 发出请求,银行服务器认为这是合法操作。
问题的根源在于:服务器仅凭 Cookie 无法验证请求是否来自本站页面。同步令牌的思路就是给每个请求附加一个攻击者无法获取的“暗号”。
二、同步令牌模式的核心原理
同步令牌模式包含三个关键点:
- 服务端生成随机令牌,与当前用户的会话绑定,存储在 Session 中;
- 页面渲染时把令牌嵌入表单或请求头,随请求一起提交;
- 服务端校验请求中的令牌与 Session 中的令牌是否一致,不一致则拒绝。
攻击者虽然能让浏览器自动带上 Cookie,但无法读取目标站点的页面内容(受同源策略保护),因此拿不到令牌。没有令牌,伪造的请求就会被服务端拒绝。
令牌需要满足两个条件:足够随机(不可预测)和与会话绑定(不可复用)。
三、实现示例
3.1 服务端生成与存储
以 PHP 为例,在用户会话建立时生成令牌:
session_start();
function generateCsrfToken(): string {
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
return $_SESSION['csrf_token'];
}
random_bytes 是密码学安全的随机源,不要用 rand() 或 mt_rand(),它们可预测。
3.2 前端嵌入
在表单中放置隐藏字段:
<form action="/transfer" method="POST">
<input type="hidden" name="csrf_token"
value="<?= htmlspecialchars(generateCsrfToken()) ?>">
<!-- 其他字段 -->
</form>
对于 AJAX 请求,可以把令牌放在自定义请求头中:
fetch('/api/transfer', {
method: 'POST',
headers: {
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content
},
body: JSON.stringify(data)
});
3.3 服务端校验
function validateCsrfToken(): bool {
$token = $_POST['csrf_token']
?? $_SERVER['HTTP_X_CSRF_TOKEN']
?? '';
if (empty($token) || empty($_SESSION['csrf_token'])) {
return false;
}
return hash_equals($_SESSION['csrf_token'], $token);
}
校验时必须使用 hash_equals 而非 ==,前者是恒定时间比较,能防止时序攻击。
四、踩坑实录
同步令牌看起来简单,但实际落地时坑不少,以下是最常见的几类。
坑一:只在 POST 上校验
很多开发者只对 POST 请求做校验,认为 GET 是“安全”的。但如果业务中存在 GET /delete?id=1 这类写操作,CSRF 照样成立。原则是:所有改变状态的请求都必须校验令牌,无论方法是什么。
坑二:令牌放在 URL 里
把令牌拼在 URL 上(如 ?csrf_token=xxx)会带来泄露风险:URL 可能被记录在浏览器历史、服务器访问日志、Referer 头中。令牌应放在请求体或自定义请求头里。
坑三:多标签页导致令牌失效
如果每次渲染页面都重新生成令牌并覆盖 Session,用户在多个标签页操作时,旧标签页的令牌就会失效,提交时报错。正确做法是同一会话内保持令牌稳定,或者在令牌轮换时允许一个宽限窗口。
坑四:Session 与令牌的生命周期不同步
用户登录状态变化(登录、登出、权限提升)时,应当重新生成令牌,防止会话固定攻击。但要注意,登出后如果页面还缓存着旧令牌,再次提交会失败,需要前端配合刷新。
坑五:忽略 SameSite Cookie 的协同作用
现代浏览器支持 SameSite=Lax/Strict,能在很大程度上缓解 CSRF。但它不能完全替代令牌:老旧浏览器不支持,且 Lax 对顶级导航的 POST 仍有放行场景。建议令牌与 SameSite 双管齐下,而不是二选一。
坑六:API 与 SPA 场景下的误区
纯前后端分离的 SPA 如果使用 JWT 放在 Authorization 头中,天然不受 CSRF 影响,因为攻击者无法跨域读取或设置该头。但如果 JWT 存在 Cookie 里,就依然需要 CSRF 防护。判断标准只有一个:凭证是否由浏览器自动携带。
坑七:令牌校验被绕过
常见疏漏包括:校验逻辑写在某个基类里但个别接口忘了调用;或者校验失败后仍然继续执行了业务逻辑。建议把校验做成中间件或统一入口,避免遗漏。
五、与其他防护的配合
同步令牌不是孤立的。一个完整的 Web 安全加固体系还应包括:
- XSS 防护:XSS 能读取页面中的令牌,从而绕过 CSRF 防护。所以输出转义、CSP 策略必不可少;
- SQL 注入防护:参数化查询,避免攻击者通过注入直接篡改数据;
- 安全响应头:
SameSite、Secure、HttpOnly三件套; - 关键操作二次验证:转账、改密等敏感操作增加密码或验证码确认。
六、小结
同步令牌模式之所以经久不衰,是因为它用最简单的机制解决了最本质的问题:让服务器能够验证请求确实来自本站页面。实现上并不复杂,但细节决定成败——随机源要安全、比较要恒定时间、令牌要与会话绑定、所有写操作都要覆盖。避开上述几个坑,再配合 SameSite、XSS 防护等措施,就能构建起一道可靠的 CSRF 防线。安全没有银弹,但把每一个环节做对,攻击者的空间就会被压缩到最小。
未经允许不得转载:任鹏个人博客 » Token 机制防 CSRF:同步令牌模式的实现与踩坑


朋友圈点赞图在线生成源码