JWT需黑名单机制解决无状态导致的无法主动吊销问题,核心是用jti或token.Raw作唯一键存入Redis或内存map,并设置与exp对齐的TTL,每次验证时检查是否在黑名单中。

JWT 本身是无状态的,签发后服务端不存它,所以“让某个 JWT 失效”这件事,必须靠额外的状态管理来补足。最常用、也最可控的方式就是黑名单机制——不是删 token,而是记下它“已注销”,每次验证时多查一次。
为什么不能直接用原始 token 字符串当黑名单 key
常见错误是把从 Authorization 头里取到的完整字符串(比如 "Bearer eyJhbGciOiJIUzI1Ni...")直接塞进 map 当 key。这会立刻踩三个坑:
- 没
strings.TrimSpace,token 前后带空格或换行,导致同一 token 被重复加入黑名单 - 没去掉
"Bearer "前缀,"Bearer xxx"和"xxx"被当成两个 key - 没用
parsedToken.Raw,而用了tokenString—— JWT 解析后可能因 base64url 填充差异产生不同字符串,但Raw是签名前原始 payload+header 的字节切片,唯一且稳定
正确做法:提取时统一处理:strings.TrimSpace(strings.TrimPrefix(authHeader, "Bearer ")),验证时用 token.Raw 作 key。
用 sync.RWMutex + map[string]struct{} 而不是 sync.Map
高并发下查黑名单,读远多于写,sync.Map 反而更慢、内存更高。实测 QPS 过千时,sync.Map 的读性能比加锁普通 map 低 15%~20%,且 GC 压力更大。
立即学习“go语言免费学习笔记(深入)”;
推荐结构:
var mu sync.RWMutex
var blacklist = make(map[string]struct{})
添加时:mu.Lock(); blacklist[tokenRaw] = struct{}{}; mu.Unlock()
查询时:mu.RLock(); _, blocked := blacklist[tokenRaw]; mu.RUnlock()
注意:别在查询路径里做 len(blacklist) 或遍历——这些操作会阻塞写,毛刺明显。
黑名单怎么和 JWT 过期时间对齐
JWT 自带 exp,但 map 里的 key 不会自动消失。不清理,跑一周可能积几万条无效记录,而真正要拦的只是最近几分钟登出的那几十个。
两种可行方案:
- Redis 方案:写入时设 TTL,值为
time.Until(expTime),由 Redis 自动驱逐 - 本地定时清理:启一个 goroutine,每 5 分钟调一次
jwt.ParseUnverified快速解出exp字段,删掉已过期项(注意锁粒度,别全量锁 map 一整分钟)
纯内存不清理,等于给攻击者留长期可用的失效窗口——尤其当用户改密或管理员踢人时,延迟清理会让拦截失效。
登出接口只加黑名单,不删 token 也不清客户端存储
登出逻辑里唯一该做的事,就是把当前 token 的 Raw 加进黑名单。其他动作都是错的:
- “删 token”没意义——服务端本来就不存它,所谓删只是加一条记录
- “清客户端 localStorage”是前端责任,服务端无法强制,且清了也不代表旧 token 不再被重放
- 返回登出成功后,客户端仍可能带着旧 token 发请求——你得在验证环节拦住它,这才是黑名单存在的全部理由
最容易被忽略的一点:管理员强制踢人场景下,token 可能还在网络传输途中。加黑名单是唯一能确保“下一毫秒就生效”的干预手段。

















