找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

194

积分

0

好友

24

主题
发表于 昨天 23:30 | 查看: 1| 回复: 0

在数据安全日益重要的今天,保护用户的敏感信息(如手机号、身份证号、银行卡号)是每个开发者的重要职责。直接将这类信息明文存储在数据库中,一旦发生数据泄露,后果不堪设想。

传统的做法是在每个业务代码中进行手动加解密,但这会导致代码重复、逻辑分散且容易遗漏。本文将介绍一种基于注解的自动化加解密方案,通过 Spring Boot + MyBatis 实现,让敏感字段的加密存储与解密使用对业务代码完全透明。

图片

传统加密方式面临的问题

1. 代码高度重复 在每个涉及敏感数据的查询和插入操作中,都需要手动调用加解密方法,导致相同逻辑遍布各处。

// 查询时需要手动解密
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);

2. 维护与修改困难

  • 新增一个需要加密的字段时,需要修改所有相关的业务方法。
  • 极易在某个查询或更新操作中遗漏加解密步骤。
  • 加解密逻辑与业务逻辑深度耦合,代码可读性差。

3. 维护成本高昂

  • 加密逻辑分散在各个业务层,难以统一管理和监控。
  • 出现问题(如加解密异常)时,排查链路长,定位困难。
  • 新成员理解业务时,还需额外理解分散的加密规则。

理想的解决方案

我们期望的解决方案应具备以下特点:

  • 声明式:通过简单的注解标记需要加密的字段。
  • 自动化:数据的加密存储与解密读取过程自动完成,无需业务层干预。
  • 非侵入:业务代码无需感知加密逻辑的存在。
  • 高性能:加解密过程对系统性能影响可控。

解决方案设计

核心思路

  1. 注解标记:使用自定义的 @Encrypted 注解标注实体类中的敏感字段。
  2. 拦截处理:利用 MyBatis拦截器 在SQL执行前后自动进行加解密操作。
  3. 透明操作:对上层业务代码而言,整个加解密过程是无感的。

技术架构

业务层代码 -> MyBatis Mapper -> 拦截器(自动加/解密) -> 数据库

本质是在数据持久化层(MyBatis)与数据库之间,插入一个透明的加解密处理层。

核心实现步骤

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长度为12字节
    private static final SecretKey secretKey = // 密钥应从安全配置获取,此处简化

    public static String encrypt(String plaintext) throws Exception {
        // 生成随机初始化向量(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(StandardCharsets.UTF_8));

        // 组合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) throws Exception {
        // 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, StandardCharsets.UTF_8);
    }

    // 简单判断字符串是否已被加密(根据特定格式,实际可根据需求调整)
    public static boolean isEncrypted(String value) {
        try {
            if (value == null) return false;
            byte[] data = Base64.getDecoder().decode(value);
            // 简单判断长度,一个有效的“IV+密文”结构应大于IV长度
            return data.length > IV_LENGTH;
        } catch (IllegalArgumentException e) {
            return false; // 非Base64字符串,视为未加密
        }
    }
}

加密后的数据格式为:Base64(IV + 密文)

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})
})
@Slf4j
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 = invocation.getArgs()[1];
            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 (obj instanceof Map || obj instanceof Collection || isBasicType(obj.getClass())) {
            return;
        }
        processFields(obj, true);
    }

    private void decryptResult(Object result) {
        if (result instanceof List) {
            for (Object item : (List<?>) result) {
                processFields(item, false);
            }
        } else if (result != null) {
            processFields(result, false);
        }
    }

    private void processFields(Object obj, boolean isEncrypt) {
        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) {
                        String strValue = (String) value;
                        boolean alreadyEncrypted = CryptoUtil.isEncrypted(strValue);
                        if (isEncrypt && !alreadyEncrypted) {
                            // 加密:仅加密未加密的明文
                            String encrypted = CryptoUtil.encrypt(strValue);
                            field.set(obj, encrypted);
                        } else if (!isEncrypt && alreadyEncrypted) {
                            // 解密:仅解密切实是密文的数据
                            String decrypted = CryptoUtil.decrypt(strValue);
                            field.set(obj, decrypted);
                        }
                        // 其他情况(如重复加密/解密)保持原样
                    }
                } catch (Exception e) {
                    log.error("处理加密字段[{}]时发生异常", field.getName(), e);
                }
            }
        }
    }

    private boolean isBasicType(Class<?> clazz) {
        return clazz.isPrimitive() || clazz == String.class || Number.class.isAssignableFrom(clazz) || clazz == Boolean.class;
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {
        // 可读取自定义配置
    }
}

