在数据安全日益受到重视的今天,如何有效保护用户的敏感信息成为每一位开发者必须直面的挑战。例如,用户的手机号、身份证号、银行卡号等数据,如果以明文形式直接存储在数据库中,一旦发生数据泄露,后果将不堪设想。
传统的做法是在每个数据查询和插入的地方手动调用加解密方法。但这种做法会导致代码严重重复,逻辑分散,不仅开发效率低下,还极易因遗漏处理而造成安全隐患。今天,我们将分享一种基于注解的自动加解密方案,通过 Spring Boot 与 MyBatis 的巧妙结合,实现对指定字段的自动加密存储与自动解密使用,让业务代码保持简洁与纯粹。

传统加密方式面临的问题
代码重复太多
在传统的实现方式中,加解密逻辑无处不在:
// 每个查询都要手动处理
User user = userMapper.findById(id);
user.setPhone(decrypt(user.getPhone()));
user.setEmail(decrypt(user.getEmail()));
user.setIdCard(decrypt(user.getIdCard()));
return user;
// 插入时也要手动加密
User newUser = new User();
newUser.setPhone(encrypt(phone));
newUser.setEmail(encrypt(email));
userMapper.insert(newUser);
修改维护困难
- 新增加密字段时,需要修改大量的业务代码。
- 容易在某个查询或写入流程中遗忘加解密步骤。
- 加解密逻辑像“牛皮癣”一样散布在各个业务方法中,代码整洁度极差。
维护成本高昂
- 加密逻辑分散,难以统一管理和升级。
- 一旦出现加解密相关问题,排查链路长,定位困难。
- 新成员接手项目时,需要花费大量时间理解这套分散的加密体系。
我们理想中的方案是怎样的?
理想的解决方案应该满足以下几点:
- 声明式配置:只需在需要加密的字段上加一个注解即可。
- 过程透明:加解密过程对业务代码完全透明,自动完成。
- 业务无感:业务逻辑层无需关心数据如何加解密。
- 性能可控:加解密过程高效,对正常业务性能的影响在可接受范围内。
解决方案核心设计
核心思路
- 注解标记:使用自定义的
@Encrypted 注解来标记实体类中需要加密的字段。
- 拦截处理:利用 MyBatis 的拦截器(Interceptor)在 SQL 执行前后自动进行加解密操作。
- 透明操作:整个加解密过程在框架层完成,对上层业务代码无侵入,实现“透明化”处理。
技术架构
业务代码 → MyBatis Mapper → 拦截器 → 自动加解密 → 数据库
简而言之,就是在业务逻辑与 数据库 之间,插入了一层透明的加解密处理层。
核心实现步骤
1. 定义加密注解
首先,我们定义一个简单的注解,用于标记需要加密的实体类字段:
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Encrypted {
// 是否支持模糊查询(此处为扩展点,本文不深入)
boolean supportFuzzyQuery() default false;
}
在实体类中使用该注解:
public class User {
private Long id;
private String username;
@Encrypted // 标记此字段需要自动加密
private String phone;
@Encrypted // 标记此字段需要自动加密
private String email;
}
2. 实现加密工具类
我们采用 AES-GCM 算法进行加密,该算法能同时保证数据的机密性和完整性。
public class CryptoUtil {
private static final String ALGORITHM = "AES/GCM/NoPadding";
private static final int IV_LENGTH = 12; // GCM推荐的IV长度
public static String encrypt(String plaintext) {
// 生成随机IV
byte[] iv = new byte[IV_LENGTH];
new SecureRandom().nextBytes(iv);
// 执行加密
Cipher cipher = Cipher.getInstance(ALGORITHM);
cipher.init(Cipher.ENCRYPT_MODE, secretKey, new GCMParameterSpec(128, iv));
byte[] ciphertext = cipher.doFinal(plaintext.getBytes());
// 将IV与密文拼接后,进行Base64编码存储
byte[] encryptedData = new byte[iv.length + ciphertext.length];
System.arraycopy(iv, 0, encryptedData, 0, iv.length);
System.arraycopy(ciphertext, 0, encryptedData, iv.length, ciphertext.length);
return Base64.getEncoder().encodeToString(encryptedData);
}
public static String decrypt(String encryptedText) {
// Base64解码
byte[] encryptedData = Base64.getDecoder().decode(encryptedText);
// 分离出IV和密文
byte[] iv = Arrays.copyOfRange(encryptedData, 0, IV_LENGTH);
byte[] ciphertext = Arrays.copyOfRange(encryptedData, IV_LENGTH, encryptedData.length);
// 执行解密
Cipher cipher = Cipher.getInstance(ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, secretKey, new GCMParameterSpec(128, iv));
byte[] plaintext = cipher.doFinal(ciphertext);
return new String(plaintext);
}
// 一个简单的启发式方法,用于判断字符串是否已被加密(避免重复加密)
public static boolean isEncrypted(String value) {
// 根据加密后格式判断,例如包含特定分隔符
return value != null && value.contains(":");
}
}
加密后的数据格式为:Base64(IV):Base64(密文)。
3. 实现 MyBatis 拦截器
这是整个方案最核心的部分,拦截器负责在数据入库前自动加密,在查询出库后自动解密。
@Intercepts({
@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
})
public class EncryptionInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
String methodName = invocation.getMethod().getName();
// UPDATE/INSERT 操作:拦截参数并加密标记字段
if ("update".equals(methodName)) {
Object parameter = getParameter(invocation);
if (shouldEncrypt(parameter)) {
encryptFields(parameter);
}
}
// 执行原始SQL
Object result = invocation.proceed();
// SELECT 操作:拦截结果并解密标记字段
if ("query".equals(methodName)) {
decryptResult(result);
}
return result;
}
private void encryptFields(Object obj) {
if (obj == null) return;
// 只处理实体对象,忽略基本类型、Map、Collection等
if (isBasicType(obj.getClass()) || obj instanceof Map || obj instanceof Collection) {
return;
}
Class<?> clazz = obj.getClass();
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
if (field.isAnnotationPresent(Encrypted.class)) {
try {
field.setAccessible(true);
Object value = field.get(obj);
// 如果是String类型且未被加密过,则进行加密
if (value instanceof String && !CryptoUtil.isEncrypted((String) value)) {
String encrypted = CryptoUtil.encrypt((String) value);
field.set(obj, encrypted);
}
} catch (Exception e) {
log.error("加密字段失败: {}", field.getName(), e);
}
}
}
}
private void decryptResult(Object result) {
if (result instanceof List) {
for (Object item : (List<?>) result) {
decryptFields(item);
}
} else if (result != null) {
decryptFields(result);
}
}
private void decryptFields(Object obj) {
Class<?> clazz = obj.getClass();
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
if (field.isAnnotationPresent(Encrypted.class)) {
try {
field.setAccessible(true);
Object value = field.get(obj);
// 如果是String类型且已被加密,则进行解密
if (value instanceof String && CryptoUtil.isEncrypted((String) value)) {
String decrypted = CryptoUtil.decrypt((String) value);
field.set(obj, decrypted);
}
} catch (Exception e) {
log.error("解密字段失败: {}", field.getName(), e);
}
}
}
}
}
4. 配置拦截器自动生效
通过自动配置类,将我们实现的拦截器注册到 MyBatis 中。
@Configuration
@ConditionalOnProperty(name = "encryption.enabled", havingValue = "true", matchIfMissing = true)
public class EncryptionAutoConfiguration {
@Bean
public ConfigurationCustomizer encryptionConfigurationCustomizer() {
return configuration -> {
configuration.addInterceptor(new EncryptionInterceptor());
};
}
}
在 application.yml 中简单启用即可:
encryption:
enabled: true
方案使用效果
数据库存储对比
原始明文数据:
姓名: 张三
手机: 13812345678
邮箱: zhangsan@example.com
身份证: 110101199001011234
数据库实际存储(自动加密后):
姓名: 张三
手机: nTuVgMWime1:hFGa9as6JHxLT2vG8dpiRmu4wtxDnkTEr/1x
邮箱: mK7pL9xQ2rS8vN3w:jKxL9mN2pQ7rS8vT3wX4yZ6aB8cD1eF2g
身份证: X1Y2Z3A4B5C6D7E8:F9G0H1I2J3K4L5M6N7O8P9Q0R1S2T3U4V
业务代码保持简洁
在服务层,业务代码无需任何加解密操作,清晰易懂:
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
// 插入用户:数据会被拦截器自动加密后存储
public void createUser(User user) {
// 直接设置明文,拦截器会处理加密
user.setPhone("13812345678");
user.setEmail("zhangsan@example.com");
userMapper.insert(user); // 加密在此步骤自动发生
}
// 查询用户:数据会被拦截器自动解密后返回
public User getUser(Long id) {
User user = userMapper.findById(id); // 解密在此步骤自动发生
// 获取到的已是解密后的明文
System.out.println(user.getPhone()); // 输出:13812345678
System.out.println(user.getEmail()); // 输出:zhangsan@example.com
return user;
}
}
生产环境安全考量
1. 密钥管理
切勿将加密密钥硬编码在代码中。正确的做法是从安全的环境变量或配置中心获取。
@Configuration
public class EncryptionConfig {
@Value("${encryption.key}") // 从配置文件读取,配置文件本身由运维通过安全方式注入
private String encryptionKey;
@Bean
public SecretKey getSecretKey() {
byte[] keyBytes = Base64.getDecoder().decode(encryptionKey);
return new SecretKeySpec(keyBytes, "AES");
}
}
2. 日志安全
确保敏感信息不会出现在日志文件中,需要对日志输出进行脱敏处理。
public class SensitiveDataLogger {
public String maskPhone(String phone) {
if (phone == null || phone.length() != 11) return phone;
return phone.substring(0, 3) + "****" + phone.substring(7);
}
public String maskEmail(String email) {
if (email == null) return email;
int atIndex = email.indexOf("@");
if (atIndex <= 2) return "***" + email.substring(atIndex);
return email.substring(0, 2) + "***" + email.substring(atIndex);
}
}
方案总结
这套基于注解和 MyBatis 拦截器的字段级加密方案,具备以下显著优点:
- 使用简单:仅需一个注解,声明即生效。
- 代码干净:业务层零侵入,职责分离清晰。
- 安全可靠:基于 AES-GCM 等标准加密算法。
- 维护方便:加解密逻辑集中管理于拦截器和工具类中,易于升级和调试。
适用场景:
- 用户管理系统(保护手机、邮箱、身份证号)
- 支付金融系统(保护银行卡号、交易信息)
- 医疗健康系统(保护病历、个人健康信息)
- 任何涉及用户隐私、需合规存储敏感数据的业务系统。
需要注意的局限:
- 性能开销:加解密过程必然带来额外的CPU计算开销,在对性能极端敏感的场景需充分测试。
- 查询限制:字段加密后,基于该字段的
LIKE模糊查询、范围查询等将无法直接进行,需设计额外的解决方案(如映射表、密文索引等)。
- 数据量影响:海量数据下的加解密性能需要评估。
源码仓库
完整的示例代码可以在以下仓库中找到:
https://github.com/yuboon/java-examples/tree/master/springboot-column-encryption
总的来说,这是一个在业务中台或后台系统中平衡安全性与开发效率的实用方案。如果你正在为如何优雅地处理数据加密而烦恼,不妨在 云栈社区 与更多开发者交流,尝试将这套方案应用到你的项目中。