令牌服务架构
整体架构
令牌服务不是一个大泥球。我习惯把它拆成三个职责明确的部分:接入层、逻辑层、存储层。说白了,就是谁跟外界打交道、谁做核心计算、谁把数据落地。
接入层处理所有令牌申请、刷新、吊销的请求。它只做两件事:校验合法性、分发给对应逻辑处理单元。逻辑层才是心脏——它包含令牌生成、映射管理、令牌校验这三个核心组件。存储层则负责把已生成的令牌塞进持久化或者缓存里。
你想想看,如果这三层混在一起,线上出问题你都分不清是网络抖了、还是逻辑崩了、还是数据库慢了。我自己踩过这个坑:有一次令牌生成接口超时,我以为是存储扛不住,查了半天才发现是接入层的一个线程池参数配错了。
核心组件
令牌生成
令牌生成器负责吐出令牌。通常选 JWT(自包含)还是 opaque(不透明令牌)?
- JWT:信息自包含,验证时无需查存储,性能更好。
- Opaque:短随机字符串,数据全放在存储里,吊销容易,但每次校验都要查一次存储。
我在项目中遇到过一个需求——要求令牌必须能在客户端本地解析出用户角色。那就只能上 JWT。但 JWT 一旦签发无法在线吊销,除非配合黑名单。后来我们做了个折中:用 JWT 做短周期 access ,用 opaque 做 refresh 。
生成关键参数:签名算法(推荐 RS256 或 ES256)、过期时间、令牌长度、是否包含自定义声明。每次调用令牌生成器时,内部会先检查当前服务器时钟是否准确——我曾经因为 NTP 不同步踩了一次「令牌提前过期」的坑。
令牌存储
存储层的选型直接影响整个系统的吞吐和延迟。
- Redis:最快的选择,支持 TTL 自动过期,适合 opaque 令牌。
- 数据库:持久化,方便审计和统计,但延迟高,需要加缓存。
- 本地内存:超低延迟,但重启丢数据,不适合多实例。
| 存储方案 | 延迟(P99) | 持久化 | 分布式支持 | 推荐场景 |
|---|---|---|---|---|
| Redis | <1ms | 可选(RDB/AOF) | 原生集群 | 高频令牌校验 |
| MySQL/PostgreSQL | 5~20ms | 强 | 需中间件 | 审计、黑名单持久化 |
| 本地内存 | <0.1ms | 无 | 单机 | 开发/测试环境 |
映射管理
映射管理的核心任务是把令牌和用户/会话/权限的关系维护好。它不直接存储令牌,而是维护一张「令牌 → 上下文」的映射表。
数据通常包括:
- 令牌哈希(做查找 key)
- 用户标识
- 会话 ID
- 权限列表或角色 ID
- 签发时间、过期时间、最后使用时间
我习惯在映射表中多加一个 revoked_at 字段。为什么?有一次业务方要求「立即吊销令牌」,如果只删 redis key,事后根本没法审计是谁在哪一秒做的吊销。保留 soft-delete 就能追溯。
数据流分析
我把令牌服务的核心数据流画成三段:签发流、校验流、吊销流。
签发流
- 客户端携带凭证(用户名/密码、客户端凭证等)请求接入层。
- 接入层校验凭证,转发给令牌生成器。
- 生成器创建令牌,同时调用映射管理器记录映射关系。
- 映射管理器向令牌存储库写入数据(如果是 opaque 则存完整令牌和元数据;如果是 JWT 只存映射关系)。
- 组装响应,返回客户端。
校验流
- 客户端访问下游服务,传入令牌。
- 下游服务调用令牌服务的校验接口(或本地 SDK 自校验 JWT)。
- 令牌服务查询令牌存储库,拿到映射数据,验证有效性和权限。
- 返回校验结果和用户上下文。
吊销流
- 管理员或用户发起吊销请求。
- 映射管理器更新映射表
revoked_at。 - 令牌存储库中删除或打标记。
- 后续校验时发现已吊销,拒绝访问。
性能关键路径:签发和校验的延迟直接影响用户体验。我在压测时发现,如果签发流走同步写存储,QPS 上不去。后来改成异步批量写映射关系,只同步完成令牌生成并返回,映射持久化交给后台协程处理——延迟从 12ms 降到 3ms。
避坑指南
- 令牌大小膨胀:JWT 里别塞太多自定义声明。我见过一个项目把整个用户 json 放进去,令牌变大了三倍,每次 HTTP 请求头都重得不行。
- 缓存穿透:存储层要做好空值缓存和布隆过滤器。曾经有一次恶意客户端拿随机字符串模拟令牌请求,无命中直接打到存储层,把 DB 打挂了。
- 时钟偏差:签发和校验分属不同机器,时间差可能导致令牌被提前判定过期或无法生效。建议校验时留一个
skew窗口(比如 30 秒)。
好了,架构、组件、数据流都聊完了。说白了,令牌服务就是三层、三组件、三流程——设计时把每层的边界画清楚,关键路径做好缓存和异步,基本不会出大问题。