接口敏感数据加密传输和存储方法详解(详解身份证号、手机号的字段级加密方案)

身份证号、手机号这类敏感个人信息,必须同时做三件事:传输层强制 TLS 1.3 并对字段做二次加密与签名防重放;存储层用 AES-256-GCM 或国密 SM4 做字段级加密,密文入库、明文不落盘;密钥交给硬件加密机(HSM)或云 KMS 托管,应用只持有被主密钥包裹的数据密钥。只开 HTTPS,或只依赖数据库整盘透明加密(TDE),既挡不住拖库后的密文还原,也挡不住内部越权查询。下文给出分级标准、算法选型、可运行的加解密片段与密钥轮换步骤。

一、先做数据分级,再决定加密强度

把所有字段一刀切全加密,会让索引失效、查询雪崩;全部明文又必然踩合规红线。可行的做法是按敏感等级拆开处理。支付系统的分级实践给出了一套可直接照搬的映射关系:

字段 敏感等级 存储方式 技术实现
身份证号 高 密文存储 + 展示脱敏(保留前 3 后 4) AES-256-GCM,密钥由 HSM 管理
银行卡号 高 完整密文,独立分区(对齐 PCI-DSS) 密文库分区隔离,主密钥 HSM 隔离
手机号 中 密文存储 + 盲索引支持查询 SM4-GCM 或 AES-GCM,配盐防彩虹表
姓名 中 密文存储 + 展示脱敏(张*三) 与证件号绑定加密,限定服务解密
收货地址 低 一般明文,风控场景加密 按业务选择明文或密文

《个人信息保护法》第二十八条把身份证号、生物识别、医疗健康归入敏感个人信息,处理时需单独同意并采取严格保护措施。分级表的价值在于:它把”要不要加密”变成”用哪种加密”,后续架构决策都从这张表推导。

二、传输侧:TLS 只是起点

2.1 HTTPS 覆盖不到的缺口

TLS 1.3 结合 AES-256-GCM 能提供前向安全的端到端信道,抵御中间人抓包。但信道加密解决不了三类问题:客户端被抓包工具装了根证书后可明文查看请求;网关、日志、APM 探针会把解密后的报文原样记录;攻击者拿到一次合法请求就能无限重放。

2.2 字段二次加密与签名防重放

对策是在业务层再加一层。敏感字段用后端下发的临时公钥(RSA-3072 或 SM2)加密,会话密钥用 ECC-P256 协商——相同安全强度下 ECC 密钥长度比 RSA 短约 75%,移动端更划算。密钥绝不硬编码在前端产物里,必须由后端接口动态签发并带时效。

请求侧的完整校验链按下面顺序执行:

  1. 客户端按参数名字典序拼接待签串,附加 timestamp 与随机 nonce;
  2. 用 HMAC-SHA256 与 AppSecret 计算签名,放入 X-Signature 头;
  3. 服务端先校验时间戳偏移,超过 300 秒直接拒绝;
  4. 用 SETNX 把 nonce 写入 Redis,键的 TTL 与时间窗对齐,写入失败即判定重放;
  5. 重算签名并用常量时间比较,避免时序侧信道;
  6. 校验通过后才解密敏感字段,进入业务逻辑。
import hmac, hashlib, time, secrets

def sign(params: dict, secret: str) -> dict:
    params = dict(params)
    params["timestamp"] = str(int(time.time))
    params["nonce"] = secrets.token_hex(16)
    raw = "&".join(f"{k}={params[k]}" for k in sorted(params))
    params["signature"] = hmac.new(
        secret.encode, raw.encode, hashlib.sha256
    ).hexdigest
    return params

def verify(params: dict, secret: str, redis, window=300) -> bool:
    if abs(int(time.time) - int(params["timestamp"])) > window:
        return False
    if not redis.set(f"nonce:{params['nonce']}", 1, nx=True, ex=window):
        return False          # nonce 已用过,判定为重放
    raw = "&".join(f"{k}={params[k]}" for k in sorted(params) if k != "signature")
    expect = hmac.new(secret.encode, raw.encode, hashlib.sha256).hexdigest
    return hmac.compare_digest(expect, params["signature"])

三、存储侧:字段级加密的落地路径

3.1 算法选型对比

算法与模式 密钥长度 完整性校验 合规适配 典型用途
AES-256-GCM 256 位 内置 AEAD 标签 国际通用 敏感字段主力方案
SM4-GCM 128 位 内置 AEAD 标签 等保 2.0、金融政务强制 国密合规场景
ChaCha20-Poly1305 256 位 内置 国际通用 移动端、无 AES 指令集环境
AES-CBC + 随机 IV 128/256 位 需外挂 HMAC 通用 遗留系统兼容
AES-ECB 任意 无 不建议 相同明文产生相同密文,禁用

阿里云开发者社区的数据安全实践把结论写得很直白:AES 安全合规的密钥长度为 256 位,加密模式必须使用 GCM;SM4 分组与密钥长度均为 128 位,安全强度对齐 AES-128,是国内等保、金融、政务场景的强制项。GCM 属于认证加密,加密与完整性校验一次完成,能规避 ECB 模式的明文重放风险。

