|
| 1 | +# 字段加解密组件(FieldCrypt) |
| 2 | + |
| 3 | +> since 2.0.0 |
| 4 | +
|
| 5 | +FieldCrypt 是面向 MyBatis / MyBatis-Plus 的**单字段加解密组件**,通过拦截器 / 织入实现透明加解密,支持等值查询,适合做合规级数据保护。 |
| 6 | + |
| 7 | +## 1. 核心特性 |
| 8 | + |
| 9 | +- **透明加解密**:写入自动加密、查询自动解密,对业务代码侵入极小。 |
| 10 | +- **等值查询支持**:默认使用确定性加密,可直接用明文写 `WHERE`、`JOIN`、`GROUP BY` 条件。 |
| 11 | +- **注解驱动**:通过 `@Encrypted` 精确控制哪些字段/参数需要加密。 |
| 12 | +- **算法可扩展**:默认 AES-CBC,支持自定义算法(如替换为 AES-GCM 或国密算法)。 |
| 13 | +- **安全幂等**:密文统一添加 `ENC:` 前缀,防止重复加密,支持明文/密文混合存储以便平滑迁移。 |
| 14 | + |
| 15 | +## 2. 适用场景与选型建议 |
| 16 | + |
| 17 | +FieldCrypt 是一种**应用层单字段加密方案**。在决定使用之前,建议先根据业务需求(查询能力 vs 安全性)进行方案选型: |
| 18 | + |
| 19 | +| 方案类型 | 核心机制 | 查询能力 | 安全性 | |
| 20 | +|---|---|---|---| |
| 21 | +| **确定性加密**<br>(FieldCrypt 默认) | 相同明文 -> 相同密文 | ✅ 支持等值 (`=`, `IN`)<br>❌ 不支持范围/模糊 | ⭐ 中<br>存在频率分析风险 | |
| 22 | +| **随机化加密** | 相同明文 -> 随机密文 | ❌ 仅支持解密后读取<br>❌ 无法直接 SQL 查询 | ⭐⭐⭐ 高<br>每次密文不同,抗分析 | |
| 23 | +| **盲索引 (Blind Index)** | 随机密文 + 哈希索引列 | ✅ 支持等值 (查索引列)<br>❌ 不支持范围/模糊 | ⭐⭐⭐⭐ 高<br>密文随机,仅索引泄露等值信息 | |
| 24 | +| **Token 化** | 敏感值替换为无意义 Token | ✅ 支持等值 (查 Token)<br>❌ 不支持范围/模糊 | ⭐⭐⭐⭐⭐ 极高<br>数据库不存敏感数据,仅存映射引用 | |
| 25 | +| **保序 / 可搜索加密** | 保留顺序特征 / 分词索引 | ✅ 支持范围 (`>`, `<`) 或<br>✅ 支持模糊 (`LIKE`) | ⭐ 低<br>泄露顺序或文本分布信息 | |
| 26 | + |
| 27 | +**✅ 推荐使用 FieldCrypt 的场景:** |
| 28 | +- 业务需要对敏感字段(如手机号、身份证)进行加密存储以满足合规要求。 |
| 29 | +- **必须保留等值查询能力**(如 `WHERE mobile = ?`),且无法接受引入重量级中间件。 |
| 30 | +- 接受“确定性加密”带来的安全折衷(即:虽然密文不可逆,但相同数据的密文相同)。 |
| 31 | + |
| 32 | +**❌ 不推荐使用的场景:** |
| 33 | +- 需要对加密字段进行**模糊查询** (`LIKE`)、**范围查询** (`>`, `<`) 或**排序**。 |
| 34 | +- 对安全性有极高要求,必须使用随机化加密且同时需要查询(需转向“盲索引”方案)。 |
| 35 | +- 期望在数据库代理层解决,对应用完全透明(建议使用 ShardingSphere 等中间件)。 |
| 36 | + |
| 37 | +--- |
| 38 | + |
| 39 | +## 3. 模块说明 |
| 40 | + |
| 41 | +- **ballcat-fieldcrypt-core**:核心模块,包含注解、算法接口、元数据解析等 |
| 42 | +- **ballcat-fieldcrypt-mybatis**:MyBatis 集成,提供参数加密和结果解密拦截器 |
| 43 | +- **ballcat-fieldcrypt-mybatis-plus**:MyBatis-Plus 集成,支持 Wrapper 条件加密 |
| 44 | +- **ballcat-spring-boot-starter-fieldcrypt**:Spring Boot Starter,自动装配(推荐使用) |
| 45 | + |
| 46 | +业务接入时通常只需要引入 Starter,对 MyBatis / MP 项目都是无侵入集成。 |
| 47 | + |
| 48 | +--- |
| 49 | + |
| 50 | +## 4. 快速接入与基本用法 |
| 51 | + |
| 52 | +### 4.1 Maven 引入 |
| 53 | + |
| 54 | +在业务服务中加入 Starter 依赖: |
| 55 | + |
| 56 | +```xml |
| 57 | +<dependency> |
| 58 | + <groupId>org.ballcat</groupId> |
| 59 | + <artifactId>ballcat-spring-boot-starter-fieldcrypt</artifactId> |
| 60 | +</dependency> |
| 61 | +``` |
| 62 | + |
| 63 | +### 4.2 核心注解 |
| 64 | + |
| 65 | +首先在实体类以及需要加密的字段上添加相应注解: |
| 66 | + |
| 67 | +```java |
| 68 | +@EncryptedEntity // 必须添加!用于快速过滤非加密实体,减少反射开销 |
| 69 | +public class User { |
| 70 | + // 使用默认算法加密 |
| 71 | + @Encrypted |
| 72 | + private String mobile; |
| 73 | + |
| 74 | + // 指定算法和参数(需配合自定义算法实现) |
| 75 | + @Encrypted(algo = "AES_GCM", params = "keyId=123") |
| 76 | + private String secretData; |
| 77 | +} |
| 78 | +``` |
| 79 | + |
| 80 | +**`@Encrypted` 属性说明:** |
| 81 | +- `algo`: 指定加密算法标识(如 `AES_GCM`),留空则使用全局默认算法。 |
| 82 | +- `params`: 传递给算法实现的自定义参数(如密钥版本号、盐值等)。 |
| 83 | +- `mapKeys`: 当字段类型为 `Map` 时,指定需要加密的 Key。 |
| 84 | + |
| 85 | +::: tip 性能优化说明 |
| 86 | +为了提高拦截器性能,组件会**忽略**所有未标注 `@EncryptedEntity` 的类。 |
| 87 | +因此,请务必在实体类上添加该注解,否则内部字段的 `@Encrypted` 将不会生效。 |
| 88 | +::: |
| 89 | + |
| 90 | +### 4.3 场景一:使用 MyBatis XML |
| 91 | + |
| 92 | +对于原生 MyBatis XML 方式,**推荐使用实体对象作为参数**,拦截器会自动识别 `@Encrypted` 字段进行加密。 |
| 93 | + |
| 94 | +**Mapper 接口:** |
| 95 | + |
| 96 | +```java |
| 97 | +@Mapper |
| 98 | +public interface UserMapper { |
| 99 | + // 推荐:传入实体对象,自动加密 mobile 字段 |
| 100 | + User selectByMobile(@Param("u") User u); |
| 101 | +} |
| 102 | +``` |
| 103 | + |
| 104 | +**XML 配置:** |
| 105 | + |
| 106 | +```xml |
| 107 | +<select id="selectByMobile" resultType="...User"> |
| 108 | + <!-- 直接使用 #{u.mobile},拦截器已将其替换为密文 --> |
| 109 | + SELECT * FROM t_user WHERE mobile = #{u.mobile} |
| 110 | +</select> |
| 111 | +``` |
| 112 | + |
| 113 | +### 4.4 场景二:使用 MyBatis-Plus Wrapper |
| 114 | + |
| 115 | +对于 MyBatis-Plus,组件支持在 `Wrapper` 中透明处理加密条件。 |
| 116 | + |
| 117 | +```java |
| 118 | +// 1. 插入:直接 set 明文,自动加密入库 |
| 119 | +User u = new User(); |
| 120 | +u.setMobile("13800138000"); |
| 121 | +userMapper.insert(u); |
| 122 | + |
| 123 | +// 2. 查询:读取时自动解密 |
| 124 | +User dbUser = userMapper.selectById(u.getId()); |
| 125 | + |
| 126 | +// 3. Wrapper 查询:构造条件时传入明文 |
| 127 | +LambdaQueryWrapper<User> qw = Wrappers.lambdaQuery(User.class); |
| 128 | +qw.eq(User::getMobile, "13800138000"); // 自动加密匹配 |
| 129 | +userMapper.selectList(qw); |
| 130 | +``` |
| 131 | + |
| 132 | +::: warning MyBatis-Plus 使用注意事项 |
| 133 | +使用 `QueryWrapper` / `UpdateWrapper` 时,**必须传入实体 Class**(如 `Wrappers.lambdaQuery(User.class)`)。 |
| 134 | +如果不传 Class,MyBatis-Plus 无法获取字段元数据,加密织入逻辑将失效,导致直接使用明文查询而查不到数据。 |
| 135 | +::: |
| 136 | + |
| 137 | +### 4.5 其他场景(非实体参数) |
| 138 | + |
| 139 | +如果方法参数不是实体对象(如直接传 `String` 或 `Map`),需使用参数级注解: |
| 140 | + |
| 141 | +```java |
| 142 | +@Mapper |
| 143 | +public interface UserMapper { |
| 144 | + // 单个字符串参数加密 |
| 145 | + int deleteByMobile(@Encrypted @Param("mobile") String mobile); |
| 146 | + |
| 147 | + // Map 参数加密,需指定 key |
| 148 | + int insertByMap(@Encrypted(mapKeys = {"mobile"}) @Param("data") Map<String, Object> data); |
| 149 | +} |
| 150 | +``` |
| 151 | + |
| 152 | +## 5. 配置说明 |
| 153 | + |
| 154 | +### 5.1 推荐配置 |
| 155 | + |
| 156 | +```yaml |
| 157 | +ballcat: |
| 158 | + fieldcrypt: |
| 159 | + enabled: true # 全局开关 |
| 160 | + aes-key: "your-base64-key..." # 256 位AES密钥 |
| 161 | + fail-fast: true # 加解密失败是否抛出异常 |
| 162 | +``` |
| 163 | +
|
| 164 | +::: warning 安全提示 |
| 165 | +`aes-key` 是数据安全的核心凭证,**严禁以明文形式**出现在代码库或配置文件中。 |
| 166 | + |
| 167 | +1. **基础防护**:使用 **Jasypt** 等工具对配置文件中的敏感值进行加密,确保即使配置文件泄露,攻击者也无法直接获取密钥。 |
| 168 | +2. **进阶防护**:对接 **KMS (Key Management Service)** 或 **Vault**。通过自定义 `CryptoAlgorithm` 实现密钥的动态获取或信封加密,确保明文密钥仅在内存中短暂存在,绝不落盘。 |
| 169 | + ::: |
| 170 | + |
| 171 | +### 5.2 全量配置项详解 |
| 172 | + |
| 173 | +| 配置项 | 类型 | 默认值 | 说明 | |
| 174 | +|--------|------|--------|------| |
| 175 | +| `ballcat.fieldcrypt.enabled` | Boolean | `true` | 全局开关,控制整个加解密功能是否启用。 | |
| 176 | +| `ballcat.fieldcrypt.enable-parameter` | Boolean | `true` | 参数加密拦截器开关。 | |
| 177 | +| `ballcat.fieldcrypt.enable-result` | Boolean | `true` | 结果解密拦截器开关。 | |
| 178 | +| `ballcat.fieldcrypt.fail-fast` | Boolean | `true` | 加/解密异常是否快速失败;`true` 时抛出异常,`false` 时记录告警并保留原值。 | |
| 179 | +| `ballcat.fieldcrypt.restore-plaintext` | Boolean | `true` | SQL 执行后是否将方法参数恢复为明文;建议保持开启,避免业务逻辑读取到密文。 | |
| 180 | +| `ballcat.fieldcrypt.default-algo` | String | `AES_CBC_FIXED_IV` | 默认算法 ID;留空/未知时自动回退至 `AES_CBC_FIXED_IV`。切换算法需考虑数据迁移。 | |
| 181 | +| `ballcat.fieldcrypt.aes-key` | String | - | AES 密钥(Base64 编码);默认算法 `AES_CBC_FIXED_IV` 必需。 | |
| 182 | + |
| 183 | +## 6. 运行机制与数据迁移 |
| 184 | + |
| 185 | +### 6.1 加解密流程 |
| 186 | + |
| 187 | +组件通过 MyBatis 拦截器 + MyBatis-Plus Wrapper 织入,实现透明加解密: |
| 188 | + |
| 189 | +```mermaid |
| 190 | +sequenceDiagram |
| 191 | + participant App as 业务代码 |
| 192 | + participant Interceptor as 加密拦截器 |
| 193 | + participant DB as 数据库 |
| 194 | +
|
| 195 | + Note over App, DB: 写入流程 |
| 196 | + App->>Interceptor: 传入明文参数 (User{mobile="138..."}) |
| 197 | + Interceptor->>Interceptor: 识别 @Encrypted 字段 |
| 198 | + Interceptor->>Interceptor: 执行加密 (User{mobile="ENC:xyz..."}) |
| 199 | + Interceptor->>DB: 执行 SQL (INSERT/UPDATE) |
| 200 | + DB-->>Interceptor: 执行完成 |
| 201 | + Interceptor->>App: 回滚参数为明文 (User{mobile="138..."}) |
| 202 | +
|
| 203 | + Note over App, DB: 查询流程 |
| 204 | + App->>DB: 执行查询 |
| 205 | + DB-->>Interceptor: 返回结果集 (mobile="ENC:xyz...") |
| 206 | + Interceptor->>Interceptor: 识别结果字段 |
| 207 | + Interceptor->>Interceptor: 执行解密 |
| 208 | + Interceptor-->>App: 返回明文对象 (User{mobile="138..."}) |
| 209 | +``` |
| 210 | + |
| 211 | +### 6.2 混合存储与平滑迁移 |
| 212 | + |
| 213 | +FieldCrypt 采用 **`ENC:` 前缀机制** 来保证幂等性和兼容性: |
| 214 | + |
| 215 | +- **加密时**:检查值是否以 `ENC:` 开头。如果是,视为已加密,跳过;否则进行加密并添加前缀。 |
| 216 | +- **解密时**:检查值是否以 `ENC:` 开头。如果是,去掉前缀并解密;否则视为明文,原样返回。 |
| 217 | + |
| 218 | +**存量数据迁移方案:** |
| 219 | +1. **第一阶段(共存期)**:上线 FieldCrypt,新写入数据会自动加密(带 `ENC:`),旧数据仍为明文。读取时组件能自动兼容这两种格式。 |
| 220 | +2. **第二阶段(清洗期)**:编写脚本批量读取旧数据,重新 update 回去(组件会自动加密),或者直接在数据库层面用 SQL/程序批量刷数。 |
| 221 | +3. **第三阶段(完成)**:所有敏感字段均为密文。 |
| 222 | + |
| 223 | +### 6.3 明文回滚机制 |
| 224 | + |
| 225 | +默认开启 `restore-plaintext: true`。 |
| 226 | +在 MyBatis 执行 SQL 时,拦截器会修改参数对象中的字段为密文。为了防止这个“脏对象”污染后续的业务逻辑(例如 Service 层在插入后又要用到该对象),组件会在 SQL 执行完毕后,自动将参数恢复为之前的明文状态。 |
| 227 | + |
| 228 | + |
| 229 | +## 7. 扩展与自定义 |
| 230 | + |
| 231 | +### 7.1 自定义加密算法 |
| 232 | + |
| 233 | +实现 `CryptoAlgorithm` 接口并注册为 Bean,即可替换默认算法或新增算法: |
| 234 | + |
| 235 | +```java |
| 236 | +@Component |
| 237 | +public class AesGcmAlgorithm implements CryptoAlgorithm { |
| 238 | + @Override |
| 239 | + public String algo() { return "AES_GCM"; } // 算法标识 |
| 240 | +
|
| 241 | + @Override |
| 242 | + public String encrypt(String plain, CryptoContext ctx) { ... } |
| 243 | +
|
| 244 | + @Override |
| 245 | + public String decrypt(String cipher, CryptoContext ctx) { ... } |
| 246 | +} |
| 247 | +``` |
| 248 | + |
| 249 | +### 7.2 自定义 CryptoEngine |
| 250 | + |
| 251 | +若需定制更复杂的路由策略(如多租户不同密钥、不同前缀策略),可实现 `CryptoEngine` 接口并替换默认 Bean。 |
| 252 | + |
| 253 | + |
| 254 | +## 8. 常见问题 (FAQ) |
| 255 | + |
| 256 | +**Q: 这个方案安全吗?** |
| 257 | +A: 默认方案(AES-CBC + 固定 IV)主要用于合规和防拖库,属于“基础防护”。如需更高安全性(如防频率分析),请扩展使用随机 IV 算法(如 AES-GCM),但会失去等值查询能力。 |
| 258 | + |
| 259 | +**Q: 已有明文数据如何处理?** |
| 260 | +A: 详见“6.2 混合存储与平滑迁移”。组件会将无前缀数据视为明文,支持渐进式迁移。 |
| 261 | + |
| 262 | +**Q: 加密后字段长度如何规划?** |
| 263 | +A: 密文长度 ≈ `(原长度 * 4/3) + padding + 前缀(4字节)`。建议手机号字段预留 `VARCHAR(64)`。 |
| 264 | + |
| 265 | + |
| 266 | +## 9. 故障排查 |
| 267 | + |
| 268 | +### 9.1 MyBatis-Plus 查询不到数据? |
| 269 | +- **检查 Wrapper 构造**:确认是否传入了 `Class`,如 `Wrappers.lambdaQuery(User.class)`。 |
| 270 | +- **检查日志**:开启 DEBUG 日志,确认是否有 `ByteBuddy weaver installed` 提示。 |
| 271 | + |
| 272 | +### 9.2 写入后数据库仍是明文? |
| 273 | +- **检查注解**:确认实体类上有 `@EncryptedEntity`,字段上有 `@Encrypted`。 |
| 274 | +- **检查对象复用**:如果在同一个对象上连续做多次操作,可能会因为回滚机制导致后续操作使用明文。 |
| 275 | + |
| 276 | +### 9.3 Mapper 返回 String/List 未解密? |
| 277 | +- **检查注解**:非实体类返回结果(如 `List<String>`),需要在 Mapper 方法上添加 `@DecryptResult` 注解。 |
0 commit comments