詳解Mybatis攔截器安全加解密MySQL數(shù)據(jù)實(shí)戰(zhàn)
需求背景
- 公司為了通過一些金融安全指標(biāo)(政策問題)和防止數(shù)據(jù)泄漏,需要對(duì)用戶敏感數(shù)據(jù)進(jìn)行加密,所以在公司項(xiàng)目中所有存儲(chǔ)了用戶信息的數(shù)據(jù)庫都需要進(jìn)行數(shù)據(jù)加密改造。包括Mysql、redis、mongodb、es、HBase等。
- 因?yàn)樵陧?xiàng)目中是使用springboot+mybatis方式連接數(shù)據(jù)庫進(jìn)行增刪改查,并且項(xiàng)目是中途改造數(shù)據(jù)。所以為了不影響正常業(yè)務(wù),打算這次改動(dòng)盡量不侵入到業(yè)務(wù)代碼,加上mybatis開放的各種攔截器接口,所以就以此進(jìn)行改造數(shù)據(jù)。
- 本篇文章講述如何在現(xiàn)有項(xiàng)目中盡量不侵入業(yè)務(wù)方式進(jìn)行Mysql加密數(shù)據(jù),最后為了不降低查詢性能使用了注解,所以最后還是部分侵入業(yè)務(wù)。
Mybatis攔截器
Mybatis只能攔截指定類里面的方法:Executor、ParameterHandler、StatementHandler、ResultSetHandler。
- Executor:攔截執(zhí)行器方法;
- ParameterHandler:攔截參數(shù)方法;
- StatementHandler:攔截sql構(gòu)建方法;
- ResultSetHandler:攔截查詢結(jié)果方法;
Mybatis提供的攔截器接口Interceptor
public interface Interceptor {
Object intercept(Invocation invocation) throws Throwable;
default Object plugin(Object target) {
return Plugin.wrap(target, this);
}
default void setProperties(Properties properties) {
// NOP
}
}- Object intercept():代理對(duì)象都會(huì)調(diào)用的方法,這里可以執(zhí)行自定義攔截處理;
- Object plugin():可以用于判斷攔截器執(zhí)行類型;
- void setProperties():指定配置文件的屬性;
自定義攔截器中除了要實(shí)現(xiàn)Interceptor接口,還需要添加@Intercepts注解指定攔截對(duì)象。@Intercepts注解需配合@Signature注解使用
@Intercepts注解可以指定多個(gè)@Signature,type指定攔截類,method指定攔截方法,args攔截方法里的參數(shù)類型。
/**
* @author Clinton Begin
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Intercepts {
Signature[] value();
}
/**
* @author Clinton Begin
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({})
public @interface Signature {
Class<?> type();
String method();
Class<?>[] args();
}案例實(shí)戰(zhàn)
依據(jù)上述的mybatis攔截器的使用,下面就把實(shí)戰(zhàn)案例代碼提供一下。
Mybatis自定義攔截器
- 在業(yè)務(wù)代碼里用戶信息是以明文傳遞的,所以為了不改動(dòng)業(yè)務(wù)代碼,那么需要攔截器在插入或查詢數(shù)據(jù)庫數(shù)據(jù)前先加密,查詢結(jié)果解密操作。
- 首先搭建一個(gè)springboot的項(xiàng)目,這里指定兩個(gè)mybatis攔截器,一個(gè)攔截請(qǐng)求參數(shù),一個(gè)攔截響應(yīng)數(shù)據(jù),并把攔截器注入到spring容器內(nèi)。
/**
* 對(duì)mybatis入?yún)⑦M(jìn)行攔截加密
* @author zrh
*/
@Slf4j
@Component
@Intercepts(@Signature(type = ParameterHandler.class, method = "setParameters", args = {PreparedStatement.class}))
public class MybatisEncryptInterceptor implements Interceptor {
@Resource
private com.mysql.web.mybatis.Interceptor.MybatisCryptHandler handler;
@Override
public Object intercept (Invocation invocation) {
return invocation;
}
@SneakyThrows
@Override
public Object plugin (Object target) {
if (target instanceof ParameterHandler) {
// 對(duì)請(qǐng)求參數(shù)進(jìn)行加密操作
handler.parameterEncrypt((ParameterHandler) target);
}
return target;
}
@Override
public void setProperties (Properties properties) {
}
}注意:ResultSetHandler對(duì)象對(duì)增刪改方法沒有攔截,需要增加Executor對(duì)象;
/**
* 對(duì)mybatis查詢結(jié)果進(jìn)行攔截解密,并對(duì)請(qǐng)求參數(shù)進(jìn)行攔截解密還原操作
* @author zrh
*/
@Slf4j
@Component
@Intercepts({
@Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class}),
@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
})
public class MybatisDecryptInterceptor implements Interceptor {
@Resource
private MybatisCryptHandler handler;
@Override
public Object intercept (Invocation invocation) throws Exception {
// 獲取執(zhí)行mysql執(zhí)行結(jié)果
Object result = invocation.proceed();
if (invocation.getTarget() instanceof Executor) {
// 對(duì)增刪改操作方法的請(qǐng)求參數(shù)進(jìn)行解密還原操作
checkEncryptByUpdate(invocation.getArgs());
return result;
}
// 對(duì)查詢方法的請(qǐng)求參數(shù)進(jìn)行解密還原操作
checkEncryptByQuery(invocation.getTarget());
// 對(duì)查詢結(jié)果進(jìn)行解密
return handler.resultDecrypt(result);
}
@Override
public Object plugin (Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties (Properties properties) {
}
/**
* 對(duì)請(qǐng)求參數(shù)進(jìn)行解密還原操作
* @param target
*/
private void checkEncryptByQuery (Object target) {
try {
final Class<?> targetClass = target.getClass();
final Field parameterHandlerFiled = targetClass.getDeclaredField("parameterHandler");
parameterHandlerFiled.setAccessible(true);
final Object parameterHandler = parameterHandlerFiled.get(target);
final Class<?> parameterHandlerClass = parameterHandler.getClass();
final Field parameterObjectField = parameterHandlerClass.getDeclaredField("parameterObject");
parameterObjectField.setAccessible(true);
final Object parameterObject = parameterObjectField.get(parameterHandler);
handler.decryptFieldHandler(parameterObject);
} catch (Exception e) {
log.error("對(duì)請(qǐng)求參數(shù)進(jìn)行解密還原操作異常:", e);
}
}
/**
* 對(duì)請(qǐng)求參數(shù)進(jìn)行解密還原操作
* @param args
*/
private void checkEncryptByUpdate (Object[] args) {
try {
Arrays.stream(args).forEach(handler::decryptFieldHandler);
} catch (Exception e) {
log.error("對(duì)請(qǐng)求參數(shù)進(jìn)行解密還原操作異常:", e);
}
}
}在上述攔截器中,除了對(duì)入?yún)⑦M(jìn)行加密和查詢結(jié)果解密操作外,還多了一步對(duì)請(qǐng)求參數(shù)進(jìn)行解密還原操作。
這是因?yàn)閷?duì)請(qǐng)求參數(shù)進(jìn)行加密操作時(shí)改動(dòng)的是原對(duì)象,如果不還原解密數(shù)據(jù),這個(gè)對(duì)象如果在后續(xù)還有其他操作,那就會(huì)使用密文,導(dǎo)致數(shù)據(jù)紊亂。
這里其實(shí)想過不改動(dòng)原對(duì)象,而是把原請(qǐng)求對(duì)象克隆一份,在克隆對(duì)象上進(jìn)行加密,然后在去查詢數(shù)據(jù)庫??上Э赡苁亲约簩?duì)mybatis不夠熟悉吧,試了很久也不能把mybatis內(nèi)的原對(duì)象替換為克隆對(duì)象,所以才就想了這個(gè)還原解密參數(shù)的方式。

