由于系统已成型,客户方突然要求对账号、手机号、身份证号、银行卡号进行加密才能符合数据保密规范,并且只有拿到私钥的人才可以解开密文数据; 刚接手这个需求还觉得挺简单,大不了在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(分页插件:查询
解密
)
一、maven依赖版本:
<!--mybatis核心包 -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.4.5</version>
</dependency>
<!--pagehelper 分页 -->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>5.0.0</version>
</dependency>
二、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);
}
}
}
注:以上内容仅提供参考和交流,请勿用于商业用途,如有侵权联系本人删除!
持续更新中…
如有对思路不清晰或有更好的解决思路,欢迎与本人交流,QQ群:273557553
你遇到的问题是小编创作灵感的来源!