使用Mybatis-plus拦截加密数据

使用自定义注解来标识需要加密的po和字段,并通过mybaitsplus的插件工具类Interceptor类来实现对数据的拦截与加密转换操作。
一、自定义加密注解
作用在类上的注解

package com.richstonedt.cmp.model.annotate;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.TYPE})//该参数代表是作用在类
@Retention(RetentionPolicy.RUNTIME)
public @interface SensitiveBean {
    String value() default "";
}

作用在字段上的注解

package com.richstonedt.cmp.model.annotate;
import java.lang.annotation.*;

@Target({ ElementType.FIELD })
@Retention(RetentionPolicy.RUNTIME)
public @interface EncryptField {
    String value() default "";
}

二、加密拦截
拦截执行为update的操作。首先对包含了SensitiveBean注解的类进行判断,包含则对字字段中包含EncryptField注解的字段数据进行加密操作。(此操作过程通过反射和Field进行操作,有更好的建议可以提出来哦~)
在定义拦截操作时,最重要的是@Intercept中的定义,method决定能拦截到的mybatis的操作方法的类别

package com.richstonedt.cmp.common.interceptor;

import com.richstonedt.cmp.common.util.EncryptUtil;
import com.richstonedt.cmp.model.annotate.EncryptField;
import com.richstonedt.cmp.model.annotate.SensitiveBean;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.binding.MapperMethod;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.executor.parameter.ParameterHandler;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ParameterMap;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Signature;
import org.springframework.util.StringUtils;

import java.lang.reflect.Field;

//update包含insert、update、delete
@Intercepts({
        @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})
})
@Slf4j
public class ParammeterInterceptor implements Interceptor {

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        String methodName = invocation.getMethod().getName();
        if (!"update".equalsIgnoreCase(methodName) && !"insert".equalsIgnoreCase(methodName)) {
           return invocation.proceed();
        }
        // 获取该sql语句放入的参数
        Object parameter = invocation.getArgs()[1];
        if (parameter instanceof ParameterMap) {
            MapperMethod.ParamMap paramMap = (MapperMethod.ParamMap) invocation.getArgs()[1];
            parameter = paramMap.get("param1");
        }

        if (parameter instanceof MapperMethod.ParamMap) {
            MapperMethod.ParamMap paramMap = (MapperMethod.ParamMap) invocation.getArgs()[1];
            try {
                parameter = paramMap.get("et");
            } catch (Exception e) {
                parameter = paramMap.get("param1");
            }
        }

