使用mybatis的拦截器对实体类上的敏感字段进行加密解密

 

目的:

对信息进行加密保存到数据库中,在读取数据库时能够看到解密后的数据。

关键点:

  1. 敏感实体类:比如要对User对象的密码字段进行加密解密,这个User类就是敏感实体类
  2. 敏感实体类注解:标明目标对象,为了过滤掉其他无关对象
  3. 敏感实体类的加密解密的字段:标明目标字段,在这个字段对其进行加密解密
  4. 拦截器上的参数method = "update" :代表着update和insert还有delete操作

步骤:

  1. 使用mybatis对数据进行增删改查
  2. 自定义两种注解:
    第一种是作用于实体类上的注解,用于拦截器扫描,以找出目标实体类。
    第二种是作用于实体类中的字段上的,用于拦截器扫描,以找出需要目标实体类中的目标字段进行加密解密。
    @Target(ElementType.FIELD)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface DecryptField {
        String value() default "";
    }
    
    @Target(ElementType.FIELD)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface EncryptField {
        String value() default "";
    }
    
    @Target({ElementType.TYPE})//该参数代表是作用在类、方法上的
    @Retention(RetentionPolicy.RUNTIME)
    public @interface SensitiveBean {
        String value() default "";
    }

     

  3. 添加mybatis的拦截器(org.apache.ibatis.plugin.Interceptor),并在xml中注册该拦截器,实现Interceptor这个接口,并添加注解,拦截增删改查
    
        
    	
    
     
    @Intercepts({
        @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
        @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})
    })

    这个注解代表着拦截执行器Executor的所有查询操作和所有更新操作(insert,update,delete)

  4. 在这个接口中需要做许多业务的操作
    首先需要明白拦截器拦截的参数 invocation
    这个参数可以获取到这条sql语句的所有信息,包括传入参数和sql语句,甚至语句的返回值。
    所以,接下来的业务步骤就是判断该sql对象是否存在返回值,如果有,返回值的对象是否是敏感实体类。
    以及该sql是不是更新操作,如果是,对该对象上的敏感字段进行加密


    ①首先需要对该sql语句进行判断,是否查询操作,这里是获取它的返回值,如果不存在就代表着是非查询操作,存在就对这个对象获取敏感实体类注解,如果不存在敏感实体类注解,就直接pass不操作。
    这么做的目的是为了节省性能,如果是对非敏感实体类进行查询操作,这样就能直接在开头就过滤掉了

    ②到了这一步就是非查询操作了,只需要判断传入参数是否存在敏感实体类的注解,如果是,则代表该传入参数需要进行加密

    ③调用invocation.proceed()方法进行执行sql语句,该方法可以直接操作sql,并获取返回值

    ④判断返回值是否为空,为空就直接返回

    ⑤到了这一步,就代表着对查询操作的返回值进行解密了,以上的操作都已经决定了这里是敏感实体类的结果集。所以就直接对逐条数据进行判断,使用for循环判断对象上的每一个对象是否带有解密的注解,如果有,就对这个字段进行解密。

     
