使用mybatis拦截器处理敏感字段

目录

  • mybatis拦截器处理敏感字段
    • 前言
    • 思路解析
    • 代码
    • 趟过的坑(敲黑板重点)
  • mybatis Excutor 拦截器的使用
    • 这里假设一个场景
    • 实现过程的关键步骤和代码
    • 重点

mybatis拦截器处理敏感字段
前言
由于公司业务要求,需要在不影响已有业务上对 数据库中已有数据的敏感字段加密解密,个人解决方案利用mybatis的拦截器加密解密敏感字段

思路解析
  • 利用注解标明需要加密解密的entity类对象以及其中的数据
  • mybatis拦截Executor.class对象中的query,update方法
  • 在方法执行前对parameter进行加密解密,在拦截器执行后,解密返回的结果

代码
1、配置拦截器(interceptor后为自己拦截器的包路径)

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; /*** 是否进行加密查询* @return 1 true 代表加密 0 false 不加密*/ 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; } /*** 校验执行器方法 是否在白名单中* @param statementid* @return true 包含 false 不包含*/ 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 = https://www.it610.com/article/invocation.proceed(); logger.info(statement.getId()+"未解密结果集:"+JsonUtils.object2jsonString(CloneUtil.deepClone(returnValue))); returnValue = https://www.it610.com/article/decryptReslut(returnValue, invocation); logger.info(statement.getId()+"解密后结果集:"+JsonUtils.object2jsonString(CloneUtil.deepClone(returnValue))); logger.info("EncryptDaoInterceptor.intercept执行结束==> "); return returnValue; } /*** 解密结果集* @param @param returnValue* @param @param invocation* @param @return* @return Object* @throws **/ 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 newList= new ArrayList(); if (1 <= list.size()){for(Object object:list){Object obj = CryptPojoUtils.decrypt(object); newList.add(obj); }returnValue = https://www.it610.com/article/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; } /**** 针对不同的参数类型进行加密* @param @param parameter* @param @param invocation* @param @return* @return Object* @throws **/ 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) { } /*** 获取参数map中需要加密字段* @param statement* @param type* @return List* @throws **/ 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; } /*** 获取Dao层接口方法* @param @return* @return Method* @throws **/ 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; } /*** 判断字符串是否需要加密* @param @param mappedStatement* @param @return* @return boolean* @throws **/ 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对象
//是否需要加密解密对象@EncryptDecryptClasspublic class MerDealInfoRequest extends PagingReqMsg {//属性定义 @EncryptField@DecryptFieldprivate String cardNo; }

3、dao方法中的单一参数
List 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 {/*** 对象t注解字段加密* @param t* @param * @return*/public static 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 = https://www.it610.com/article/(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; }/*** 加密单独的字符串** @param @param t* @param @return* @return T* @throws **/public static T EncryptStr(T t){if(t instanceof String){t = (T) EncryptDecryptUtil.encryStrValue((String) t); }return t; }/*** 对含注解字段解密* @param t* @param */public static 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 = https://www.it610.com/article/(String)field.get(t); if(StringUtils.isNotEmpty(fieldValue)) {field.set(t, EncryptDecryptUtil.decryptStrValue(fieldValue)); }}}}} catch (IllegalAccessException e) {throw new RuntimeException(e); }}return t; }/*** 判断是否需要加密解密的类* @param @param t* @param @return* @return Boolean* @throws **/public static 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参数;
解决办法:自定义拦截器放到这个拦截器后,自定义拦截器先执行就可以了
//就是这个拦截器

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拦截器处理敏感字段】这里要讲的巧妙用法是用来实现在拦截器中执行额外 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(); //记录SQLlog.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"); //替换当前的参数为新的msargs[0] = ms; //insertSqlLog 方法的参数为 logargs[1] = log; //执行insertSqlLog方法invocation.proceed(); //返回真正方法执行的结果return result; }


重点
MappedStatement是一个共享的缓存对象,这个对象是存在并发问题的,所以几乎任何情况下都不能去修改这个对象(通用Mapper除外),想要对MappedStatement做修改该怎么办呢?
并不难,Executor中的拦截器方法参数中都有MappedStatement ms,这个ms就是后续方法执行要真正用到的MappedStatement,这样一来,问题就容易解决了,根据自己的需要,深层复制MappedStatement对象中自己需要修改的属性,然后修改这部分属性,之后将修改后的ms通过上面代码中args[0]=ms这种方式替换原有的参数,这样就能实现对ms的修改而且不会有并发问题了。
这里日志的例子就是一个更简单的应用,并没有创建ms,只是获取了一个新的ms替换现有的ms,然后去执行。
以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

    推荐阅读