通过MyBatis拦截器实现增删改查参数的加/解密(已上线项目)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/seesun2012/article/details/81479463

由于系统已成型,客户方突然要求对账号、手机号、身份证号、银行卡号进行加密才能符合数据保密规范,并且只有拿到私钥的人才可以解开密文数据; 刚接手这个需求还觉得挺简单,大不了在Get、Set方法中调用加密方法就搞定了,但是经过一番尝试之后原来并不是想象中的那么回事,原因是一个执行流程下来Set方法被多次调用会导致重复加密现象; 经过多番尝试,并且考虑到后期可维护性、可扩展性以及代码美观度,并抱着试一试的心态便选择了MyBatis拦截器l(插件)来实现。

实现思路:

  • 1、配置MyBatis执行参数拦截器,拦截增删改查入参(SQL编译前);
  • 2、拦截SQL语句,判断预先设定的表名是否存在SQL语句中;
  • 3、如果存在,开始执行加解密流程;
  • 4、编写CommonEntity父类,所有需要加解、密的bean都继承这个父类;
  • 5、在公共父类中实现具体的加密、解密方法;
  • 6、通过反射获取入参参数对象,回调到CommonEntity父类;
  • 7、如果使用了MyBatis分页插件,需要对PageHelper的插件的拦截器进行重写。

