详解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)
@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
    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) {
            // 对请求参数进行加密操作
            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内的原对象替换为克隆对象,所以才就想了这个还原解密参数的方式。

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

    @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(" 插入的原数据 = {}", 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处理工具类中,当前方式现只能处理请求参数和查询结果是对象类而不是字符串类型,在下篇文章中会介绍如何针对字符串进行过滤拦截。

到此这篇关于详解Mybatis拦截器安全加解密MySQL数据实战的文章就介绍到这了,更多相关Mybatis拦截器安全加解密MySQL内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

时间: 2022-01-13

mybatis-plus 拦截器敏感字段加解密的实现

目录 背景 一.查询拦截器 二.插入和更新拦截器 三.注解 背景 数据库在保存数据时,对于某些敏感数据需要脱敏或者加密处理,如果一个一个的去加显然工作量大而且容易出错,这个时候可以考虑使用拦截器,本文针对的是mybatis-plus作为持久层框架,其他场景未测试.代码如下: 一.查询拦截器 package com.sfpay.merchant.service.interceptor; import com.baomidou.mybatisplus.core.toolkit.CollectionU

mybatis 通过拦截器打印完整的sql语句以及执行结果操作

开发过程中,如果使用mybatis做为ORM框架,经常需要打印出完整的sql语句以及执行的结果做为参考. 虽然mybatis结合日志框架可以做到,但打印出来的通常都是sql和参数分开的. 有时我们需要调试这条sql的时候,就需要把参数填进去,这样未免有些浪费时间. 此时我们可以通过实现mybatis拦截器来做到打印带参数的完整的sql,以及结果通过json输出到控制台. 直接看代码和使用方法吧: MyBatis拦截器打印不带问号的完整sql语句拦截器 import java.text.DateF

MyBatis Excutor 拦截器的巧妙用法

这里要讲的巧妙用法是用来实现在拦截器中执行额外 MyBatis 现有方法的用法. 并且会提供一个解决拦截Executor时想要修改MappedStatement时解决并发的问题. 这里假设一个场景: 实现一个拦截器,记录 MyBatis 所有的 insert,update,delete 操作,将记录的信息存入数据库. 这个用法在这里就是将记录的信息存入数据库. 实现过程的关键步骤和代码: 1.首先在某个 Mapper.xml 中定义好了一个往日志表中插入记录的方法,假设方法为id="insert

Mybatis中拦截器的简单实现方法

前言 需求驱动学习,最近一周组长让我在业务模块里加日志,经过与导师以及组长讨论决定用拦截器记录日志.周五下班前已经发了提测邮件. 虽然我知道 MyBatis 有这东西,但是没在实际情况中用过,心里有点虚2333--所以才有了此文的理解. 前世今生 它的本质就是 JDK 的动态代理.首先先来复习一下动态代理我贴了一段最常见的 JDK 动态代理的代码 //服务员的接口 public interface Waiter { void serve(); } //服务员的实现 public class Wa

Mybatis Plugin拦截器开发过程详解

这篇文章主要介绍了Mybatis Plugin拦截器开发过程详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 1.Plugin MyBatis 允许使用插件来拦截的方法调用包括: • Executor (update, query, flushStatements, commit, rollback,getTransaction, close, isClosed) • ParameterHandler (getParameterObject,

Mybatis Interceptor 拦截器的实现

Mybatis采用责任链模式,通过动态代理组织多个拦截器(插件),通过这些拦截器可以改变Mybatis的默认行为(诸如SQL重写之类的),由于插件会深入到Mybatis的核心,因此在编写自己的插件前最好了解下它的原理,以便写出安全高效的插件. 拦截器(Interceptor)在 Mybatis 中被当做插件(plugin)对待,官方文档提供了 Executor,ParameterHandler,ResultSetHandler,StatementHandler 共4种,并且提示"这些类中方法的细

简单了解mybatis拦截器实现原理及实例

这篇文章主要介绍了简单了解mybatis拦截器实现原理及实例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 例行惯例,先看些基本概念: 1 拦截器的作用就是我们可以拦截某些方法的调用,在目标方法前后加上我们自己逻辑 2 Mybatis拦截器设计的一个初衷是为了供用户在某些时候可以实现自己的逻辑而不必去动Mybatis固有的逻辑. 自定义拦截器 /** * mybatis 自定义拦截器 * 三步骤: * 1 实现 {@link Intercept

java MyBatis拦截器Inteceptor详细介绍

有许多java初学者对于MyBatis拦截器Inteceptor不是很了解,在这里我来为各位整理下篇关于java中MyBatis拦截器Inteceptor详解, 本文主要分析MyBatis的插件机制,实际就是Java动态代理实现的责任链模式实现. 根据官方文档.Mybatis只允许拦截以下方法,这个决定写拦截器注解签名参数. 代码如下 Executor (update, query, flushStatements, commit, rollback, getTransaction, close

MyBatis拦截器实现分页功能实例

由于业务关系 巴拉巴拉巴拉 好吧 简单来说就是 原来的业务是 需要再实现类里写 selectCount 和selectPage两个方法才能实现分页功能 现在想要达到效果是 只通过一个方法就可以实现 也就是功能合并 所以就有了下面的实践 既然是基于MyBatis 所以就先搭建一个Mybatis的小项目 1.01导入 mybatis和mysql的包 1.02.配置文件 Configuration.xml 中添加 <environments default="development"&

mybatis拦截器与分页插件实例教程

mybatis介绍 拦截器的一个作用就是我们可以拦截某些方法的调用,我们可以选择在这些被拦截的方法执行前后加上某些逻辑,也可以在执行这些被拦截的方法时执行自己的逻辑而不再执行被拦截的方法. MyBatis 是一款优秀的持久层框架,它支持定制化 SQL.存储过程以及高级映射.MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集.MyBatis 可以使用简单的 XML 或注解来配置和映射原生类型.接口和 Java 的 POJO(Plain Old Java Objects,普

Executor拦截器高级教程QueryInterceptor的规范

Executor 拦截器高级教程 - QueryInterceptor 规范 这篇文档涉及下面几个方面 1. Executor query 方法介绍 2. 拦截器配置和调用顺序 3. 拦截 query 方法的技巧 4. 拦截 query 方法的规范 5. 如何配置不同的 Executor 插件 1. Executor query 方法介绍 在 MyBatis 的拦截器的文档部分,我们知道 Executor 中的 query 方法可以被拦截,如果你真正写过这个方法的拦截器,你可能会知道在 Exec