使用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-06
Spring常用注解 使用注解來構(gòu)造IoC容器的方法
下面小編就為大家分享一篇Spring常用注解 使用注解來構(gòu)造IoC容器的方法,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2018-01-01
更簡單更高效的Mybatis?Plus最新代碼生成器AutoGenerator
這篇文章主要為大家介紹了更簡單更高效的Mybatis?Plus最新代碼生成器AutoGenerator使用詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-02-02
springboot整合spring-data-redis遇到的坑
使用springboot整合redis,使用默認的序列化配置,然后使用redis-client去查詢時查詢不到相應(yīng)的key.問題出在哪,怎么解決呢?下面小編給大家?guī)砹藄pringboot整合spring-data-redis遇到的坑,需要的的朋友參考下吧2017-04-04
SpringBoot整合Redis使用RedisTemplate和StringRedisTemplate
Spring?Boot?Data(數(shù)據(jù))?Redis?中提供了RedisTemplate和StringRedisTemplate,其中StringRedisTemplate是RedisTemplate的子類,兩個方法基本一致。本文介紹了SpringBoot整合Redis使用RedisTemplate和StringRedisTemplate的方法,需要的可以參考一下2022-12-12

