⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 风象南 「风象南」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注微信公众号:【芋道源码】有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言都将得到认真回复。甚至不知道如何读源码也可以请教噢。
  4. 新的源码解析文章实时收到通知。每周更新一篇左右。
  5. 认真的源码交流微信群。

前言

在数据安全越来越受重视的今天,如何保护用户的敏感信息成为每个开发者都要面对的问题。比如用户的手机号、身份证、银行卡这些信息,如果直接存在数据库里,一旦数据泄露,后果很严重。

传统的做法是在每个查询和插入的地方手动加解密,但这样做代码会变得很乱,而且容易遗漏。今天分享一个基于注解的自动加解密方案,通过 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);

修改很麻烦

  • 新增加密字段要改很多地方
  • 容易忘记某个查询的加解密
  • 代码到处都是加解密逻辑

维护成本高

  • 加密逻辑分散在各个方法里
  • 出了问题很难排查
  • 新人接手要理解整套加密逻辑

我们需要什么样的方案

理想中的方案应该是:

  • 在需要加密的字段上加个注解就行
  • 加解密过程自动完成
  • 业务代码不用关心加密逻辑
  • 性能要好,不能影响正常业务

解决方案设计

核心思路

1. 注解标记:用 @Encrypted 注解标记要加密的字段

2. 拦截处理:MyBatis 拦截器自动处理加解密

3. 透明操作:业务代码感觉不到加密的存在

技术架构

业务代码 → 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;

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
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. 自动配置

配置拦截器自动生效:

@Configuration
@ConditionalOnProperty(name = "encryption.enabled", havingValue = "true", matchIfMissing = true)
public class EncryptionAutoConfiguration {

@Bean
public ConfigurationCustomizer encryptionConfigurationCustomizer() {
return configuration -> {
configuration.addInterceptor(new EncryptionInterceptor());
};
}
}

只需要在配置文件中启用:

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);
}
}

总结

这个基于注解的字段级加密方案有以下几个优点:

使用简单:只需要在字段上加个注解就行

代码干净:业务代码不用关心加解密逻辑

安全可靠:使用标准的加密算法

容易维护:所有加密逻辑集中管理

适用场景:

  • 用户管理系统
  • 支付系统
  • 医疗信息系统
  • 任何需要保护敏感数据的系统

不适用场景:

  • 对性能要求极高的系统
  • 需要对加密字段进行复杂查询的场景
  • 数据量特别大的系统

如果你也有保护敏感数据的需求,这个方案值得考虑。代码量不大,但效果很明显。

文章目录
  1. 1. 前言
  2. 2. 遇到的实际问题
    1. 2.0.1. 传统加密方式的问题
    2. 2.0.2. 我们需要什么样的方案
  • 3. 解决方案设计
    1. 3.0.1. 核心思路
    2. 3.0.2. 技术架构
  • 4. 核心实现
    1. 4.0.1. 1. 定义注解
    2. 4.0.2. 2. 加密工具
    3. 4.0.3. 3. MyBatis 拦截器
    4. 4.0.4. 4. 自动配置
  • 5. 使用效果
    1. 5.0.1. 数据库存储情况
    2. 5.0.2. 业务代码使用
  • 6. 安全考虑
    1. 6.0.1. 1. 密钥管理
    2. 6.0.2. 2. 日志安全
  • 7. 总结