SpringBoot+MyBatis实现数据库字段级加密

 更新时间:2025年11月17日 08:17:57   作者:风象南  
在数据安全越来越受重视的今天,如何保护用户的敏感信息成为每个开发者都要面对的问题,本文将分享一个基于注解的自动加解密方案,感兴趣的小伙伴可以了解下

前言

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

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

总结

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

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

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

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

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

适用场景

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

不适用场景

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

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

到此这篇关于SpringBoot+MyBatis实现数据库字段级加密的文章就介绍到这了,更多相关SpringBoot MyBatis数据库字段加密内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • SpringBoot集成PageHelper及使用方法详解

    SpringBoot集成PageHelper及使用方法详解

    这篇文章主要介绍了SpringBoot集成PageHelper及使用方法详解,PageHelper 是一个开源的 Java 分页插件,它可以帮助开发者简化分页操作,本文提供部分相关代码,需要的朋友可以参考下
    2023-10-10
  • 浅谈使用java实现阿里云消息队列简单封装

    浅谈使用java实现阿里云消息队列简单封装

    这篇文章主要介绍了浅谈使用java实现阿里云消息队列简单封装,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2018-03-03
  • SpringBoot中的@Import注解四种使用方式详解

    SpringBoot中的@Import注解四种使用方式详解

    这篇文章主要介绍了SpringBoot中的@Import注解四种使用方式详解,@Import注解只可以标注在类上,可以结合 @Configuration注解、ImportSelector、ImportBeanDefinitionRegistrar一起使用,也可以导入普通的类,需要的朋友可以参考下
    2023-12-12
  • Eclipse导出安卓apk文件的图文教程

    Eclipse导出安卓apk文件的图文教程

    这篇文章主要为大家详细介绍了Eclipse导出安卓apk文件的图文教程,以图文结合的方式为大家分享了Eclipse是如何导出安卓apk文件的步骤,感兴趣的小伙伴们可以参考一下
    2016-06-06
  • Java自旋锁及自旋的好处详解

    Java自旋锁及自旋的好处详解

    这篇文章主要介绍了Java自旋锁及自旋的好处详解,自旋就是自己在这里不停地循环,直到目标达成,而不像普通的锁那样,如果获取不到锁就进入阻塞,需要的朋友可以参考下
    2023-10-10
  • java PDF添加图层的方法 支持多页图层添加

    java PDF添加图层的方法 支持多页图层添加

    这篇文章主要为大家详细介绍了java PDF添加图层的方法,支持多页图层添加,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2018-02-02
  • 浅谈JSON的数据交换、缓存问题和同步问题

    浅谈JSON的数据交换、缓存问题和同步问题

    这篇文章主要介绍了浅谈JSON的数据交换、缓存问题和同步问题,具有一定借鉴价值,需要的朋友可以参考下
    2017-12-12
  • SpringBoot整合MyBatisPlus详解

    SpringBoot整合MyBatisPlus详解

    这篇文章详细介绍了SpringBoot整合mybatisplus的全过程,文中有详细的代码示例,具有一定的参考价值,需要的朋友可以参考一下
    2023-04-04
  • Spring动态多数据源配置实例Demo

    Spring动态多数据源配置实例Demo

    本篇文章主要介绍了Spring动态多数据源配置实例Demo,具有一定的参考价值,有兴趣的可以了解一下。
    2017-01-01
  • IDEA怎么生成UML类图的实现

    IDEA怎么生成UML类图的实现

    这篇文章主要介绍了IDEA怎么生成UML类图的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-09-09

最新评论