1、Tokenization 概述

先问个问题:你做数据脱敏的时候,最头疼的是什么?
我猜八成是“既要保护敏感信息,又要保持业务可用性”。
嗯,Tokenization 就是专治这个的。

说白了,Tokenization 就是把敏感数据(比如身份证号、信用卡号)换成一个无意义的“替身”——。这个替身长得很像原数据,甚至格式、长度都一模一样,但内容完全随机。你拿着 去做业务,查不到真实信息;只有拿着映射表的系统,才能还原回去。

核心要点: 本身不含敏感信息,仅通过映射关系关联原始值。映射表就是命根子,丢了就全完了。

什么是 Tokenization

我一开始接触这个术语的时候,以为就是个加密的变种。后来踩了坑才发现,两者思路完全不同。
加密是算法+密钥,同样的输入永远得到同样的密文;Tokenization 是随机替换,同一个“张三”可能每次生成不同的 ,全看你的映射策略。

Tokenization 的三个关键特征:

  • 格式保留: 脱敏后的数据长度、字符集和原始数据一致,不破坏数据库字段约束。
  • 确定性 vs. 随机性: 你可以选“确定性 ”(同一输入永远映射到同一 ),也可以选“随机 ”(每次不同),看业务场景。
  • 可逆性: 通过映射表可还原。如果不需要还原,那叫“不可逆 Tokenization”,接近 Hash 的效果。
我个人的习惯: 在测试环境用随机 ,保持数据多样性;在生产环境用确定性 ,保证跨系统关联不坏。

Tokenization 在数据脱敏中的作用

你可能觉得,脱敏方法那么多——遮蔽、泛化、加密、假名化……为什么偏偏要提 Tokenization?
我在一个金融项目里遇到过:要对交易日志做脱敏,但下游风控系统需要根据卡号 BIN 码做路由。如果用遮蔽,卡号被遮掉中间几位,BIN 码对不上,风控直接报错。用 Tokenization 呢?我可以把卡号前六位保留(非敏感部分),后十位替换成 ,既合规又能跑风控。说白了,Tokenization 能在“安全”和“可用”之间做精细控制。

Tokenization 与其他脱敏技术的对比
技术格式保留可逆性数据关联性典型场景
Tokenization是可逆(需映射表)可配置跨系统数据共享、测试数据准备
遮蔽(Masking)部分保留不可逆弱界面显示、报表脱敏
泛化(Generalization)否不可逆弱统计分析、数据发布
加密(Encryption)否可逆(需密钥)强(确定性)存储加密、传输加密

你仔细看表格就会发现,Tokenization 在“格式保留”和“可配置的关联性”上是独一份的。这也是为什么在数据脱敏性能优化里,Tokenization 常常是首选方案——因为它对业务系统的影响最小。

典型处理流程:解析 → 映射 → 替换

先别急着写代码。我从一个真实项目中提炼的流程,记好这三步,后面每一章都在这个框架上做性能优化。

  1. 解析(Parse) —— 把输入数据拆成“敏感部分”和“非敏感部分”。
    比如一个身份证号:前6位地区码、中间8位出生日期、后4位顺序码。你可以只对后4位做 Tokenization,前14位原样保留。这一步的关键是识别模式。
  2. 映射(Map) —— 建立原始值到 的关联。
    查映射表,如果已经存在就复用(确定性场景),如果不存在就生成新 并存入表里。这里的性能瓶颈通常是并发写和缓存命中率。
  3. 替换(Replace) —— 用 替换原始值,拼回完整数据。
    注意替换时不要破坏原始数据的结构。比如 JSON 里的一个字段,你要改的是值,不是 key 也不是引号。

我当年做第一个 Tokenization 系统时,在替换环节挂过一晚上。原因是我直接用字符串 replace,结果把卡号里出现的“1234”全换成了 ,包括校验和里的部分,导致所有下游验卡通不过。所以,替换一定要基于“解析后的原子字段”,不能无脑替换全局。

我曾经踩过的坑: 在一个信用卡脱敏项目里,因为没做“前6位保留”的解析,结果 替换后卡号 BIN 码变了,银行的反欺诈系统直接拦截了所有交易。后来我在解析步骤里加了“保留前6位”的白名单规则,问题才解决。所以,解析的粒度直接决定脱敏的可用性。
Tokenization 核心处理流程图 原始数据 解析 映射 替换 脱敏数据 映射表(库) 注意:解析步骤决定哪些字段需要 化;映射步骤是性能瓶颈(缓存+并发写入);替换必须基于解析后的原子字段。

上面这张图是我每次给团队画流程时用的标准模板。你注意一下映射表(库)那条虚线双向箭头——往映射表里写数据是高频操作,我们后面会专门花一章讲如何用缓存和分片加速它。

一个小例子:用 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
一点说明: 上面的代码为了演示去掉了线程安全和持久化。但在生产里,我至少会加一层 LRU 缓存,把高频的映射对放内存,避免每次查数据库。这个技巧在第 7 章会展开讲。

回头总结一下

  • Tokenization 不是加密,是“替身”机制,核心在映射表。
  • 解析的粒度决定了脱敏的效果和可用性——别把不该动的字段给换了。
  • 映射是性能热点,替换要小心别破坏数据完整性。

嗯,这一章就先把地基打牢。后面我们会在“高性能映射缓存”、“并发安全写入”、“格式感知的解析器”这些点上逐个突破。你先把这三个步骤刻在脑子里,后面每一章的优化,都是针对其中某个步骤的。