跳到主要内容

防重复提交

· 阅读需 6 分钟

提交按钮被双击、网络超时后自动重试、浏览器回退再次提交,都可能让同一个表单被服务器处理两次。使用 Token 令牌机制可以有效地防止 CSRF 攻击和重复提交:在提交表单时,服务器会生成一个 Token 令牌并将其存储在 Redis 中,然后作为表单的隐藏字段或 URL 参数传递给客户端,提交时校验、用完即删。

为什么这个问题值得处理​

重复提交的业务后果往往比表面看起来严重:下单接口被处理两次就是两笔订单,扣款请求被重试两次就是重复扣款。前端把提交按钮置灰只能挡住手滑,挡不住网络层的自动重试,更挡不住恶意构造的请求,所以这层防护最终要落在服务端。

CSRF 攻击的场景则是攻击者诱导用户的浏览器向目标站点发起请求,它能得逞的原因是浏览器会自动携带 Cookie。而 Token 不放在 Cookie 里,第三方站点拿不到这个值,因此一套 Token 机制可以同时防住这两类问题。

基本原理​

Token 令牌机制是一种常用的 Web 应用程序防止重复提交和 CSRF 攻击的方法。它的基本思想是在每次提交表单时,服务器会生成一个 Token 令牌,并将其存储在 Redis 中。然后,将这个 Token 令牌作为表单的一个隐藏字段或 URL 参数传递给客户端。客户端提交表单时,将这个 Token 令牌一并提交给服务器。服务器在处理表单时,会检查这个 Token 令牌是否正确,并在处理完表单后删除这个 Token 令牌。

两个防护目标在机制上各有侧重:防重复提交靠的是"校验通过后立即删除",同一个 Token 第二次到达时已经不存在,请求会被拒绝;防 CSRF 靠的是 Token 只下发给合法页面,攻击者构造的请求里没有它。Redis 在这里承担两件事:集中存储,让多实例部署时任何一台机器都能完成校验;过期控制,利用 TTL 自动清理没有被消费的 Token。

实现步骤​

1)在服务器端生成一个 Token 令牌,并将其存储在 Redis 中。Token 令牌可以使用随机数、UUID 或其他方法生成,需要保证其足够随机和唯一。写入时顺便设置过期时间:

# NX 保证不覆盖已有 Token,EX 设置过期时间(秒),避免无效 Token 长期堆积
SET submit:token:<tokenValue> 1 EX 600 NX

2)将这个 Token 令牌传递给客户端。如果是表单提交,可以将 Token 令牌作为一个隐藏字段传递;如果是 AJAX 请求,可以将 Token 令牌作为一个 HTTP 头传递。

3)客户端提交表单时,将这个 Token 令牌一并提交给服务器。如果是表单提交,可以在表单的提交事件中添加一段 JavaScript 代码,将 Token 令牌添加为一个隐藏字段;如果是 AJAX 请求,可以在发送请求前将 Token 令牌添加为一个 HTTP 头。

4)服务器在处理表单时,会检查这个 Token 令牌是否正确。如果 Token 令牌正确,则说明这是一个有效的请求,可以继续处理;否则,说明这是一个重复提交或 CSRF 攻击,应该拒绝处理。

5)在处理完表单后,服务器需要从 Redis 中删除这个 Token 令牌,以防止重复使用。

第 4、5 步的"校验"和"删除"必须作为一个原子操作完成。如果先 GET 再 DEL 分两步执行,两个并发请求可能同时通过校验,防重复提交就失效了。可以用一段 Lua 脚本让 Redis 原子地完成校验加删除:

-- 校验并删除:Token 存在则删除并返回 1,否则返回 0
-- 整段脚本在 Redis 中原子执行,并发请求只有一个能成功
if redis.call('GET', KEYS[1]) then
return redis.call('DEL', KEYS[1])
else
return 0
end

安全注意事项​

Token 令牌机制可以有效地防止重复提交和 CSRF 攻击,但是需要注意保护 Token 令牌的安全性,否则可能会被攻击者利用。具体来说,需要注意以下几点:

Token 令牌需要足够随机和唯一,否则可能会被攻击者猜测或重复使用。

Token 令牌需要保护其传输的安全性,否则可能会被攻击者截获或篡改。传输一律走 HTTPS,不要把 Token 拼在会被记录到日志的 URL 里。

Token 令牌需要设置有效期限,否则可能会被攻击者重复使用。

Token 令牌需要保护其存储在 Redis 中的安全性,否则可能会被攻击者盗取或篡改。Redis 实例不应暴露在公网,并开启访问认证。

踩坑与注意​

注意

校验和删除分两步写是最常见的错误实现。压测或用户快速双击时,两个请求都能通过 GET 校验,各自处理一遍业务。务必用 Lua 脚本或等价手段保证原子性。

过期时间要结合用户实际填表时长来定。设得太短,用户填一个长表单的工夫 Token 已经过期,提交必然失败;设得太长,无效 Token 又会在 Redis 里堆积。

校验失败时前端要给出明确反馈,引导用户刷新页面重新获取 Token 后再提交,而不是静默丢弃请求,否则用户只会觉得"点了没反应"然后继续狂点。

另外要想清楚删除的时机:业务处理失败时是否归还 Token、允许用户直接重试,取决于具体业务对幂等的要求,没有统一答案。

小结​

Token 令牌机制的核心是"一次性凭证":服务器发放、Redis 存储、提交校验、用完即删,一套流程同时解决了重复提交和 CSRF 两个问题。落地时重点盯住四件事——Token 的随机性、传输与存储的安全、合理的有效期,以及校验删除的原子性。这些做到了,它就是一个成本很低、收益明确的通用防护手段。

评论 / COMMENTS