Mybatis拦截器安全加解密MySQL数据实战

public Object plugin (Object target) {

if (target instanceof ParameterHandler) {

// 对请求参数进行加密操作

handler.parameterEncrypt((ParameterHandler) target);

}

return target;

}

@Override

public void setProperties (Properties properties) {

}

}

复制代码

  • 注意:ResultSetHandler对象对增删改方法没有拦截,需要增加Executor对象;

/**

  • 对mybatis查询结果进行拦截解密,并对请求参数进行拦截解密还原操作

  • @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 {

// 获取执行mysql执行结果

Object result = invocation.proceed();

if (invocation.getTarget() instanceof Executor) {

// 对增删改操作方法的请求参数进行解密还原操作

checkEncryptByUpdate(invocation.getArgs());

return result;

}

// 对查询方法的请求参数进行解密还原操作

checkEncryptByQuery(invocation.getTarget());

// 对查询结果进行解密

return handler.resultDecrypt(result);

}

@Override

public Object plugin (Object target) {

return Plugin.wrap(target, this);

}

@Override

public void setProperties (Properties properties) {

}

/**

  • 对请求参数进行解密还原操作

  • @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(“对请求参数进行解密还原操作异常:”, e);

}

}

/**

  • 对请求参数进行解密还原操作

  • @param args

*/

private void checkEncryptByUpdate (Object[] args) {

try {

Arrays.stream(args).forEach(handler::decryptFieldHandler);

} catch (Exception e) {

log.error(“对请求参数进行解密还原操作异常:”, e);

}

}

}

复制代码

  • 在上述拦截器中,除了对入参进行加密和查询结果解密操作外,还多了一步对请求参数进行解密还原操作。

  • 这是因为对请求参数进行加密操作时改动的是原对象,如果不还原解密数据,这个对象如果在后续还有其他操作,那就会使用密文,导致数据紊乱。

  • 这里其实想过不改动原对象,而是把原请求对象克隆一份,在克隆对象上进行加密,然后在去查询数据库。可惜可能是自己对mybatis不够熟悉吧,试了很久也不能把mybatis内的原对象替换为克隆对象,所以才就想了这个还原解密参数的方式。

2.png

  • 如果对请求参数对象和查询结果对象里的所有字段都进行加解密,那上述配置就基本完成。但在本次安全加解密需求中只针对指定字段(如手机号和真实姓名),现在这种全量字段加解密就不行,而且性能也低,毕竟加解密是很耗费服务器CPU运算资源的。

  • 所以需要增加注解,在指定对象的属性字段才进行加解密。

/**

  • 作用于类:标识当前实体需要进行结果解密操作.

  • 作用于字段:标识当前实体的字段需要进行加解密操作.

  • 作用于方法:标识当前mapper方法会被切面进行拦截,并进行数据的加解密操作.

  • 注意:如果作用于字段,那当前类必须先标注该注解,因为会优先判断类是否需要加解密,然后在判断字段是否需要加解密,否则只作用于字段不会起效

  • @author zrh

  • @date 2022/1/4

*/

@Documented

@Inherited

@Target({ElementType.FIELD, ElementType.METHOD, ElementType.TYPE})

@Retention(RetentionPolicy.RUNTIME)

public @interface Crypt {

/**

  • 默认字段需要解密

*/

boolean decrypt () default true;

/**

  • 默认字段需要加密

*/

boolean encrypt () default true;

/**

  • 字段为对象时有用,默认当前对象不需要进行加解密

*/

boolean subObject () default false;

/**

  • 需要进行加密的字段列下标

*/

int[] encryptParamIndex () default {};

}

复制代码

  • 其注解使用方式如下:

1.png

  • AesTools是对数据进行AES对称加解密工具类