        if (parameter != null && isSensitiveBean(parameter)) {
            // 对参数内含注解的字段进行加密
            encryptField(parameter);
        }
        return invocation.proceed();
    }

    /**
     * 判断是否带有敏感的实体类
     *
     * @param t
     * @param <T>
     * @return
     */
    public <T> boolean isSensitiveBean(T t) {
        Class object = null;
        try {
            object = (Class) t;
        } catch (Exception e) {
            object = t.getClass();
        }
        if (object != null && object.isAnnotationPresent(SensitiveBean.class)) {
            return true;
        }
        return false;
    }

    /**
     * 加密类中的敏感字段
     *
     * @param t
     * @param <T>
     */
    private <T> void encryptField(T t) {
        Field[] declaredFields = t.getClass().getDeclaredFields();
        if (declaredFields != null && declaredFields.length > 0) {
            for (Field field : declaredFields) {
                // 查找当字段带有加密注解,并且字段类型为字符串类型
                if (field.isAnnotationPresent(EncryptField.class)) {
                    field.setAccessible(true);
                    String fieldValue = null;
                    try {
                        fieldValue = (String) field.get(t);
                    } catch (IllegalAccessException e) {
                        e.printStackTrace();
                    }
                    if (!StringUtils.isEmpty(fieldValue)) {
                        try {
                            field.set(t, EncryptUtil.aesEncrypt(fieldValue));
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }
    }
}

三、解密拦截
同加密拦截相同,通过SensitiveBean和EncryptField进行判断是否需要解密,并通过进一步字符特征判断是否是加密过的数据,再考虑解密。

package com.richstonedt.cmp.common.interceptor;

import com.richstonedt.cmp.common.util.EncryptUtil;
import com.richstonedt.cmp.model.annotate.EncryptField;
import com.richstonedt.cmp.model.annotate.SensitiveBean;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;

@Intercepts({
        @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
})
@Slf4j
public class ResultInterceptor implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        Object returnValue = invocation.proceed();
        try {
            // 当返回值类型为数组集合时,就判断是否需要进行数据解密
            if (returnValue instanceof ArrayList<?>) {
                List<?> list = (List<?>) returnValue;
                if (CollectionUtils.isEmpty(list)) {
                    return returnValue;
                }
                Object object = list.get(0);
                // 判断第一个对象是否有解密注解
                if (object != null && isSensitiveBean(object)) {
                    for (Object o : list) {
                        decryptField(o);
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
            return returnValue;
        }
        return returnValue;
    }

    /**
     * 解密
     *
     * @param t
     * @param <T>
     */
    public <T> void decryptField(T t) {
        Field[] declaredFields = t.getClass().getDeclaredFields();
        if (declaredFields != null && declaredFields.length > 0) {
            // 遍历这些字段
            for (Field field : declaredFields) {
                // 如果这个字段存在解密注解就进行解密
                if (field.isAnnotationPresent(EncryptField.class) && field.getType().toString().endsWith("String")) {
                    field.setAccessible(true);
                    try {
                        // 获取这个字段的值
                        String fieldValue = (String) field.get(t);
                        // 判断这个字段的数值是否不为空
                        if (!StringUtils.isEmpty(fieldValue)) {
                            if (EncryptUtil.isEncrypted(fieldValue)){
                                // 进行解密
                                String encryptData = EncryptUtil.aesDecrypt(fieldValue);
                                // 将值反射到对象中
                                field.set(t, encryptData);
                            }else {
                                log.info("该数据未被加密");
                            }
                        }
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }

    /**
     * 判断是否带有敏感的实体类
     *
     * @param t
     * @param <T>
     * @return
     */
    public <T> boolean isSensitiveBean(T t) {
        Class object=null;
        try {
            object=(Class) t;
        }catch (Exception e){
            object=t.getClass();
        }
        if (object!=null&&object.isAnnotationPresent(SensitiveBean.class)) {
            return true;
        }
        return false;
    }
}

四、加密方法
EncryptUtil的加密解密方法可自行定义。aesEncrypt为加密,aesDecrypt为解密。因为本人当时的项目加密要求是后面提的,因多种原因使用了AES加密,偏移量模式选择了ECB(默认为CBC),选择ECB是因为postgresql数据库中的加密方法为aes的ecb

package com.richstonedt.cmp.common.util;

import org.apache.commons.codec.binary.Base64;
import org.springframework.util.StringUtils;

import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.math.BigDecimal;
import java.security.Key;
import java.util.regex.Pattern;

public class EncryptUtil {
    private static final String KEY = "f4k9f5w7f8g4er26";  // 密匙,必须16位
    private static final String OFFSET = "5e8y6w45ju8w9jq8"; // 偏移量
    private static final String ENCODING = "UTF-8"; // 编码
    private static final String ALGORITHM = "AES"; //算法
    private static final String CIPHER_ALGORITHM = "AES/ECB/PKCS5Padding"; // 默认的加密算法,CBC模式
    private static final String BASE64_REGX= "^([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{4}|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}==)$"; //验证

    /**
     * 加密
     * @param data
     * @return
     * @throws Exception
     */
    public static String aesEncrypt(String data) throws Exception {
        Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
        SecretKeySpec skeySpec = new SecretKeySpec(KEY.getBytes("ASCII"), ALGORITHM);
        //IvParameterSpec iv = new IvParameterSpec(OFFSET.getBytes());//CBC模式偏移量IV
        cipher.init(Cipher.ENCRYPT_MODE, skeySpec);
        byte[] encrypted = cipher.doFinal(data.getBytes(ENCODING));
        return new Base64().encodeToString(encrypted);//加密后再使用BASE64做转码
    }

    /**
     * 解密
     * @param data
     * @return
     * @throws Exception
     */
    public static String aesDecrypt(String data) throws Exception {
        data.replace(" ","+");
        Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
        SecretKeySpec skeySpec = new SecretKeySpec(KEY.getBytes("ASCII"), ALGORITHM);
        //IvParameterSpec iv = new IvParameterSpec(OFFSET.getBytes()); //CBC模式偏移量IV
        cipher.init(Cipher.DECRYPT_MODE, skeySpec);
        byte[] buffer = new Base64().decode(data);//先用base64解码
        byte[] encrypted=encrypted = cipher.doFinal(buffer);
        return new String(encrypted, ENCODING);
    }


    /**
     * 验证是否加密
     * @param data
     * @return
     */
    public static Boolean isEncrypted(String data){
        if (Pattern.matches(BASE64_REGX,data)&&data.endsWith("==")){
            return true;
        }
        return false;
    }
}