使用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这个接口,并添加注解,拦截增删改查
    <plugins>
        <!-- 加密解密的拦截器 -->
    	<plugin interceptor="com.wisdom.scm.common.persistence.interceptor.AESInterceptor" />
    </plugins>
     
    @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;
}
/**
     * <p>声明这是一个泛型方法,让所有参数都能够调用这个方法扫描带有解密注解的字段,
     * 进行解密,然后显示在前端页面中</p>
     *
     * @param <T>
     */
    public <T> 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 <T>
     * @return
     */
    public <T> boolean sensitiveBean(T t) {
        // 判断是否带有敏感实体类的注解
        if (t.getClass().isAnnotationPresent(SensitiveBean.class)) {
            logger.info("带有敏感实体类的注解");
            return true;
        } else {
            return false;
        }
    }
/**
     * <p>声明这是一个泛型方法,让所有参数都能够调用这个方法扫描带有加密注解的字段,
     * 进行加密,然后存在数据库中</p>
     *
     * @param <T>
     */
    public <T> 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

  • 1
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值