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)—— 安全与性能
令牌放哪,怎么放,直接影响安全性。这里讨论的是服务端如何存储令牌与客户端的关联状态。
主流方案有三种:
- 无状态存储(JWT自带):令牌里包含所有信息,服务端不存任何东西。优点是水平扩展极简单,缺点是无法主动撤销。
- 集中式存储(Redis/DB):令牌作为key,用户会话信息作为value。撤销只需删key,但每次验证多一次网络IO。
- 混合存储:JWT为主体,但在Redis里存一个撤销校验的快照(标识+黑名单)。我目前团队就用的这种,兼顾速度与可撤性。
我曾经踩过一个坑:JWT里放了refresh_的ID,但服务端没有做过期同步,导致用户改了密码后老refresh_仍然有效,被攻击者利用。后来我们强制每次签发新令牌时,把旧令牌对应的刷新令牌加入一个时效性黑名单。
存储策略的选择也跟令牌生命周期相关:
| 策略 | 适用令牌类型 | 撤销延迟 | 扩展性 |
|---|---|---|---|
| 无状态(JWT自带) | 短期访问令牌(几小时) | 无法撤销 | 极好 |
| 集中式(Redis) | 长期刷新令牌或Opaque | 即时 | 需集群,中等 |
| 混合(JWT+黑名单) | 访问+刷新双令牌 | 秒级 | 好 |
最后说一件容易被忽视的事:令牌存储必须考虑加密。即使存到Redis,也建议对令牌值做一次哈希(比如SHA-256),这样即使Redis被拖库,攻击者也无法直接拿令牌冒充用户。我的团队就从明文存储改为了哈希存储,安全审计终于通过了。
我的个人工作流:设计一个新令牌方案时,我习惯先确定三个问题——令牌要能被撤销吗?验证速度要求多高?服务间信任边界在哪?回答完这三个问题,格式和存储策略基本就定了。域则单独根据服务目录划分。