使用mybatis攔截器處理敏感字段
mybatis攔截器處理敏感字段
前言
由于公司業(yè)務(wù)要求,需要在不影響已有業(yè)務(wù)上對 數(shù)據(jù)庫中已有數(shù)據(jù)的敏感字段加密解密,個人解決方案利用mybatis的攔截器加密解密敏感字段
思路解析
- 利用注解標(biāo)明需要加密解密的entity類對象以及其中的數(shù)據(jù)
- mybatis攔截Executor.class對象中的query,update方法
- 在方法執(zhí)行前對parameter進行加密解密,在攔截器執(zhí)行后,解密返回的結(jié)果
代碼
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、攔截器的實現(xiàn)
特別注意:因為Dao方法參數(shù)有可能單一參數(shù),多參數(shù)map形式,以及entity對象參數(shù)類型,所以不通類型需有不通的處理方式(本文參數(shù) 單一字符串和entity對象,返回的結(jié)果集 List<?> 和entity)
后續(xù)在攔截器中添加了相應(yīng)的開關(guān),控制參數(shù)是否加密查詢,解密已實現(xiàn)兼容
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 白名單查詢異常,默認本地數(shù)據(jù)加密關(guān)閉[]:",e.getStackTrace()); return false; } return ENCRYPT_SWTICH; } /** * 校驗執(zhí)行器方法 是否在白名單中 * @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開始執(zhí)行==> "); MappedStatement statement = (MappedStatement) invocation.getArgs()[MAPPED_STATEMENT_INDEX]; Object parameter = invocation.getArgs()[PARAMETER_INDEX]; logger.info(statement.getId()+"未加密參數(shù)串:"+JsonUtils.object2jsonString(CloneUtil.deepClone(parameter))); /* * * 判斷是否攔截白名單 或 加密開關(guān)是否配置, * 如果不在白名單中,并且本地加密開關(guān) 已打開 執(zhí)行參數(shù)加密 * * */ if(!isWhiteList(statement.getId()) && getFuncSwitch()){ parameter = encryptParam(parameter, invocation); logger.info(statement.getId()+"加密后參數(shù):"+JsonUtils.object2jsonString(CloneUtil.deepClone(parameter))); } invocation.getArgs()[PARAMETER_INDEX] = parameter; Object returnValue = invocation.proceed(); logger.info(statement.getId()+"未解密結(jié)果集:"+JsonUtils.object2jsonString(CloneUtil.deepClone(returnValue))); returnValue = decryptReslut(returnValue, invocation); logger.info(statement.getId()+"解密后結(jié)果集:"+JsonUtils.object2jsonString(CloneUtil.deepClone(returnValue))); logger.info("EncryptDaoInterceptor.intercept執(zhí)行結(jié)束==> "); return returnValue; } /** * 解密結(jié)果集 * @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<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; } /*** * 針對不同的參數(shù)類型進行加密 * @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) { } /** * 獲取參數(shù)map中需要加密字段 * @param statement * @param type * @return List<String> * @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對象
//是否需要加密解密對象 @EncryptDecryptClass public class MerDealInfoRequest extends PagingReqMsg { //屬性定義 @EncryptField @DecryptField private String cardNo; }
3、dao方法中的單一參數(shù)
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 { /** * 對象t注解字段加密 * @param t * @param <T> * @return */ 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; } /** * 加密單獨的字符串 * * @param @param t * @param @return * @return T * @throws * */ public static <T> T EncryptStr(T t){ if(t instanceof String){ t = (T) EncryptDecryptUtil.encryStrValue((String) t); } return t; } /** * 對含注解字段解密 * @param t * @param <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; } /** * 判斷是否需要加密解密的類 * @param @param t * @param @return * @return Boolean * @throws * */ 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、在實現(xiàn)上述功能后的測試中,其中select查詢方法的參數(shù)在加密成功后,但是Executor執(zhí)行器執(zhí)行方法參數(shù)依舊為未加密的參數(shù),找各路大神都沒有解決的思路,最后發(fā)現(xiàn)項目中引用了開源的分頁插件, OffsetLimitInterceptor攔截器把參數(shù)設(shè)置成為final的,所以自定義攔截器沒有修改成功這個sql參數(shù);
解決辦法:自定義攔截器放到這個攔截器后,自定義攔截器先執(zhí)行就可以了
<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]; //攔截器把參數(shù)設(shè)置成為final的,所以自定義攔截器沒有修改到這個參數(shù) 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、數(shù)據(jù)庫存量數(shù)據(jù)處理
在添加攔截器后,必須對數(shù)據(jù)庫的存量數(shù)據(jù)進行處理,如果不進行處理,查詢參數(shù)已經(jīng)加密,但是數(shù)據(jù)依舊是明文,會導(dǎo)致查詢條件不匹配
mybatis Excutor 攔截器的使用
這里要講的巧妙用法是用來實現(xiàn)在攔截器中執(zhí)行額外 MyBatis 現(xiàn)有方法的用法。
并且會提供一個解決攔截Executor時想要修改MappedStatement時解決并發(fā)的問題。
這里假設(shè)一個場景
實現(xiàn)一個攔截器,記錄 MyBatis 所有的 insert,update,delete 操作,將記錄的信息存入數(shù)據(jù)庫。
這個用法在這里就是將記錄的信息存入數(shù)據(jù)庫。
實現(xiàn)過程的關(guān)鍵步驟和代碼
1.首先在某個 Mapper.xml 中定義好了一個往日志表中插入記錄的方法,假設(shè)方法為id="insertSqlLog"。
2.日志表相關(guān)的實體類為SqlLog.
3.攔截器簽名:
@Intercepts({@org.apache.ibatis.plugin.Signature( type=Executor.class, method="update", args={MappedStatement.class, Object.class})}) public class SqlInterceptor implements Interceptor
4.接口方法簡單實現(xiàn):
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()); //執(zhí)行真正的方法 Object result = invocation.proceed(); //記錄影響行數(shù) log.setResult(Integer.valueOf(Integer.parseInt(result.toString()))); //記錄時間 log.setWhencreated(new Date()); //TODO 還可以記錄參數(shù),或者單表id操作時,記錄數(shù)據(jù)操作前的狀態(tài) //獲取insertSqlLog方法 ms = ms.getConfiguration().getMappedStatement("insertSqlLog"); //替換當(dāng)前的參數(shù)為新的ms args[0] = ms; //insertSqlLog 方法的參數(shù)為 log args[1] = log; //執(zhí)行insertSqlLog方法 invocation.proceed(); //返回真正方法執(zhí)行的結(jié)果 return result; }
重點
MappedStatement是一個共享的緩存對象,這個對象是存在并發(fā)問題的,所以幾乎任何情況下都不能去修改這個對象(通用Mapper除外),想要對MappedStatement做修改該怎么辦呢?
并不難,Executor中的攔截器方法參數(shù)中都有MappedStatement ms,這個ms就是后續(xù)方法執(zhí)行要真正用到的MappedStatement,這樣一來,問題就容易解決了,根據(jù)自己的需要,深層復(fù)制MappedStatement對象中自己需要修改的屬性,然后修改這部分屬性,之后將修改后的ms通過上面代碼中args[0]=ms這種方式替換原有的參數(shù),這樣就能實現(xiàn)對ms的修改而且不會有并發(fā)問題了。
這里日志的例子就是一個更簡單的應(yīng)用,并沒有創(chuàng)建ms,只是獲取了一個新的ms替換現(xiàn)有的ms,然后去執(zhí)行。
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
Java并發(fā)編程學(xué)習(xí)之ThreadLocal源碼詳析
這篇文章主要給大家介紹了關(guān)于Java并發(fā)編程學(xué)習(xí)之源碼分析ThreadLocal的相關(guān)資料,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2018-06-06Spring常用注解 使用注解來構(gòu)造IoC容器的方法
下面小編就為大家分享一篇Spring常用注解 使用注解來構(gòu)造IoC容器的方法,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2018-01-01更簡單更高效的Mybatis?Plus最新代碼生成器AutoGenerator
這篇文章主要為大家介紹了更簡單更高效的Mybatis?Plus最新代碼生成器AutoGenerator使用詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-02-02springboot整合spring-data-redis遇到的坑
使用springboot整合redis,使用默認的序列化配置,然后使用redis-client去查詢時查詢不到相應(yīng)的key.問題出在哪,怎么解決呢?下面小編給大家?guī)砹藄pringboot整合spring-data-redis遇到的坑,需要的的朋友參考下吧2017-04-04SpringBoot整合Redis使用RedisTemplate和StringRedisTemplate
Spring?Boot?Data(數(shù)據(jù))?Redis?中提供了RedisTemplate和StringRedisTemplate,其中StringRedisTemplate是RedisTemplate的子類,兩個方法基本一致。本文介紹了SpringBoot整合Redis使用RedisTemplate和StringRedisTemplate的方法,需要的可以參考一下2022-12-12