常见令牌化技术:从随机到可逆
说到令牌化,其实就三大流派。我这些年做项目,三种都碰过,各有各的坑。今天我把它们捋清楚。
随机令牌生成
这是最直白的方式。说白了,就是给原始数据随机分配一个占位符。
- 生成方式:用安全随机数生成器(CSPRNG)产生固定长度的字节串,然后做 Base64 或 HEX 编码。
- 映射关系:令牌和原始值之间没有数学联系,全靠一张映射表。
- 典型场景:支付卡号(PAN)替换,PCI DSS 合规。
我的经验:我在一个电商项目中用过随机令牌。当时觉得简单,后来发现查表性能是瓶颈。千万级记录,每次令牌化都要查一次数据库,延迟一下子就上去了。
// 伪代码:生成随机令牌
function generateToken(length) {
const bytes = crypto.randomBytes(length);
return bytes.toString('hex');
}
| 优点 | 缺点 |
|---|---|
| 安全性高,无法逆向 | 需要维护大映射表 |
| 实现简单 | 表膨胀后性能急剧下降 |
| 无固定输出长度限制 | 数据丢失后无法恢复 |
哈希令牌
用哈希函数对明文做单向处理,输出固定长度的摘要。
- 常见算法:SHA-256、SHA-3。搭配盐值(salt)防止彩虹表攻击。
- 为什么还需要加盐?因为纯哈希相同输入永远相同,容易被遍历。
- 注意:哈希是不可逆的。你没法从令牌反算出原文。
避坑指南:我曾经见过一个客户,直接拿 MD5 做令牌化,而且不带盐。三个月后黑客凭彩虹表把整张用户表反推出来了……嗯,从那以后我再也不敢用弱哈希做令牌化。
// 加盐哈希令牌示例
const salt = crypto.randomBytes(16).toString('hex');
const hash = crypto.createHash('sha256')
.update(data + salt)
.digest('hex');
哈希令牌的好处是不需要维护映射表。但坏处也明显:
- 如果原始数据有规律(比如身份证号前6位是地区码),哈希结果依然有碰撞风险。
- 令牌长度固定(比如256位),有些旧系统存不下。
适用场景:数据只需要做关联查询,不需要还原原文。比如日志脱敏、用户ID匿名化。
可逆令牌(基于FPE)
这才是今天的主角。FPE——格式保留加密,能让你加密后的数据保持原始格式。
- 核心思路:用对称加密算法(如AES),配合FF1/FF3等模式,在保持数据格式的前提下完成加密。
- 效果:明文"123456789"加密后依然是9位数字,比如"847391265"。
- 为什么需要它?很多遗留系统对数据格式有硬性要求。比如信用卡号必须是16位数字,邮编只能是5位数字。普通加密改用Base64就全乱了。
我在一个银行项目中,需要把客户的社保号(SSN)做令牌化。SSN的格式是9位数字,划成3-2-4的段。如果用随机令牌,后端对接的系统会直接报错,因为它校验位不对。后来我上了FPE,加密后依然是9位数字,校验位也自己重新计算,系统完全无感。
// 使用FF1模式加密示例(Python-like)
from ff1 import FF1
cipher = FF1(key, radix=10) # 数字域,radix=10
plaintext = "123456789"
ciphertext = cipher.encrypt("", plaintext) # 输出"847391265"
| 特性 | 随机令牌 | 哈希令牌 | FPE令牌 |
|---|---|---|---|
| 可逆性 | 需查表 | 否 | 是(有密钥) |
| 格式保留 | 否 | 否 | 是 |
| 性能 | 依赖数据库 | 快(本地计算) | 快(本地计算) |
| 安全性 | 极高 | 高(加盐) | 高(基于AES) |
| 典型场景 | 支付令牌 | 日志脱敏 | 遗留系统集成的令牌化 |
什么时候该选FPE?你想想看,如果下游系统对数据格式有强校验(比如正则表达式、校验位、长度限制),那FPE就是唯一解。
重要提醒:FPE虽然方便,但它不是万能药。加密后的数据依然在同一个值域内,如果你对数据做频率分析,依然可能泄露信息。比如某地区邮政编码只有几个,加密后如果只保留5位,攻击者可以通过频次推断。这种情况下,我建议你结合随机噪声或动态盐值来补强。
三种技术,没有的优劣。我自己的习惯是:
- 对接外部系统且要求格式不变 → 优先FPE
- 内部数据只做聚合分析 → 哈希令牌(加盐)
- 支付数据合规或需要细粒度控制 → 随机令牌
嗯,就这样。别以为令牌化很简单,选错了方案,后面改起来真要命。