基于Mybatis层面对敏感字段的加密

在SpringBoot项目中,如何优雅的实现自定义注解+拦截器对敏感字段的加解密

 我们经常会面对对一些身份信息或是电话号码,以及真实姓名等敏感信息进行手动加密,那么这样不仅显得十分臃肿还很不优雅,甚至还会存在错加密、楼加密、开发人员需要知道实际的加密规则等情况。

本文就告诉大家如何使用SpringBoot + Mybatis拦截器 + 注解完成基于mapper层面的数据加密

一、什么是Mybatis Plugin

在mybatis官方文档中,对于Mybatis Plugin的介绍是这样的

  Mybatis允许你在已映射语句执行过程中的某一点进行拦截调用
//语句执行拦截
Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)

// 参数获取、设置时进行拦截
ParameterHandler (getParameterObject, setParameters)

// 对返回结果进行拦截
ResultSetHandler (handleResultSets, handleOutputParameters)

//sql语句拦截
StatementHandler (prepare, parameterize, batch,update,query)

简单来说,就是执行整个sql的周期中,我们可以任意切入到某一点sql的参数、sql执行结果集、sql语句本身等进行切面处理。所以,基于这个特性我们可以使用使其对我们需要进行加密的数据进行切面统一加密处理

二、实现基于注解的敏感信息加密拦截器

2.1 实现思路

对于数据的加密与解密,应当存在两个拦截器对数据进行拦截操作

参照官方文档,因此此处我们应当使用ParameterHandler拦截器对入参进行加密
使用ResultSetHandler拦截器对出参进行解密操作。

基于Mybatis层面对敏感字段的加密_第1张图片

目标需要解密、解密字段可能需要灵活变更,此时我们定义一个注解,对需要加密的字段进行注解,那么便可以配合拦截器对需要的数据惊醒加密与解密操作了,那么我们可以看到mybatis的拦截器需要实现以下方法

public interface Interceptor {
 
  //主要参数拦截方法
  Object intercept(Invocation invocation) throws Throwable;
 
  //mybatis插件链
  default Object plugin(Object target) {return Plugin.wrap(target, this);}
 
  //自定义插件配置文件方法
  default void setProperties(Properties properties) {}
 
}

2.1 定义需要加密解密的敏感信息注解

定义注解敏感信息类(如实体类POJO、PO)的注解

/**
 * @author kunBoy
 * @create 2021-04-13 10:23
 * 注解敏感信息类的注解
 */
@Inherited
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface SensitiveData {
}

定义注解敏感字段的注解

/**
 * @author kunBoy
 * @create 2021-04-13 10:23
 * 注解敏感字段的注解
 */
@Inherited
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface SensitiveFiled {
}

2.3 定义加密接口机器实现类

应以加密接口,方便以后拓展加密方法(如AES加密算法拓展支持PBE算法,只需要注入式指定一下即可)

/**
 * @author kunBoy
 * @create 2021-04-13 10:34
 */
public interface EncryptUtil {

    /**
     *
     * @param aesFields paramsObject所申明的字段
     * @param paramsObject mapper中paramsType的实例
     * @param 
     * @return
     * @throws IllegalAccessException 字段不可访问的异常
     * 这里为了写这个接口为了以后可以拓展掐他的加密类型
     */
    <T> T aesEncrypt(Field[] aesFields, T paramsObject) throws IllegalAccessException;
}

AESUtil工具类(也可自行封装)

/**
 * @version V1.0
 * @desc AES 加密工具类
 */
@Component
public class AESUtil {

    private static final String KEY_ALGORITHM = "AES";
    private static final String DEFAULT_CIPHER_ALGORITHM = "AES/ECB/PKCS5Padding";//默认的加密算法

    /**
     * AES 加密操作
     *
     * @param content  待加密内容
     * @return 返回Base64转码后的加密数据
     */
    public static String encrypt(String content, String key) {
        try {
            Cipher cipher = Cipher.getInstance(DEFAULT_CIPHER_ALGORITHM);// 创建密码器

            byte[] byteContent = content.getBytes(StandardCharsets.UTF_8);

            cipher.init(Cipher.ENCRYPT_MODE, getSecretKey(key));// 初始化为加密模式的密码器

            byte[] result = cipher.doFinal(byteContent);// 加密

            //Base64是一种基于64个可打印字符来表示二进制数据的表示方法。
            return Base64Utils.encodeToString(result);//通过Base64转码返回
        } catch (Exception ex) {
            Logger.getLogger(AESUtil.class.getName()).log(Level.SEVERE, null, ex);
        }

        return null;
    }

