一、写在前面

日常开发中,经常有一些敏感数据,直接写入数据库的话,很容易泄露。
本文基于mybatis拦截器插件,实现敏感数据的加解密。

二、编码实现

1、注解

import java.lang.annotation.*;

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface EncryptedField {
    String algorithm() default "AES";
}

2、拦截器插件


import com.example.encryption.annotation.EncryptedField;
import com.example.encryption.service.EncryptionService;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.plugin.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.lang.reflect.Field;
import java.util.Properties;

@Component
@Intercepts({
    @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
    @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, org.apache.ibatis.session.RowBounds.class, org.apache.ibatis.session.ResultHandler.class})
})
public class EncryptDecryptInterceptor implements Interceptor {
    @Autowired
    private EncryptionService encryptionService;

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        MappedStatement ms = (MappedStatement) invocation.getArgs()[0];
        Object parameter = invocation.getArgs()[1];

        // 加密处理 INSERT/UPDATE
        if (ms.getSqlCommandType() == SqlCommandType.INSERT || ms.getSqlCommandType() == SqlCommandType.UPDATE) {
            handleEncryption(parameter);
        }

        Object result = invocation.proceed();

        // 解密处理 SELECT
        if (ms.getSqlCommandType() == SqlCommandType.SELECT) {
            handleDecryption(result);
        }

        return result;
    }

    private void handleEncryption(Object parameter) throws Exception {
        if (parameter == null) return;

        for (Field field : parameter.getClass().getDeclaredFields()) {
            if (field.isAnnotationPresent(EncryptedField.class)) {
                field.setAccessible(true);
                Object value = field.get(parameter);
                if (value instanceof String) {
                    field.set(parameter, encryptionService.encrypt((String) value));
                }
            }
        }
    }

    private void handleDecryption(Object result) throws Exception {
        if (result == null) return;

        if (result instanceof java.util.Collection) {
            for (Object obj : (java.util.Collection<?>) result) {
                decryptObject(obj);
            }
        } else {
            decryptObject(result);
        }
    }

    private void decryptObject(Object obj) throws Exception {
        for (Field field : obj.getClass().getDeclaredFields()) {
            if (field.isAnnotationPresent(EncryptedField.class)) {
                field.setAccessible(true);
                Object value = field.get(obj);
                if (value instanceof String) {
                    field.set(obj, encryptionService.decrypt((String) value));
                }
            }
        }
    }

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

    @Override
    public void setProperties(Properties properties) {}
}

3、配置插件

import com.example.encryption.interceptor.EncryptDecryptInterceptor;
import org.mybatis.spring.boot.autoconfigure.ConfigurationCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MyBatisConfig {
    @Bean
    public ConfigurationCustomizer configurationCustomizer(EncryptDecryptInterceptor interceptor) {
        return configuration -> configuration.addInterceptor(interceptor);
    }
}

4、实体类


import com.example.encryption.annotation.EncryptedField;

public class User {
    private Long id;
    private String username;
    
    @EncryptedField
    private String idCard;
    
    @EncryptedField
    private String phoneNumber;

    // Getters and Setters
    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }
    public String getUsername() { return username; }
    public void setUsername(String username) { this.username = username; }
    public String getIdCard() { return idCard; }
    public void setIdCard(String idCard) { this.idCard = idCard; }
    public String getPhoneNumber() { return phoneNumber; }
    public void setPhoneNumber(String phoneNumber) { this.phoneNumber = phoneNumber; }

    // toString
    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", username='" + username + '\'' +
                ", idCard='" + idCard + '\'' +
                ", phoneNumber='" + phoneNumber + '\'' +
                '}';
    }

}

5、测试


import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;

import com.example.encryption.entity.User;
import com.example.encryption.mapper.UserMapper;

@SpringBootApplication
public class EncryptionDemoApplication {
    public static void main(String[] args) {
        ConfigurableApplicationContext run = SpringApplication.run(EncryptionDemoApplication.class, args);
        
        UserMapper userMapper = run.getBean(UserMapper.class);
        // 增
        User user = new User();
        user.setId(1L);
        user.setUsername("test1");
        user.setIdCard("111111111111");
        user.setPhoneNumber("1311111");
        userMapper.insert(user);
        System.out.println(userMapper.selectById(1L));;

        // 改
        user.setUsername("test2");
        user.setIdCard("2222222222");
        user.setPhoneNumber("1322222222");
        userMapper.updateById(user);
        System.out.println(userMapper.selectById(1L));;
        System.out.println(userMapper.selectById(1L));;


    }
}

在这里插入图片描述

三、扩展

1、优化点

1、插件使用反射对类进行赋值、获取值,为了提高性能,可以考虑将字段进行缓存(使用ConcurrentHashMap)
2、加解密方法,可以考虑扩展成接口,加密方式可扩展。
3、本内容只支持MyBatis简单场景,MyBatisPlus、分页场景、参数为List、Map或者复杂对象,需要对参数进一步递归处理。

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