1、Tokenization 概述
先问个问题:你做数据脱敏的时候,最头疼的是什么?
我猜八成是“既要保护敏感信息,又要保持业务可用性”。
嗯,Tokenization 就是专治这个的。
说白了,Tokenization 就是把敏感数据(比如身份证号、信用卡号)换成一个无意义的“替身”——。这个替身长得很像原数据,甚至格式、长度都一模一样,但内容完全随机。你拿着 去做业务,查不到真实信息;只有拿着映射表的系统,才能还原回去。
什么是 Tokenization
我一开始接触这个术语的时候,以为就是个加密的变种。后来踩了坑才发现,两者思路完全不同。
加密是算法+密钥,同样的输入永远得到同样的密文;Tokenization 是随机替换,同一个“张三”可能每次生成不同的 ,全看你的映射策略。
Tokenization 的三个关键特征:
- 格式保留: 脱敏后的数据长度、字符集和原始数据一致,不破坏数据库字段约束。
- 确定性 vs. 随机性: 你可以选“确定性 ”(同一输入永远映射到同一 ),也可以选“随机 ”(每次不同),看业务场景。
- 可逆性: 通过映射表可还原。如果不需要还原,那叫“不可逆 Tokenization”,接近 Hash 的效果。
Tokenization 在数据脱敏中的作用
你可能觉得,脱敏方法那么多——遮蔽、泛化、加密、假名化……为什么偏偏要提 Tokenization?
我在一个金融项目里遇到过:要对交易日志做脱敏,但下游风控系统需要根据卡号 BIN 码做路由。如果用遮蔽,卡号被遮掉中间几位,BIN 码对不上,风控直接报错。用 Tokenization 呢?我可以把卡号前六位保留(非敏感部分),后十位替换成 ,既合规又能跑风控。说白了,Tokenization 能在“安全”和“可用”之间做精细控制。
| 技术 | 格式保留 | 可逆性 | 数据关联性 | 典型场景 |
|---|---|---|---|---|
| Tokenization | 是 | 可逆(需映射表) | 可配置 | 跨系统数据共享、测试数据准备 |
| 遮蔽(Masking) | 部分保留 | 不可逆 | 弱 | 界面显示、报表脱敏 |
| 泛化(Generalization) | 否 | 不可逆 | 弱 | 统计分析、数据发布 |
| 加密(Encryption) | 否 | 可逆(需密钥) | 强(确定性) | 存储加密、传输加密 |
你仔细看表格就会发现,Tokenization 在“格式保留”和“可配置的关联性”上是独一份的。这也是为什么在数据脱敏性能优化里,Tokenization 常常是首选方案——因为它对业务系统的影响最小。
典型处理流程:解析 → 映射 → 替换
先别急着写代码。我从一个真实项目中提炼的流程,记好这三步,后面每一章都在这个框架上做性能优化。
- 解析(Parse) —— 把输入数据拆成“敏感部分”和“非敏感部分”。
比如一个身份证号:前6位地区码、中间8位出生日期、后4位顺序码。你可以只对后4位做 Tokenization,前14位原样保留。这一步的关键是识别模式。 - 映射(Map) —— 建立原始值到 的关联。
查映射表,如果已经存在就复用(确定性场景),如果不存在就生成新 并存入表里。这里的性能瓶颈通常是并发写和缓存命中率。 - 替换(Replace) —— 用 替换原始值,拼回完整数据。
注意替换时不要破坏原始数据的结构。比如 JSON 里的一个字段,你要改的是值,不是 key 也不是引号。
我当年做第一个 Tokenization 系统时,在替换环节挂过一晚上。原因是我直接用字符串 replace,结果把卡号里出现的“1234”全换成了 ,包括校验和里的部分,导致所有下游验卡通不过。所以,替换一定要基于“解析后的原子字段”,不能无脑替换全局。
上面这张图是我每次给团队画流程时用的标准模板。你注意一下映射表(库)那条虚线双向箭头——往映射表里写数据是高频操作,我们后面会专门花一章讲如何用缓存和分片加速它。
一个小例子:用 Python 写个雏形
虽然这部分不是代码课,但光讲理论有点干。我贴一段最简单的 Tokenization 示例,让你直观感受一下“解析-映射-替换”在代码里长什么样。
import random, string
# 映射表,生产环境要用数据库或 Redis
_map = {}
def parse_sensitive(input_str: str, format_spec: dict):
"""解析,返回敏感部分列表"""
# 假设 format_spec = {"敏感位置": [(start, end)]}
parts = []
for start, end in format_spec["sensitive_positions"]:
parts.append(input_str[start:end])
return parts
def map_to_(sensitive_part: str) -> str:
"""映射,如果已存在则复用"""
if sensitive_part in _map:
return _map[sensitive_part]
# 生成随机 (保留格式:相同长度的数字+字母)
length = len(sensitive_part)
= ''.join(random.choices(string.ascii_uppercase + string.digits, k=length))
_map[sensitive_part] =
return
def replace_tokens(original: str, format_spec: dict, tokens: list) -> str:
"""替换,注意保持非敏感部分不变"""
# 简单实现:从后往前替换,避免破坏位置
positions = format_spec["sensitive_positions"]
result = list(original)
for idx, (start, end) in enumerate(reversed(positions)):
result[start:end] = tokens[-(idx+1)]
return ''.join(result)
# 使用示例
input_data = "6222021234567890" # 信用卡号(假设 16 位)
spec = {"sensitive_positions": [(6, 16)]} # 保留前6位 BIN,后10位脱敏
sensitive_parts = parse_sensitive(input_data, spec)
tokens = [map_to_(part) for part in sensitive_parts]
output = replace_tokens(input_data, spec, tokens)
print(f"原始: {input_data}")
print(f"脱敏: {output}")
# 输出 eg: 原始: 6222021234567890 → 脱敏: 622202AB3KX9M2N
回头总结一下
- Tokenization 不是加密,是“替身”机制,核心在映射表。
- 解析的粒度决定了脱敏的效果和可用性——别把不该动的字段给换了。
- 映射是性能热点,替换要小心别破坏数据完整性。
嗯,这一章就先把地基打牢。后面我们会在“高性能映射缓存”、“并发安全写入”、“格式感知的解析器”这些点上逐个突破。你先把这三个步骤刻在脑子里,后面每一章的优化,都是针对其中某个步骤的。