基于mybatis的数据库脱敏

  • 背景
  • 思路
  • 实现
  • 思考

背景

最近接到需求需要对数据库中的电话、身份证号等敏感信息进行脱敏加密处理,再加上之前面试时也被问到相关问题,所有在此记录。脱敏对象是数据库的字段,所以在数据库存取的出入口进行加解密操作是最合适的,项目中使用mybatis作为ORM框架,所以使用基于mybatis的数据库脱敏。

思路

对数据库中的数据进行脱敏处理,核心思想就是在入库时对敏感字段进行加密,在出库时对敏感字段解密。看清了这个问题,我们的关注点就有两个。

  1. 何时?入库和出库
  2. 何地?入参和查询结果

mybatis框架中的plugin,能够对上面两个关注点进行很好的控制,再结合自定义注解,对需要脱敏的字段进行标注,就能够满足我们的需求。

实现

理论知识储备

  1. 定义自定义注解,用于标识敏感字段
1
2
3
4
5
6
7
8
9
10
less复制代码/**
* 标识字段入库信息需要加密
* @see com.vcg.veer.sign.utils.DesUtils
* @author zhouyao
* @date 2021/10/27 9:22 上午
**/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Encrypt {
}
  1. mybatis插件逻辑(对项目中使用的pagehelper和mybatis-processor插件兼容)
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
263
264
ini复制代码
/**
* 敏感字段入库、出库处理
*
* @author zhouyao
* @date 2021/10/27 9:25 上午
**/
@Intercepts(
{
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})
}
)
public class EncryptInterceptor implements Interceptor {

private final String EXAMPLE_SUFFIX = "Example";


@Override
public Object intercept(Invocation invocation) throws Throwable {
Object[] args = invocation.getArgs();
MappedStatement ms = (MappedStatement) args[0];
Object parameter = args[1];
Class<?> argClass = parameter.getClass();
String argClassName = argClass.getName();
//兼容mybatis-processor
if (needHandleExample(argClassName)){
handleExample(args);
}else{
//自定义的mapper文件增删查改参数处理
handleCustomizeMapperParams(args);
}

//update 方法
if (args.length == 2 ){
return invocation.proceed();
}
//兼容pagehelper
if(args.length == 4){
RowBounds rowBounds = (RowBounds) args[2];
ResultHandler resultHandler = (ResultHandler) args[3];
Executor executor = (Executor) invocation.getTarget();
CacheKey cacheKey;
BoundSql boundSql;
//4 个参数时
boundSql = ms.getBoundSql(parameter);
cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql);
List<Object> queryResult = executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
//处理需要解密的字段
decryptFieldIfNeeded(queryResult);
return queryResult;
}

return invocation.proceed();
}

/**
* 对数据进行解密
* @param queryResult
*/
private void decryptFieldIfNeeded(List<Object> queryResult) throws IllegalAccessException {
if (CollectionUtils.isEmpty(queryResult)) {
return;
}
Object o1 = queryResult.get(0);
Class<?> resultClass = o1.getClass();
Field[] resultClassDeclaredFields = resultClass.getDeclaredFields();
List<Field> needDecryptFieldList = new ArrayList<>();
for (Field resultClassDeclaredField : resultClassDeclaredFields) {
Encrypt encrypt = resultClassDeclaredField.getDeclaredAnnotation(Encrypt.class);
if (encrypt == null){
continue;
}
Class<?> type = resultClassDeclaredField.getType();
if (!String.class.isAssignableFrom(type)){
throw new IllegalStateException("@Encrypt should annotated on String field");
}
needDecryptFieldList.add(resultClassDeclaredField);
}
if (CollectionUtils.isEmpty(needDecryptFieldList)){
return;
}
for (Field field : needDecryptFieldList) {
field.setAccessible(true);
for (Object o : queryResult) {
String fieldValue = (String) field.get(o);
if (!StringUtils.hasText(fieldValue)){
continue;
}
field.set(o,DesUtils.decrypt(fieldValue));
}
}
}

/**
* 处理自定义mapper参数
* @param args
*/
private void handleCustomizeMapperParams(Object[] args) throws Exception {
Object param = args[1];
encryptObjectField(param);
}

private void encryptObjectField(Object param) throws Exception {
Class<?> paramClass = param.getClass();
//mybatis @param注解会处理为多参数
if (Map.class.isAssignableFrom(paramClass)){
Map mapParam = (Map) param;
Set<Object> params = new HashSet<>();
params.addAll(mapParam.values());
for (Object o : params) {
encryptObjectField(o);
}
return;
}
Field[] paramClassDeclaredFields = paramClass.getDeclaredFields();
// 遍历参数的所有字段查找需要加密的字段
for (Field paramClassDeclaredField : paramClassDeclaredFields) {
Encrypt encrypt = paramClassDeclaredField.getDeclaredAnnotation(Encrypt.class);
if (encrypt != null){
//加密
encryptField(param,paramClassDeclaredField);
}
}
}

