第三章 令牌存储与映射
令牌发出去之后,怎么跟原始数据对上号?说白了就是存个映射关系。这个环节要是搞砸了,令牌就形同虚设。我之前在金融项目里踩过坑——映射表设计得太随意,结果泄露了一批用户身份证号。嗯,今天咱们就把这块彻底讲透。
3.1 映射关系:一对一还是多对一?
令牌和原始数据之间,最常见的是一对一映射。一个令牌对应一条原始数据,比如用户的信用卡号或者手机号。但有些场景下,一个用户可能有多个令牌指向同一份敏感数据(例如多设备登录),这时候就是多对一的。
我的习惯:除非业务明确要求多令牌共享数据,否则一律用一对一。多对一容易导致吊销混乱——你吊销了一个令牌,其他令牌还活着,但原始数据可能已经失效了。
映射表里的核心字段就这么几个:
- 令牌ID - 全局唯一,通常用UUID或自增ID
- 令牌哈希 - 存储令牌的哈希值,而不是明文令牌(后面会讲原因)
- 原始数据 - 加密后存储,比如AES加密后的身份证号
- 过期时间 - 令牌的TTL,过了时间自动失效
- 用户ID - 可选的,方便按用户注销所有令牌
- 创建时间 - 审计用
3.2 映射表设计:这么设计最稳
以MySQL为例,实打实的建表语句:
CREATE TABLE _mapping (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
_hash VARCHAR(128) NOT NULL UNIQUE,
encrypted_data BLOB NOT NULL,
expire_at DATETIME NOT NULL,
user_id VARCHAR(64),
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX idx_expire_at (expire_at),
INDEX idx_user_id (user_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
注意 _hash 设了UNIQUE,防止同一个哈希重复插入。而且 expire_at 建了索引,清理过期令牌时效率高。
要是用Redis呢?更轻量:
// 存储映射(过期时间自动处理)
SET :<令牌哈希> <加密后的原始数据> EX 3600
// 读取映射
GET :<令牌哈希>
数据库和KV存储各有优缺,直接对比:
| 维度 | 关系数据库(MySQL/PostgreSQL) | KV存储(Redis/Memcached) |
|---|---|---|
| 持久性 | 强,数据不丢 | 取决于配置,可持久化但一般做缓存 |
| 查询能力 | 支持条件查询(如按用户ID查所有令牌) | 只能按key精确查询 |
| 性能 | 毫秒级,但高并发需连接池 | 微秒级,极低延迟 |
| 自动过期 | 需要后台脚本清理 | 原生支持TTL |
| 适用场景 | 需要审计、多条件查询的令牌 | 高吞吐、临时令牌、会话令牌 |
我曾经犯过的错:在一家电商公司,我把所有令牌映射都放进了Redis,没有做持久化。结果一次节点重启,上百万个令牌全部丢失,用户全部需要重新登录。后来我在Redis前面加了一层Redis Cluster + AOF持久化,同时在MySQL里保留了一份备份(异步同步)。
3.3 映射安全:不能让人顺着令牌查到原始数据
映射表如果被拖库,后果就是所有原始数据泄露。你必须做好这几点:
- 存储令牌的哈希,而非明文。客户端传令牌过来,你哈希后再查表。
- 原始数据必须加密,推荐AES-256-GCM。密钥跟令牌服务分离,最好放在KMS里。
- 映射表本身加访问控制:只允许令牌服务进程访问,禁止任何人直接连数据库。
- 日志脱敏:不要在日志里打印令牌、哈希或原始数据。
令牌哈希用什么算法?我个人习惯用SHA-256+盐。你想想看,如果两个令牌哈希相同怎么办?概率极低,但你还是要在数据库层面加UNIQUE约束,捕住它。
顺便提一下双令牌机制(访问令牌+刷新令牌),很多架构喜欢用。但这并不降低映射表的安全要求——刷新令牌里依然藏着用户身份,映射表该保护的一分不能少。
3.4 整体结构:一张SVG讲清楚
下面这张图展示了令牌存储与映射的核心流程,注意安全边界在哪里:
记住,映射表是整个令牌化服务中最核心的资产。你保护好了它,令牌化才真正有意义。