需求背景

  • 公司为了通过一些金融安全指标(政策问题)和防止数据泄漏,需要对用户敏感数据进行加密,所以在公司项目中所有存储了用户信息的数据库都需要进行数据加密改造。包括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)
@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 {@Resourceprivate com.mysql.web.mybatis.Interceptor.MybatisCryptHandler handler;@Overridepublic Object intercept (Invocation invocation) {return invocation;}@SneakyThrows@Overridepublic Object plugin (Object target) {if (target instanceof ParameterHandler) {// 对请求参数进行加密操作handler.parameterEncrypt((ParameterHandler) target);}return target;}@Overridepublic 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 {@Resourceprivate MybatisCryptHandler handler;@Overridepublic 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);}@Overridepublic Object plugin (Object target) {return Plugin.wrap(target, this);}@Overridepublic 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内的原对象替换为克隆对象,所以才就想了这个还原解密参数的方式。

  • 如果对请求参数对象和查询结果对象里的所有字段都进行加解密,那上述配置就基本完成。但在本次安全加解密需求中只针对指定字段(如手机号和真实姓名),现在这种全量字段加解密就不行,而且性能也低,毕竟加解密是很耗费服务器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 {// 初始化cipherENCODING_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<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 = 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<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();}/*** 查询结果对外解密方法* @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<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) {}}/*** 统一管理内存* @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<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 当前类对象* @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 = 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;}
}

数据表准备

  • 用户的敏感信息包括有手机号、真实姓名、身份证、银行卡号、支付宝账号等几种。下面使用手机号和姓名字段进行加解密案例。
  • 先准备一张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*/
@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 {@Autowiredprivate PhoneDataMapper phoneDataMapper;/*** 添加示例接口* @param phone* @return*/@PostMapping("/aop/insert")public String insert (@RequestParam String phone) {PhoneData build = PhoneData.build(phone);phoneDataMapper.insert(build);log.info(" 插入的原数据 = {}", 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(" 插入的原数据 = {}", JSON.toJSONString(build));return "ok";}/*** 查询示例接口* @param phone* @return*/@GetMapping("/aop/select")public String select (@RequestParam String phone) {final PhoneData build = PhoneData.build(phone);// 对象类型入参查询对象数据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 {/*** 新增数据* @param phoneData*/@Insert("insert into phone_data (phone, user_phone, name, real_name) values (#{phone}, #{userPhone}, #{name}, #{realName})")void insert (PhoneData phoneData);/*** 更新数据* @param phoneData*/@Update("update phone_data set phone = #{phone}, user_phone = #{userPhone}, name = #{name}, real_name = #{realName} where id = #{id}")void updateById (PhoneData phoneData);/*** 无参查询对象类型数据* @return*/@Select("select id, phone, user_phone userPhone, name, real_name realName from phone_data where user_phone = #{userPhone}")List<PhoneData> selectList (PhoneData phoneData);
}
  • 项目启动,访问添加、更新、查询接口,其sql日志打印出结果如下:
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   :  插入的原数据 = {"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   :  插入的原数据 = {"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数据库中的数据

总结

总结一下上述实现逻辑:

在Mybatis自定义拦截器中,对各种对数据库的查询参数进行拦截,判断当前对象内字段是否需要加密,如果有注解就进行加密操作,否则就不操作。

再对数据库查询出的结果进行拦截,判断查询结果对象内字段是否需要解密,如果有注解就进行解密操作,否则就不操作。并且增加了一步对本次请求参数进行参数还原解密操作。

这样通过类和字段上增加注解,就完成自动安全加解密操作。

上述拦截器实现方式有一定局限性:

整个代码逻辑还可以在优化,比如之前对请求参数还原解密方式的优化。

主要实现逻辑是在MybatisCryptHandler处理工具类中,当前方式现只能处理请求参数和查询结果是对象类而不是字符串类型,在下篇文章中会介绍如何针对字符串进行过滤拦截。

如果代码中有问题或者有可以优化的思路,欢迎指出。

说明:

所有的面试题目都不是一成不变的,特别是像一线大厂,上面的资料只是给大家一个借鉴作用,最主要的是给自己增加知识的储备,有备无患。最后给大家分享Spring系列的学习笔记和面试题,包含spring面试题、spring cloud面试题、spring boot面试题、spring教程笔记、spring boot教程笔记、最新阿里巴巴开发手册(63页PDF总结)、2022年Java面试手册。一共整理了1184页PDF文档。私信博主(777)领取,祝大家更上一层楼!!!

Mybatis拦截器安全加解密MySQL数据实战相关推荐

  1. mybatis拦截器实现数据脱敏拦截器使用

    目录 1.主要代码如下: 1.注解 2.定义枚举脱敏规则 3.增加mybatis拦截器 4.Javabean中增加注解,如果是查询返回的map,会根据desensitionMap的规则进行脱敏 2.原 ...

  2. MySQL拦截器获取xml id_关于mybatis拦截器,有谁知道怎么对结果集进行拦截,将指定字段查询结果进行格式化...

    用MyBatis结果集拦截器做过这样一个需求: 由于项目需求经常变动,项目MySQL数据库都是存放JSON字符串,例如:用户的基本信息随着版本升级可能会有变动 数据表 CREATE TABLE `ac ...

  3. Mybatis拦截器 mysql load data local 内存流处理

    Mybatis 拦截器不做解释了,用过的基本都知道,这里用load data local主要是应对大批量数据的处理,提高性能,也支持事务回滚,且不影响其他的DML操作,当然这个操作不要涉及到当前所lo ...

  4. java使用mybatis拦截器对数据库敏感字段进行加密存储并解密

    记录业务中遇到的使用场景:灵活对数据库敏感字段进行加密和解密 文章目录 前言 一.创建数据库表和实体类 二.Mapper.Service.Controller等 三.自定义注解 四.加密工具类 五.参 ...

  5. 使用mybatis拦截器实现字段加密解密

    前言 .项目中我们存储一些用户信息的使用后根据规定,不可以存储明文,尤其是密码,实现的办法有好多种,今天承接上一篇文章mybatis拦截器,利用拦截器实现使用注解的方式在数据插入前进行加密,查询是自动 ...

  6. 基于mybatis拦截器实现数据权限

    数据权限是很多系统常见的功能,实现的方式也是很多的,最近在做项目的时候,自己基于mybatis拦截器做了一个数据权限的功能. **功能设计 a)  需要做数据权限功能的表加上一个权限id字段. 权限i ...

  7. mybatis拦截器(一)

    实现一个对sql指定字段进行加密解密的处理,使用mybatis的拦截器,完全使用spring boot实现,没有使用xml去配置拦截器 mybatis核心对象 从MyBatis代码实现的角度来看,My ...

  8. list mybatis 接收 类型_基于mybatis拦截器实现的一款简易影子表自动切换插件

    近期因工作需要,小编基于mybatis拦截器开发了一款简易影子表自动切换插件,可以根据配置实现动态修改表名,即将对原source table表的操作自动切换到对target table表的操作.该插件 ...

  9. 一步步教你mybatis分页,mybatis分页拦截器 使用,mybatis拦截器分页

              mybatis 分页详解.mybatis分页查询,mybatis分页拦截器使用.struts2下mybatis分页 mybatis默认是支持分页的,内部通过创建可滚动的Result ...

最新文章

  1. 9个必知的 Python 操作文件/文件夹方法
  2. _matroska_decode_buffer in
  3. USB、TTL电平、232电平之间的相互转换
  4. 【转】为什么要写技术博
  5. NYOJ 161 取石子(四)
  6. 自定义图片,实现透明度动态变化
  7. mysql inner join where_mysql中,inner join和where的结合问题
  8. 壹佰文章总结| 关于ASP.NETCore的分享之路
  9. java连加密的mysql_Java 实现加密数据库连接
  10. 配置的android版本,Android SDK配置(V3.0.0及以上版本)
  11. C++求复数的角度_高考数学一轮复习33,复数,常见类型及解题策略
  12. Entity Framework连接Mysql数据库并生成Model和DAL层
  13. 逼死强迫症?聊聊应用推送角标的那些事儿
  14. jenkins + maven + nexus + [ svn 或 GitLab 或 GitHub ]
  15. hping3安装及使用
  16. 好用的蓝牙连接测试工具
  17. Java之力[从蛮荒到撬动地球]之设计模式
  18. Linux的sh脚本编写基础知识
  19. 用户界面、交互体验设计优秀的产品
  20. 全国高考今日开启 报名人数再创历史新高 2022全国高考时间科目安排 优积科技-祝同学们高考顺利

热门文章

  1. 2018年第20周(2018-5-18)周末总结
  2. SpringBoot33-springboot开发部署与测试-spring boot测试
  3. lcms质谱仪_岛津三重四极杆液相色谱质谱联用仪LCMS-8050
  4. 古武_囚徒健身 保罗-威德 笔记
  5. Nick: Words From The Core Team
  6. Ubuntu18.04 安装CUDA前应注意的显卡、驱动版本信息
  7. SQL server查询没有学全所有课程的同学的信息
  8. 量子逻辑门之受控Y门(C-Y)
  9. scipy之傅里叶变换
  10. Android项目仿驾校宝典的答题APP