3.2 AES-256-GCM 的正确写法

工程上最容易出错的两点是 IV 复用和把 IV 写死。GCM 的 IV 一旦在同一密钥下重复,认证密钥就会泄露。正确做法是每次加密生成 12 字节随机 IV,并把 IV 前置拼在密文里一起存储。

import os, base64
from cryptography.hazmat.primitives.ciphers.aead import AESGCM

def encrypt(plain: str, key: bytes, aad: bytes = b"") -> str:
    iv = os.urandom(12)                      # 每条记录独立 IV,禁止复用
    ct = AESGCM(key).encrypt(iv, plain.encode, aad)
    return base64.b64encode(iv + ct).decode   # IV || 密文 || 16 字节 tag

def decrypt(cipher_b64: str, key: bytes, aad: bytes = b"") -> str:
    blob = base64.b64decode(cipher_b64)
    return AESGCM(key).decrypt(blob[:12], blob[12:], aad).decode

key = AESGCM.generate_key(bit_length=256)
c = encrypt("11010519950101234X", key, aad=b"user_id=10086")
assert decrypt(c, key, aad=b"user_id=10086") == "11010519950101234X"

把 user_id 放进附加认证数据(AAD),可以防止攻击者把 A 用户的密文搬到 B 用户记录上——密文没变,但 AAD 不匹配,解密直接失败。

业务代码不该到处调用加解密工具类。Java 侧的通行做法是在实体字段上打 @Encrypted 注解,由 MyBatis TypeHandler 在写入前自动加密、查询后自动解密,业务层完全无感。JPA 用 AttributeConverter、GORM 用自定义 Scanner/Valuer 是同类思路。

3.3 信封加密与密钥轮换

单一密钥加密全库数据,等于把所有鸡蛋放进一个篮子。信封加密(Envelope Encryption)把密钥拆成两层:

  1. 在 KMS 或 HSM 中创建主密钥(CMK),设定不可导出,禁止落盘;
  2. 每个租户或每张表申请一个数据密钥(DEK),KMS 同时返回明文 DEK 与被 CMK 包裹的密文 DEK;
  3. 用明文 DEK 加密业务字段,随后立即从内存清除明文 DEK;
  4. 密文 DEK 与业务数据同库存放,解密时先调 KMS 解出 DEK,再解字段;
  5. 明文 DEK 在应用内存缓存并设短 TTL,减少 KMS 调用量;
  6. 按 90 天周期轮换密钥,密文中写入 key_version 标识,支持新旧密钥并存的灰度重加密。

密码存储是另一条路。密码不需要还原,用 bcrypt 或 scrypt 加盐哈希即可;若采用 PBKDF2-HMAC-SHA256,迭代次数建议不低于 10000 次,随机盐不少于 16 字节。MD5 早已不满足要求。

四、加密之后:查询、展示与日志

4.1 密文字段怎么查

GCM 每次加密结果都不同,WHERE phone = ? 直接失效。手机号这类需要精确匹配的字段,用”确定性加密”或”盲索引”补一列:对明文加 HMAC 派生密钥算摘要,存入 phone_hash 并建索引,查询时先对入参算同样的摘要再比对。派生密钥必须与主加密密钥分离。需要模糊匹配(如按手机号前三位筛选)时可用前缀哈希索引,但要评估由此暴露的统计信息。

4.2 展示层动静分离脱敏

静态脱敏改写落库数据,身份证号 11010519950101234X 存成 110105********34X;动态脱敏按角色实时过滤,客服只看后四位,风控看完整值,靠数据库代理网关重写 SQL 自动挂规则。两者配合,才能既保住可用性又限制暴露面。

4.3 别让日志成为后门

敏感字段的操作日志需要单独处理,日志中只记录脱敏值如 138****1234,禁止明文输出;同时记录”谁在何时解密了哪条记录”,支持事后溯源。异常堆栈里打印整个请求对象,是最常见的泄露路径。

五、三个高频踩坑点

密钥和密文放在同一个数据库,拖库时一并被拿走,等于没加密。密钥必须外置到 KMS 或独立密钥库,并用不同的访问凭据。

前端加密被当成安全边界。前端代码可反编译,任何在浏览器或 App 内固化的密钥都视同公开;前端加密的作用是防止运维和中间链路直接看到明文,不能替代后端校验。

只加密不管权限。密文字段仅允许风控、支付核心等少数服务解密,按 RBAC 模型收口,并对解密动作全量审计。缺了这一层,一个越权接口就能把整库明文导出去。

常见问题(FAQ)

Q1:数据库开了 TDE,还需要字段级加密吗?

需要。TDE 只防物理文件被拷走,SQL 查询和越权访问拿到的仍是明文。

Q2:加密后手机号无法模糊查询怎么办?

增加盲索引列,存 HMAC 前缀摘要并建索引;全文模糊改走独立检索服务。

Q3:AES-GCM 的 IV 可以固定吗?

不可以。同密钥下 IV 复用会泄露认证密钥,每条记录须用 12 字节随机 IV。

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

相关推荐

返回顶部