日常我们在存储数据的时候,经常会碰到一些敏感性的数据,比如用户的身份证号和手机号等。这些数据一般是不允许我们在数据库中做明文存储的,这就需要我们在存储字段的时候,对这些字段做加密操作,同时在读取出来的时候,也要对相应字段做解密操作。
这些操作如果我们在service层做处理,可能需要针对各个表的对应数据做解析,这时候代码就会相对很多,也很乱。而这种操作也非常类似于拦截器的操作。我们对于请求做拦截,然后实现相应的操作。而mybatis-plus也提供了针对性的拦截器,我们可以通过扩展拦截器来实现这样的需求。
首先我们先定义三个注解,分别是@SensitiveData, @SensitiveField, @Mask。 分别介绍一下, 这三个注解,都是和实体相关的。 @SentiveData放在实体类上,用于标识对应的实体类中存在敏感数据, @SensiveField放在字段上,用于标识该字段是敏感字段,需要进行加解密操作, @Mask一般放在手机号上,保存的时候不加密,读取的时候,把中间四位替换为****。 先给出注解的定义
@Inherited
@Target({ ElementType.FIELD })
@Retention(RetentionPolicy.RUNTIME)
public @interface Mask {
}
@Inherited
@Target({ ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
public @interface SensitiveData {
}
@Inherited
@Target({ ElementType.FIELD })
@Retention(RetentionPolicy.RUNTIME)
public @interface SensitiveField {
}
接下来就是拦截器的写法了,mybatis给我们提供了对应的插件扩展,对于mybatis-plus同样适用。mybatis在插入的时候有一个方法叫做setParameter, 会对参数做设置, 查询的时候有一个方法叫做handleResultSet, 会对结果做操作,我们只需要拦截这两个请求,设置参数的时候,加密敏感字段;操作结果的时候,解密敏感字段即可。给出两个拦截器代码。
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.executor.parameter.ParameterHandler;
import org.apache.ibatis.plugin.*;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.stereotype.Component;
import java.lang.reflect.Field;
import java.sql.PreparedStatement;
import java.util.Objects;
/**
* <br>
* .
* <p>
* Copyright: Copyright (c) 2021/1/18 5:09 下午
* <p>
* Company: zhongdian
* <p>
*
* @author tianlong
* @version 1.0.0
* @date: 2021/1/18 5:09 下午
* @email tianlong03@cestc.com
* @desc
*/
@Slf4j
@Component
@Intercepts({
@Signature(type = ParameterHandler.class, method = "setParameters", args = {PreparedStatement.class}),
})
public class EncryptInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
try {
ParameterHandler parameterHandler = (ParameterHandler) invocation.getTarget();
// 获取参数对像,即 mapper 中 paramsType 的实例
Field parameterField = parameterHandler.getClass().getDeclaredField("parameterObject");
parameterField.setAccessible(true);
//取出实例
Object parameterObject = parameterField.get(parameterHandler);
if (parameterObject != null) {
Class<?> parameterObjectClass = parameterObject.getClass();
//校验该实例的类是否被@SensitiveData所注解
SensitiveData sensitiveData = AnnotationUtils.findAnnotation(parameterObjectClass, SensitiveData.class);
if (Objects.nonNull(sensitiveData)) {
//取出当前当前类所有字段,传入加密方法
Field[] declaredFields = parameterObjectClass.getDeclaredFields();
encrypt(declaredFields, parameterObject);
}
}
return invocation.proceed();
} catch (Exception e) {
log.error("加密失败", e);
}
return invocation.proceed();
}
/**
* 切记配置,否则当前拦截器不会加入拦截器链
*/
@Override
public Object plugin(Object o) {
return Plugin.wrap(o, this);
}
public <T> T encrypt(Field[] declaredFields, T paramsObject) throws IllegalAccessException {
for (Field field : declaredFields) {
//取出所有被EncryptDecryptField注解的字段
SensitiveField sensitiveField = field.getAnnotation(SensitiveField.class);
if (!Objects.isNull(sensitiveField)) {
field.setAccessible(true);
Object object = field.get(paramsObject);
//暂时只实现String类型的加密
if (object instanceof String) {
String value = (String) object;
//加密 这里我使用自定义的AES加密工具
field.set(paramsObject, EncryptUtils.encrypt(value));
}
}
}
return paramsObject;
}
}
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.ibatis.executor.resultset.ResultSetHandler;
import org.apache.ibatis.plugin.*;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.stereotype.Component;
import java.lang.reflect.Field;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Objects;
/**
* <br>
* .
* <p>
* Copyright: Copyright (c) 2021/1/18 10:51 上午
* <p>
* Company: zhongdian
* <p>
*
* @author tianlong
* @version 1.0.0
* @date: 2021/1/18 10:51 上午
* @email tianlong03@cestc.com
* @desc
*/
@Intercepts({
@Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class})
})
@Slf4j
@Component
public class DecryptInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object resultObject = invocation.proceed();
try {
if (Objects.isNull(resultObject)) {
return null;
}
//基于selectList
if (resultObject instanceof ArrayList) {
ArrayList resultList = (ArrayList) resultObject;
if (!CollectionUtils.isEmpty(resultList) && needToDecrypt(resultList.get(0))) {
for (Object result : resultList) {
//逐一解密
decrypt(result);
}
}
//基于selectOne
} else {
if (needToDecrypt(resultObject)) {
EncryptUtils.decrypt((String) resultObject);
}
}
return resultObject;
} catch (Exception e) {
log.error("解密失败", e);
}
return resultObject;
}
private boolean needToDecrypt(Object object) {
Class<?> objectClass = object.getClass();
SensitiveData sensitiveData = AnnotationUtils.findAnnotation(objectClass, SensitiveData.class);
return Objects.nonNull(sensitiveData);
}
@Override
public Object plugin(Object o) {
return Plugin.wrap(o, this);
}
public <T> T decrypt(T result) throws IllegalAccessException {
//取出resultType的类
Class<?> resultClass = result.getClass();
Field[] declaredFields = resultClass.getDeclaredFields();
for (Field field : declaredFields) {
//取出所有被EncryptDecryptField注解的字段
SensitiveField sensitiveField = field.getAnnotation(SensitiveField.class);
if (!Objects.isNull(sensitiveField)) {
field.setAccessible(true);
Object object = field.get(result);
//只支持String的解密
if (object instanceof String) {
String value = (String) object;
//对注解的字段进行逐一解密
field.set(result, EncryptUtils.decrypt(value).replaceAll("(\\d{4})\\d{10}(\\w{4})", "$1*****$2"));
}
}
Mask mask = field.getAnnotation(Mask.class);
if (!Objects.isNull(mask)) {
field.setAccessible(true);
Object object = field.get(result);
if (object instanceof String) {
String phone = (String) object;
field.set(result, phone.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2"));
}
}
}
return result;
}
}
同时解密的时候,会扫描Mask,运用正则表达式,做掩码。
这个里面使用了加密的工具类: EncryptUtils,这里的加密方法可以根据自己的需求自己定义,我这里使用的是非对称加密sm2. 具体加密就不给出了,只要自己定义一个加密解密就行了。