mybatis拦截器处理敏感字段
前言
由于公司业务要求,需要在不影响已有业务上对 数据库中已有数据的敏感字段加密解密,个人解决方案利用mybatis的拦截器加密解密敏感字段
思路解析
- 利用注解标明需要加密解密的entity类对象以及其中的数据
- mybatis拦截Executor.class对象中的query,update方法
- 在方法执行前对parameter进行加密解密,在拦截器执行后,解密返回的结果
代码
1、配置拦截器(interceptor后为自己拦截器的包路径)
<plugins>
<plugin interceptor="com.github.miemiedev.mybatis.paginator.OffsetLimitInterceptor">
<property name="dialectClass" value="com.github.miemiedev.mybatis.paginator.dialect.OracleDialect" />
</plugin>
<plugin interceptor="com.XXX.XXXX.service.encryptinfo.DaoInterceptor" />
</plugins>
2、拦截器的实现
特别注意:因为Dao方法参数有可能单一参数,多参数map形式,以及entity对象参数类型,所以不通类型需有不通的处理方式(本文参数 单一字符串和entity对象,返回的结果集 List<?> 和entity)
后续在拦截器中添加了相应的开关,控制参数是否加密查询,解密已实现兼容
package com.ips.fpms.service.encryptinfo;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import com.xxx.xxx.dao.WhiteListDao;
import com.xxx.xxx.entity.db.WhiteListEntity;
import com.xxx.xxx.service.util.SpringBeanUtils;
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.Plugin;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.xxx.xxx.annotation.EncryptField;
import com.xxx.xxx.annotation.EncryptMethod;
import com.xxx.xxx.common.utils.CloneUtil;
import com.xxx.core.psfp.common.support.JsonUtils;
import com.xxx.xxx.service.util.CryptPojoUtils;
@Intercepts({
@Signature(type=Executor.class,method="update",args={MappedStatement.class,Object.class}),
@Signature(type=Executor.class,method="query",args={MappedStatement.class,Object.class,RowBounds.class,ResultHandler.class})
})
public class EncryptDaoInterceptor implements Interceptor{
private final Logger logger = LoggerFactory.getLogger(EncryptDaoInterceptor.class);
private WhiteListDao whiteListDao;
static int MAPPED_STATEMENT_INDEX = 0;
static int PARAMETER_INDEX = 1;
static int ROWBOUNDS_INDEX = 2;
static int RESULT_HANDLER_INDEX = 3;
static String ENCRYPTFIELD = "1";
static String DECRYPTFIELD = "2";
private static final String ENCRYPT_KEY = "encry146local";
private static final String ENCRYPT_NUM = "146";
private static boolean ENCRYPT_SWTICH = true;
private boolean getFuncSwitch(){
if(whiteListDao == null){
whiteListDao = SpringBeanUtils.getBean("whiteListDao",WhiteListDao.class);
}
try{
WhiteListEntity entity = whiteListDao.selectOne(ENCRYPT_KEY,ENCRYPT_NUM);
if(entity!=null && "1".equals(entity.getFlag())){
ENCRYPT_SWTICH = true;
}else{
ENCRYPT_SWTICH = false;
}
}catch (Exception e){
logger.error(this.getClass().getName()+".getFuncSwitch 白名单查询异常,默认本地数据加密关闭[]:",e.getStackTrace());
return false;
}
return ENCRYPT_SWTICH;
}
private boolean isWhiteList(String statementid){
boolean result = false;
String whiteStatementid = "com.ips.fpms.dao.WhiteListDao.selectOne";
if(whiteStatementid.indexOf(statementid)!=-1){
result = true;
}
return result;
}
@Override
public Object intercept(Invocation invocation) throws Throwable {
logger.info("EncryptDaoInterceptor.intercept开始执行==> ");
MappedStatement statement = (MappedStatement) invocation.getArgs()[MAPPED_STATEMENT_INDEX];
Object parameter = invocation.getArgs()[PARAMETER_INDEX];
logger.info(statement.getId()+"未加密参数串:"+JsonUtils.object2jsonString(CloneUtil.deepClone(parameter)));
if(!isWhiteList(statement.getId()) && getFuncSwitch()){
parameter = encryptParam(parameter, invocation);
logger.info(statement.getId()+"加密后参数:"+JsonUtils.object2jsonString(CloneUtil.deepClone(parameter)));
}
invocation.getArgs()[PARAMETER_INDEX] = parameter;
Object returnValue = invocation.proceed();
logger.info(statement.getId()+"未解密结果集:"+JsonUtils.object2jsonString(CloneUtil.deepClone(returnValue)));
returnValue = decryptReslut(returnValue, invocation);
logger.info(statement.getId()+"解密后结果集:"+JsonUtils.object2jsonString(CloneUtil.deepClone(returnValue)));
logger.info("EncryptDaoInterceptor.intercept执行结束==> ");
return returnValue;
}
public Object decryptReslut(Object returnValue,Invocation invocation){
MappedStatement statement = (MappedStatement) invocation.getArgs()[MAPPED_STATEMENT_INDEX];
if(returnValue!=null){
if(returnValue instanceof ArrayList<?>){
List<?> list = (ArrayList<?>) returnValue;
List<Object> newList = new ArrayList<Object>();
if (1 <= list.size()){
for(Object object:list){
Object obj = CryptPojoUtils.decrypt(object);
newList.add(obj);
}
returnValue = newList;
}
}else if(returnValue instanceof Map){
String[] fields = getEncryFieldList(statement,DECRYPTFIELD);
if(fields!=null){
returnValue = CryptPojoUtils.getDecryptMapValue(returnValue,fields);
}
}else{
returnValue = CryptPojoUtils.decrypt(returnValue);
}
}
return returnValue;
}
public Object encryptParam(Object parameter,Invocation invocation){
MappedStatement statement = (MappedStatement) invocation.getArgs()[MAPPED_STATEMENT_INDEX];
try {
if(parameter instanceof String){
if(isEncryptStr(statement)){
parameter = CryptPojoUtils.encryptStr(parameter);
}
}else if(parameter instanceof Map){
String[] fields = getEncryFieldList(statement,ENCRYPTFIELD);
if(fields!=null){
parameter = CryptPojoUtils.getEncryptMapValue(parameter,fields);
}
}else{
parameter = CryptPojoUtils.encrypt(parameter);
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
logger.info("EncryptDaoInterceptor.encryptParam方法异常==> " + e.getMessage());
}
return parameter;
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
}
private String[] getEncryFieldList(MappedStatement statement,String type){
String[] strArry = null;
Method method = getDaoTargetMethod(statement);
Annotation annotation =method.getAnnotation(EncryptMethod.class);
if(annotation!=null){
if(type.equals(ENCRYPTFIELD)){
String encryString = ((EncryptMethod) annotation).encrypt();
if(!"".equals(encryString)){
strArry =encryString.split(",");
}
}else if(type.equals(DECRYPTFIELD)){
String encryString = ((EncryptMethod) annotation).decrypt();
if(!"".equals(encryString)){
strArry =encryString.split(",");
}
}else{
strArry = null;
}
}
return strArry;
}
private Method getDaoTargetMethod(MappedStatement mappedStatement){
Method method = null;
try {
String namespace = mappedStatement.getId();
String className = namespace.substring(0,namespace.lastIndexOf("."));
String methedName= namespace.substring(namespace.lastIndexOf(".") + 1,namespace.length());
Method[] ms = Class.forName(className).getMethods();
for(Method m : ms){
if(m.getName().equals(methedName)){
method = m;
break;
}
}
} catch (SecurityException e) {
e.printStackTrace();
logger.info("EncryptDaoInterceptor.getDaoTargetMethod方法异常==> " + e.getMessage());
return method;
} catch (ClassNotFoundException e) {
e.printStackTrace();
logger.info("EncryptDaoInterceptor.getDaoTargetMethod方法异常==> " + e.getMessage());
return method;
}
return method;
}
private boolean isEncryptStr(MappedStatement mappedStatement) throws ClassNotFoundException{
boolean reslut = false;
try {
Method m = getDaoTargetMethod(mappedStatement);
m.setAccessible(true);
Annotation[][] parameterAnnotations = m.getParameterAnnotations();
if (parameterAnnotations != null && parameterAnnotations.length > 0) {
for (Annotation[] parameterAnnotation : parameterAnnotations) {
for (Annotation annotation : parameterAnnotation) {
if (annotation instanceof EncryptField) {
reslut = true;
}
}
}
}
} catch (SecurityException e) {
e.printStackTrace();
logger.info("EncryptDaoInterceptor.isEncryptStr异常:==> " + e.getMessage());
reslut = false;
}
return reslut;
}
}
2、注解的entity对象
//是否需要加密解密对象
@EncryptDecryptClass
public class MerDealInfoRequest extends PagingReqMsg {
//属性定义
@EncryptField
@DecryptField
private String cardNo;
}
3、dao方法中的单一参数
List<Dealer> selectDealerAndMercode(@EncryptField String idcardno);
4、封装的工具类(EncryptDecryptUtil.decryptStrValue 解密方法 EncryptDecryptUtil.decryptStrValue 加密方法)
package com.xxx.xxx.service.util;
import java.lang.reflect.Field;
import java.util.ArrayList;
import org.apache.commons.lang.StringUtils;
import org.apache.pdfbox.Encrypt;
import org.apache.poi.ss.formula.functions.T;
import com.xxx.xxx.annotation.DecryptField;
import com.xxx.xxx.annotation.EncryptDecryptClass;
import com.xxx.xxx.annotation.EncryptField;
import com.xxx.xxx.common.utils.EncryptDecryptUtil;
public class CryptPojoUtils {
public static <T> T encrypt(T t) {
if(isEncryptAndDecrypt(t)){
Field[] declaredFields = t.getClass().getDeclaredFields();
try {
if (declaredFields != null && declaredFields.length > 0) {
for (Field field : declaredFields) {
if (field.isAnnotationPresent(EncryptField.class) && field.getType().toString().endsWith("String")) {
field.setAccessible(true);
String fieldValue = (String) field.get(t);
if (StringUtils.isNotEmpty(fieldValue)) {
field.set(t, EncryptDecryptUtil.encryStrValue(fieldValue) );
}
field.setAccessible(false);
}
}
}
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
return t;
}
public static <T> T EncryptStr(T t){
if(t instanceof String){
t = (T) EncryptDecryptUtil.encryStrValue((String) t);
}
return t;
}
public static <T> T decrypt(T t) {
if(isEncryptAndDecrypt(t)){
Field[] declaredFields = t.getClass().getDeclaredFields();
try {
if (declaredFields != null && declaredFields.length > 0) {
for (Field field : declaredFields) {
if (field.isAnnotationPresent(DecryptField.class) && field.getType().toString().endsWith("String")) {
field.setAccessible(true);
String fieldValue = (String)field.get(t);
if(StringUtils.isNotEmpty(fieldValue)) {
field.set(t, EncryptDecryptUtil.decryptStrValue(fieldValue));
}
}
}
}
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
return t;
}
public static <T> Boolean isEncryptAndDecrypt(T t){
Boolean reslut = false;
if(t!=null){
Object object = t.getClass().getAnnotation(EncryptDecryptClass.class);
if(object != null){
reslut = true;
}
}
return reslut;
}
}
趟过的坑(敲黑板重点)
1、在实现上述功能后的测试中,其中select查询方法的参数在加密成功后,但是Executor执行器执行方法参数依旧为未加密的参数,找各路大神都没有解决的思路,最后发现项目中引用了开源的分页插件, OffsetLimitInterceptor拦截器把参数设置成为final的,所以自定义拦截器没有修改成功这个sql参数;
解决办法:自定义拦截器放到这个拦截器后,自定义拦截器先执行就可以了
<plugins>
//就是这个拦截器
<plugin interceptor="com.github.miemiedev.mybatis.paginator.OffsetLimitInterceptor">
<property name="dialectClass" value="com.github.miemiedev.mybatis.paginator.dialect.OracleDialect" />
</plugin>
<plugin interceptor="com.ips.fpms.service.encryptinfo.DaoInterceptor" />
</plugins>
public Object intercept(final Invocation invocation) throws Throwable {
final Executor executor = (Executor) invocation.getTarget();
final Object[] queryArgs = invocation.getArgs();
final MappedStatement ms = (MappedStatement)queryArgs[MAPPED_STATEMENT_INDEX];
//拦截器把参数设置成为final的,所以自定义拦截器没有修改到这个参数
final Object parameter = queryArgs[PARAMETER_INDEX];
final RowBounds rowBounds = (RowBounds)queryArgs[ROWBOUNDS_INDEX];
final PageBounds pageBounds = new PageBounds(rowBounds);
final int offset = pageBounds.getOffset();
final int limit = pageBounds.getLimit();
final int page = pageBounds.getPage();
.....省略代码....
}
2、数据库存量数据处理
在添加拦截器后,必须对数据库的存量数据进行处理,如果不进行处理,查询参数已经加密,但是数据依旧是明文,会导致查询条件不匹配
mybatis Excutor 拦截器的使用
这里要讲的巧妙用法是用来实现在拦截器中执行额外 MyBatis 现有方法的用法。
并且会提供一个解决拦截Executor时想要修改MappedStatement时解决并发的问题。
这里假设一个场景
实现一个拦截器,记录 MyBatis 所有的 insert,update,delete 操作,将记录的信息存入数据库。
这个用法在这里就是将记录的信息存入数据库。
实现过程的关键步骤和代码
1.首先在某个 Mapper.xml 中定义好了一个往日志表中插入记录的方法,假设方法为id="insertSqlLog"。
2.日志表相关的实体类为SqlLog.
3.拦截器签名:
@Intercepts({@org.apache.ibatis.plugin.Signature(
type=Executor.class,
method="update",
args={MappedStatement.class, Object.class})})
public class SqlInterceptor implements Interceptor
4.接口方法简单实现:
public Object intercept(Invocation invocation) throws Throwable {
Object[] args = invocation.getArgs();
MappedStatement ms = (MappedStatement) args[0];
Object parameter = args[1];
SqlLog log = new SqlLog();
Configuration configuration = ms.getConfiguration();
Object target = invocation.getTarget();
StatementHandler handler = configuration.newStatementHandler((Executor) target, ms,
parameter, RowBounds.DEFAULT, null, null);
BoundSql boundSql = handler.getBoundSql();
//记录SQL
log.setSqlclause(boundSql.getSql());
//执行真正的方法
Object result = invocation.proceed();
//记录影响行数
log.setResult(Integer.valueOf(Integer.parseInt(result.toString())));
//记录时间
log.setWhencreated(new Date());
//TODO 还可以记录参数,或者单表id操作时,记录数据操作前的状态
//获取insertSqlLog方法
ms = ms.getConfiguration().getMappedStatement("insertSqlLog");
//替换当前的参数为新的ms
args[0] = ms;
//insertSqlLog 方法的参数为 log
args[1] = log;
//执行insertSqlLog方法
invocation.proceed();
//返回真正方法执行的结果
return result;
}
重点
MappedStatement是一个共享的缓存对象,这个对象是存在并发问题的,所以几乎任何情况下都不能去修改这个对象(通用Mapper除外),想要对MappedStatement做修改该怎么办呢?
并不难,Executor中的拦截器方法参数中都有MappedStatement ms,这个ms就是后续方法执行要真正用到的MappedStatement,这样一来,问题就容易解决了,根据自己的需要,深层复制MappedStatement对象中自己需要修改的属性,然后修改这部分属性,之后将修改后的ms通过上面代码中args[0]=ms这种方式替换原有的参数,这样就能实现对ms的修改而且不会有并发问题了。
这里日志的例子就是一个更简单的应用,并没有创建ms,只是获取了一个新的ms替换现有的ms,然后去执行。
以上为个人经验,希望能给大家一个参考,也希望大家多多支持编程网。