//以下是拦截器需要实现的方法
@Override
public Object intercept(Invocation invocation) throws Throwable {
    MappedStatement statement = (MappedStatement) invocation.getArgs()[0];
    // 获取该sql语句的类型,例如update,insert
    String methodName = invocation.getMethod().getName();
    // 获取该sql语句放入的参数
    Object parameter = invocation.getArgs()[1];
    // 如果是查询操作,并且返回值不是敏感实体类对象,并且传入参数不为空,就直接调用执行方法,返回这个方法的返回值
    // 方法中可以判断这个返回值是否是多条数据,如果有数据,就代表着是select 操作,没有就代表是update insert delete,
    // 因为mybatis的dao层不能为非select操作设置返回值
    if (statement.getResultMaps().size() > 0) {
        // 获取到这个返回值的类属性
        Class type = statement.getResultMaps().get(0).getType();
        // 返回值没有带敏感实体类对象注解
        if (!type.isAnnotationPresent(SensitiveBean.class)) {
            // 直接执行语句并返回
            return invocation.proceed();
        }
    }
    // 如果该参数为空,就不进行判断敏感实体类,直接执行sql
    // 并且
    // 如果判断该参数带有敏感实体类的注解,才对这个实体类进行扫描查看是否有加密解密的注解
    if (parameter != null && sensitiveBean(parameter)) {
        // 如果有就扫描是否是更新操作和是否有加密注解
        // 如果是更新或者插入时,就对数据进行加密后保存在数据库
        if (StringUtils.equalsIgnoreCase("update", methodName) || 
            StringUtils.equalsIgnoreCase("insert", methodName)) {
        // 对参数内含注解的字段进行加密
        encryptField(parameter);
        }
     }

     // 继续执行sql语句,调用当前拦截的执行方法
     Object returnValue = invocation.proceed();
     try {
         // 当返回值类型为数组集合时,就判断是否需要进行数据解密
         if (returnValue instanceof ArrayList) {
            List list = (List) returnValue;
            // 判断结果集的数据是否为空
            if (list == null || list.size() < 1) {
                return returnValue;
            }
            Object object = list.get(0);
            // 为空值就返回数据
            if (object == null) {
                return returnValue;
            }
            // 判断第一个对象是否有解密注解
            Field[] fields = object.getClass().getDeclaredFields();
            // 定义一个临时变量
            int len;
            if (fields != null && 0 < (len = fields.length)) {
                for (Object o : list) {
                    //调用解密,在这个方法中针对某个带有解密注解的字段进行解密
                    decryptField(o);
                }
            }
        }
   } catch (Exception e) {
       e.printStackTrace();
       return returnValue;
   }
   return returnValue;
}
/**
     * 

声明这是一个泛型方法,让所有参数都能够调用这个方法扫描带有解密注解的字段, * 进行解密,然后显示在前端页面中

* * @param */ public void decryptField(T t) { // 获取对象的域 Field[] declaredFields = t.getClass().getDeclaredFields(); if (declaredFields != null && declaredFields.length > 0) { // 遍历这些字段 for (Field field : declaredFields) { // 如果这个字段存在解密注解就进行解密 if (field.isAnnotationPresent(DecryptField.class) && field.getType().toString().endsWith("String")) { field.setAccessible(true); try { // 获取这个字段的值 String fieldValue = (String) field.get(t); // 判断这个字段的数值是否不为空 if (StringUtils.isNotEmpty(fieldValue)) { // 首先调用一个方法判断这个数据是否是未经过加密的,如果可以解密就进行解密,解密失败就返回元数据 boolean canDecrypt; canDecrypt = UpdateUtils.judgeDataForView(fieldValue); if (canDecrypt) { // 进行解密 String encryptData = Cryptos.aesDecrypt(fieldValue); if (encryptData.equals("解密失败")) { logger.error("该字段不是被加密的字段,需要联系管理员进行修改数据"); } // 将值反射到对象中 field.set(t, encryptData); } else { // 不能解密的情况下,就不对这个对象做任何操作,即默认显示元数据 } } } catch (IllegalAccessException e) { e.printStackTrace(); } } } } }
/**
     * 查看这个类是否带有敏感实体类注解,有就返回true,否则返回false
     *
     * @param t
     * @param 
     * @return
     */
    public  boolean sensitiveBean(T t) {
        // 判断是否带有敏感实体类的注解
        if (t.getClass().isAnnotationPresent(SensitiveBean.class)) {
            logger.info("带有敏感实体类的注解");
            return true;
        } else {
            return false;
        }
    }
/**
     * 

声明这是一个泛型方法,让所有参数都能够调用这个方法扫描带有加密注解的字段, * 进行加密,然后存在数据库中

* * @param */ public void encryptField(T t) { Field[] declaredFields = t.getClass().getDeclaredFields(); if (declaredFields != null && declaredFields.length > 0) { for (Field field : declaredFields) { // 查找当字段带有加密注解,并且字段类型为字符串类型 if (field.isAnnotationPresent(EncryptField.class) && field.getType().toString().endsWith("String")) { field.setAccessible(true); String fieldValue = null; try { // 获取这个值 fieldValue = (String) field.get(t); } catch (IllegalAccessException e) { e.printStackTrace(); } // 判断这个值是否为空 if (StringUtils.isNotEmpty(fieldValue)) { try { // 不为空时,就进行加密 field.set(t, Cryptos.aesEncrypt(fieldValue)); } catch (IllegalAccessException e) { e.printStackTrace(); } } } } } }

 

执行方案的充分必要条件

  1. 保证密钥不进行修改,如果需求更改为需要动态修改密钥,就需要进行二次开发
  2. 注册拦截器

影响:

  1. 数据库中的敏感数据得到加密,提高安全性
  2. 每次执行sql语句时都会触发到拦截器,给后台服务器增加压力
  3. 在解密数据和加密数据时,会导致用户等待时间增加

针对对象:

所有经由mybatis层执行的sql语句(不包括hibernate和jdbc)

触发的条件:

  1. 更新、插入、查询、删除等操作
  2. 目标实体上带有敏感实体类的注解
  3. 目标字段上带有加密解密的注解

使用的工具:

mybatis拦截器(插件)

使用拦截器+反射+泛型的好处

  1. 只需要在目标实体类上添加敏感实体类的注解,就能定位这个实体类
  2. 只需要在对象中的字段上添加加密解密的注解,就能实现数据的加密解密

具体代码(在其他人的博客中有提到):

https://blog.csdn.net/wizard_rp/article/details/79821671

你可能感兴趣的:(工作)