csrf token 必须一次一用,核心目的是防止重放攻击(replay attack)。如果复用 token,攻击者截获该 token 后,可以在有效期内多次发起恶意请求,导致数据被重复篡改或资金被重复转移。实现时,服务器在生成 token 后应立即将其标记为已使用或直接销毁,确保同一个 token 只能成功处理一次请求。

为什么复用 Token 会导致重放攻击?
复用 Token 意味着只要攻击者拿到了这个令牌,就可以无限次地伪造请求。例如,在一个转账场景中,如果 Token 不失效,攻击者可以反复提交相同的转账指令,直到达到系统限额或用户发现为止。这种风险在于时间窗口内的安全性完全依赖于 Token 的保密性,而一旦泄露,后果是灾难性的。

import hashlib
import time
class CSRFProtection:
def __init__(self):
self.used_tokens = set()
def generate_token(self, user_id):
# 模拟生成唯一 Token
raw = f"{user_id}-{time.time()}"
return hashlib.sha256(raw.encode()).hexdigest()
def validate_and_consume(self, token, user_id):
# 检查 Token 是否存在且未被使用
if token in self.used_tokens:
return False, "Token already used or invalid"
# 验证逻辑(简化)
if not token:
return False, "Empty token"
# 标记为已使用,实现“一次一用”
self.used_tokens.add(token)
return True, "Success"
# 测试用例
protection = CSRFProtection()
token = protection.generate_token("user123")
print(protection.validate_and_consume(token, "user123")) # 第一次:成功
print(protection.validate_and_consume(token, "user123")) # 第二次:失败,因为已消费
运行上述代码,第一次调用返回 (True, 'Success'),第二次调用返回 (False, 'Token already used or invalid')。这证明了通过维护一个已使用 Token 集合,可以有效阻止重放。
如何在不增加服务器负担的情况下实现一次性验证?
为了平衡安全性和性能,通常不会将所有 Token 都存储在内存中,而是结合数据库或缓存(如 Redis)进行状态管理。当请求到达时,服务器先验证签名有效性,再检查该 Token 是否已被标记为“已消耗”。如果有效且未消耗,则执行业务逻辑并立即更新状态为“已消耗”。这种方式确保了即使在高并发下,每个 Token 也只能被使用一次。

import redis
import json
# 假设已连接 Redis
r = redis.Redis(host='localhost', port=6379, db=0)
def secure_csrf_check(token, user_id):
key = f"csrf:{token}"
# 原子操作:获取并删除 Token,确保只读一次
# GETDEL 是 Redis 4.0+ 的特性,或者使用 Lua 脚本保证原子性
current_token = r.getdel(key)
if not current_token:
return False, "Token missing or already consumed"
# 解析并验证用户ID匹配
try:
data = json.loads(current_token)
if data.get('user_id') != user_id:
return False, "User mismatch"
except:
return False, "Invalid token format"
return True, "Valid and consumed"
# 预置 Token
r.setex(f"csrf:valid_token_123", 300, json.dumps({"user_id": "user123"}))
print(secure_csrf_check("valid_token_123", "user123")) # 成功
print(secure_csrf_check("valid_token_123", "user123")) # 失败,已被 getdel 移除
使用 Redis 的 getdel 命令可以原子性地获取并删除 Key,从而天然支持“一次一用”。第一次调用返回成功,第二次调用因 Key 不存在而返回失败。这种方法既保证了安全性,又利用了缓存的高性能特性。
如果 Token 验证失败,程序应如何处理以防止信息泄露?
当 Token 验证失败(包括过期、无效或已重用)时,程序应统一返回通用的错误信息,如“请求无效”或“安全验证失败”,而不应区分具体原因。如果返回“Token 已重用”,攻击者就能确认他们之前截获的 Token 是有效的,从而继续尝试其他攻击手段。此外,应记录此类事件用于审计,但不对客户端暴露细节。











