加密后的数据支持模糊搜索方法详解(详解盲索引、N-Gram 分词与密文检索选型)

密文字段做不了 LIKE '%张%',可行路径不是让数据库去解密,而是额外建一列”可比对的索引”:前缀场景用盲索引(对明文片段做 HMAC 后建索引),中缀场景用 N-Gram 分词哈希,全文场景把令牌化后的索引下沉到独立检索服务。保序加密能撑范围查询但抗不住频率分析,全同态加密目前只适合离线计算。真实业务里绝大多数需求是”精确查询 + 少量前缀模糊”,用确定性加密叠盲索引就能覆盖,不必上密码学重武器。

一、为什么 AES 密文天然不支持 LIKE

AES-GCM 每次加密都用随机 IV,同一个手机号加密两次得到两串完全不同的密文。数据库看到的是无序字节流,字符位置关系被彻底打散,LIKE、>、ORDER BY 全部失效。想恢复这些能力,只有两条路:把明文的某些特征”可控地泄露”到一个辅助列上,或者换一种保留特定运算性质的加密算法。前者是工程主流,后者是密码学方案。

绕不开的取舍在于:任何支持模糊匹配的密文索引,都会泄露一部分明文信息。设计时要回答的问题不是”泄不泄露”,而是”泄露多少可以接受”。

二、主流方案横向对比

方案 支持的查询 查询性能 安全强度 实现难度
确定性加密(固定 IV / SIV) 精确等值 高 中(暴露重复值分布) 低
盲索引(HMAC 前缀摘要) 前缀模糊 中 中 中
N-Gram 分词哈希 前缀、中缀 中 中低(片段频率可统计) 中
加密布隆过滤器 包含判断 高 中 中高
可搜索加密 SSE 关键词等值 中 高 高(缺成熟中间件)
保序加密 OPE 范围、排序 高 低(已知明文/频率攻击) 中
全同态加密 FHE 任意运算 极低 极高 极高

选型建议来自公开实践的一条经验值:若业务构成接近”80% 精确查询 + 15% 前缀模糊 + 5% 全文模糊”,采用确定性加密 + 盲索引 + Elasticsearch 的混合架构即可。

三、盲索引:落地成本最低的方案

3.1 原理与表结构

盲索引的做法是给密文列配一个”指纹列”:对明文取若干长度的前缀,逐个用独立派生密钥算 HMAC,截断后存入索引列。查询时对入参做同样处理,把模糊查询翻译成等值匹配。

CREATE TABLE users (
  id           BIGINT PRIMARY KEY,
  name_cipher  VARBINARY(256),   -- AES-256-GCM 全字段密文
  name_bi1     CHAR(16),         -- 前 1 字符盲索引
  name_bi2     CHAR(16),         -- 前 2 字符盲索引
  name_bi3     CHAR(16),         -- 前 3 字符盲索引
  KEY idx_bi1 (name_bi1),
  KEY idx_bi2 (name_bi2),
  KEY idx_bi3 (name_bi3)
);

多级索引(首字 → 前 2 字 → 前 3 字)能让查询词越长、命中越精准,是常见优化手段。派生密钥必须与主加密密钥分离,否则一旦索引密钥泄露,攻击者可以离线爆破整列明文。

3.2 生成与查询改写

import hmac, hashlib

BI_KEY = b"blind-index-derived-key-not-master"

def blind(token: str, bits: int = 64) -> str:
    """截断 HMAC 作为盲索引,截断长度决定碰撞率与泄露量的平衡"""
    digest = hmac.new(BI_KEY, token.encode, hashlib.sha256).digest
    keep = bits // 8
    return digest[:keep].hex

def build_prefix_index(plain: str, max_len: int = 3) -> dict:
    return {f"bi{i}": blind(plain[:i]) for i in range(1, max_len + 1)}

# 写入:"张三丰" -> {'bi1': ..., 'bi2': ..., 'bi3': ...}
print(build_prefix_index("张三丰"))

def rewrite_like(keyword: str):
    """LIKE '张三%' 改写成盲索引等值条件"""
    n = min(len(keyword), 3)
    return f"name_bi{n} = %s", (blind(keyword[:n]),)

print(rewrite_like("张三"))   # ("name_bi2 = %s", ('...',))

公开测试数据显示,10 万条记录规模下盲索引查询耗时约 0.8 秒,相比全表解密再比对提升约 82%,但存在约 5% 的假阳性。假阳性来自哈希截断碰撞,处理方式是把索引命中的结果集拉回应用层解密后二次过滤。

四、N-Gram 分词哈希:把中缀查询变成集合匹配

前缀索引撑不住 LIKE '%三%'。此时把字段切成滑动窗口片段,每个片段单独哈希,存成一张倒排副表。