4. 自动配置

通过Spring Boot的自动配置机制,方便地启用该拦截器。

@Configuration
@ConditionalOnProperty(name = "app.encryption.enabled", havingValue = "true", matchIfMissing = true)
public class EncryptionAutoConfiguration {
    @Bean
    public ConfigurationCustomizer encryptionConfigurationCustomizer() {
        return configuration -> {
            configuration.addInterceptor(new EncryptionInterceptor());
        };
    }
}

application.yml 中简单配置即可启用:

app:
  encryption:
    enabled: true

方案效果演示

数据库存储对比

原始明文数据

姓名: 张三
手机: 13812345678
邮箱: zhangsan@example.com

数据库实际存储(加密后)

姓名: 张三
手机: nTuVgMWime1:hFGa9as6JHxLT2vG8dpiRmu4wtxDnkTEr/1x (示例,非真实加密结果)
邮箱: mK7pL9xQ2rS8vN3w:jKxL9mN2pQ7rS8vT3wX4yZ6aB8cD1eF2g

业务层代码使用

业务代码完全无需处理加解密细节,与操作普通字段无异。

@Service
public class UserService {
    @Autowired
    private UserMapper userMapper;

    // 保存用户:手机号和邮箱在入库时会被拦截器自动加密
    public void createUser(User user) {
        user.setPhone("13812345678");
        user.setEmail("zhangsan@example.com");
        userMapper.insert(user); // 加密在insert执行前自动发生
    }

    // 查询用户:查询结果中的加密字段会被拦截器自动解密
    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. 密钥管理

切勿将密钥硬编码在代码中。应从安全的环境变量、配置中心或专业的密钥管理服务(如HashiCorp Vault, AWS KMS)中获取。

@Configuration
public class EncryptionConfig {
    @Value("${app.encryption.secret-key}")
    private String base64EncodedKey;

    @Bean
    public SecretKey secretKey() {
        byte[] keyBytes = Base64.getDecoder().decode(base64EncodedKey);
        // 确保密钥长度符合AES要求(如128, 192, 256位)
        return new SecretKeySpec(keyBytes, "AES");
    }
}

2. 日志脱敏

避免敏感信息在日志中明文输出,应使用脱敏工具进行处理。

@Component
public class SensitiveDataMasker {
    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);
    }
}

总结与适用场景

方案优势

  • 开发友好:通过注解声明,接入成本极低。
  • 职责清晰:加解密逻辑集中在拦截器,与业务代码解耦。
  • 安全合规:采用 AES-GCM 等强加密算法,满足一般性数据安全要求。
  • 易于维护:加密策略统一管理,修改和扩展方便。

适用场景

  • 用户中心(保护手机、邮箱、身份证号)
  • 支付系统(保护银行卡号、CVV)
  • 医疗健康系统(保护病历、健康信息)
  • 任何涉及个人敏感信息(PII)存储的业务系统

局限性

  • 性能开销:加解密操作会带来额外的CPU消耗,在高并发、大数据量场景下需进行性能评估。
  • 查询限制:加密后的数据无法直接进行数据库层面的模糊查询(LIKE)、范围查询和排序。若需此类功能,需设计更复杂的方案(如盲索引)。
  • 迁移成本:对已有明文数据的存量系统进行改造时,需要考虑数据迁移方案。

总的来说,这种基于 MyBatis拦截器 的注解驱动加密方案,为Spring Boot应用提供了一种优雅、非侵入的字段级数据保护手段,非常适合在安全合规要求下,对特定敏感字段进行透明加解密的场景。

代码仓库

完整示例代码可参考:


https://github.com/yuboon/java-examples/tree/master/springboot-column-encryption
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区(YunPan.Plus) ( 苏ICP备2022046150号-2 )

GMT+8, 2025-12-3 14:20 , Processed in 1.171121 second(s), 41 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2025 CloudStack.

快速回复 返回顶部 返回列表