注解方式实现数据库字段加密与解密

目录

前言

一些敏感信息存入数据需要进行加密处理,比如电话号码,身份证号码等,从数据库取出到前端展示时需要解密,如果分别在存入取出时去做处理,会很繁锁,至此,我查了很多相关资料,最后得到一个比较完美的解决方案。

实现步骤

定义注解

1、实体注解@SensitiveEntity

bash 复制代码
import java.lang.annotation.*;

@Target({ElementType.TYPE, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface SensitiveEntity {

}

2、字段注解@SensitiveEntity

bash 复制代码
import java.lang.annotation.*;

@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface SensitiveField {

}

加密工具类

AesFieldUtils.java

bash 复制代码
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.Base64Utils;

import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import java.util.regex.Matcher;
import java.util.regex.Pattern;


@Slf4j
@Component
public class AesFieldUtils {
    /**
     * 加密算法
     */
    private final String KEY_ALGORITHM = "AES";
    /**
     * 算法/模式/补码方式
     */
    private final String DEFAULT_CIPHER_ALGORITHM = "AES/ECB/PKCS5Padding";
    /**
     * 编码格式
     */
    private final String CODE = "utf-8";
    /**
     * base64验证规则
     */
    private static final String BASE64_RULE = "^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)=?$";

    /**
     * 正则验证对象
     */
    private static final Pattern PATTERN = Pattern.compile(BASE64_RULE);
    /**
     * 加解密 密钥key
     */
    @Value("${aes.key}")
    private String key;


    /**
     * @param content 加密字符串
     * @return 加密结果
     */
    public String encrypt(String content) {
        return encrypt(content, key);
    }

    /**
     * 加密
     *
     * @param content 加密参数
     * @param key     加密key
     * @return 结果字符串
     */
    public String encrypt(String content, String key) {
        //判断如果已经是base64加密字符串则返回原字符串
        if (isBase64(content)) {
            return content;
        }
        // 为了安全起见,暂时不加密,需要时再放开
        //return content;
         byte[] encrypted = encrypt2bytes(content, key);
        if (null == encrypted || encrypted.length < 1) {
            log.error("加密字符串[{}]转字节为null", content);
            return null;
        }
        return Base64Utils.encodeToString(encrypted);

    }

    /**
     * @param content 加密字符串
     * @param key     加密key
     * @return 返回加密字节
     */
    public byte[] encrypt2bytes(String content, String key) {
        try {
            byte[] raw = key.getBytes(CODE);
            SecretKeySpec secretKeySpec = new SecretKeySpec(raw, KEY_ALGORITHM);
            Cipher cipher = Cipher.getInstance(DEFAULT_CIPHER_ALGORITHM);
            cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec);
            return cipher.doFinal(content.getBytes(CODE));
        } catch (Exception e) {
            log.error("failed to encrypt: {} of {}", content, e);
            return null;
        }
    }

    /**
     * @param content 加密字符串
     * @return 返回加密结果
     */
    public String decrypt(String content) {
        try {
            return decrypt(content, key);
        } catch (Exception e) {
            log.error("failed to decrypt: {}, e: {}", content, e);
            return null;
        }
    }

    /**
     * 解密
     *
     * @param content 解密字符串
     * @param key     解密key
     * @return 解密结果
     */
    public String decrypt(String content, String key) throws Exception {
        //不是base64格式字符串则不进行解密
        if (!isBase64(content)) {
            return content;
        }
        // 为了安全起见,暂时不加密解密,需要时再放开
        //return content;
        return decrypt(Base64Utils.decodeFromString(content), key);
    }

    /**
     * @param content 解密字节
     * @param key     解密key
     * @return 返回解密内容
     */
    public String decrypt(byte[] content, String key) throws Exception {
        if (key == null) {
            log.error("AES key should not be null");
            return null;
        }

        byte[] raw = key.getBytes(CODE);
        SecretKeySpec keySpec = new SecretKeySpec(raw, KEY_ALGORITHM);
        Cipher cipher = Cipher.getInstance(DEFAULT_CIPHER_ALGORITHM);
        cipher.init(Cipher.DECRYPT_MODE, keySpec);
        try {
            byte[] original = cipher.doFinal(content);
            return new String(original, CODE);
        } catch (Exception e) {
            log.error("failed to decrypt content: {}/ key: {}, e: {}", content, key, e);
            return null;
        }
    }

    /**
     * 判断是否为 base64加密
     *
     * @param str 参数
     * @return 结果
     */
    public static boolean isBase64(String str) {
        Matcher matcher = PATTERN.matcher(str);
        return matcher.matches();
    }

}

其中aes.key为加密key随便填

定义mybatis拦截器

MyBatisInterceptor.java

bash 复制代码
import cn.hutool.core.util.ReflectUtil;
import com.hw.common.annotation.SensitiveEntity;
import com.hw.common.service.AesService;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.binding.MapperMethod;
import org.apache.ibatis.executor.parameter.ParameterHandler;
import org.apache.ibatis.executor.resultset.ResultSetHandler;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.plugin.*;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;

import javax.annotation.Resource;
import java.lang.reflect.Field;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Objects;
import java.util.Properties;

@Component
@Intercepts({
        @Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class}),
        @Signature(type = ParameterHandler.class, method = "setParameters", args = PreparedStatement.class),
        @Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class})
})
@Slf4j
public class MyBatisInterceptor implements Interceptor {
    @Resource
    private AesService aesService;

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        Object target = invocation.getTarget();
        //拦截sql结果处理器
        if (target instanceof ResultSetHandler) {
            return resultDecrypt(invocation);
        }
        //拦截sql参数处理器
        if (target instanceof ParameterHandler) {
            return parameterEncrypt(invocation);
        }
        //拦截sql语句处理器
        if (target instanceof StatementHandler) {
            return replaceSql(invocation);
        }
        return invocation.proceed();
    }

    /**
     * 对mybatis映射结果进行字段解密
     *
     * @param invocation 参数
     * @return 结果
     * @throws Throwable 异常
     */
    private Object resultDecrypt(Invocation invocation) throws Throwable {
        //取出查询的结果
        Object resultObject = invocation.proceed();
        if (Objects.isNull(resultObject)) {
            return null;
        }
        //基于selectList
        if (resultObject instanceof ArrayList) {
            ArrayList resultList = (ArrayList) resultObject;
            if (CollectionUtils.isEmpty(resultList)) {
                return resultObject;
            }
            for (Object result : resultList) {
                if (needToDecrypt(result)) {
                    //逐一解密
                    aesService.decrypt(result);
                }
            }
            //基于selectOne
        } else {
            if (needToDecrypt(resultObject)) {
                aesService.decrypt(resultObject);
            }
        }
        return resultObject;
    }

    /**
     * mybatis映射参数进行加密
     *
     * @param invocation 参数
     * @return 结果
     * @throws Throwable 异常
     */
    private Object parameterEncrypt(Invocation invocation) throws Throwable {
        //@Signature 指定了 type= parameterHandler 后,这里的 invocation.getTarget() 便是parameterHandler
        //若指定ResultSetHandler ,这里则能强转为ResultSetHandler
        ParameterHandler parameterHandler = (ParameterHandler) invocation.getTarget();
        // 获取参数对像,即 mapper 中 paramsType 的实例
        Field parameterField = parameterHandler.getClass().getDeclaredField("parameterObject");
        parameterField.setAccessible(true);
        //取出实例
        Object parameterObject = parameterField.get(parameterHandler);

        if(parameterHandler.getParameterObject() instanceof MapperMethod.ParamMap){
            MapperMethod.ParamMap paramMap = (MapperMethod.ParamMap) parameterHandler.getParameterObject();
            parameterObject = paramMap.get("param1");
        }
        if (null == parameterObject) {
            return invocation.proceed();
        }
        Class<?> parameterObjectClass = parameterObject.getClass();
        //校验该实例的类是否被@SensitiveEntity所注解
        SensitiveEntity sensitiveEntity = AnnotationUtils.findAnnotation(parameterObjectClass, SensitiveEntity.class);
        //未被@SensitiveEntity所注解 则为null
        if (Objects.isNull(sensitiveEntity)) {
            return invocation.proceed();
        }
        //取出当前当前类所有字段,传入加密方法
        //Field[] declaredFields = parameterObjectClass.getDeclaredFields();
        Field[] allFields = ReflectUtil.getFields(parameterObjectClass);
        aesService.encrypt(allFields, parameterObject);
        return invocation.proceed();
    }

    /**
     * 替换mybatis Sql中的加密Key
     *
     * @param invocation 参数
     * @return 结果
     * @throws Throwable 异常
     */
    private Object replaceSql(Invocation invocation) throws Throwable {
        StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
        BoundSql boundSql = statementHandler.getBoundSql();
        //获取到原始sql语句
        String sql = boundSql.getSql();
        if (null == sql){
            return invocation.proceed();
        }
        //通过反射修改sql语句
        Field field = boundSql.getClass().getDeclaredField("sql");
        field.setAccessible(true);
        field.set(boundSql, sql);
        return invocation.proceed();
    }

    /**
     * 判断是否包含需要加解密对象
     *
     * @param object 参数
     * @return 结果
     */
    private boolean needToDecrypt(Object object) {
        if(Objects.isNull(object)){
            return false;
        }
        Class<?> objectClass = object.getClass();
        Class<?> parentClass = objectClass.getSuperclass();
        SensitiveEntity sensitiveEntity = AnnotationUtils.findAnnotation(objectClass, SensitiveEntity.class);
        SensitiveEntity parentSensitiveEntity = AnnotationUtils.findAnnotation(parentClass, SensitiveEntity.class);

        return Objects.nonNull(sensitiveEntity) || Objects.nonNull(parentSensitiveEntity);
    }

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

    @Override
    public void setProperties(Properties properties) {

    }

}

总结

mybatis拦截器非常强大,它可以对所有存入数据库的数据进行处理,包括更新,插入,查询和删除...,对数据做统一处理非常方便。

相关推荐
NineData19 分钟前
NineData云原生智能数据管理平台新功能发布|2025年5月版
数据库·云原生·oracle·devops·ninedata
不会编程的猫星人19 分钟前
Oracle杀进程注意事项
数据库·microsoft·oracle
GUIQU.23 分钟前
【Oracle】安装单实例
数据库·oracle
老胖闲聊29 分钟前
Python Django完整教程与代码示例
数据库·python·django
践行见远34 分钟前
django之请求处理过程分析
数据库·django·sqlite
行星0081 小时前
Postgresql常用函数操作
数据库·postgresql
程序员葵安1 小时前
【Java Web】9.Maven高级
java·数据库·后端·maven
海棠一号2 小时前
Android Settings 数据库生成、监听与默认值配置
android·数据库
新时代苦力工2 小时前
MVCC机制:Undo Log版本链与ReadView机制
数据库·mysql
GUIQU.2 小时前
【Oracle】存储过程
数据库·oracle