令牌生成原理:随机令牌、哈希令牌与结构令牌
令牌是怎么变出来的?这问题我入行时也琢磨过很久。
说白了,令牌就是把敏感数据换成一个“替身”。但这个替身不能乱造,得有一套规则。根据生成方式的不同,业界把它分成三大类:随机令牌、哈希令牌,还有结构令牌。
每种都有各自的脾气。用错了地方,轻则性能崩盘,重则数据全爆。我吃过这个亏,所以今天给你掰开揉碎讲清楚。
随机令牌——最简单,也最容易被滥用
随机令牌,就是纯靠随机数砸出来的字符串。没有任何逻辑依赖,生成器给什么就是什么。
场景单一、不需要反向追溯、对长度没限制的时候。比如电商平台的优惠券码、临时会话ID。
我曾经接手过一个项目,团队用随机令牌做信用卡号掩码。结果呢?每次交易都要去数据库查映射表,QPS一上去直接雪崩。为什么?随机令牌没有算法可逆性,完全依赖存储——你想想看,查一次数据库几十毫秒,并发一高不崩才怪。
- 无状态依赖,生成成本极低
- 不可逆,必须依赖后端存储
- 长度不可控,可能很长(UUID v4有36个字符)
来个Python代码,自己跑跑看:
import secrets
import string
def generate_random_(length=16):
"""生成随机字母数字令牌"""
alphabet = string.ascii_letters + string.digits
return ''.join(secrets.choice(alphabet) for _ in range(length))
# 试一下
print(generate_random_()) # 输出类似 'A3fG7kL9mN2pQ4rT'
注意secrets模块,别用random。我见过有人用random.choices做令牌,结果被暴力枚举出规律——嗯,当时被安全审计批得挺惨。
哈希令牌——用单向函数做“指纹”
哈希令牌的本质是对原始数据做不可逆的摘要。同一份原始数据,永远得到相同的令牌值。
这玩意儿的核心优势:不需要存储映射表。系统收到令牌,直接对原始数据(如果有)重算一次哈希,对比就知道是不是同一个。
我负责的一个老系统就用了MD5,结果被彩虹表秒破。后来全部迁移到SHA-256加盐(salt)。
哈希令牌主要用在数据去重场景,比如数据湖里的敏感字段标记。但你想想看,原始数据本身要是有规律(比如身份证号前六位是地区码),哈希值也会呈现“前缀相似”的问题。怎么解决?加盐、加算法迭代。
import hashlib
import os
def generate_hash_(data: str, salt: bytes = None) -> str:
"""
生成加盐的SHA-256哈希令牌。
如果不传salt,自动生成16字节随机盐。
"""
if salt is None:
salt = os.urandom(16)
# 将盐和原始数据拼接
salted = salt + data.encode('utf-8')
digest = hashlib.sha256(salted).hexdigest()
# 返回时把盐附在令牌前面(方便验证)
return salt.hex() + ':' + digest
# 测试
= generate_hash_('6217001234567890')
print() # 输出类似 'a1b2c3d4...:e5f6g7h8...'
优点:无需外部存储,长度固定,碰撞概率极低
缺点:原始数据规律会残留到哈希值中;不可逆导致需要额外维护映射才能还原
结构令牌——保留格式,兼顾可逆
结构令牌是令牌化里的“瑞士军刀”。
它的核心思想:生成的令牌长得和原始数据一模一样——信用卡号还是16位数字,手机号还是11位,连校验位都给你保留。但里面的数字是加密算出来的,不是原内容。
能做到这个,靠的是格式保留加密(FPE)。我后面一整章都在讲FPE,这里先给你一个直观理解。
# 伪代码,展示结构令牌的生成流程
def generate_structural_(plaintext: str) -> str:
# 1. 解析原始数据的格式(长度、字符集、校验位规则)
format_info = parse_format(plaintext)
# 2. 使用FPE加密算法(比如FF1)对原始数据做定长加密
ciphertext = fpe_encrypt(plaintext, format_info)
# 3. 检查校验位是否还合法,如果不合法就重新调整个别位
if not verify_checksum(ciphertext):
ciphertext = adjust_last_digit(ciphertext)
return ciphertext
注意那行“调整校验位”。我曾经在这个点翻过车。
有一次客户要求生成的令牌必须通过Luhn算法校验,结果FPE算出来以后有个别卡号校验位不对。我没做后处理就直接交货了,对方收银系统把1%的令牌当成无效卡号直接拒单……后来我养成了习惯:结构令牌生成后必须跑一遍格式验证,该修正就修正。
- 遗留系统迁移,数据库字段长度和类型不能改
- 第三方系统对接,对方要求输入格式完全一致
- 令牌需要在前端展示或通过短信发送(长度受限)
三种令牌对比速查
| 特性 | 随机令牌 | 哈希令牌 | 结构令牌 |
|---|---|---|---|
| 生成速度 | 极快 | 快 | 中等(需FPE计算) |
| 可逆性 | 不可逆(需存储映射) | 不可逆 | 可逆(拥有密钥) |
| 格式保持 | 否 | 否 | 是(原格式不变) |
| 长度固定 | 否(取决于生成参数) | 是(如SHA-256为64字符) | 是(与原始数据等长) |
| 依赖外部存储 | 是(映射表) | 否 | 否(密钥管理即可) |
| 安全性风险 | 映射表泄露即全量暴露 | 彩虹表/原始规律残留 | 密钥泄露即全量暴露 |
| 典型应用 | 临时凭据、优惠码 | 数据去重、日志脱敏 | 金融卡号、身份证、手机号 |
怎么选?我的经验法则
- 只需要临时标识,丢了都无所谓? → 随机令牌
- 需要去重计算,且不需还原? → 哈希令牌(记得加盐)
- 必须保留原始格式,且可能需还原? → 结构令牌(FPE)
- 混合场景? → 我一般用随机令牌+哈希令牌做双重索引,结构令牌只用在格式强约束的字段上
国内外GDPR、个人信息保护法明确说了,可重标识的哈希值属于“假名化”而非“匿名化”。说白了,只要你手里还有原始数据能重算比对,这就不能算匿名。我见过有人拿SHA-256脱敏后就敢对外发布数据集,结果被监管罚款——别学他。
上面这张图把三条路径串起来了。你从左边的原始数据出发,根据你的业务约束选择一条分支,就能走到对应的令牌类型。
嗯,记住两点:第一,没有银弹;第二,选择的标准不是“哪个最安全”,而是“哪个能在安全、性能、格式三者中找到平衡”。我见过太多团队一上来就拍“用AES-CTR做令牌”,最后改需求改到崩溃——就是因为他们没提前想清格式约束。