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

网友投稿 253 2022-12-06

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

}

/**

* 解密结果集

* @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 = 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 = Crypthttp://PojoUtils.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对象

//是否需要加密解密对象

@EncryptDecryptClass

public class MerDealInfoRequest extends PagingReqMsg {

//属性定义

@EncryptField

@DecryptField

private 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 = (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 = (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 现有方法的用法。

并且会提供一个解决拦截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,然后去执行。

版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:Java 反射(Reflect)详解
下一篇:SpringBoot指标监控的实现
相关文章

 发表评论

暂时没有评论,来抢沙发吧~