文件介绍:

  • 1、mybatis-config.xml(拦截器入口配置)
  • 2、ParameterInterceptor.java(MyBatis拦截:执行参数加密解密
  • 3、CommonEntity.java(公共实体类:加解密处理类)
  • 4、PageInterceptor.java(分页插件:查询解密

一、mybatis-config.xml(拦截器入口配置):

<configuration>

	<!-- 部分代码省略 -->
	...
	
	<plugins>
		<!-- 【敏感信息】加密拦截 -->
		<plugin interceptor="com.seesun2012.dao.interceptor.ParameterInterceptor" />
		<!-- 【解密拦截】分页查询插件 -->
		<plugin interceptor="com.github.pagehelper.PageHelper" />
		<!-- 【乐观锁】插件 -->
		<plugin interceptor="com.chrhc.mybatis.locker.interceptor.OptimisticLocker" />
	</plugins>
	
</configuration>

二、ParameterInterceptor.java(所有增删改查入参都会经过这里):

package com.seesun2012.dao.interceptor;

import java.lang.reflect.Field;
import java.sql.PreparedStatement;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import org.apache.ibatis.executor.parameter.ParameterHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ParameterMapping;
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.springframework.util.StringUtils;
import com.seesun2012.dao.entity.CommonEntity;


/**
 * 通用参数拦截器(敏感信息加密)
 *
 * @author CSDN博客:seesun2012
 *
 */
@SuppressWarnings({"unchecked"})
@Intercepts({
	//执行参数接口,method为接口名称,args为参数对象(注意:不同版本个数不同,该版本:5.0.0)
	@Signature(type=ParameterHandler.class, method="setParameters", args={PreparedStatement.class})
})
public class ParameterInterceptor implements Interceptor {

	@Override
	public Object intercept(Invocation invocation) throws Throwable {
		ParameterHandler parameterHandler = (ParameterHandler) invocation.getTarget();
	    PreparedStatement ps = (PreparedStatement) invocation.getArgs()[0];
	    // 反射获取 BoundSql 对象,此对象包含生成的sql和sql的参数map映射
	    Field boundSqlField = parameterHandler.getClass().getDeclaredField("boundSql");
	    boundSqlField.setAccessible(true);
	    BoundSql boundSql = (BoundSql) boundSqlField.get(parameterHandler);
	    List<String> paramNames = new ArrayList<>();
	    // 【敏感信息加密】表是否存在SQL语句中
	    boolean hasTab = CommonEntity.checkTable(boundSql.getSql());
	    if (!hasTab) {
	        return invocation.proceed();
	    }
	    Field parameterField = parameterHandler.getClass().getDeclaredField("parameterObject");
	    Field mappedStatement = parameterHandler.getClass().getDeclaredField("mappedStatement");
	    parameterField.setAccessible(true);
	    mappedStatement.setAccessible(true);
	    Object parameterObject = parameterField.get(parameterHandler);
	    MappedStatement ms = (MappedStatement)mappedStatement.get(parameterHandler);
	    
	    // 【关键】:改写参数(注:这里只要拿到这个参数,可以自行处理,CommonEntity只是作者本人的加解密思路)
	    parameterObject = CommonEntity.processColumn(parameterObject, paramNames, boundSql.getParameterMappings(), ms.getSqlCommandType().name());
	    // 改写的参数设置到原parameterHandler对象
	    parameterField.set(parameterHandler, parameterObject);
	    parameterHandler.setParameters(ps);
	    
	    return null;
	}

	//这个地方可以读取xml中的配置,可以尝试将表名和字段名配置在此,也可以可换成注解注入进去
	@Override
	public Object plugin(Object target) {
	    return Plugin.wrap(target, this);
	}
	@Override
	public void setProperties(Properties properties) {
	}

}

三、CommonEntity.java(公共实体类:加解密处理类):

import java.beans.PropertyDescriptor;
import java.io.Serializable;
import java.security.Key;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.crypto.Cipher;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.DESedeKeySpec;

import org.apache.ibatis.mapping.ParameterMapping;
import org.springframework.beans.BeanUtils;
import org.springframework.util.Base64Utils;

/**
 *
 * 【通用实体bena】每一个需要加密的bean实体,都必须继承此类
 * 
 * @author CSDN博客:seesun2012
 * 
 */
public abstract class CommonEntity implements Serializable {

	private static final long serialVersionUID = 0L;

	private static final String Algorithm = "DESede";
	private static final byte[] key = "6T8SA4N0I3U4C9J8A7SI8A9XJ13A6V5M8S".getBytes();
	//表名一律大写
	private static final String[] STR_TAB_ARR = "T_USER_INFO,T_PAY_INFO".split(",");	//后期将改成@注解注在Bean头部
	private static final String[] STR_COL_ARR = "idCard,panAccount".split(",");		//后期将改成@注解注在属性名上

	/**
	 * 需要加密的参数(可在具体业务对应bean子类中重写)
	 */
	public String[] getEncryptionArr() {
		String strArr = "IdCard,PanAccount";
		return strArr.split(",");
	}


	/**
	 * myBatis执行回调函数
	 */
	public Object myBatisCallBack(String stemType) throws Exception {
		return toDecrypt(stemType, getEncryptionArr());
	};

	/**
	 * 回调JavaBean加密函数
	 *
	 * @param obj			实体对象
	 * @param stemType		操作类型
	 */
	public static Object skill(Object obj, String stemType) throws Exception {
		if (CommonEntity.class.equals(obj.getClass().getSuperclass())) {
			return obj.getClass().getSuperclass().getDeclaredMethod("myBatisCallBack", String.class).invoke(obj, stemType);
		}
		if (CommonEntity.class.equals(obj.getClass().getSuperclass().getSuperclass())) {
			return obj.getClass().getSuperclass().getSuperclass().getDeclaredMethod("myBatisCallBack", String.class).invoke(obj, stemType);
		}
		return obj.getClass().getSuperclass().getSuperclass().getDeclaredMethod("myBatisCallBack", String.class).invoke(obj, stemType);
	}

	/**
	 * 加密/解密
	 */
	public Object toDecrypt(String stemType, String[] columnArr) throws Exception {
		switch (stemType.toUpperCase()) {
			case "QUERY":
				setEDValue(columnArr, "dc");
				break;
			case "SELECT":
				setEDValue(columnArr, "dc");
				break;
			case "UPDATE":
				setEDValue(columnArr, "ec");
				break;
			case "INSERT":
				setEDValue(columnArr, "ec");
				break;
			case "DELETE":
				setEDValue(columnArr, "ec");
				break;
		}
		return this;
	}

	public void setEDValue(String[] columnArr, String edType) throws Exception {
		PropertyDescriptor ps = null;
		if ("ec".equals(edType)) {	//加密
			for (int i = 0, len = columnArr.length; i < len; i++) {
				ps = BeanUtils.getPropertyDescriptor(this.getClass(), columnArr[i]);
	            if (ps != null && ps.getReadMethod() != null && ps.getWriteMethod() != null) {
	                if (!isEmpty(ps.getReadMethod().invoke(this))) {
	                    ps.getWriteMethod().invoke(this, des3EncodeECB(String.valueOf(ps.getReadMethod().invoke(this))));
	                }
	            }
			}
		}
		if ("dc".equals(edType)) {	//解密
			for (int i = 0, len = columnArr.length; i < len; i++) {
				ps = BeanUtils.getPropertyDescriptor(this.getClass(), columnArr[i]);
	            if (ps != null && ps.getReadMethod() != null && ps.getWriteMethod() != null) {
	                if (!isEmpty(ps.getReadMethod().invoke(this))) {
	                    ps.getWriteMethod().invoke(this, des3DecodeECB(ps.getReadMethod().invoke(this).toString()));
	                }
	            }
			}
		}
	}

	/**
	 * 加密增删改查回调参数(回调)
	 *
	 * @param paramObj			要加密的参数对象
	 * @param paramNames			字段bean对应属性名
	 * @param parameterMappings
	 * @param sqlType
	 * @return
	 * @throws Throwable
	 */
	@SuppressWarnings({"unchecked"})
	public static Object processColumn(Object paramObj, List<ParameterMapping> parameterMappings, String sqlType) throws Throwable {
		if (paramObj==null) {
    		return paramObj;
		}
		if (paramObj instanceof CommonEntity) {
			skill(paramObj, sqlType);
			return paramObj;
		}
		Map<String, Object> paramMap = null;
		for (ParameterMapping map : parameterMappings) {
			if (checkColumn(map.getProperty())) {
				if (paramObj instanceof HashMap<?, ?>) {
					paramMap = (Map<String, Object>) paramObj;
					paramMap.put(map.getProperty(), isEmpty(paramMap.get(map.getProperty())) ? null : des3EncodeECB(paramMap.get(map.getProperty()).toString()));
					return paramObj;
				}
				if (paramObj instanceof String) {
					paramObj = isEmpty(paramObj) ? null : des3EncodeECB(paramObj.toString());
					return paramObj;
				} else{
					return paramObj;
				}
			}
		}
		return paramObj;
	}

	/**
     * 分页查询参数解密,每一个经过查询的方法都必须执行此方法,否则无法还原真实参数(回调)
     *
     * @param paramObj				查询参数对象
     * @param parameterMappings		查询字段集合
     * @param sqlType				查询类型
     */
    @SuppressWarnings({ "unchecked" })
	public static Object decodelumn(boolean hasTab, Object paramObj, List<ParameterMapping> parameterMappings, String sqlType) throws Throwable {
    	if (paramObj==null || !hasTab) {
    		return paramObj;
		}
		if (paramObj instanceof CommonEntity) {
			skill(paramObj, sqlType);
			return paramObj;
		}
		Map<String, Object> paramMap = null;
		for (ParameterMapping map : parameterMappings) {
			if (checkColumn(map.getProperty())) {
				if (paramObj instanceof HashMap<?, ?>) {
					paramMap = (Map<String, Object>) paramObj;
					paramMap.put(map.getProperty(), isEmpty(paramMap.get(map.getProperty())) ? null : des3DecodeECB(paramMap.get(map.getProperty()).toString()));
					return paramObj;
				}
				if (paramObj instanceof String) {
					paramObj = isEmpty(paramObj) ? null : des3DecodeECB(paramObj.toString());
					return paramObj;
				} else{
					return paramObj;
				}
			}
		}
		return paramObj;
	}


    /**
	 * ECB加密,不要IV(禁止修改)
	 *
	 * @param key 			密钥
	 * @param data			明文
	 * @return				Base64编码的(密文)
	 */
	public static String des3EncodeECB(String data) {
		if (data==null) {
			return null;
		}
		try {
			Key deskey = null;
			DESedeKeySpec spec = new DESedeKeySpec(key);
			SecretKeyFactory keyfactory = SecretKeyFactory.getInstance(Algorithm);
			deskey = keyfactory.generateSecret(spec);
			Cipher cipher = Cipher.getInstance(Algorithm);
			cipher.init(Cipher.ENCRYPT_MODE, deskey);
			byte[] bOut = cipher.doFinal(data.getBytes());
			return Base64Utils.encodeToString(bOut);
		} catch (Exception e) {
			return data;
		}
	}

	/**
	 * ECB解密,不要IV(禁止修改)
	 *
	 * @param key 			密钥
	 * @param data			Base64编码的密文(明文)
	 */
	public static String des3DecodeECB(String data) {
		if (data==null) {
			return null;
		}
		try {
			Key deskey = null;
			DESedeKeySpec spec = new DESedeKeySpec(key);
			SecretKeyFactory keyfactory = SecretKeyFactory.getInstance(Algorithm);
			deskey = keyfactory.generateSecret(spec);
			Cipher cipher = Cipher.getInstance(Algorithm);
			cipher.init(Cipher.DECRYPT_MODE, deskey);
			byte[] bOut = cipher.doFinal(Base64Utils.decode(data.getBytes()));
			return new String(bOut);
		} catch (Exception e) {
			return data;
		}
	}


	public static boolean checkTable(String sql){
		sql = sql.toUpperCase();
		for (String tab : STR_TAB_ARR) {
			if (sql.indexOf(tab)>=0) return true;
		}
		return false;
	}

	public static boolean checkColumn(String column){
		column = column.toLowerCase();
		for (String col : STR_COL_ARR) {
			if (column.equals(col)) return true;
		}
		return false;
	}

	public static boolean isEmpty(Object str) {
		return (str == null || "".equals(str));
	}



}

四、PageInterceptor.java(分页插件:查询解密)

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Properties;

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.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.Plugin;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;

import com.github.pagehelper.Dialect;
import com.github.pagehelper.PageException;
import com.github.pagehelper.cache.Cache;
import com.github.pagehelper.cache.CacheFactory;
import com.github.pagehelper.util.MSUtils;
import com.github.pagehelper.util.StringUtil;
import com.rfpay.dao.entity.CommonEntity;

/**
 * 通用分页拦截器(重写版)
 *
 * @author CSDN博客:seesun2012
 *
 */
@SuppressWarnings({"rawtypes", "unchecked"})
@Intercepts({
    @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
    @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class})
})
public class PageInterceptor implements Interceptor {

