三、令牌化架构设计:三个核心组件
令牌化系统听着玄乎,拆开了其实就是三个事:怎么存映射关系、怎么生成令牌、怎么把令牌和原始数据安全地收起来。
我当初设计第一个生产级令牌系统时,就吃了架构不清的亏。后来才明白,这三块必须独立设计,耦合越少越好。
3.1 令牌映射表:系统的灵魂
说白了,映射表就是一张保留令牌和原始数据对应关系的表。每次业务系统把明文扔过来,我就生成一个令牌存进去,再返回令牌给业务方。
我个人习惯用这张表的结构,你可以参考一下:
| 字段 | 类型 | 说明 |
|---|---|---|
| _id | VARCHAR(64) PK | 令牌唯一标识 |
| plaintext_hash | VARCHAR(64) INDEX | 明文的SHA-256哈希值,用于去重检索 |
| plaintext_encrypted | VARBINARY(MAX) | 加密后的原始数据 |
| created_at | DATETIME | 创建时间 |
| expires_at | DATETIME NULL | 过期时间,空值表示永不过期 |
重点来了:plaintext_hash 字段我建议一定加上。去重场景下,先查 hash 再决定是否生成新令牌,能省下大量存储和算力。
3.2 令牌生成算法:随机性决定安全性
令牌怎么生成?很多人直接上UUID。我干过这事,坑不小。
UUID v4 虽然是随机,但长度固定32字符,而且不含校验位。一旦数据中心切换时时钟回拨,就有概率撞令牌。嗯,这里要注意,撞令牌在金融系统里可是灾难。
我更推荐的做法,是用定长随机字节 + 自定义编码:
// Java 示例:生成16字节令牌,输出为Base64URL格式
import java.security.SecureRandom;
import java.util.Base64;
public class TokenGenerator {
private static final SecureRandom SR = new SecureRandom();
public static String generateToken(int byteLength) {
byte[] tokenBytes = new byte[byteLength];
SR.nextBytes(tokenBytes);
// Base64URL 不含 '+' '/' '=',适合URL传递
return Base64.getUrlEncoder().withoutPadding().encodeToString(tokenBytes);
}
}
为什么选16字节?128位随机性,配合SecureRandom,碰撞概率理论上低于宇宙射线。你想想看,一个16位的随机数,能提供2^16种可能性,够吗?不够。128位,足够从容。
我的经验:令牌最好不要包含任何业务信息。我曾经见过有人在令牌里编码了用户ID + 时间戳,结果攻击者解析出规律后,批量遍历出其他用户的敏感数据。令牌就该像个黑盒,攻击者拿到它,什么都推导不出来。
3.3 令牌存储策略:数据库 vs HSM
存储策略决定了系统的安全水位。我见过三个主流方案,咱们对比看:
| 存储方案 | 安全性 | 性能 | 成本 | 适用场景 |
|---|---|---|---|---|
| 纯关系数据库 | 中 | 高 | 低 | 开发测试、低合规要求 |
| 数据库 + 列加密 | 中高 | 中 | 中 | 通用生产环境 |
| 硬件安全模块(HSM) | 极高 | 低(受限于HSM吞吐) | 极高 | 金融、医疗、PCI DSS |
避坑指南:我曾经在项目中选型HSM时,忽略了它的QPS上限。上线后业务高峰期,令牌生成接口排队超时,差点引发事故。千万记得做压测,确定HSM的吞吐量是否匹配你的峰值。
我建议的通用策略:数据库存储 + 列级加密。明文在入库前用AES-256-GCM加密,密钥由独立的密钥管理系统管理。这样即使数据库被脱库,攻击者拿到的是密文,没有密钥也白搭。
3.4 三个组件如何协同
还是那句话,别让它们混在一起。我给你梳理个调用流程:
- 业务系统请求令牌化——传入明文
- 生成算法模块:调用 SecureRandom 生成令牌字符串
- 映射表模块:将令牌 + 加密后的明文案写入数据库
- 返回令牌给业务系统——原始数据永不出库
解耦的好处很明显。哪天你想换生成算法,只改生成模块就行。哪天你想从数据库迁移到HSM,只改存储模块。每次只动一个点,风险可控。
总结一句话:架构设计别贪多,映射表、生成算法、存储策略,三个轮子各司其职,系统才能跑得稳、跑得久。