如果對(duì)請(qǐng)求參數(shù)對(duì)象和查詢結(jié)果對(duì)象里的所有字段都進(jìn)行加解密,那上述配置就基本完成。但在本次安全加解密需求中只針對(duì)指定字段(如手機(jī)號(hào)和真實(shí)姓名),現(xiàn)在這種全量字段加解密就不行,而且性能也低,畢竟加解密是很耗費(fèi)服務(wù)器CPU運(yùn)算資源的。
所以需要增加注解,在指定對(duì)象的屬性字段才進(jìn)行加解密。
/**
* <p>作用于類:標(biāo)識(shí)當(dāng)前實(shí)體需要進(jìn)行結(jié)果解密操作.
* <p>作用于字段:標(biāo)識(shí)當(dāng)前實(shí)體的字段需要進(jìn)行加解密操作.
* <p>作用于方法:標(biāo)識(shí)當(dāng)前mapper方法會(huì)被切面進(jìn)行攔截,并進(jìn)行數(shù)據(jù)的加解密操作.
* <p>注意:如果作用于字段,那當(dāng)前類必須先標(biāo)注該注解,因?yàn)闀?huì)優(yōu)先判斷類是否需要加解密,然后在判斷字段是否需要加解密,否則只作用于字段不會(huì)起效
*
* @author zrh
* @date 2022/1/4
*/
@Documented
@Inherited
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Crypt {
/**
* 默認(rèn)字段需要解密
*/
boolean decrypt () default true;
/**
* 默認(rèn)字段需要加密
*/
boolean encrypt () default true;
/**
* 字段為對(duì)象時(shí)有用,默認(rèn)當(dāng)前對(duì)象不需要進(jìn)行加解密
*/
boolean subObject () default false;
/**
* 需要進(jìn)行加密的字段列下標(biāo)
*/
int[] encryptParamIndex () default {};
}其注解使用方式如下:

