MyBatis自定义拦截器实现敏感数据脱敏加密

826 阅读4分钟

日常开发中经常会遇到用户敏感数据加密及脱敏入库的需求

一、需求

原始需求:数据在保存时进行加密或脱敏处理,对于加密数据取出时解密避免泄露用户敏感信息

需求分析:数据从前端通过http协议传输到后端经过业务逻辑处理后存入数据库,其中经历三个环节

1.前后端之间传输,是否需要加密,如果需要的话可以使用HTTPS加密;
2.数据到达后端服务,一般需要经过一些业务处理,此时加密没有意义;
3.数据经过处理后一般需要落库,在落库前需要对敏感数据进行加密;

入库前最后一步完成对数据加密,防止数据泄露暴露用户隐私

加密算法:加密算法有对称加密和非对称加密,考虑加密和解密效率选用对称算法加密

加密字段:加密字段具有不确定性,应该在数据库表结构设计时就确定需要加密脱敏的字段

tips:

1.某个字段加密后,该字段存取性能下降,加密字段越多性能下降就越多,需要注意
2.字段被加密后,该字段索引就没有太大意义,比如对手机号加密或脱敏,原来根据手机号设置的唯一索引可能就失效了
3.一些SQL查询可能也无法实现,如果是加密则原来的精确查询或普通查询都会失效,如果是脱敏的话可以根据手机号后四位进行查询,具体需要在设计上就定好规则。
4.如果是加密的话,原字段长度需要扩充,加密后的密文肯定比原文长
5.不要对主键加密
6.有时为了减少关联查询,会对表设计冗余字段。如果加密后需要考虑冗余数据的加密处理

二、实现方案

通过MyBatis加密脱敏数据,一般有两种方案:

1.使用拦截器对insert和update类型语句拦截,通过注解标注需要加密或脱敏的字段。读取加密数据时拦截query类型SQL,解密后放入字段 2.使用Mybatis类型转换器TypeHandler实现

2.1 拦截器加密脱敏字段

2.1.1 定义注解

为了标记需要加密和脱敏的字段我们可以设计两个注解来实现:

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Encrypt {
    
     /**
     * 加密算法
     * @return
     */
    String type() default "";
    
}


@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Desensitize {

    /**
     * 前几位不脱敏
     * @return
     */
    int first() default 0;

    /**
     * 后几位不脱敏
     * @return
     */
    int last() default 0;
}

1.Encrypt注解为了加密使用,通过type可以设置不同的加密算法
2.Desensitize注解为了脱敏使用,通过first和last制定字符串前几位和后几位不需要加密

2.1.2 定义拦截器

在拦截器中拦截,判断目标方法是否是insert或update类型,如果是获取入参对象遍历其字段及字段上的注解是否包含目标注解,如果有则进行相应的业务处理。

Intercepts:注解需要一个Signature(拦截点)参数数组,通过Signature制定拦截那个对象里的哪些方法

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Intercepts {
    /**
     * 定义拦截点
     * 只有符合拦截点的条件才会进入到拦截器
     */
    Signature[] value();
}

Signature:制定需要拦截哪个2类对象的哪个方法

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({})
public @interface Signature {
  /**
   * 定义拦截的类 Executor、ParameterHandler、StatementHandler、ResultSetHandler当中的一个
   */
  Class<?> type();

  /**
   * 在定义拦截类的基础之上,在定义拦截的方法
   */
  String method();

  /**
   * 在定义拦截方法的基础之上在定义拦截的方法对应的参数,
   * JAVA里面方法可能重载,故注意参数的类型和顺序
   */
  Class<?>[] args();
}
@Slf4j
@RequiredArgsConstructor
@Intercepts({@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}), @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}), @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}), @Signature(type = Executor.class, method = "queryCursor", args = {MappedStatement.class, Object.class, RowBounds.class})})
public class EncryptFieldInterceptor implements Interceptor {
    /**
     * 参数数组的长度
     */
    private static final int ARGS_LENGTH = 6;

    /**
     * 拦截方法
     * @param invocation 调用信息
     * @return 调用结果
     * @throws Throwable
     */
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // 1.获取参数MappedStatement,唯一编号:`${namespace}.${id}`记录该接口各种信息
        MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
        // 2.获取BoundSql,记录了几乎完整的SQL,参数使用占位符 ? 代替
        BoundSql boundSql = null;
        if (invocation.getArgs().length == ARGS_LENGTH) {
            boundSql = (BoundSql) invocation.getArgs()[ARGS_LENGTH - 1];
        } else {
            Object parameter = invocation.getArgs()[1];
            boundSql = mappedStatement.getBoundSql(parameter);
        }
        // 3.判断SQL是哪种类型
        if (mappedStatement.getSqlCommandType().equals(SqlCommandType.INSERT)) {
            // 4.获取入参对象class及字段
            Class<?> javaType = boundSql.getParameterObject().getClass();
            Field[] fields = javaType.getDeclaredFields();
            // 5.遍历字段及其注解,如果有目标注解进行相应处理
            for (Field field : fields) {
                Annotation[] annotations = field.getAnnotations();
                field.setAccessible(true);
                Object originField =  field.get(boundSql.getParameterObject());
                if (originField != null && annotations.length > 0) {
                    for (Annotation annotation : annotations) {
                        if (annotation instanceof Encrypt) {
                            String encryptField = EncryptUtils.encrypt((String) originField);
                            field.set(boundSql.getParameterObject(), encryptField);
                        } else if (annotation instanceof Desensitize) {
                            int first = ((Desensitize) annotation).first();
                            int last = ((Desensitize) annotation).last();
                            String desensitizeField = EncryptUtils.desensitize((String) originField, first, last);
                            field.set(boundSql.getParameterObject(), desensitizeField);
                        }
                    }
                }
                field.setAccessible(false);
            }

        }
        // 6.执行目标方法
        return invocation.proceed();
    }

    /**
     * 应用插件。如应用成功,则会创建目标对象的代理对象
     *
     * @param target 目标对象
     * @return 应用的结果对象,可以是代理对象,也可以是 target 对象,也可以是任意对象。具体的,看代码实现
     */
    @Override
    public Object plugin(Object target) {
        return Interceptor.super.plugin(target);
    }

    /**
     * 设置拦截器属性
     *
     * @param properties 属性
     */
    @Override
    public void setProperties(Properties properties) {
        Interceptor.super.setProperties(properties);
    }
}