    /**
     * AES 解密操作
     *
     * @param content
     * @return
     */
    public static String decrypt(String content, String key) {

        try {
            //实例化
            Cipher cipher = Cipher.getInstance(DEFAULT_CIPHER_ALGORITHM);

            //使用密钥初始化,设置为解密模式
            cipher.init(Cipher.DECRYPT_MODE, getSecretKey(key));

            //执行操作
            //Base64是一种基于64个可打印字符来表示二进制数据的表示方法。
            byte[] result = cipher.doFinal(Base64Utils.decodeFromString(content));

            return new String(result, StandardCharsets.UTF_8);
        } catch (Exception ex) {
            Logger.getLogger(AESUtil.class.getName()).log(Level.SEVERE, null, ex);
        }
        return null;
    }

    /**
     * 生成加密秘钥
     *
     * @return
     */
    private static SecretKeySpec getSecretKey(String key) {
        //返回生成指定算法密钥生成器的 KeyGenerator 对象
        KeyGenerator kg = null;
        try {
            kg = KeyGenerator.getInstance(KEY_ALGORITHM);
            //AES 要求密钥长度为 128
            kg.init(128, new SecureRandom(key.getBytes()));
            //生成一个密钥
            SecretKey secretKey = kg.generateKey();
            return new SecretKeySpec(secretKey.getEncoded(), KEY_ALGORITHM);// 转换为AES专用密钥
        } catch (NoSuchAlgorithmException ex) {
            Logger.getLogger(AESUtil.class.getName()).log(Level.SEVERE, null, ex);
        }
        return null;
    }
}

实现类

/**
 * 加密工具类
 * @author kunBoy
 * @create 2021-04-13 10:39
 */
@Component
public class AESEncrypt implements EncryptUtil {

    @Value("${aes.key}")
    private String key;

    /**
     *
     * @param aesFields paramsObject所申明的字段
     * @param paramsObject mapper中paramsType的实例
     * @param 
     * @return
     * @throws IllegalAccessException 字段不可访问的异常
     */
    @Override
    public <T> T aesEncrypt(Field[] aesFields, T paramsObject) throws IllegalAccessException {
        for (Field aesField : aesFields) {
            //取出所有被EncryptDecryptFiled注解的字段
            SensitiveFiled filed = aesField.getAnnotation(SensitiveFiled.class);
            if (!Objects.isNull(filed)) {
                //将此对象的 accessible 标志设置为指示的布尔值。值为 true 则指示反射的对象在使用时应该取消 Java 语言访问检查。
                aesField.setAccessible(true);
                Object object = aesField.get(paramsObject);
                //这里暂时只对String类型来加密
                if (object instanceof String) {
                    String value = (String) object;
                    //开始对字段加密使用自定义的AES加密工具
                    aesField.set(paramsObject, AESUtil.encrypt(value, key));
                }
            }
        }
        return paramsObject;
    }
}

2.4 实现入参加密拦截器

前面已提到了需要自定义拦截器的话需要实现Mybatis给我们的三个方法

ParameterHandler 是MyBatis四大核心对象之一,ParameterHandler 相比于其他的组件就简单很多了,ParameterHandler 译为参数处理器,负责为 PreparedStatement 的 sql 语句参数动态赋值,这个接口很简单只有两个方法:

public interface ParameterHandler {
  // 用于读取参数;
  Object getParameterObject();
  // 用于对 PreparedStatement 绑定实参。
  void setParameters(PreparedStatement ps)
      throws SQLException;
}
此处我们使用了ParameterHandler.setParameters()方法,拦截了mapper.xml中paramsType的实例(即在每个含有parameType属性mapper语句中,都执行该拦截器,对paramsType的实例进行拦截处理)
/**
 * 加密拦截器
 * @author kunBoy
 * @create 2021-04-13 11:06
 */
@Slf4j
@Component
/**
 * @Intercepts注解开启拦截器
 * type 属性指定当前拦截器使用StatementHandler 、ResultSetHandler、ParameterHandler,Executor的一种
 * method 属性指定使用以上四种类型的具体方法(可进入class内部查看其方法)。
 * args 属性指定预编译语句
 */
@Intercepts({
        //@Signature注解定义拦截器的实际类型
        @Signature(type = ParameterHandler.class, method = "setParameters", args = PreparedStatement.class),
})
public class EncryptInterceptor implements Interceptor {

    private final AESEncrypt encryptUtil;

