利用mybatis插件和注解完成数据入库的加解密

536 阅读3分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

## 引言

因为业务需求的变更,最近接到了一个关于在最小改变原有代码基础上,通过注解去对敏感数据入库出库的加解密的需求。

1. 设置aes加密方法

import org.springframework.stereotype.Component;

import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;

/**
 * author:
 * @Param
 * @return
 **/
@Component
public class ADESUtils implements EncrptUtils {
    private static final String defaultV = "6859505890402435";
    private static final String key = "1061697007556132";
    private static final String head = "aes_";
    private static SecretKeySpec getKey(String strKey) throws Exception {
        byte[] arrBTmp = strKey.getBytes();
        byte[] arrB = new byte[16]; // 创建一个空的16位字节数组(默认值为0)
        for (int i = 0; i < arrBTmp.length && i < arrB.length; i++) {
            arrB[i] = arrBTmp[i];
        }
        SecretKeySpec skeySpec = new SecretKeySpec(arrB, "AES");
        return skeySpec;
    }

    /**
     * 加密.
     * @param content
     * @return
     * @throws Exception
     */
    public String encrypt(String content) throws Exception {
        final Base64.Encoder encoder = Base64.getEncoder();
        SecretKeySpec skeySpec = getKey(key);
        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        IvParameterSpec iv = new IvParameterSpec(defaultV.getBytes());
        cipher.init(Cipher.ENCRYPT_MODE, skeySpec, iv);
        byte[] encrypted = cipher.doFinal(content.getBytes());
        String encodedText = (head + encoder.encodeToString(encrypted));
        return encodedText;
    }

    /**
     * 判断加密.
     * @param content
     * @return
     * @throws Exception
     */
    public boolean isEncrypt(String content) throws Exception {
        String decrypt = content.substring(0, 4);
        if (decrypt.equals(head)) {
            return true;
        }
        return false;
    }

    /**
     * 解密.
     * @param content
     * @return
     * @throws Exception
     */
    public String decrypt(String content) throws Exception {
        final Base64.Decoder decoder = Base64.getDecoder();
        SecretKeySpec skeySpec = getKey(key);
        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        IvParameterSpec iv = new IvParameterSpec(defaultV.getBytes());
        cipher.init(Cipher.DECRYPT_MODE, skeySpec, iv);
        byte[] base64 = decoder.decode(content.substring(4));
        byte[] original = cipher.doFinal(base64);
        String originalString = new String(original);
        return originalString;
    }
}

** 2.对象加解密**

import com.copote.comb.common.annotate.DecryptField;
import com.copote.comb.common.annotate.EncryptField;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.lang.reflect.Field;
import java.util.Objects;

/**
* author:lxw.
* @Param
* @return
**/
@Component
public class CryptPojoUtils {

   @Value("${key.rule}")
   private String ruler;

   @Autowired
   private EncrptUtils encrptUtils;

   /**
    * 对对象t加密.
    * @return
    */
   public <T> T encrypt(Field[] declaredFields, T paramsObject) throws IllegalAccessException {
       for (Field field : declaredFields) {
           //取出所有被EncryptTransaction注解的字段
           EncryptField encryptTransaction = field.getAnnotation(EncryptField.class);
           if (!Objects.isNull(encryptTransaction)) {
               field.setAccessible(true);
               Object object = field.get(paramsObject);
               //暂时只实现String类型的加密
               if (object instanceof String) {
                   String value = (String) object;
                   //加密
                   try {
                       field.set(paramsObject, encrptUtils.encrypt(value));
                   } catch (Exception e) {
                       e.printStackTrace();
                   }
               }
           }
       }
       return paramsObject;
   }

   /**
    * 对象解密.
    * @param <T>
    * @return
    */
   public <T> T decrypt(T result) throws Exception {
       //取出resultType的类
       Class<?> resultClass = result.getClass();
       Field[] declaredFields = resultClass.getDeclaredFields();
       for (Field field : declaredFields) {
           //取出所有被DecryptTransaction注解的字段
           DecryptField decryptTransaction = field.getAnnotation(DecryptField.class);
           if (!Objects.isNull(decryptTransaction)) {
               field.setAccessible(true);
               Object object = field.get(result);
               //String的解密
               if (object instanceof String) {
                   String value = (String) object;
                   if (encrptUtils.isEncrypt(value)) {
                       //对注解的字段进行逐一解密
                       try {
                           field.set(result, encrptUtils.decrypt(value));
                       } catch (Exception e) {
                           e.printStackTrace();
                       }
                   }

               }
           }
       }
       return result;
   }
}

3.设置注解

import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * author:lxw.
 * @Param
 * @return
 **/
