mybatis拦截器处理敏感字段
前言
由于公司业务要求,需要在不影响已有业务上对 数据库中已有数据的敏感字段加密解密,个人解决方案利用mybatis的拦截器加密解密敏感字段
思路解析
- 利用注解标明需要加密解密的entity类对象以及其中的数据
- mybatis拦截Executor.class对象中的query,update方法
- 在方法执行前对parameter进行加密解密,在拦截器执行后,解密返回的结果
代码
1、配置拦截器(interceptor后为自己拦截器的包路径)
1
2
3
4
5
6
|
< 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、拦截器的实现
特别注意:因为Dao方法参数有可能单一参数,多参数map形式,以及entity对象参数类型,所以不通类型需有不通的处理方式(本文参数 单一字符串和entity对象,返回的结果集 List<?> 和entity)
后续在拦截器中添加了相应的开关,控制参数是否加密查询,解密已实现兼容
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
|
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<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;
}
/***
* 针对不同的参数类型进行加密
* @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) {
}
/**
* 获取参数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对象
1
2
3
4
5
6
7
8
|
//是否需要加密解密对象
@EncryptDecryptClass
public class MerDealInfoRequest extends PagingReqMsg {
//属性定义
@EncryptField
@DecryptField
private String cardNo;
}
|
3、dao方法中的单一参数
1
|
List<Dealer> selectDealerAndMercode( @EncryptField String idcardno);
|
4、封装的工具类(EncryptDecryptUtil.decryptStrValue 解密方法 EncryptDecryptUtil.decryptStrValue 加密方法)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
|
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、在实现上述功能后的测试中,其中select查询方法的参数在加密成功后,但是Executor执行器执行方法参数依旧为未加密的参数,找各路大神都没有解决的思路,最后发现项目中引用了开源的分页插件, OffsetLimitInterceptor拦截器把参数设置成为final的,所以自定义拦截器没有修改成功这个sql参数;
解决办法:自定义拦截器放到这个拦截器后,自定义拦截器先执行就可以了
1
2
3
4
5
6
7
|
< 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 >
|
1
2
3
4
5
6
7
8
9
10
11
12
13
|
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.拦截器签名:
1
2
3
4
5
|
@Intercepts ({ @org .apache.ibatis.plugin.Signature(
type=Executor. class ,
method= "update" ,
args={MappedStatement. class , Object. class })})
public class SqlInterceptor implements Interceptor
|
4.接口方法简单实现:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
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,然后去执行。
以上为个人经验,希望能给大家一个参考,也希望大家多多支持服务器之家。
原文链接:https://blog.csdn.net/alleged/article/details/83313875