    @Autowired
    public EncryptInterceptor(AESEncrypt encryptUtil) {
        this.encryptUtil = encryptUtil;
    }
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        //@Signature 指定了 type= parameterHandler 后,这里的 invocation.getTarget() 便是parameterHandler
        //若指定ResultSetHandler ,这里则能强转为ResultSetHandler
        ParameterHandler parameterHandler = (ParameterHandler) invocation.getTarget();
        //获取参数对象,即mapper中paramsType的实例
        Field paramsFiled = parameterHandler.getClass().getDeclaredField("parameterObject");
        //将此对象的 accessible 标志设置为指示的布尔值。值为 true 则指示反射的对象在使用时应该取消 Java 语言访问检查。
        paramsFiled.setAccessible(true);
        //取出实例
        Object parameterObject = paramsFiled.get(parameterHandler);
        if (parameterObject != null) {
            Class<?> parameterObjectClass = parameterObject.getClass();
            //校验该实例的类是否被@SensitiveData所注解
            SensitiveData sensitiveData = AnnotationUtils.findAnnotation(parameterObjectClass, SensitiveData.class);
            if (Objects.nonNull(sensitiveData)) {
                //取出当前类的所有字段,传入加密方法
                Field[] declaredFields = parameterObjectClass.getDeclaredFields();
                encryptUtil.aesEncrypt(declaredFields, parameterObject);
            }
        }
        //获取原方法的返回值
        return invocation.proceed();
    }

    /**
     * 一定要配置,加入此拦截器到拦截器链
     * @param target
     * @return
     */
    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {

    }
}

至此完成了自定义加密拦截器

2.5 定义解密接口及其实现类

解密接口,其中result为mapper.xml中resultType的实例

/**
 * @author kunBoy
 * @create 2021-04-13 11:44
 */

public interface DecryptUtil {

    /**
     * 解密
     *
     * @param result
     * @param 
     * @return
     * @throws IllegalAccessException
     */
    <T> T decrypt(T result) throws IllegalAccessException;
}
/**
 * @author kunBoy
 * @create 2021-04-13 11:45
 */
@Component
public class AESDecrypt implements DecryptUtil {

    @Value("${aes.key}")
    private String key;


    /**
     * 解密
     *
     * @param result
     * @param 
     * @return
     * @throws IllegalAccessException
     */
    @Override
    public <T> T decrypt(T result) throws IllegalAccessException {
        //取出resultType的类
        Class<?> resultClass = result.getClass();
        Field[] declaredFields = resultClass.getDeclaredFields();
        for (Field declaredField : declaredFields) {
            //去除所有被EncryptDecryptFiled注解的字段
            SensitiveFiled sensitiveFiled = declaredField.getAnnotation(SensitiveFiled.class);
            if (!Objects.isNull(sensitiveFiled)) {
                //将此对象的 accessible 标志设置为指示的布尔值。值为 true 则指示反射的对象在使用时应该取消 Java 语言访问检查。
                declaredField.setAccessible(true);
                //这里的result就相当于是字段的访问器
                Object object = declaredField.get(result);
                //只支持String解密
                if (object instanceof String) {
                    String value = (String) object;
                    //对注解在这段进行逐一解密
                    declaredField.set(result, AESUtil.decrypt(value, key));
                }
            }
        }
        return result;
    }
}

2.6 定义出参解密拦截器

/**
 * @author kunBoy
 * @create 2021-04-13 11:56
 */
@Slf4j
@Component
/**
 * 这里是对找出来的字符串结果集进行解密所以是ResultSetHandler
 * args是指定预编译语句
 */
@Intercepts({
        @Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class})
})
public class DecryptInterceptor implements Interceptor {

    @Autowired
    AESDecrypt aesDecrypt;

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        //取出查询的结果
        Object resultObject = invocation.proceed();
        if (Objects.isNull(resultObject)) {
            return null;
        }
        //基于selectList
        if (resultObject instanceof ArrayList) {
            ArrayList resultList = (ArrayList) resultObject;
            if (!CollectionUtils.isEmpty(resultList) && needToDecrypt(resultList.get(0))) {
                for (Object result : resultList) {
                    //逐一解密
                    aesDecrypt.decrypt(result);
                }
            }
            //基于selectOne
        }else {
            if (needToDecrypt(resultObject)) {
                aesDecrypt.decrypt(resultObject);
            }
        }
        return resultObject;
    }

    /**
     * 对单个结果集判空的一个方法
     * @param object
     * @return
     */
    private boolean needToDecrypt(Object object) {
        Class<?> objectClass = object.getClass();
        SensitiveData sensitiveData = AnnotationUtils.findAnnotation(objectClass, SensitiveData.class);
        return Objects.nonNull(sensitiveData);
    }

    /**
     * 将此过滤器加入到过滤器链当中
     * @param target
     * @return
     */
    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {

    }
}

至此已经完成了所有的配置工作。

3、 注解实体类中需要加解密的字段

@ApiModel(value="")
@Data
@SensitiveData
@TableName("archive_archive")
public class Archive implements Serializable {

private static final long serialVersionUID = 1L;

    @SensitiveFiled
    @TableId(type = IdType.ASSIGN_ID)
    private String archiveId;

    private String archiveName;

    private String archiveVersion;

    private String archiveNum;

    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8", locale = "zh")
    private Date enterTime;

    private String enterPerson;

    @SensitiveFiled
    private String moduleId;

    @SensitiveFiled
    private String orgId;


}

一篇拙作,还希望能够帮助到有需要的家人们

你可能感兴趣的:(mybatis)