秒杀系统高并发优化实战(C++ / Drogon):3.7 退出登录接口:校验 Token + 删 Redis 会话即吊销
3.2 把"登录签发 + 写 Redis 会话"讲完,3.4 把"退出即吊销"预览了一句。这一篇把退出登录做成完整接口:POST /api/user/logout,从 Authorization: Bearer 取 Token、验签、删 sess:{jti}——删掉那一刻,旧 Token 立即失效。
配套代码:
src/controllers/UserController.cc(logout+extractBearerToken)、src/service/UserService.cc(logout/authenticate)、src/service/SessionStore.cc(revoke)。
一、是什么、坑在哪、本质一句话
是什么:退出登录 = 客户端带上 Token,服务端验签通过后删除 Redis 里的 sess:{jti}。JWT 本身无法被"改",但此后每次鉴权都会因 EXISTS sess:{jti} 失败而被拒。
坑在哪:
- Token 不带 Bearer 前缀:直接拿
Authorization当 token 用,验签必失败。 - 吊销后仍返回"成功":
DEL一个已不存在的 key 返回 0,若不分清会让客户端误以为"退掉了"。 - 只删不验签:不验签就删,攻击者拿任意字符串就能把别人的会话删掉(虽然后端要先知道 jti,但严谨起见验签前置)。
本质一句话:退出的本质是"让凭证失效"——验签前置 + 删 Redis 权威态,一步即吊销,且要区分"退掉了"和"本来就没登录"。
二、控制器:Bearer 提取 + 401 处理
UserController::logout 先从 Authorization 头抽 Bearer 前缀后的 token;抽不到(没带 / 格式错)直接 401。
1 | // src/controllers/UserController.cc(节选) |
三、服务层:verify → revoke(DEL 即吊销)
UserService::logout 先 jwt_->verify 验签(失败 → TOKEN_INVALID,401);通过则 sessions_->revoke(jti)。revoke 的 DEL 返回被删 key 数:0 表示会话本来就不存(已过期/已退出),据此区分"退掉了"和"本来就没登录"。
1 | // src/service/UserService.cc(logout 节选) |
1 | // src/service/SessionStore.cc(revoke) |
四、authenticate 中间件(已就绪,等前端接)
UserService::authenticate 是给"网关/中间件"用的:验签 + 查 sess:{jti} 是否存在,返回 userId(失败为 0)。它和 Jwt / LoginGuard 一起已经写好,就等"开始有前端"阶段把 /api/seckill 从 body 双通道切到 Authorization: Bearer 取 userId。
1 | // src/service/UserService.cc |
当前阶段一的取舍:
/api/seckill现在userId走 JSON body、不校验 token(登录模块 3.1~3.7 已就绪但未接入主链路)。这是为压测便利的有意取舍——保留 body 双通道,登录鉴权接入留待"开始有前端"阶段统一做。详见 README「项目边界:前端页面」。
五、可运行验证(端到端含登出)
1 | # 一键自检:发码→注册→登录→撞库锁定→登出→旧 token 失效 |
功能抉择(本篇核心权衡)
① 为什么退出的 revoke 要"验签前置 + DEL",而不是只 DEL?
只 DEL 不验签,意味着任意字符串都能触发删会话(虽然后端需先构造合法 jti,但严谨上不应给这扇窗)。验签前置保证"只有持有合法 Token 的人才能吊销自己的会话",且 TOKEN_INVALID 与 ALREADY_LOGGED_OUT 语义分明。
② 为什么 DEL 返回 0 时不算错误,仍回 OK?DEL 一个不存在的 key 返回 0,这是"会话早已过期/早已退出"的正常状态,不是故障。区分 OK(真删了)与 ALREADY_LOGGED_OUT(本就没登录)只是给客户端更准的反馈,不影响"退出成功"的语义——用户的目的(让凭证失效)已经达成。
③ 为什么 /api/seckill 现在不接 token,留到前端阶段?
阶段一验收硬指标是"压测 QPS 提升一个量级 + 不超卖不重复下单",鉴权对这条指标零贡献;而且 body 双通道让压测脚本不必先走登录拿 token,跑基线更顺。等前端阶段统一把 userId 从 Bearer 取,避免现在改一次、那时又改一次。
小结
- 退出登录 =
extractBearer→ 验签 →revoke(DEL sess:{jti}),删掉即吊销,旧 Token 立即失效。 - Bearer 抽不到 → 401
missing bearer token;验签失败 → 401TOKEN_INVALID。 revoke用DEL返回值区分"退掉了"与"本来就没登录",但都算退出成功。authenticate(验签 + EXISTS)已就绪,等前端阶段把/api/seckill切到Bearer取 userId。- 当前
/api/seckill保留 body 双通道、不校验 token,是阶段一压测便利的有意取舍。🐾
配套源码
本文对应代码已开源:Hespethorn/seckill-cpp
C++ / Drogon 高并发秒杀系统实战,含完整的压测基线、技术决策记录(ADR)与可复现脚本:
- 阶段一(直打数据库):QPS≈440,0 超卖
- 阶段二(Redis 缓存):读接口 list 10557 QPS / detail 8024 QPS
如果对你有帮助,欢迎 star ⭐