/**

  • 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);

//转化成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加解密参数异常:”, e);

}

}

/**

  • AES加密

  • @param content 加密内容

  • @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加密出错:{}”, content);

}

return encryptStr;

}

/**

  • AES解密

  • @param content 解密内容

  • @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解密出错:{}”, content);

}

return decryptStr;

}

}

复制代码

  • MybatisCryptHandler是对请求入参对象和查询结果对象进行加解密操作工具类。

  • 代码稍许复杂,但实现逻辑简单,主要为了防止重复加密,内置缓存,对递归对象扫描检索,反射+注解获取需要加解密字段等。

/**

  • @author zrh

  • @date 2022/1/2

*/

@Slf4j

@Component

public class MybatisCryptHandler {

private final static ThreadLocal THREAD_LOCAL = ThreadLocal.withInitial(() -> new ArrayList());

private static final List EMPTY_FIELD_ARRAY = new ArrayList();

/**

  • Cache for {@link Class#getDeclaredFields()}, allowing for fast iteration.

*/

private static final Map<Class<?>, List> declaredFieldsCache = new ConcurrentHashMap<>(256);

/**

  • 参数对外加密方法

  • @param handler

*/

public void parameterEncrypt (ParameterHandler handler) {

Object parameterObject = handler.getParameterObject();

if (null == parameterObject || parameterObject instanceof String) {

return;

}

encryptFieldHandler(parameterObject);

removeLocal();

}

/**

  • 参数加密规则方法

  • @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 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();

}

/**

  • 查询结果对外解密方法

  • @param resultData

*/

public Object resultDecrypt (Object resultData) {

if (resultData instanceof List) {

return ((List<?>) resultData).stream().map(this::resultObjHandler).collect(Collectors.toList());

}

return resultObjHandler(resultData);

}

/**

  • 查询结果解密规则方法

  • @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();

}

/**

  • 解析注解 - 子对象

  • @param field

  • @return

*/

private static boolean checkSubObject (Field field) {

Crypt crypt = field.getAnnotation(Crypt.class);

return null != crypt && crypt.subObject();

}

/**

  • 对请求参数进行解密还原,

  • @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 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) {

}

}

/**

  • 统一管理内存

  • @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 当前类对象

  • @return 字段数组

*/

private static Field[] getAllFields (Class<?> clazz) {

List 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 当前类对象

  • @return 字段数组

*/

private static Field[] getAllFieldsCache (Class<?> clazz) {

List 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 getDeclaredFields (Class<?> clazz) {

List 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;

}

}

复制代码

数据表准备

  • 用户的敏感信息包括有手机号、真实姓名、身份证、银行卡号、支付宝账号等几种。下面使用手机号和姓名字段进行加解密案例。

  • 先准备一张Mysql数据表,表里有两个手机号和两个姓名字段,可以用于安全加解密对比。

CREATE TABLE phone_data (

id int(11) NOT NULL AUTO_INCREMENT COMMENT ‘主键’,

phone varchar(122) DEFAULT NULL COMMENT ‘明文手机号’,

user_phone varchar(122) DEFAULT NULL COMMENT ‘密文手机号’,

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=‘测试加解密数据表’;

复制代码

项目demo搭建

  • 首先搭建一个springboot的项目,把一些基础配置类创建:如controller、service、mapper、xml、entity,为了快速简易的demo示例,这里去掉service层

/**

  • @Author: ZRH

  • @Date: 2022/1/5 13:47

*/

最后

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助。

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,不论你是刚入门Java开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门!

如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
EFAULT NULL COMMENT ‘明文姓名’,

real_name varchar(122) DEFAULT NULL COMMENT ‘密文姓名’,

PRIMARY KEY (id)

) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COMMENT=‘测试加解密数据表’;

复制代码

项目demo搭建

  • 首先搭建一个springboot的项目,把一些基础配置类创建:如controller、service、mapper、xml、entity,为了快速简易的demo示例,这里去掉service层

/**

  • @Author: ZRH

  • @Date: 2022/1/5 13:47

*/

最后

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助。

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

[外链图片转存中…(img-Z0svuooB-1715840889821)]

[外链图片转存中…(img-vtQmNrnf-1715840889822)]

[外链图片转存中…(img-3teeq5gs-1715840889822)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,不论你是刚入门Java开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门!

如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值