@Inherited
@Target({ ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
public @interface CryptAnnotation {
    String value() default "";
}
/**
 * author:lxw.
 * 解密注解
 */
@Documented
@Inherited
@Target({ElementType.FIELD })
@Retention(RetentionPolicy.RUNTIME)
public @interface DecryptField {
    String value() default "";
}
/**
 * author:lxw.
 * 加密注解
 */
@Documented
@Inherited
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface EncryptField {
    String value() default "";
}

4.入库拦截对象加密

import com.copote.comb.common.annotate.CryptAnnotation;
import org.apache.ibatis.executor.parameter.ParameterHandler;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Plugin;
import org.apache.ibatis.plugin.Signature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.stereotype.Component;

import java.lang.reflect.Field;
import java.sql.PreparedStatement;
import java.util.List;
import java.util.Objects;
import java.util.Properties;

/**
 * 数据库更新操作拦截器.
 * 一、支持的使用场景
 * ①场景一:通过mybatis-plus BaseMapper自动映射的方法
 * ②场景一:通过mapper接口自定义的方法,更新对象为自定义DO
 * 二、使用方法
 * ①在自定义的DO上加上注解 CryptAnnotation
 * ②在需要加解密的字段属性上加上CryptAnnotation
 * @author: lxw
 */
@Component
@Intercepts(value =
        {@Signature(type = ParameterHandler.class// 确定要拦截的对象
                , method = "setParameters", args = PreparedStatement.class// 拦截方法的参数
        ) })
public class UpdateInterceptor implements Interceptor {

    @Autowired
    private CryptPojoUtils cryptService;

    @Override
    public Object intercept(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 = parameterHandler.getParameterObject();

        if (parameterObject != null) {
            if (parameterObject instanceof List) {
                List<Object> list = (List)parameterObject;
                if (list.size() != 0) {
                    for (int i = 0; i < list.size(); i++) {
                        Object obj = list.get(i);
                        Class<?> objectClass = obj.getClass();
                        //校验该实例的类是否被@SensitiveData所注解
                        CryptAnnotation sensitiveData = AnnotationUtils.findAnnotation(objectClass, CryptAnnotation.class);
                        if (Objects.nonNull(sensitiveData)) {
                            Field[] declaredFields = objectClass.getDeclaredFields();
                            cryptService.encrypt(declaredFields, obj);
                        }
                    }
                }
            } else {
                Class<?> parameterObjectClass = parameterObject.getClass();
                //校验该实例的类是否被@SensitiveData所注解
                CryptAnnotation sensitiveData = AnnotationUtils.findAnnotation(parameterObjectClass, CryptAnnotation.class);
                if (Objects.nonNull(sensitiveData)) {
                    //取出当前当前类所有字段,传入加密方法
                    Field[] declaredFields = parameterObjectClass.getDeclaredFields();
                    cryptService.encrypt(declaredFields,parameterObject);
                }
            }
        }
        return invocation.proceed();
    }

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

    @Override
    public void setProperties(Properties properties) {

    }

}

5.出库拦截解密

import com.copote.comb.common.annotate.CryptAnnotation;
import org.apache.ibatis.executor.resultset.ResultSetHandler;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Plugin;
import org.apache.ibatis.plugin.Signature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.stereotype.Component;

import java.sql.Statement;
import java.util.ArrayList;
import java.util.Objects;
import java.util.Properties;

/**
 * 查询拦截器.
 * 查询条件加密使用方式:使用 @Param("decrypt")注解的自定义类型
 * 返回结果解密使用方式: ①在自定义的DO上加上注解 CryptAnnotation  ②在需要加解密的字段属性上加上CryptAnnotation
 * @author: lxw
 */
@Component
@Intercepts({
        @Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class})
})
public class QueryInterceptor implements Interceptor {

    @Autowired
    private CryptPojoUtils encryptDecrypt;

    @Autowired
    private ADESUtils adesUtils;

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        Object result = invocation.proceed();
        if (Objects.isNull(result)) {
            return null;
        }

        if (result instanceof ArrayList) {
            ArrayList resultList = (ArrayList) result;
            if (resultList.size() != 0) {
                for (int i = 0; i < resultList.size(); i++) {
                    if (needToDecrypt(resultList.get(i))) {
                        encryptDecrypt.decrypt(resultList.get(i));
                    }
                }
            }
        } else {
            if (needToDecrypt(result)) {
                encryptDecrypt.decrypt(result);
            }
        }
        return result;
    }

    public boolean needToDecrypt(Object object) {
        Class<?> parameterObjectClass = object.getClass();
        CryptAnnotation sensitiveData = AnnotationUtils.findAnnotation(parameterObjectClass, CryptAnnotation.class);
        if (Objects.nonNull(sensitiveData)) {
            return true;
        }
        return false;
    }

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

    @Override
    public void setProperties(Properties properties) {

    }
}

6.对需要拦截实体类添加注解

@Data
@CryptAnnotation
public class OpenProgramOfflineEntity implements Serializable {

    @ApiModelProperty(value = "业务值key")
    @EncryptField
    @Deprecated
    private String busiKey;

    @ApiModelProperty(value = "业务名")
    private String busiName;

    @ApiModelProperty(value = "业务值")
    private String busiValue;
    
}