    protected Cache<CacheKey, MappedStatement> msCountMap = null;
    private Dialect dialect;
    private String default_dialect_class = "com.github.pagehelper.PageHelper";
    private Field additionalParametersField;

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        try {
            Object[] args = invocation.getArgs();
            MappedStatement ms = (MappedStatement) args[0];
            SqlCommandType sqlCommandType = ms.getSqlCommandType();
            Object parameter = args[1];
            Object pageParameter = args[1];
            RowBounds rowBounds = (RowBounds) args[2];
            ResultHandler resultHandler = (ResultHandler) args[3];
            Executor executor = (Executor) invocation.getTarget();
            CacheKey cacheKey;
            BoundSql boundSql;
            if(args.length == 4){
                boundSql = ms.getBoundSql(parameter);
                cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql);
            } else {
                cacheKey = (CacheKey) args[4];
                boundSql = (BoundSql) args[5];
            }
            List resultList;
            if (!dialect.skip(ms, parameter, rowBounds)) {
                Map<String, Object> additionalParameters = (Map<String, Object>) additionalParametersField.get(boundSql);
                if (dialect.beforeCount(ms, parameter, rowBounds)) {
                    CacheKey countKey = executor.createCacheKey(ms, parameter, RowBounds.DEFAULT, boundSql);
                    countKey.update("_Count");
                    MappedStatement countMs = msCountMap.get(countKey);
                    if (countMs == null) {
                        countMs = MSUtils.newCountMappedStatement(ms);
                        msCountMap.put(countKey, countMs);
                    }
                    String countSql = dialect.getCountSql(ms, boundSql, parameter, rowBounds, countKey);
                    BoundSql countBoundSql = new BoundSql(ms.getConfiguration(), countSql, boundSql.getParameterMappings(), parameter);
                    for (String key : additionalParameters.keySet()) {
                        countBoundSql.setAdditionalParameter(key, additionalParameters.get(key));
                    }
                    Object countResultList = executor.query(countMs, parameter, RowBounds.DEFAULT, resultHandler, countKey, countBoundSql);
                    Long count = (Long) ((List) countResultList).get(0);
                    if (!dialect.afterCount(count, parameter, rowBounds)) {
                        return dialect.afterPage(new ArrayList(), parameter, rowBounds);
                    }
                }
                if (dialect.beforePage(ms, parameter, rowBounds)) {
                    CacheKey pageKey = cacheKey;
                    parameter = dialect.processParameterObject(ms, parameter, boundSql, pageKey);
                    String pageSql = dialect.getPageSql(ms, boundSql, parameter, rowBounds, pageKey);
                    BoundSql pageBoundSql = new BoundSql(ms.getConfiguration(), pageSql, boundSql.getParameterMappings(), parameter);
                    for (String key : additionalParameters.keySet()) {
                        pageBoundSql.setAdditionalParameter(key, additionalParameters.get(key));
                    }
                    resultList = executor.query(ms, pageParameter, RowBounds.DEFAULT, resultHandler, pageKey, pageBoundSql);
                } else {
                    resultList = executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, cacheKey, boundSql);
                }
            } else {
                resultList = executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
            }
            /** ============================ result结果集解密 ============================ **/
            for (Object obj : resultList) {
				if (obj instanceof CommonEntity) {
					CommonEntity.skill(obj, sqlCommandType.name());
				}
			}
            /** ================== 执行参数解密:这里返回回来的是密文,一旦被再次调用就是大坑 ================== **/
            CommonEntity.processColumn(parameter, boundSql.getParameterMappings(), ms.getSqlCommandType().name());
            return dialect.afterPage(resultList, parameter, rowBounds);
        } finally {
            dialect.afterAll();
        }
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {
        msCountMap = CacheFactory.createCache(properties.getProperty("msCountCache"), "ms", properties);
        String dialectClass = properties.getProperty("dialect");
        if (StringUtil.isEmpty(dialectClass)) {
            dialectClass = default_dialect_class;
        }
        try {
            Class<?> aClass = Class.forName(dialectClass);
            dialect = (Dialect) aClass.newInstance();
        } catch (Exception e) {
            throw new PageException(e);
        }
        dialect.setProperties(properties);
        try {
            additionalParametersField = BoundSql.class.getDeclaredField("additionalParameters");
            additionalParametersField.setAccessible(true);
        } catch (NoSuchFieldException e) {
            throw new PageException(e);
        }
    }

}



持续更新中…,后期将结合spring+注解完成表明、字段配置!

如有对思路不清晰或有更好的解决思路,欢迎与本人交流,QQ群:273557553,个人微信:seesun2012

你的关注是小编创作的动力!
































猜你喜欢

转载自blog.csdn.net/seesun2012/article/details/81479463
今日推荐