详解Mybatis安全加解密MySQL数据实战
需求背景
公司为了通过⼀些⾦融安全指标(政策问题)和防⽌数据泄漏,需要对⽤户敏感数据进⾏加密,所以在公司项⽬中所有存储了⽤户信息的数据库都需要进⾏数据加密改造。包括Mysql、redis、mongodb、es、HBase等。
因为在项⽬中是使⽤springboot+mybatis⽅式连接数据库进⾏增删改查,并且项⽬是中途改造数据。所以为了不影响正常业务,打算这次改动尽量不侵⼊到业务代码,加上mybatis开放的各种接⼝,所以就以此进⾏改造数据。
本篇⽂章讲述如何在现有项⽬中尽量不侵⼊业务⽅式进⾏Mysql加密数据,最后为了不降低查询性能使⽤了注解,所以最后还是部分侵⼊业务。
Mybatis
Mybatis只能拦截指定类⾥⾯的⽅法:Executor、ParameterHandler、StatementHandler、ResultSetHandler。
Executor:拦截执⾏器⽅法;
ParameterHandler:拦截参数⽅法;
StatementHandler:拦截sql构建⽅法;
ResultSetHandler:拦截查询结果⽅法;
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():代理对象都会调⽤的⽅法,这⾥可以执⾏⾃定义拦截处理;
- Object plugin():可以⽤于判断执⾏类型;
- void setProperties():指定配置⽂件的属性;
⾃定义中除了要实现Interceptor接⼝,还需要添加@Intercepts注解指定拦截对象。@Intercepts注解需配合@Signature注解使⽤
@Intercepts注解可以指定多个@Signature,type指定拦截类,method指定拦截⽅法,args拦截⽅法⾥的参数类型。
/**
* @author Clinton Begin
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
电脑c语言软件怎么下载@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();
}
案例实战
依据上述的mybatis的使⽤,下⾯就把实战案例代码提供⼀下。
Mybatis⾃定义
在业务代码⾥⽤户信息是以明⽂传递的,所以为了不改动业务代码,那么需要在插⼊或查询数据库数据前先加密,查询结果解密操作。
⾸先搭建⼀个springboot的项⽬,这⾥指定两个mybatis,⼀个拦截请求参数,⼀个拦截响应数据,并把注⼊到spring容器内。
/**
* 对mybatis⼊参进⾏拦截加密
* @author zrh
*/
@Slf4j
@Component
@Intercepts(@Signature(type = ParameterHandler.class, method = "setParameters", args = {PreparedStatement.class}))
public class MybatisEncryptInterceptor implements Interceptor {
@Resource
batis.Interceptor.MybatisCryptHandler handler;
@Override
public Object intercept (Invocation invocation) {
return invocation;
}
@SneakyThrows
@Override
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 (Target() instanceof Executor) {
// 对增删改操作⽅法的请求参数进⾏解密还原操作
Args());
return result;
}
// 对查询⽅法的请求参数进⾏解密还原操作
Target());
// 对查询结果进⾏解密
sultDecrypt(result);
}
@Override
public Object plugin (Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties (Properties properties) {
}
/**
drawbuffer
* 对请求参数进⾏解密还原操作
* @param target
*/
private void checkEncryptByQuery (Object target) {
try {
final Class<?> targetClass = Class();
final Field parameterHandlerFiled = DeclaredField("parameterHandler");
parameterHandlerFiled.setAccessible(true);
final Object parameterHandler = (target);
final Class<?> parameterHandlerClass = Class();
final Field parameterObjectField = DeclaredField("parameterObject");
parameterObjectField.setAccessible(true);
final Object parameterObject = (parameterHandler);
handler.decryptFieldHandler(parameterObject);
} catch (Exception e) {
<("对请求参数进⾏解密还原操作异常:", e);
}
}
/**
* 对请求参数进⾏解密还原操作
* @param args
*/
private void checkEncryptByUpdate (Object[] args) {
try {
Arrays.stream(args).forEach(handler::decryptFieldHandler);
} catch (Exception e) {
<("对请求参数进⾏解密还原操作异常:", e);
}
}
}
在上述中,除了对⼊参进⾏加密和查询结果解密操作外,还多了⼀步对请求参数进⾏解密还原操作。
这是因为对请求参数进⾏加密操作时改动的是原对象,如果不还原解密数据,这个对象如果在后续还有其他操作,那就会使⽤密⽂,导致数据紊乱。
这⾥其实想过不改动原对象,⽽是把原请求对象克隆⼀份,在克隆对象上进⾏加密,然后在去查询数
据库。可惜可能是⾃⼰对mybatis不够熟悉吧,试了很久也不能把mybatis内的原对象替换为克隆对象,所以才就想了这个还原解密参数的⽅式。
如果对请求参数对象和查询结果对象⾥的所有字段都进⾏加解密,那上述配置就基本完成。但在本次安全加解密需求中只针对指定字段(如⼿机号和真实姓名),现在这种全量字段加解密就不⾏,⽽且性能也低,毕竟加解密是很耗费服务器CPU运算资源的。
所以需要增加注解,在指定对象的属性字段才进⾏加解密。
/**
* <p>作⽤于类:标识当前实体需要进⾏结果解密操作.
* <p>作⽤于字段:标识当前实体的字段需要进⾏加解密操作.
* <p>作⽤于⽅法:标识当前mapper⽅法会被切⾯进⾏拦截,并进⾏数据的加解密操作.
* <p>注意:如果作⽤于字段,那当前类必须先标注该注解,因为会优先判断类是否需要加解密,然后在判断字段是否需要加解密,否则只作⽤于字段不会起效
*
* @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 {};
其注解使⽤⽅式如下:
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 = Instance(DEFAULT_CIPHER_ALGORITHM);
DECRYPT_CIPHER = Instance(DEFAULT_CIPHER_ALGORITHM);
//转化成JAVA的密钥格式
SecretKeySpec keySpec = new Bytes("ASCII"), KEY_ALGORITHM);
ENCODING_CIPHER.init(Cipher.ENCRYPT_MODE, keySpec);
DECRYPT_CIPHER.init(Cipher.DECRYPT_MODE, keySpec);
} catch (Exception e) {
<("初始化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.Bytes(ENCODING));
encryptStr = Encoder().encodeToString(encrypted);
} catch (Exception e) {
log.info("mybatis -> AES加密出错:{}", content);
}
return encryptStr;
}
/**
* AES解密
* @param content 解密内容
* @returnvue生命周期每个阶段可以做
*/
public static String decryptECB (String content) {
if (StringUtils.isEmpty(content)) {
return content;
}
String decryptStr = content;
try {
byte[] decrypt = DECRYPT_CIPHER.Decoder().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<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);
/**
* 参数对外加密⽅法
* @param handler
*/
public void parameterEncrypt (ParameterHandler handler) {
Object parameterObject = ParameterObject();
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 = Class();
if (!clazz.isAnnotationPresent(Crypt.class)) {
return;
}
if (checkLocal(sourceObject)) {
return;
}
setLocal(sourceObject);
try {
Field[] declaredFields = DeclaredFields();
// 获取满⾜加密注解条件的字段
final List<Field> collect = Arrays.stream(declaredFields).filter(this::checkEncrypt).List());            for (Field item : collect) {
item.setAccessible(true);
Object value = (sourceObject);
if (null != value && value instanceof String) {
item.set(sourceObject, ptECB((String) value));
}
}
} catch (Exception e) {
}
}
/**
* 解析注解 - 加密密⽅法
* @param field
* @return
*/
private boolean checkEncrypt (Field field) {
Crypt crypt = Annotation(Crypt.class);
return null != crypt && pt();
}
/**
* 查询结果对外解密⽅法
* @param resultData
*/
public Object resultDecrypt (Object resultData) {
if (resultData instanceof List) {
return ((List<?>) resultData).stream().map(this::resultObjHandler).List());
}
return resultObjHandler(resultData);
}
/
**
* 查询结果解密规则⽅法
* @param result
*/
private Object resultObjHandler (Object result) {
if (null == result) {
return null;
}
Class<?> clazz = Class();
//获取所有要解密的字段
Field[] declaredFields = getAllFieldsCache(clazz);
Arrays.stream(declaredFields).forEach(item -> {
try {
item.setAccessible(true);
Object value = (result);
if (null != value && value instanceof String) {
item.set(result, AesTools.decryptECB((String) value));
}
} catch (Exception e) {
<("DecryptException -> checkDecrypt:", e);
}
});
Arrays.stream(declaredFields).filter(item -> checkSubObject(item)).forEach(item -> {
item.setAccessible(true);
try {
Object data = (result);
if (data instanceof List) {
((List<?>) data).forEach(this::resultObjHandler);
}
} catch (IllegalAccessException e) {
<("DecryptException -> checkSubObject:{}", e);
}
});
return result;
}
/**
* 解析注解 - 解密⽅法
* @param field
* @return
*/
private static boolean checkDecrypt (Field field) {
Crypt crypt = Annotation(Crypt.class);
return null != crypt && crypt.decrypt();
}
/
**
* 解析注解 - ⼦对象
* @param field
* @return
*/
private static boolean checkSubObject (Field field) {
Crypt crypt = Annotation(Crypt.class);
return null != crypt && crypt.subObject();
}
/**
* 对请求参数进⾏解密还原,
手机mysql安装配置教程
* @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 = Class();
if (!clazz.isAnnotationPresent(Crypt.class)) {
html5比html4好在哪里return;
}
try {
Field[] declaredFields = DeclaredFields();
// 获取满⾜加密注解条件的字段
final List<Field> collect = Arrays.stream(declaredFields).filter(this::checkEncrypt).List());
for (Field item : collect) {
item.setAccessible(true);
Object value = (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 ().contains(o);
}
private void setLocal (Object o) {
().add(o);
}
private void removeLocal () {
().clear();
}
/**
* 获取本类及其⽗类的属性的⽅法
* @param clazz 当前类对象
* @return 字段数组
*/
private static Field[] getAllFields (Class<?> clazz) {
List<Field> fieldList = new ArrayList<>();
while (clazz != null) {
fieldList.addAll(new ArrayList<>(Arrays.DeclaredFields())));
clazz = Superclass();
}
Field[] fields = new Field[fieldList.size()];
Array(fields);
}
/**
* 获取本类及其⽗类的属性的⽅法
* @param clazz 当前类对象
* @return 字段数组
*/
private static Field[] getAllFieldsCache (Class<?> clazz) {
List<Field> fieldList = new ArrayList<>();
while (clazz != null) {
if (clazz.isAnnotationPresent(Crypt.class)) {
fieldList.addAll(getDeclaredFields(clazz));
}
clazz = Superclass();
}
Field[] fields = new Field[fieldList.size()];
Array(fields);
}
private static List<Field> getDeclaredFields (Class<?> clazz) {
List<Field> result = (clazz);
if (result == null) {
try {
// 获取满⾜注解解密条件的字段
result = Arrays.DeclaredFields()).filter(MybatisCryptHandler::checkDecrypt).List());
// 放⼊本地缓存
declaredFieldsCache.put(clazz, (result.isEmpty() ? EMPTY_FIELD_ARRAY : result));
} catch (Exception e) {
<("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搭建

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。