2. 字段级加密SDK的架构设计:模块划分与数据流

做字段级加密,架构设计是重中之重。我见过不少团队上来就硬编码加解密逻辑,结果后来字段一多,密钥一换,整个项目跟着重构。说白了,模块边界划不清楚,后面维护成本会翻好几倍。

这一章咱们就聊聊SDK内部到底长什么样。我习惯把架构拆成五个核心模块——密钥管理、加解密引擎、字段选择与适配、配置与策略、审计与日志。先看整体分层关系,再逐个展开细节。

核心思想: 各模块职责单一,数据流转清晰,互不依赖具体实现。每个模块只做好一件事,出了问题也好定位。
字段级加密SDK 模块分层架构与数据流 应用层(业务系统 / API 入口) SDK 统一入口(Encrypt / Decrypt 接口) 字段选择与适配模块(识别敏感字段、类型转换、嵌套处理) 加解密引擎(AES-GCM / 自定义算法、数据包组装) 密钥管理模块(密钥获取、缓存、轮换) 审计与日志模块(操作记录、错误追踪) 配置与策略模块(字段映射、加密策略、过期策略)
个人经验: 上面的架构图是我在二次重构时定下来的。第一版我把密钥获取和加解密写在一个类里,后来密钥源从本地文件换成KMS,改了半个月。拆开之后,换密钥源只需要改密钥管理模块,加解密引擎一行不动。

2.1 密钥管理模块 —— 命脉所在

职责: 统一管理加密密钥的生命周期:获取、缓存、轮换、失效处理。

这个模块很容易被低估。你以为存个密钥就完事了?其实坑很多。比如密钥过期了,SDK该抛异常还是静默回退?我曾经遇到过这样的线上事故:密钥轮换时缓存没刷新,旧密钥被删除后新密钥还没生效,结果有一分钟所有加密操作都失败了。

子能力 说明 我踩过的坑
密钥获取 支持本地文件、KMS、Vault等多种数据源 连KMS超时没设短超时,接口堵死
密钥缓存 LRU缓存+过期刷新,减少远程调用 缓存过期时间设得太长,轮换后还用旧密钥
密钥轮换 支持版本号机制,灰度切换 多版本同时存在,加解密版本不匹配
访问控制 限制密钥使用范围(如仅加密、仅解密) 审计中发现密文被误解密
注意: 密钥缓存一定要结合版本号。我曾经因为没加版本号,旧密钥换掉后,新加密的数据用旧密钥解密,结果所有字段都变成乱码。那一次回滚花了3个小时。

2.2 加解密引擎 —— 核心运算层

加解密引擎负责真正的算法执行。我建议引擎本身不做任何策略判断,它只接收“输入数据 + 密钥材料”,然后返回加密结果或解密结果。

  • 算法选择: 默认推荐 AES-256-GCM(兼顾安全与性能),也可扩展国密 SM4-GCM。
  • 数据包格式: 非对称的 IV + 密文 + 认证标签,按预定协议组装。
  • 性能优化: 使用 ThreadLocal 复用 Cipher 实例,减少对象创建开销。
  • 兼容性: 旧版本的数据包格式要能识别并兼容解密。
避坑指南: 加解密引擎必须设计为无状态。有状态的引擎(比如内部持有上次加密的上下文)在多线程环境下会出大问题。我见过同事把Cipher实例放在静态变量里,结果并发加密时数据错乱了。

2.3 字段选择与适配模块 —— 精准打击

这个模块是SDK里最容易被忽视的部分。说白了,它解决三个问题:哪些字段要加密?字段是什么类型?嵌套对象怎么处理?

我习惯用注解 + 配置文件的混合模式。举一个实际例子:

// Java 注解声明敏感字段
@Encrypted(algorithm = "AES-GCM", keyId = "user-key-v2")
private String idCard;

@Encrypted(encryptMode = EncryptMode.SKIP_IF_NULL)
private String phone;

// 对于嵌套对象,还需要指定脱敏策略
@EncryptedFields({
  @EncryptedField(path = "address.province", enable = false),
  @EncryptedField(path = "address.detail", encrypt = true)
})
private Address address;

注意:你想想看,嵌套对象的处理有多麻烦?JSON里的深层字段,如果硬编码路径,改一个字段名所有加密配置都要跟着改。所以我推荐用 路径表达式(类似JSONPath) 来做字段定位,这样配置和代码完全解耦。

2.4 配置与策略模块 —— 调度大脑

这个模块不直接处理数据,但所有加密行为都由它控制。用表格看更清楚:

配置项 作用 示例值
字段映射表 指定类/字段对应的密钥和算法 user.idCard → keyId=v2, algo=AES-GCM
加密策略 条件判断:为空、不加密、脱敏 encrypt_if_present
回滚策略 升级后自动回退至旧版字段配置 rollback_to: v1.0
密钥优先级 多个密钥版本同时存在时的选择规则 latest_active
个人经验: 配置最好热加载,不必重启。我曾在生产环境临时禁用一个字段的加密,靠动态配置完成的,否则就要走一次紧急发布流程。

2.5 审计与日志模块 —— 出了问题别抓瞎

加密操作一旦出问题,溯源非常重要。这个模块负责记录:

  1. 每条加密/解密请求的请求ID、时间戳
  2. 涉及的字段名、密钥版本
  3. 操作结果(成功/失败,失败原因)
  4. 性能指标:加解密耗时、缓存命中率

为什么要强调这个?有一次我排查线上慢问题,发现加密延迟突然飙高,最后查日志看到缓存未命中数暴增——原来是密钥管理模块的连接池被占满了。没有日志,这个问题根本没法定位。

2.6 数据流全局透视

上面几个模块之间是怎么配合的?打个比方:假设一个请求进来要加密用户身份证:

  1. 应用层调用 sdk.encrypt(user)
  2. 字段选择模块扫描对象,发现 idCard 上有 @Encrypted 注解,确认需要加密。
  3. 配置与策略模块查出算法为 AES-GCM,密钥标识 user-key-v2。
  4. 密钥管理模块根据标识获取密钥(先查缓存,缓存不存在则从远程取)。
  5. 加解密引擎用该密钥对 idCard 的明文进行加密,返回密文。
  6. 字段选择模块将密文填回对象的对应字段。
  7. 审计模块记录本次操作日志。

解密流程基本逆向,只是密钥管理模块要能根据数据包中的密钥版本号找到正确密钥。

核心原则: 各模块之间的依赖方向是单向的——数据流从上到下,配置从侧面注入,审计从各层异步采集。这样可以避免循环依赖,也方便单独替换某个模块。