3、令牌化服务核心概念:令牌、令牌域、令牌格式、存储策略

好,咱们开讲第四章?不,第三章。这一章要啃的是令牌化服务最基础但也最容易出岔子的地方——四个核心概念。说白了,你搞懂令牌、令牌域、令牌格式、存储策略,后面所有接口设计才有根。我当年刚上手时,以为令牌就是个字符串,域就是个名字空间,结果线上出了事故才明白,每一个细节都决定安全底线。

一句话记住本章核心:令牌是通行证,域是围墙,格式是证件样式,存储策略是你把证件放哪、怎么锁。

令牌()—— 身份与权限的载体

令牌是什么?你想想看,用户登录后你给他一串密文,他每次请求带着它,你就可以识别他是谁、能干什么。这就是令牌。它本质上是一个临时的、可撤销的凭证,替代了用户密码这种长期秘密。

我在项目中遇到过这样的场景:有个团队把用户ID直接加密后当令牌用,结果泄露后攻击者可以伪造任意用户。为什么?因为令牌里缺少了颁发时间、有效期、域标识这些关键属性。一个正规的令牌至少包含:

  • 主体(Subject):谁持有这个令牌,通常是用户ID或客户端ID。
  • 颁发者(Issuer):谁发的,比如你的认证服务器URL。
  • 有效期(Expiration):精确到秒的过期时间,不能用永久令牌。
  • 令牌域(Domain):说明这个令牌能在哪些服务范围使用(后面的重点)。
  • 签名或加密信息:防止被篡改。

举个例子,一个标准JWT令牌的载荷层大概长这样:

{
  "sub": "user_abc123",
  "iss": "https://auth.mycompany.com",
  "exp": 1715000000,
  "domain": "payment-service",
  "roles": ["admin"]
}

但注意,JWT只是格式之一,关于格式我们稍后细说。

我的习惯:令牌里不要放明文敏感信息,比如手机号、完整地址。即使加密,也要做到最小暴露。我曾经把用户邮箱放进去,后来发现日志输出会打印令牌,差点出事。

令牌域( Domain)—— 隔离与边界

域这个概念,说白了就是从空间上把令牌的使用范围圈起来。比如你的系统里有支付服务、订单服务、用户服务,你不可能让支付令牌能调用用户列表接口吧?域就是做这个隔离的。

每个令牌在颁发时就绑定了一个或一组域名(或服务标识)。网关或资源服务器收到请求后,先检查令牌的域是否匹配当前接口的域,不匹配直接拒绝。我见过不少项目把域写死在代码里,或者干脆不做检查——结果一个买商品的接口被调用来查用户隐私。

怎么定义域?通常有三种方式:

  • 服务名:如 payment、order。最简单,适合微服务架构。
  • 路径前缀:如 /api/v1/payments/*。更灵活,但访问控制粒度要小心。
  • 权限全集:域 + 角色 + 资源组合。比如 domain=payment:role=admin:resource=refund。

我之前帮一个客户重构令牌系统,他们用了一个超级域叫“ALL”,结果任意令牌都能访问所有服务。修复时花了三周改网关逻辑。避坑指南:域的定义最好在令牌颁发时就固定,不要允许运行时动态修改,否则容易被绕过。

域类型 适用场景 风险点
静态域(名称固定) 服务间调用 多域冒名需额外校验
表达式域(含通配) 统一网关、多版本API 正则注入、误匹配
范围域(时间/IP限制) 敏感操作(如转账) 实现复杂,易漏判

重要:令牌域的检查必须在服务端做,不能在客户端校验!客户端发过来的域信息永远不要信任。

令牌格式( Format)—— 标准与结构

格式决定了令牌怎么存、怎么传、怎么验证。目前主流格式就那么几种,我挑最常用的讲。

  • JWT(JSON Web ):自包含格式,负载里直接存声明,服务端不需要查询数据库即可验证。但缺点是一旦签发,无法主动撤销(除非配合黑名单)。
  • Opaque (不透明令牌):一串无意义字符串,服务端必须通过存储(如Redis)查询才能拿到权限信息。可撤性极强,但每次验证都要查一次db。
  • Reference (引用令牌):类似Opaque但通常更轻量,常与JWT结合:主体用JWT但引用一个短ID,快速查缓存。
  • Bearer (无状态):严格来说是一种传输方式(放在Authorization头),格式本身可以是JWT或Opaque,但标准里要求服务端不能解析内容。

举个表格对比一下:

格式 是否自包含 可撤销性 验证速度 代表性应用
JWT 是 难(需黑名单) 快(本地解码) OAuth2的access_
Opaque 否 易(删除存储即可) 慢(需查询) Session ID
Reference 部分自包含 中等(需查引用表) 中等 分布式session

我个人偏爱对公网暴露的服务用Opaque令牌,对内部服务间通信用JWT(省去每次验证查询)。但前提是内部网络可信——嗯,这个也常常做不到,所以我更倾向统一用Opaque + 缓存加速。

技巧:无论什么格式,令牌的传输必须走HTTPS,令牌不要出现在URL参数里。我见过有人在微信截图里暴露了带令牌的URL,惨烈。

存储策略(Storage Strategy)—— 安全与性能

令牌放哪,怎么放,直接影响安全性。这里讨论的是服务端如何存储令牌与客户端的关联状态。

主流方案有三种:

  1. 无状态存储(JWT自带):令牌里包含所有信息,服务端不存任何东西。优点是水平扩展极简单,缺点是无法主动撤销。
  2. 集中式存储(Redis/DB):令牌作为key,用户会话信息作为value。撤销只需删key,但每次验证多一次网络IO。
  3. 混合存储:JWT为主体,但在Redis里存一个撤销校验的快照(标识+黑名单)。我目前团队就用的这种,兼顾速度与可撤性。

我曾经踩过一个坑:JWT里放了refresh_的ID,但服务端没有做过期同步,导致用户改了密码后老refresh_仍然有效,被攻击者利用。后来我们强制每次签发新令牌时,把旧令牌对应的刷新令牌加入一个时效性黑名单。

存储策略的选择也跟令牌生命周期相关:

策略 适用令牌类型 撤销延迟 扩展性
无状态(JWT自带) 短期访问令牌(几小时) 无法撤销 极好
集中式(Redis) 长期刷新令牌或Opaque 即时 需集群,中等
混合(JWT+黑名单) 访问+刷新双令牌 秒级 好

最后说一件容易被忽视的事:令牌存储必须考虑加密。即使存到Redis,也建议对令牌值做一次哈希(比如SHA-256),这样即使Redis被拖库,攻击者也无法直接拿令牌冒充用户。我的团队就从明文存储改为了哈希存储,安全审计终于通过了。

令牌化核心概念关系图 令牌 () 令牌域 存储策略 令牌格式 生命周期 核心关系说明 • 令牌格式决定了令牌的编码方式与自包含程度(JWT/Opaque) • 令牌域定义了令牌的权限边界,防止跨服务滥用 • 存储策略影响可撤销性、验证性能与安全性 • 生命周期(颁发、刷新、撤销)与域、存储策略联动 • 四者相互制约:例如JWT+无状态存储的可撤销性差,需配合短有效期

我的个人工作流:设计一个新令牌方案时,我习惯先确定三个问题——令牌要能被撤销吗?验证速度要求多高?服务间信任边界在哪?回答完这三个问题,格式和存储策略基本就定了。域则单独根据服务目录划分。