07 - 退出登录:看似简单,其实是最难的一环
“退出登录”听起来是登录里最简单的动作——不就是把登录状态清掉吗?
但如果你用的是 JWT,它其实是整个体系里最反直觉、最容易出错的一环。这一章专门把它讲透。
先看简单的:Session 方案退出,为什么毫不费力
第 03 章说过,Session 方案里,登录状态记在**服务端的账本(Redis)**里。所以退出就一句话:
退出登录 = 把账本上你那一页撕掉
redis.delete(f"session:{session_id}")
删掉之后:
用户下次带着那个 session_id 再来
│
服务端拿 session_id 去查账本 → 查不到了
│
"登录已过期,请重新登录" ✅
干净利落。因为状态在服务端,服务端对它有绝对的控制权,想让它失效就让它失效。 这就是第 05 章说的”Session 让服务端管得住”。
再看难的:JWT 方案退出,难在哪
问题的根子,还是第 04 章那个伏笔:JWT 是无状态的,服务端不记账本。
打个最形象的比方:
Session 的凭证像"你在我这存的一笔钱"——我账上把它划掉,你就没了。
JWT 的凭证像"我签给你的一张 2 小时内有效的支票"——
支票已经在你口袋里了,只要没过期、签名是真的,
任何人拿着它来兑现,我都得认。
我总不能追到你口袋里去把支票抢回来吧?
所以当用户点”退出登录”时,服务端面对的尴尬是:
用户手机里那个 token:
✅ 签名是真的(服务端自己签的)
✅ 还没到过期时间
│
▼
按 JWT 的规则,它就是有效的!
服务端凭什么拒绝它?——默认情况下,拒绝不了。
这就是核心难题:退出登录,本质是要”提前作废一张还没到期的 token”,但 JWT 的设计里压根没给你这个开关。
三种应对思路(各有代价)
既然 JWT 天生没这个能力,就得额外想办法。业界主要有三招:
思路一:黑名单(最常用)
退出时,把这个 token 记到一个”作废名单”里(放 Redis)。之后每次验证时,除了验签名、验过期,再多查一步黑名单。
退出:redis 里记下 "这个 token 作废了",过期时间 = token 剩余寿命
验证:验签 ✅ → 验过期 ✅ → 查黑名单:在里面吗?
├─ 在 → 拒绝(已退出)
└─ 不在 → 放行
- 代价:每次请求都要查一次 Redis。这等于把”无状态”给破坏了——你又回到了”服务端要存东西、要查东西”的老路。JWT 引以为傲的”不查库”优势没了。
- 这就是第 06 章演示的方案。它是最直接、最好理解的解法。
思路二:短命 Access Token + 刷新令牌(Refresh Token)
换个思路:既然没法作废,那就让 token 短命到”作废没意义”。
给用户发两张证:
Access Token(干活用):寿命很短,比如 15 分钟
Refresh Token(续命用):寿命长,比如 7 天,专门用来换新的 Access Token
平时:拿 Access Token 访问接口
过期:Access 过期了,用 Refresh Token 悄悄换一张新的(用户无感)
退出:把服务端记录的 Refresh Token 作废,不让你再续命
→ 手里那张 Access Token 最多再撑 15 分钟就自然失效
- 代价:架构变复杂(要管两种 token、要有刷新接口)。而且退出后,那张短命的 Access Token 在剩下的十几分钟里理论上仍然有效——只是因为它太短命,风险被压到很小,一般能接受。
- 这是目前最主流的工程做法,第 09 章会展开讲双令牌。
思路三:改签名密钥(“核武器”,一般不用)
服务端换掉那个签发用的密钥。密钥一换,所有旧 token 的签名瞬间全部对不上,全体失效。
- 代价:这是”一键全员下线”,会把所有用户都踢下线,不是针对某一个人。所以它只用于极端情况(比如密钥泄露了),日常退出登录绝不会用它。
三种思路对比
| 思路 | 能不能精确作废单个用户 | 代价 | 适用 |
|---|---|---|---|
| 黑名单 | ✅ 能 | 每次请求多查一次 Redis,牺牲无状态 | 需要”立刻、精确”作废 |
| 短命 + 刷新令牌 | 半能(最多延迟十几分钟) | 架构复杂,退出后短暂残留 | 主流选择,安全性和体验平衡 |
| 换密钥 | ❌ 只能全员一起 | 全体用户被踢下线 | 密钥泄露等紧急事故 |
实际项目里,思路二和思路一常常一起用:平时靠短命 token 兜底,遇到”退出登录""改密码""管理员强制下线”这种需要立刻生效的场景,就把对应的 Refresh Token(或该用户的 token)拉进黑名单。
一个必须想清楚的连带问题:改密码 / 账号被盗
退出登录的难题,其实是一类问题的代表。下面这些场景,本质都是”要提前作废已发出的 token”,全都撞上同一堵墙:
用户改了密码 → 旧 token 该不该失效?(应该,否则改密码没意义)
发现账号被盗 → 要立刻把盗号者踢下线
管理员封禁某用户 → 要立刻让他的所有 token 失效
用户在新设备登录 → 要不要挤掉旧设备?
如果你用纯 JWT,这些需求你都做不到——因为你没有作废 token 的能力。所以只要业务里有上面任何一条需求,你就必然要引入服务端存储(黑名单/版本号),也就必然要放弃一部分”无状态”。
一个常用技巧:在用户记录里存一个
token_version版本号,签发 token 时把版本号写进去。改密码/踢下线时,把用户的版本号 +1。验证时比对版本号,对不上就拒绝。这样一次就能作废该用户的所有 token,而不用逐个拉黑。
小结
- 退出登录的难度完全取决于你选了哪条路线:Session 极简单,JWT 很棘手。
- 根因:JWT 无状态,服务端没有”作废一张已发出 token”的开关(像追不回已经签出去的支票)。
- 三种应对:黑名单(精确但牺牲无状态)、短命 token + 刷新令牌(主流,有短暂残留)、换密钥(核武器,全员下线)。
- “改密码、踢下线、封号、账号被盗”和退出登录是同一类问题——只要有这些需求,纯 JWT 就不够用,必须引入服务端状态。
- 记住这句话:你想让 JWT 能被”退出/作废”,就得给它加上服务端存储,也就等于放弃了它一部分无状态的优势。天下没有免费的午餐。
到这里,登录的核心机制就全通了。接下来两章讲”周边”:先是绕不开的安全问题(密码怎么存、凭证怎么被偷),再是进阶场景(双令牌、多端登录、SSO)。
上一章 ← 06 - 完整例子:一次登录到退出的旅程 | 下一章 → 08 - 绕不开的安全问题 | 回到 README 目录