令牌服务架构

整体架构

令牌服务不是一个大泥球。我习惯把它拆成三个职责明确的部分:接入层、逻辑层、存储层。说白了,就是谁跟外界打交道、谁做核心计算、谁把数据落地。

接入层处理所有令牌申请、刷新、吊销的请求。它只做两件事:校验合法性、分发给对应逻辑处理单元。逻辑层才是心脏——它包含令牌生成、映射管理、令牌校验这三个核心组件。存储层则负责把已生成的令牌塞进持久化或者缓存里。

你想想看,如果这三层混在一起,线上出问题你都分不清是网络抖了、还是逻辑崩了、还是数据库慢了。我自己踩过这个坑:有一次令牌生成接口超时,我以为是存储扛不住,查了半天才发现是接入层的一个线程池参数配错了。

令牌服务整体架构 客户端 接入层(网关) 令牌生成器 令牌存储库 映射管理器 下游服务 / 资源服务器 实线:生成流程;虚线:校验流程
设计提示:接入层建议做成无状态节点,方便水平扩展。我见过一个朋友的项目把接入层和逻辑层紧耦合,扩容接入层必须同时扩容逻辑层——资源浪费不说,故障域也跟着扩大了。

核心组件

令牌生成

令牌生成器负责吐出令牌。通常选 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 无 单机 开发/测试环境
注意事项:不要把所有令牌都塞进同一个 Redis 实例。我见过一个线上事故——生产环境令牌数据把 Redis 内存打满,导致正常业务缓存被逐出,连锁反应很可怕。建议令牌存储单独分配一个 Redis namespace 甚至独立实例。

映射管理

映射管理的核心任务是把令牌和用户/会话/权限的关系维护好。它不直接存储令牌,而是维护一张「令牌 → 上下文」的映射表。

数据通常包括:

  1. 令牌哈希(做查找 key)
  2. 用户标识
  3. 会话 ID
  4. 权限列表或角色 ID
  5. 签发时间、过期时间、最后使用时间

我习惯在映射表中多加一个 revoked_at 字段。为什么?有一次业务方要求「立即吊销令牌」,如果只删 redis key,事后根本没法审计是谁在哪一秒做的吊销。保留 soft-delete 就能追溯。

数据流分析

我把令牌服务的核心数据流画成三段:签发流、校验流、吊销流。

签发流

  1. 客户端携带凭证(用户名/密码、客户端凭证等)请求接入层。
  2. 接入层校验凭证,转发给令牌生成器。
  3. 生成器创建令牌,同时调用映射管理器记录映射关系。
  4. 映射管理器向令牌存储库写入数据(如果是 opaque 则存完整令牌和元数据;如果是 JWT 只存映射关系)。
  5. 组装响应,返回客户端。

校验流

  1. 客户端访问下游服务,传入令牌。
  2. 下游服务调用令牌服务的校验接口(或本地 SDK 自校验 JWT)。
  3. 令牌服务查询令牌存储库,拿到映射数据,验证有效性和权限。
  4. 返回校验结果和用户上下文。

吊销流

  1. 管理员或用户发起吊销请求。
  2. 映射管理器更新映射表 revoked_at。
  3. 令牌存储库中删除或打标记。
  4. 后续校验时发现已吊销,拒绝访问。

性能关键路径:签发和校验的延迟直接影响用户体验。我在压测时发现,如果签发流走同步写存储,QPS 上不去。后来改成异步批量写映射关系,只同步完成令牌生成并返回,映射持久化交给后台协程处理——延迟从 12ms 降到 3ms。

避坑指南

  • 令牌大小膨胀:JWT 里别塞太多自定义声明。我见过一个项目把整个用户 json 放进去,令牌变大了三倍,每次 HTTP 请求头都重得不行。
  • 缓存穿透:存储层要做好空值缓存和布隆过滤器。曾经有一次恶意客户端拿随机字符串模拟令牌请求,无命中直接打到存储层,把 DB 打挂了。
  • 时钟偏差:签发和校验分属不同机器,时间差可能导致令牌被提前判定过期或无法生效。建议校验时留一个 skew 窗口(比如 30 秒)。

好了,架构、组件、数据流都聊完了。说白了,令牌服务就是三层、三组件、三流程——设计时把每层的边界画清楚,关键路径做好缓存和异步,基本不会出大问题。