有效Token需绑定用户ID、请求路径和时间戳,用Redis原子校验并设5分钟过期,须防并发失效、降级兜底及全链路监控。

Token生成必须绑定用户会话和操作上下文
单纯用 md5(uniqid()) 生成 Token,攻击者可预生成并批量提交。真正有效的 Token 要包含三要素:当前用户 ID、请求路径(如 /order/create)、时间戳(精度到秒或分钟)。ThinkPHP 中建议在控制器里用 session('user.id') + $this->request->url(true) + time() // 或 floor(time()/60) 拼接后加密。
常见错误是把 Token 存在 hidden 字段但没校验来源 URL,导致一个 Token 在多个页面复用;或者用 rand() 生成,被暴力猜解。
- Token 过期时间建议设为 5 分钟(
Redis::setex($key, 300, $token)),太短影响体验,太长增加重放风险 - 生成后立即写入 Redis,Key 建议格式为
token:{$userId}:{$actionHash},其中$actionHash = md5($url . $method) - 前端渲染 Token 时,确保每次加载表单都重新请求接口获取新 Token,而不是页面初始化时缓存一份
提交时 Redis 校验要原子性删除
收到表单后,不能先 GET 再 DEL —— 中间可能被并发请求抢走。必须用 Lua 脚本或 GETSET(已废弃)/ SET key value EX seconds NX 的变体。ThinkPHP Redis 封装不直接支持原子读删,得手写:
if ($redis->eval("if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end", 1, $key, $token)) {
// 校验通过
} else {
throw new HttpException(400, 'Token 已使用或无效');
}
容易踩的坑:用 $redis->get($key) === $token && $redis->del($key),在高并发下大概率失效;还有人漏掉对 $token 是否为空的判断,导致空字符串也能过校验。
立即学习“PHP免费学习笔记(深入)”;
- 务必校验 Token 字符串长度(如 32 位 hex)和格式,防止传入恶意 payload
- Redis 返回 nil 或 0 时,统一返回 400 错误,不要暴露是“不存在”还是“不匹配”
- 如果业务允许幂等,可跳过删除,改用
INCR记录提交次数,限制单 Token 最多提交 1 次
Form 表单必须携带隐藏 Token 字段且服务端强校验
前端不能靠 JS 禁用按钮来防重复,那是纯装饰。真实防线在服务端——只要请求没带 token 字段,或字段值查不到对应 Redis Key,一律拦截。
ThinkPHP 的 validatePost() 或中间件里,要明确检查:$this->request->post('token') 存在、非空、长度合规、且能在 Redis 中匹配并原子删除。
- 别把 Token 放 query string 里(如
?token=xxx),会被浏览器历史、代理、CDN 缓存 - CSRF Token 和防重提 Token 是两回事,别混用同一个值;前者防跨域伪造,后者防同源快速连点
- Ajax 提交时,注意
contentType: 'application/json'会导致post()拿不到数据,要用$this->request->param('token')
Redis 故障时要有降级策略,不能直接崩
Redis 挂了,表单就全不可用?不行。得有 fallback:比如降级为 Session 内存校验(仅开发/低流量环境),或记录 IP+UA+URL 的轻量 Hash 到本地文件(sys_get_temp_dir() . '/token_fallback_' . md5($ip.$ua.$url)),哪怕只撑 30 秒也比 500 强。
更现实的做法是加开关配置:config('app.token_check_enabled'),运维可通过配置中心动态关闭 Token 校验,同时日志告警 Redis 不可用。
- 所有 Redis 操作必须包 try-catch,捕获
RedisException或连接超时,避免未处理异常穿透到用户 - 本地 fallback 文件需加锁(
flock)和过期清理,否则磁盘打满 - 千万别在降级逻辑里用
sleep(1)等待 Redis 恢复——这是雪崩前奏



















