Realization of Encryption and Decryption of Sensitive Fields in mybatis plus Interceptor
- 2021-12-11 17:38:27
- OfStack
Directory background 1. Query interceptor 2. Insert and update interceptor 3. Notes
Background
When the database saves data, some sensitive data need desensitization or encryption. If it is obviously heavy and error-prone to add one by one, interceptors can be considered at this time. This paper aims at mybatis-plus as the persistence layer framework, and other scenarios have not been tested. The code is as follows:
1. Query interceptor
package com.sfpay.merchant.service.interceptor;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.sfpay.merchant.service.service.CryptService;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.binding.MapperMethod;
import org.apache.ibatis.cache.CacheKey;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
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.beans.factory.annotation.Autowired;
import java.util.ArrayList;
import java.util.Map;
import java.util.Objects;
/**
* @describe: Query interceptor
* Query Criteria Encryption Usage: Use @Param("decrypt") Custom type of annotation
* Return result decryption usage: ① In the custom DO Add a note to it CryptAnnotation ② Add the field attributes to be encrypted and decrypted CryptAnnotation
* @author: ***
* @date: 2021/3/30 17:51
*/
@Slf4j
@Intercepts({
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class,
RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class,
RowBounds.class, ResultHandler.class})
})
public class QueryInterceptor implements Interceptor {
/**
* Query parameter name, ParamMap Adj. key Value
*/
private static final String DECRYPT = "decrypt";
@Autowired
private CryptService cryptService;
@Autowired
private UpdateInterceptor updateInterceptor;
@Override
public Object intercept(Invocation invocation) throws Throwable {
// Gets the query parameters and whether the query criteria need to be encrypted
Object[] args = invocation.getArgs();
Object parameter = args[1];
Object result = null;
// Setting the Execution Identity
boolean flag = true;
if (parameter instanceof MapperMethod.ParamMap) {
Map paramMap = (Map) parameter;
if (paramMap.containsKey(DECRYPT)) {
Object queryParameter = paramMap.get(DECRYPT);
if (updateInterceptor.needToCrypt(queryParameter)) {
// Execute sql Restore the encrypted message
MappedStatement mappedStatement = (MappedStatement) args[0];
result = updateInterceptor.proceed(invocation, mappedStatement, queryParameter);
flag = false;
}
}
}
// Do you need to execute
if (flag) {
result = invocation.proceed();
}
if (Objects.isNull(result)) {
return null;
}
// Returns the list data, loops through
if (result instanceof ArrayList) {
ArrayList resultList = (ArrayList) result;
if (CollectionUtils.isNotEmpty(resultList) && updateInterceptor.needToCrypt(resultList.get(0))) {
for (Object o : resultList) {
cryptService.decrypt(o);
}
}
} else if (updateInterceptor.needToCrypt(result)) {
cryptService.decrypt(result);
}
// Return results
return result;
}
}
2. Insert and update the interceptor
package com.sfpay.merchant.service.interceptor;
import com.baomidou.mybatisplus.annotation.TableId;
import com.sfpay.merchant.common.util.annotation.CryptAnnotation;
import com.sfpay.merchant.service.service.CryptService;
import org.apache.ibatis.binding.MapperMethod;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
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.defaults.DefaultSqlSession;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.util.Arrays;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
/**
* @describe: 数据库更新操作拦截器
* 1. 支持的使用场景
* ①场景1:通过mybatis-plus BaseMapper自动映射的方法
* ②场景1:通过mapper接口自定义的方法,更新对象为自定义DO
* 2. 使用方法
* ①在自定义的DO上加上注解 CryptAnnotation
* ②在需要加解密的字段属性上加上CryptAnnotation
* ③自定义映射方法在需要加解密的自定义DO参数使用@Param("et")
* @author: ***
* @date: 2021/3/31 17:51
*/
@Intercepts({@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})})
public class UpdateInterceptor implements Interceptor {
@Autowired
private CryptService cryptService;
/**
* 更新参数名称,ParamMap的key值
*/
private static final String CRYPT = "et";
@Override
public Object intercept(Invocation invocation) throws Throwable {
//代理类方法参数,该拦截器拦截的update方法有两个参数args = {MappedStatement.class, Object.class}
Object[] args = invocation.getArgs();
//获取方法参数
MappedStatement mappedStatement = (MappedStatement) args[0];
Object parameter = args[1];
if (Objects.isNull(parameter)) {
//无参数,直接放行
return invocation.proceed();
}
// 如果是多个参数或使用Param注解(Param注解会将参数放置在ParamMap中)
if (parameter instanceof MapperMethod.ParamMap) {
Map paramMap = (Map) parameter;
if (paramMap.containsKey(CRYPT)) {
Object updateParameter = paramMap.get(CRYPT);
if (needToCrypt(updateParameter)) {
//执行sql,还原加解密后的报文
return proceed(invocation, mappedStatement, updateParameter);
}
}
} else if (parameter instanceof DefaultSqlSession.StrictMap) {
//不知道是啥意思,直接过
return invocation.proceed();
} else if (needToCrypt(parameter)) {
//执行sql,还原加解密后的报文
return proceed(invocation, mappedStatement, parameter);
}
//其他场景直接放行
return invocation.proceed();
}
/**
* 执行sql,还原加解密后的报文
*
* @param invocation
* @param mappedStatement
* @param parameter
* @return
* @throws IllegalAccessException
* @throws InstantiationException
* @throws InvocationTargetException
*/
Object proceed(Invocation invocation, MappedStatement mappedStatement, Object parameter) throws IllegalAccessException, InstantiationException, InvocationTargetException {
//先复制1个对象备份数据
Object newInstance = newInstance(parameter);
//调用加解密服务
cryptService.encrypt(parameter);
//执行操作,得到返回结果
Object result = invocation.proceed();
//把加解密后的字段还原
reductionParameter(mappedStatement, newInstance, parameter);
//返回结果
return result;
}
/**
* 先复制1个对象备份数据,便于加解密后还原原报文
*
* @param parameter
* @return
* @throws IllegalAccessException
* @throws InstantiationException
*/
private Object newInstance(Object parameter) throws IllegalAccessException, InstantiationException {
Object newInstance = parameter.getClass().newInstance();
BeanUtils.copyProperties(parameter, newInstance);
return newInstance;
}
/**
* 把加解密后的字段还原,同时把mybatis返回的tableId返回给参数对象
*
* @param mappedStatement
* @param newInstance
* @param parameter
* @throws IllegalAccessException
*/
private void reductionParameter(MappedStatement mappedStatement, Object newInstance, Object parameter) throws IllegalAccessException {
//获取映射语句命令类型
SqlCommandType sqlCommandType = mappedStatement.getSqlCommandType();
if (SqlCommandType.INSERT == sqlCommandType) {
//从参数属性中找到注解是TableId的字段
Field[] parameterFields = parameter.getClass().getDeclaredFields();
Optional<Field> optional = Arrays.stream(parameterFields).filter(field -> field.isAnnotationPresent(TableId.class)).findAny();
if (optional.isPresent()) {
Field field = optional.get();
field.setAccessible(true);
Object id = field.get(parameter);
//覆盖参数加解密的值
BeanUtils.copyProperties(newInstance, parameter);
field.set(parameter, id);
} else {
//覆盖参数加解密的值
BeanUtils.copyProperties(newInstance, parameter);
}
} else {
//覆盖参数加解密的值
BeanUtils.copyProperties(newInstance, parameter);
}
}
/**
* 是否需要加解密:
* ①是否属于基本类型,void类型和String类型,如果是,不加解密
* ②DO上是否有注解 ③ 属性是否有注解
*
* @param object
* @return
*/
public boolean needToCrypt(Object object) {
if (object == null) {
return false;
}
Class<?> clazz = object.getClass();
if (clazz.isPrimitive() || object instanceof String) {
//基本类型和字符串不加解密
return false;
}
//获取DO注解
boolean annotationPresent = clazz.isAnnotationPresent(CryptAnnotation.class);
if (!annotationPresent) {
//无DO注解不加解密
return false;
}
//获取属性注解
Field[] fields = clazz.getDeclaredFields();
return Arrays.stream(fields).anyMatch(field -> field.isAnnotationPresent(CryptAnnotation.class));
}
}
3. Notes
import com.sfpay.merchant.common.constant.EncryptDataTypeEnum;
import java.lang.annotation.*;
/**
* @author ***
* @Date 2020/12/30 20:13
* @description Encryption annotation class
* @Param
* @return
**/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.TYPE})
@Documented
@Inherited
public @interface CryptAnnotation {
EncryptDataTypeEnum type() default EncryptDataTypeEnum.OTHER;
}
cryptService for encryption services, how to achieve their own can be based on the actual situation to achieve.