AesTools是對(duì)數(shù)據(jù)進(jìn)行AES對(duì)稱加解密工具類
/**
* AES加密工具
*
* @author zrh
* @date 2022/1/3
*/
@Slf4j
public final class AesTools {
private AesTools () {
}
private static final String KEY_ALGORITHM = "AES";
private static final String ENCODING = "UTF-8";
private static final String DEFAULT_CIPHER_ALGORITHM = "AES/ECB/PKCS5Padding";
private static Cipher ENCODING_CIPHER = null;
private static Cipher DECRYPT_CIPHER = null;
/**
* 秘鑰
*/
private static final String KEY = "cab041-3c46-fed5";
static {
try {
// 初始化cipher
ENCODING_CIPHER = Cipher.getInstance(DEFAULT_CIPHER_ALGORITHM);
DECRYPT_CIPHER = Cipher.getInstance(DEFAULT_CIPHER_ALGORITHM);
//轉(zhuǎn)化成JAVA的密鑰格式
SecretKeySpec keySpec = new SecretKeySpec(KEY.getBytes("ASCII"), KEY_ALGORITHM);
ENCODING_CIPHER.init(Cipher.ENCRYPT_MODE, keySpec);
DECRYPT_CIPHER.init(Cipher.DECRYPT_MODE, keySpec);
} catch (Exception e) {
log.error("初始化mybatis -> AES加解密參數(shù)異常:", e);
}
}
/**
* AES加密
* @param content 加密內(nèi)容
* @return
*/
public static String encryptECB (String content) {
if (StringUtils.isEmpty(content)) {
return content;
}
String encryptStr = content;
try {
byte[] encrypted = ENCODING_CIPHER.doFinal(content.getBytes(ENCODING));
encryptStr = Base64.getEncoder().encodeToString(encrypted);
} catch (Exception e) {
log.info("mybatis -> AES加密出錯(cuò):{}", content);
}
return encryptStr;
}
/**
* AES解密
* @param content 解密內(nèi)容
* @return
*/
public static String decryptECB (String content) {
if (StringUtils.isEmpty(content)) {
return content;
}
String decryptStr = content;
try {
byte[] decrypt = DECRYPT_CIPHER.doFinal(Base64.getDecoder().decode(content));
decryptStr = new String(decrypt, ENCODING);
} catch (Exception e) {
log.info("mybatis -> AES解密出錯(cuò):{}", content);
}
return decryptStr;
}
}MybatisCryptHandler是對(duì)請(qǐng)求入?yún)?duì)象和查詢結(jié)果對(duì)象進(jìn)行加解密操作工具類。
代碼稍許復(fù)雜,但實(shí)現(xiàn)邏輯簡(jiǎn)單,主要為了防止重復(fù)加密,內(nèi)置緩存,對(duì)遞歸對(duì)象掃描檢索,反射+注解獲取需要加解密字段等。
/**
* @author zrh
* @date 2022/1/2
*/
@Slf4j
@Component
public class MybatisCryptHandler {
private final static ThreadLocal<List> THREAD_LOCAL = ThreadLocal.withInitial(() -> new ArrayList());
private static final List<Field> EMPTY_FIELD_ARRAY = new ArrayList();
/**
* Cache for {@link Class#getDeclaredFields()}, allowing for fast iteration.
*/
private static final Map<Class<?>, List<Field>> declaredFieldsCache = new ConcurrentHashMap<>(256);
/**
* 參數(shù)對(duì)外加密方法
* @param handler
*/
public void parameterEncrypt (ParameterHandler handler) {
Object parameterObject = handler.getParameterObject();
if (null == parameterObject || parameterObject instanceof String) {
return;
}
encryptFieldHandler(parameterObject);
removeLocal();
}
/**
* 參數(shù)加密規(guī)則方法
* @param sourceObject
*/
private void encryptFieldHandler (Object sourceObject) {
if (null == sourceObject) {
return;
}
if (sourceObject instanceof Map) {
((Map<?, Object>) sourceObject).values().forEach(this::encryptFieldHandler);
return;
}
if (sourceObject instanceof List) {
((List<?>) sourceObject).stream().forEach(this::encryptFieldHandler);
return;
}
Class<?> clazz = sourceObject.getClass();
if (!clazz.isAnnotationPresent(Crypt.class)) {
return;
}
if (checkLocal(sourceObject)) {
return;
}
setLocal(sourceObject);
try {
Field[] declaredFields = clazz.getDeclaredFields();
// 獲取滿足加密注解條件的字段
final List<Field> collect = Arrays.stream(declaredFields).filter(this::checkEncrypt).collect(Collectors.toList());
for (Field item : collect) {
item.setAccessible(true);
Object value = item.get(sourceObject);
if (null != value && value instanceof String) {
item.set(sourceObject, AesTools.encryptECB((String) value));
}
}
} catch (Exception e) {
}
}
/**
* 解析注解 - 加密密方法
* @param field
* @return
*/
private boolean checkEncrypt (Field field) {
Crypt crypt = field.getAnnotation(Crypt.class);
return null != crypt && crypt.encrypt();
}
/**
* 查詢結(jié)果對(duì)外解密方法
* @param resultData
*/
public Object resultDecrypt (Object resultData) {
if (resultData instanceof List) {
return ((List<?>) resultData).stream().map(this::resultObjHandler).collect(Collectors.toList());
}
return resultObjHandler(resultData);
}
/**
* 查詢結(jié)果解密規(guī)則方法
* @param result
*/
private Object resultObjHandler (Object result) {
if (null == result) {
return null;
}
Class<?> clazz = result.getClass();
//獲取所有要解密的字段
Field[] declaredFields = getAllFieldsCache(clazz);
Arrays.stream(declaredFields).forEach(item -> {
try {
item.setAccessible(true);
Object value = item.get(result);
if (null != value && value instanceof String) {
item.set(result, AesTools.decryptECB((String) value));
}
} catch (Exception e) {
log.error("DecryptException -> checkDecrypt:", e);
}
});
Arrays.stream(declaredFields).filter(item -> checkSubObject(item)).forEach(item -> {
item.setAccessible(true);
try {
Object data = item.get(result);
if (data instanceof List) {
((List<?>) data).forEach(this::resultObjHandler);
}
} catch (IllegalAccessException e) {
log.error("DecryptException -> checkSubObject:{}", e);
}
});
return result;
}
/**
* 解析注解 - 解密方法
* @param field
* @return
*/
private static boolean checkDecrypt (Field field) {
Crypt crypt = field.getAnnotation(Crypt.class);
return null != crypt && crypt.decrypt();
}
/**
* 解析注解 - 子對(duì)象
* @param field
* @return
*/
private static boolean checkSubObject (Field field) {
Crypt crypt = field.getAnnotation(Crypt.class);
return null != crypt && crypt.subObject();
}
/**
* 對(duì)請(qǐng)求參數(shù)進(jìn)行解密還原,
* @param requestObject
*/
public void decryptFieldHandler (Object requestObject) {
if (null == requestObject) {
return;
}
if (requestObject instanceof Map) {
((Map<?, Object>) requestObject).values().forEach(this::decryptFieldHandler);
return;
}
if (requestObject instanceof List) {
((List<?>) requestObject).stream().forEach(this::decryptFieldHandler);
return;
}
Class<?> clazz = requestObject.getClass();
if (!clazz.isAnnotationPresent(Crypt.class)) {
return;
}
try {
Field[] declaredFields = clazz.getDeclaredFields();
// 獲取滿足加密注解條件的字段
final List<Field> collect = Arrays.stream(declaredFields).filter(this::checkEncrypt).collect(Collectors.toList());
for (Field item : collect) {
item.setAccessible(true);
Object value = item.get(requestObject);
if (null != value && value instanceof String) {
item.set(requestObject, AesTools.decryptECB((String) value));
}
}
} catch (Exception e) {
}
}
/**
* 統(tǒng)一管理內(nèi)存
* @param o
* @return
*/
private boolean checkLocal (Object o) {
return THREAD_LOCAL.get().contains(o);
}
private void setLocal (Object o) {
THREAD_LOCAL.get().add(o);
}
private void removeLocal () {
THREAD_LOCAL.get().clear();
}
/**
* 獲取本類及其父類的屬性的方法
* @param clazz 當(dāng)前類對(duì)象
* @return 字段數(shù)組
*/
private static Field[] getAllFields (Class<?> clazz) {
List<Field> fieldList = new ArrayList<>();
while (clazz != null) {
fieldList.addAll(new ArrayList<>(Arrays.asList(clazz.getDeclaredFields())));
clazz = clazz.getSuperclass();
}
Field[] fields = new Field[fieldList.size()];
return fieldList.toArray(fields);
}
/**
* 獲取本類及其父類的屬性的方法
* @param clazz 當(dāng)前類對(duì)象
* @return 字段數(shù)組
*/
private static Field[] getAllFieldsCache (Class<?> clazz) {
List<Field> fieldList = new ArrayList<>();
while (clazz != null) {
if (clazz.isAnnotationPresent(Crypt.class)) {
fieldList.addAll(getDeclaredFields(clazz));
}
clazz = clazz.getSuperclass();
}
Field[] fields = new Field[fieldList.size()];
return fieldList.toArray(fields);
}
private static List<Field> getDeclaredFields (Class<?> clazz) {
List<Field> result = declaredFieldsCache.get(clazz);
if (result == null) {
try {
// 獲取滿足注解解密條件的字段
result = Arrays.stream(clazz.getDeclaredFields()).filter(MybatisCryptHandler::checkDecrypt).collect(Collectors.toList());
// 放入本地緩存
declaredFieldsCache.put(clazz, (result.isEmpty() ? EMPTY_FIELD_ARRAY : result));
} catch (Exception e) {
log.error("getDeclaredFields:", e);
}
}
return result;
}
}數(shù)據(jù)表準(zhǔn)備
用戶的敏感信息包括有手機(jī)號(hào)、真實(shí)姓名、身份證、銀行卡號(hào)、支付寶賬號(hào)等幾種。下面使用手機(jī)號(hào)和姓名字段進(jìn)行加解密案例。
先準(zhǔn)備一張Mysql數(shù)據(jù)表,表里有兩個(gè)手機(jī)號(hào)和兩個(gè)姓名字段,可以用于安全加解密對(duì)比。
CREATE TABLE `phone_data` ( `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主鍵', `phone` varchar(122) DEFAULT NULL COMMENT '明文手機(jī)號(hào)', `user_phone` varchar(122) DEFAULT NULL COMMENT '密文手機(jī)號(hào)', `name` varchar(122) DEFAULT NULL COMMENT '明文姓名', `real_name` varchar(122) DEFAULT NULL COMMENT '密文姓名', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COMMENT='測(cè)試加解密數(shù)據(jù)表';
項(xiàng)目demo搭建
首先搭建一個(gè)springboot的項(xiàng)目,把一些基礎(chǔ)配置類創(chuàng)建:如controller、service、mapper、xml、entity,為了快速簡(jiǎn)易的demo示例,這里去掉service層
/**
* @Author: ZRH
* @Date: 2022/1/5 13:47
*/
@Data
public class PhoneData {
private Integer id;
private String phone;
private String userPhone;
private String name;
private String realName;
public static PhoneData build (String phone) {
return build(null, phone);
}
public static PhoneData build (Integer id, String phone) {
final PhoneData phoneData = new PhoneData();
phoneData.setId(id);
phoneData.setPhone(phone);
phoneData.setUserPhone(phone);
phoneData.setName(phone);
phoneData.setRealName(phone);
return phoneData;
}
}
/**
* @Author: ZRH
* @Date: 2022/1/5 11:55
*/
@Slf4j
@RestController
public class AopMapperController {
@Autowired
private PhoneDataMapper phoneDataMapper;
/**
* 添加示例接口
* @param phone
* @return
*/
@PostMapping("/aop/insert")
public String insert (@RequestParam String phone) {
PhoneData build = PhoneData.build(phone);
phoneDataMapper.insert(build);
log.info(" 插入的原數(shù)據(jù) = {}", JSON.toJSONString(build));
return "ok";
}
/**
* 更新示例接口
* @param id
* @param phone
* @return
*/
@PostMapping("/aop/update")
public String update (@RequestParam Integer id, @RequestParam String phone) {
PhoneData build = PhoneData.build(id, phone);
phoneDataMapper.updateById(build);
log.info(" 插入的原數(shù)據(jù) = {}", JSON.toJSONString(build));
return "ok";
}
/**
* 查詢示例接口
* @param phone
* @return
*/
@GetMapping("/aop/select")
public String select (@RequestParam String phone) {
final PhoneData build = PhoneData.build(phone);
// 對(duì)象類型入?yún)⒉樵儗?duì)象數(shù)據(jù)
List<PhoneData> selectList = phoneDataMapper.selectList(build);
log.info(" selectList = {}", JSON.toJSONString(selectList));
return "ok";
}
}
/**
* @Author: ZRH
* @Date: 2021/11/25 13:48
*/
@Mapper
public interface PhoneDataMapper {
/**
* 新增數(shù)據(jù)
* @param phoneData
*/
@Insert("insert into phone_data (phone, user_phone, name, real_name) values (#{phone}, #{userPhone}, #{name}, #{realName})")
void insert (PhoneData phoneData);
/**
* 更新數(shù)據(jù)
* @param phoneData
*/
@Update("update phone_data set phone = #{phone}, user_phone = #{userPhone}, name = #{name}, real_name = #{realName} where id = #{id}")
void updateById (PhoneData phoneData);
/**
* 無參查詢對(duì)象類型數(shù)據(jù)
* @return
*/
@Select("select id, phone, user_phone userPhone, name, real_name realName from phone_data where user_phone = #{userPhone}")
List<PhoneData> selectList (PhoneData phoneData);
}項(xiàng)目啟動(dòng),訪問添加、更新、查詢接口,其sql日志打印出結(jié)果如下:
2022-01-07 14:46:35.348 DEBUG 6688 --- [ XNIO-1 task-1] c.m.web.mapper.PhoneDataMapper.insert : ==> Preparing: insert into phone_data (phone, user_phone, name, real_name) values (?, ?, ?, ?)
2022-01-07 14:46:35.348 DEBUG 6688 --- [ XNIO-1 task-1] c.m.web.mapper.PhoneDataMapper.insert : ==> Parameters: 15222222222(String), ZHlSotVArLBAviP2KWi3Cg==(String), 15222222222(String), ZHlSotVArLBAviP2KWi3Cg==(String)
2022-01-07 14:46:35.421 DEBUG 6688 --- [ XNIO-1 task-1] c.m.web.mapper.PhoneDataMapper.insert : <== Updates: 1
2022-01-07 14:46:35.422 INFO 6688 --- [ XNIO-1 task-1] c.m.web.controller.AopMapperController : 插入的原數(shù)據(jù) = {"name":"15222222222","phone":"15222222222","realName":"15222222222","userPhone":"15222222222"}
2022-01-07 14:46:54.470 DEBUG 6688 --- [ XNIO-1 task-1] c.m.w.mapper.PhoneDataMapper.updateById : ==> Preparing: update phone_data set phone = ?, user_phone = ?, name = ?, real_name = ? where id = ?
2022-01-07 14:46:54.470 DEBUG 6688 --- [ XNIO-1 task-1] c.m.w.mapper.PhoneDataMapper.updateById : ==> Parameters: 15222222222(String), ZHlSotVArLBAviP2KWi3Cg==(String), 15222222222(String), ZHlSotVArLBAviP2KWi3Cg==(String), 1(Integer)
2022-01-07 14:46:54.540 DEBUG 6688 --- [ XNIO-1 task-1] c.m.w.mapper.PhoneDataMapper.updateById : <== Updates: 1
2022-01-07 14:46:54.540 INFO 6688 --- [ XNIO-1 task-1] c.m.web.controller.AopMapperController : 插入的原數(shù)據(jù) = {"id":1,"name":"15222222222","phone":"15222222222","realName":"15222222222","userPhone":"15222222222"}
2022-01-07 14:46:55.754 DEBUG 6688 --- [ XNIO-1 task-1] c.m.w.mapper.PhoneDataMapper.selectList : ==> Preparing: select id, phone, user_phone userPhone, name, real_name realName from phone_data where user_phone = ?
2022-01-07 14:46:55.754 DEBUG 6688 --- [ XNIO-1 task-1] c.m.w.mapper.PhoneDataMapper.selectList : ==> Parameters: ZHlSotVArLBAviP2KWi3Cg==(String)
2022-01-07 14:46:55.790 DEBUG 6688 --- [ XNIO-1 task-1] c.m.w.mapper.PhoneDataMapper.selectList : <== Total: 1
2022-01-07 14:46:55.790 INFO 6688 --- [ XNIO-1 task-1] c.m.web.controller.AopMapperController : selectList = [{"id":1,"name":"15222222222","phone":"15222222222","realName":"15222222222","userPhone":"15222222222"}]MySQL數(shù)據(jù)庫中的數(shù)據(jù)

總結(jié)
總結(jié)一下上述實(shí)現(xiàn)邏輯:
- 在Mybatis自定義攔截器中,對(duì)各種對(duì)數(shù)據(jù)庫的查詢參數(shù)進(jìn)行攔截,判斷當(dāng)前對(duì)象內(nèi)字段是否需要加密,如果有注解就進(jìn)行加密操作,否則就不操作。再對(duì)數(shù)據(jù)庫查詢出的結(jié)果進(jìn)行攔截,判斷查詢結(jié)果對(duì)象內(nèi)字段是否需要解密,如果有注解就進(jìn)行解密操作,否則就不操作。
- 并且增加了一步對(duì)本次請(qǐng)求參數(shù)進(jìn)行參數(shù)還原解密操作。
- 這樣通過類和字段上增加注解,就完成自動(dòng)安全加解密操作。
上述攔截器實(shí)現(xiàn)方式有一定局限性:
- 整個(gè)代碼邏輯還可以在優(yōu)化,比如之前對(duì)請(qǐng)求參數(shù)還原解密方式的優(yōu)化。
- 主要實(shí)現(xiàn)邏輯是在MybatisCryptHandler處理工具類中,當(dāng)前方式現(xiàn)只能處理請(qǐng)求參數(shù)和查詢結(jié)果是對(duì)象類而不是字符串類型,在下篇文章中會(huì)介紹如何針對(duì)字符串進(jìn)行過濾攔截。
到此這篇關(guān)于詳解Mybatis攔截器安全加解密MySQL數(shù)據(jù)實(shí)戰(zhàn)的文章就介紹到這了,更多相關(guān)Mybatis攔截器安全加解密MySQL內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java long 轉(zhuǎn)成 String的實(shí)現(xiàn)
這篇文章主要介紹了Java long 轉(zhuǎn)成 String的實(shí)現(xiàn),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2020-09-09
Java定時(shí)任務(wù)schedule和scheduleAtFixedRate的異同
本文主要介紹了Java定時(shí)任務(wù)schedule和scheduleAtFixedRate的異同,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-05-05
JDK12的新特性之CompactNumberFormat詳解
這篇文章主要介紹了JDK12的新特性之CompactNumberFormat,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-05-05
elasticsearch索引index數(shù)據(jù)功能源碼示例
這篇文章主要為大家介紹了elasticsearch索引index功能源碼示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-04-04
使用SpringMVC訪問Controller接口返回400BadRequest
這篇文章主要介紹了使用SpringMVC訪問Controller接口返回400BadRequest,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-03-03
IDEA2020.1個(gè)性化設(shè)置的實(shí)現(xiàn)
這篇文章主要介紹了IDEA2020.1個(gè)性化設(shè)置的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-08-08

