前言
在数据安全越来越受重视的今天,如何保护用户的敏感信息成为每个开发者都要面对的问题。比如用户的手机号、身份证、银行卡这些信息,如果直接存在数据库里,一旦数据泄露,后果很严重。
传统的做法是在每个查询和插入的地方手动加解密,但这样做代码会变得很乱,而且容易遗漏。今天分享一个基于注解的自动加解密方案,通过 Spring Boot + MyBatis 实现,让敏感字段自动加密存储,自动解密使用。
遇到的实际问题
传统加密方式的问题
代码重复太多
java
// 每个查询都要手动处理
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);
修改很麻烦
- 新增加密字段要改很多地方
- 容易忘记某个查询的加解密
- 代码到处都是加解密逻辑
维护成本高
- 加密逻辑分散在各个方法里
- 出了问题很难排查
- 新人接手要理解整套加密逻辑
我们需要什么样的方案
理想中的方案应该是:
- 在需要加密的字段上加个注解就行
- 加解密过程自动完成
- 业务代码不用关心加密逻辑
- 性能要好,不能影响正常业务
解决方案设计
核心思路
1. 注解标记 :用 @Encrypted 注解标记要加密的字段
2. 拦截处理:MyBatis 拦截器自动处理加解密
3. 透明操作:业务代码感觉不到加密的存在
技术架构
业务代码 → MyBatis Mapper → 拦截器 → 自动加解密 → 数据库
简单来说,就是在业务层和数据库之间加了一层透明的加解密处理。
核心实现
1. 定义注解
先定义一个简单的注解来标记需要加密的字段:
java
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Encrypted {
// 是否支持模糊查询
boolean supportFuzzyQuery() default false;
}
使用方式:
java
public class User {
private Long id;
private String username;
@Encrypted // 这个字段会自动加密
private String phone;
@Encrypted // 这个字段也会自动加密
private String email;
}
2. 加密工具
使用 AES-GCM 算法进行加密:
java
public class CryptoUtil {
private static final String ALGORITHM = "AES/GCM/NoPadding";
private static final int IV_LENGTH = 12;
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 拦截器
拦截器是整个方案的核心,负责自动加解密:
java
@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
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);
if (value instanceof String && !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);
if (value instanceof String && isEncrypted((String) value)) {
String decrypted = CryptoUtil.decrypt((String) value);
field.set(obj, decrypted);
}
} catch (Exception e) {
log.error("解密字段失败: {}", field.getName(), e);
}
}
}
}
}
4. 自动配置
配置拦截器自动生效:
java
@Configuration
@ConditionalOnProperty(name = "encryption.enabled", havingValue = "true", matchIfMissing = true)
public class EncryptionAutoConfiguration {
@Bean
public ConfigurationCustomizer encryptionConfigurationCustomizer() {
return configuration -> {
configuration.addInterceptor(new EncryptionInterceptor());
};
}
}
只需要在配置文件中启用:
yaml
encryption:
enabled: true
使用效果
数据库存储情况
原始数据:
makefile
用户信息:
姓名: 张三
手机: 13812345678
邮箱: zhangsan@example.com
身份证: 110101199001011234
数据库存储(自动加密后):
makefile
用户信息:
姓名: 张三
手机: nTuVgMWime1:hFGa9as6JHxLT2vG8dpiRmu4wtxDnkTEr/1x
邮箱: mK7pL9xQ2rS8vN3w:jKxL9mN2pQ7rS8vT3wX4yZ6aB8cD1eF2g
身份证: X1Y2Z3A4B5C6D7E8:F9G0H1I2J3K4L5M6N7O8P9Q0R1S2T3U4V
业务代码使用
java
@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. 密钥管理
实际项目中不要把密钥写死在代码里:
java
@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. 日志安全
避免在日志中打印敏感信息:
java
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);
}
}
总结
这个基于注解的字段级加密方案有以下几个优点:
使用简单:只需要在字段上加个注解就行
代码干净:业务代码不用关心加解密逻辑
安全可靠:使用标准的加密算法
容易维护:所有加密逻辑集中管理
适用场景:
- 用户管理系统
- 支付系统
- 医疗信息系统
- 任何需要保护敏感数据的系统
不适用场景:
- 对性能要求极高的系统
- 需要对加密字段进行复杂查询的场景
- 数据量特别大的系统
如果你也有保护敏感数据的需求,这个方案值得考虑。代码量不大,但效果很明显。
仓库
ruby
https://github.com/yuboon/java-examples/tree/master/springboot-column-encryption