為了數(shù)據(jù)安全問題埋涧,有時候需要將部分敏感字段加密后再入庫围肥,查詢時又需要將其解密后返回給前端使用戏羽。我們可以用Mybatis的攔截器來實現(xiàn)這一需求梧奢。
- 定義一個注解狱掂,用來標(biāo)識需要加解密的字段。
為了盡量減少不必要的反射操作亲轨,可以將該注解同時標(biāo)識在實體類上趋惨,對于沒有被標(biāo)識的實體類,無需利用反射來操作其屬性惦蚊。
import java.lang.annotation.*;
/**
* 標(biāo)識需要加解密的類及其屬性
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.TYPE})
public @interface FieldEncrypt {
}
- 定義加密攔截器
攔截所有insert和update操作器虾,拿到實體對象,并通過反射獲取到所有標(biāo)記了@FieldExcrypt注解的屬性蹦锋,將其值進行加密并替換
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.binding.MapperMethod;
import org.apache.ibatis.executor.Executor;
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.Signature;
/**
* 加密攔截器
* <p>攔截所有insert和update操作兆沙,</p>
* <p>如果實體類中有屬性標(biāo)記有@FieldEncrypt注解,則對其進行加密替換</p>
*/
@Slf4j
@Intercepts({@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})})
public class EncryptionInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object[] args = invocation.getArgs();
MappedStatement ms = (MappedStatement) args[0];
SqlCommandType sqlCommandType = ms.getSqlCommandType();
log.debug("EncryptionInterceptor 操作類型:{}", sqlCommandType);
MapperMethod.ParamMap paramMap = (MapperMethod.ParamMap)args[1];
Object entity = paramMap.get("et");
try {
if (SqlCommandType.INSERT == sqlCommandType) {
FieldEncryptUtil.encryptField(entity);
} else if (SqlCommandType.UPDATE == sqlCommandType) {
FieldEncryptUtil.encryptField(entity);
log.debug("[EncryptionInterceptor] update operation,encrypt field: {}", entity);
}
} catch (Exception e) {
log.error("[EncryptionInterceptor] encryptField failed entity:{}", entity.getClass().getName(), e);
}
return invocation.proceed();
}
}
- 定義解密攔截器
攔截所有query操作莉掂,先執(zhí)行sql葛圃,拿到返回的結(jié)果,并通過反射獲取到所有標(biāo)記了@FieldExcrypt注解的屬性,將其值進行解密并替換装悲。
請注意:結(jié)果可能是單個Bean(如selectOne方法)昏鹃,也有可能是ArrayList。如果是集合诀诊,則以第一個元素的Class對象為準(zhǔn)洞渤。
import lombok.extern.slf4j.Slf4j;
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.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import java.util.ArrayList;
/**
* 解密攔截器
* <p>攔截所有query操作</p>
* <p>如果結(jié)果集合元素實體中,存在屬性標(biāo)記了@FieldEncrypt注解属瓣,則解密并替換</p>
*/
@Slf4j
@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 DecryptionInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object result = invocation.proceed();
try {
if(result instanceof ArrayList) {
ArrayList list = (ArrayList) result;
if(list.isEmpty()) {
return result;
}
Class objClazz = list.get(0).getClass();
if (FieldEncryptUtil.hasEncryptFields(objClazz)) {
for (Object item : list) {
FieldEncryptUtil.decryptField(item);
}
}
return result;
} else {
FieldEncryptUtil.decryptField(result);
}
} catch (Exception e) {
log.error("[DecryptionInterceptor] decryptField failed entity:{}", result.getClass().getName(), e);
}
return result;
}
}
- 加解密工具類
import com.baomidou.mybatisplus.core.toolkit.AES;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.annotation.AnnotationUtils;
import java.lang.reflect.Field;
import java.lang.reflect.Type;
import java.util.Objects;
@Slf4j
public class FieldEncryptUtil {
private static final String SECRET = "{替換成AES密鑰}";
private FieldEncryptUtil() {
}
public static String encrypt(String src) {
return AES.encrypt(src, SECRET);
}
public static String decrypt(String src) {
return AES.decrypt(src, SECRET);
}
public static boolean hasEncryptFields(Class clz) {
return AnnotationUtils.isCandidateClass(clz, FieldEncrypt.class);
}
private static boolean isFieldEncrypt(Field field) {
FieldEncrypt encryptAnno = AnnotationUtils.findAnnotation(field, FieldEncrypt.class);
return Objects.nonNull(encryptAnno);
}
/**
* 對實體類字段加密替換
*/
public static void encryptField(Object object) throws IllegalAccessException {
if (Objects.isNull(object) || !hasEncryptFields(object.getClass())) {
return;
}
Class clazz = object.getClass();
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
Type fieldClazz = field.getGenericType();
if (!(fieldClazz instanceof Class) || !String.class.isAssignableFrom((Class)fieldClazz)) {
continue;
}
if (isFieldEncrypt(field)) {
field.setAccessible(true);
Object originVal = field.get(object);
if (null == originVal || StringUtils.isBlank(originVal.toString())) {
continue;
}
String encryptedStr = encrypt(originVal.toString());
field.set(object, encryptedStr);
log.debug("[encryptField success] encrypt {}#{}", clazz.getName(), field.getName());
}
}
}
public static void decryptField(Object object) throws IllegalAccessException {
if (Objects.isNull(object) || !hasEncryptFields(object.getClass())) {
return;
}
Class clazz = object.getClass();
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
Type fieldClazz = field.getGenericType();
if (!(fieldClazz instanceof Class) || !String.class.isAssignableFrom((Class)fieldClazz)) {
continue;
}
if (isFieldEncrypt(field)) {
field.setAccessible(true);
Object originVal = field.get(object);
if (null == originVal || StringUtils.isBlank(originVal.toString())) {
continue;
}
String decryptedStr = decrypt(originVal.toString());
field.set(object, decryptedStr);
log.debug("[decryptField success] {}#{}", clazz.getName(), field.getName());
}
}
}
}
實體類示例(對身份證號進行加密存儲)
@FieldEncrypt
@TableName("user")
public class User {
@TableId
private Long id;
private String username;
private String phone;
@FieldEncrypt
private String idCard;
}
注意事項
因為我是在starter組件里實現(xiàn)的载迄,所以兩個攔截器沒有使用@Component注解標(biāo)識,而是使用AutoConfiguration聲明的抡蛙。如果你是在項目中直接實現(xiàn)的护昧,請將其交給Spring容器進行管理。
因為加解密是針對字符串而言的粗截,所以惋耙,在進行加解密時需要判斷字段類型,如果注解錯誤地標(biāo)識在了非String類型字段上熊昌,要么忽略绽榛,要么根據(jù)實際需求實現(xiàn)邏輯。
工具類中寫死了AES密鑰婿屹,這個密鑰必須全局唯一灭美,否則會出錯。
為防止意外錯誤發(fā)生昂利,導(dǎo)致代碼報錯届腐,我這里在加解密過程中若發(fā)生異常,默認是不替換蜂奸,直接返回原值犁苏。