def ngrams(text: str, n: int = 2):
    if len(text) < n:
        return [text]
    return [text[i:i + n] for i in range(len(text) - n + 1)]

print(ngrams("张三丰"))          # ['张三', '三丰']
print(ngrams("13812345678", 3)) # ['138', '381', '812', ...]

# 副表:ngram_index(record_id, gram_hash)
def index_rows(record_id: int, text: str):
    return [(record_id, blind(g)) for g in set(ngrams(text))]

def search_infix(keyword: str):
    grams = [blind(g) for g in set(ngrams(keyword))]
    ph = ",".join(["%s"] * len(grams))
    return (
        f"SELECT record_id FROM ngram_index WHERE gram_hash IN ({ph}) "
        f"GROUP BY record_id HAVING COUNT(DISTINCT gram_hash) = {len(grams)}",
        tuple(grams),
    )

HAVING COUNT 保证候选记录包含查询词的全部片段,等价于”包含”语义。代价明确:索引存储膨胀,一个 20 字的地址在 2-gram 下要写 19 行;单字查询会退化,因为单字生不出 2-gram,需要额外维护 1-gram 表。

加密布隆过滤器是同一思路的位图变体:把各片段哈希映射进位数组,加密后存为一个字段,查询时比对位模式。它省存储、比对快,但同样有假阳性,且不支持排序与范围查询。

五、保序加密与同态加密的边界

保序加密保证密文字典序与明文一致,因此能直接跑 BETWEEN 和 ORDER BY,并复用原生 B+ 树索引。问题是它把顺序关系原样保留,攻击者靠统计密文分布就能推测明文,属于已知明文攻击与频率分析的高危区,不适合高机密字段,也做不到字符串 LIKE。

全同态加密理论上是终极答案:服务器直接在密文上算 LIKE。现实是性能差着数量级——BGV 方案查 1 万条记录需要数分钟级耗时,CKKS 支持浮点但精度受限,公开建议是仅用于离线分析场景。若要在 FHE 上做字符串匹配,还得把 ASCII 比对改写成多项式运算,工程量集中在 Microsoft SEAL、IBM HElib 这类库的算法层。

对称可搜索加密 SSE 的定位介于两者之间:客户端本地建密文索引与陷门 Token,服务端只做 Token 匹配,不接触明文与密钥。安全性最贴近理论最优,但工业界成熟中间件稀缺,且主流实现只支持关键词精确相等,通配符模糊支持很弱。

六、混合架构的落地步骤

  1. 统计线上真实查询,按精确 / 前缀 / 中缀 / 全文四类打标,得出各自占比;
  2. 对精确类字段改用确定性加密(AES-SIV 或固定 IV 派生),直接建唯一索引;
  3. 对前缀类字段加多级盲索引列,用 ORM 拦截器在写入时自动填充;
  4. 对中缀与全文需求,把 N-Gram 哈希索引下沉到 Elasticsearch,只存 Token 与主键,不存明文;
  5. 在 DAO 层加查询改写器,把上层 LIKE 语义统一翻译成索引条件,业务代码不感知;
  6. 应用层对索引命中结果解密并做二次精确过滤,消除哈希碰撞导致的假阳性;
  7. 为盲索引与 N-Gram 索引配置独立派生密钥,纳入与主密钥不同的轮换周期;
  8. 上线前压测,并对模糊查询接口加返回条数上限与审计日志。

七、别忽略的安全折衷

模糊查询接口本身会泄露统计信息。攻击者反复用不同前缀试探,通过返回结果集大小就能推断姓氏分布、号段分布。可行的缓解手段包括:限制单次返回条数、对高频探测做频次管控、记录所有模糊查询的操作者与结果集规模。

某电商的分段加密实践把模糊查询性能损失控制在 15% 以内并通过等保三级评估,说明手机号、身份证号这类结构规整、可按段拆分的字段最适合分段索引方案;地址、备注这类自由文本,仍应走独立检索服务。

常见问题(FAQ)

Q1:盲索引会不会被暴力破解?

会。手机号空间有限,攻击者拿到索引密钥即可枚举,密钥必须独立托管在 KMS。

Q2:中缀模糊一定要用 N-Gram 吗?

不一定。数据量小可全量解密后内存过滤;量大且频繁则用 N-Gram 倒排更划算。

Q3:Elasticsearch 存 Token 安全吗?

只存哈希 Token 与主键、不存明文时可接受,需另配集群访问控制与传输加密。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 qiqicto@qq.com 举报,一经查实,本站将立刻删除。
赞 (0)
赵其鑫的头像赵其鑫管理团队

相关推荐

返回顶部