使用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;
}
}