/**
* 给指定字段加密
* @param targetObj
* @param paramClassDeclaredField
*/
private void encryptField(Object targetObj, Field paramClassDeclaredField) throws Exception {
paramClassDeclaredField.setAccessible(true);
Class<?> type = paramClassDeclaredField.getType();
Object fieldValue = paramClassDeclaredField.get(targetObj);
if (fieldValue == null){
return;
}

if (Collection.class.isAssignableFrom(type)) {
try {
Collection<String> collection = (Collection<String>) fieldValue;
List<String> tempList = new ArrayList<>();
Iterator<String> iterator = collection.iterator();
while (iterator.hasNext()) {
String next = iterator.next();
tempList.add(DesUtils.encrypt(next));
iterator.remove();
}
collection.addAll(tempList);
}catch (Exception ex){
//加密字段参数只支持String类型
throw new IllegalArgumentException("Encrypted fields only support String type");
}
}
else if(String.class.isAssignableFrom(type)){
//基础数据类型直接设值
paramClassDeclaredField.set(targetObj, DesUtils.encrypt(fieldValue.toString()));
}
else if (isBasicType(type)) {
//加密字段参数只支持String类型
throw new IllegalArgumentException("Encrypted fields only support String type");
} else {
//递归调用
encryptObjectField(fieldValue);
}
}

private boolean isBasicType(Class<?> clz) {
try {
return ((Class) clz.getField("TYPE").get(null)).isPrimitive();
} catch (Exception e) {
return false;
}
}

//兼容processor
private void handleExample(Object[] args) throws Exception {
Object arg = args[1];
Class<?> argClass = arg.getClass();
String argClassName = argClass.getName();
//兼容 mybatis-processor
if (argClassName.endsWith(EXAMPLE_SUFFIX)) {
//实体类的类名
String modelClassName = argClassName.substring(0, argClassName.length() - 7);
Class<?> modelClass;
try {
modelClass = Class.forName(modelClassName);
}catch(ClassNotFoundException ex){
return;
}

Method getCriteria = argClass.getDeclaredMethod("getCriteria");
getCriteria.setAccessible(true);
Object criteria = getCriteria.invoke(arg);
Class<?> criteriaClass = criteria.getClass();
Method getAllCriteria = criteriaClass.getDeclaredMethod("getAllCriteria");
Set<Object> criterions = (Set<Object>) getAllCriteria.invoke(criteria);
for (Object criterionObj : criterions) {
Class<?> criterionClass = criterionObj.getClass();
Method getCondition = criterionClass.getDeclaredMethod("getCondition");
String condition = (String) getCondition.invoke(criterionObj);
//列名
String[] conditionParts = condition.split(" ");
if (conditionParts.length != 2){
continue;
}
String columnName = conditionParts[0];
//操作 >=< like
String operateType = conditionParts[1];
Field[] modelClassDeclaredFields = modelClass.getDeclaredFields();
for (Field modelClassDeclaredField : modelClassDeclaredFields) {
Column annotation = modelClassDeclaredField.getAnnotation(Column.class);
if (annotation == null){
continue;
}
if (columnName.equalsIgnoreCase(annotation.name())){
Encrypt encrypt = modelClassDeclaredField.getDeclaredAnnotation(Encrypt.class);
if (encrypt != null) {
//加密字段只能用等于比较
if (!"=".equalsIgnoreCase(operateType)) {
throw new IllegalArgumentException("encrypt field only can be operate by '='");
}
Field value = criterionClass.getDeclaredField("value");
value.setAccessible(true);

List<Integer> list = new ArrayList<>();
list.add(1);
//重新设置参数
value.set(criterionObj,list);

break;
}
break;
}

}
}
}
}

/**
* 判断是否需要处理Example类型的查询
* @param argClassName
* @return
*/
private boolean needHandleExample(String argClassName) {
return argClassName.endsWith(EXAMPLE_SUFFIX);
}

private Object decryptIfNeeded(Invocation invocation) throws InvocationTargetException, IllegalAccessException {
return invocation.proceed();
}

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

@Override
public void setProperties(Properties properties) {
Interceptor.super.setProperties(properties);
}
}
  1. 插件的使用

在项目启动时注册插件(注意,根据mybatis插件的执行原理,此插件需要在最后注册,才能保证最先解析参数)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(dataSource);
//加解密插件
EncryptInterceptor encryptInterceptor = new EncryptInterceptor();

//分页插件
PageInterceptor pageInterceptor = new PageInterceptor();
Properties properties = new Properties();
properties.setProperty("reasonable", "true");
properties.setProperty("supportMethodsArguments", "true");
properties.setProperty("returnPageInfo", "check");
properties.setProperty("params", "count=countSql");
pageInterceptor.setProperties(properties);

//添加插件
bean.setPlugins(pageInterceptor,encryptInterceptor);

对需要加密处理的字段标注@Encrypt注解(入参和结果DTO对象字段都需要标注)

1
2
typescript复制代码    @Encrypt
private String mobile;

思考

通过mybatis的插件对数据库的增删改查实现脱敏处理还是比较简单的。重点就在于:

  1. 拦截Executor对象的query和update方法,获取查询/更新参数和查询结果集
  2. 通过反射对参数中标注自定义注解的字段进行加/解密处理

在开发过程中也遇到了由于使用了pagehelper插件,导致自定义拦截器不生效的问题,最后查阅pagehelper的文档解决了(需要根据pagehelper定义的拦截器编写规范来开发)。

完整的代码参考:

github.com/zhouyao423/…

参考文档:

pagehelper interceptor高级用法

mybatis interceptor

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

0%