zl程序教程

您现在的位置是:首页 >  数据库

当前栏目

Mybatis-plus实现在DAO层面对敏感数据的加解密

2023-04-18 12:31:43 时间

日常我们在存储数据的时候,经常会碰到一些敏感性的数据,比如用户的身份证号和手机号等。这些数据一般是不允许我们在数据库中做明文存储的,这就需要我们在存储字段的时候,对这些字段做加密操作,同时在读取出来的时候,也要对相应字段做解密操作。

这些操作如果我们在service层做处理,可能需要针对各个表的对应数据做解析,这时候代码就会相对很多,也很乱。而这种操作也非常类似于拦截器的操作。我们对于请求做拦截,然后实现相应的操作。而mybatis-plus也提供了针对性的拦截器,我们可以通过扩展拦截器来实现这样的需求。

首先我们先定义三个注解,分别是@SensitiveData, @SensitiveField, @Mask。 分别介绍一下, 这三个注解,都是和实体相关的。 @SentiveData放在实体类上,用于标识对应的实体类中存在敏感数据, @SensiveField放在字段上,用于标识该字段是敏感字段,需要进行加解密操作, @Mask一般放在手机号上,保存的时候不加密,读取的时候,把中间四位替换为****。 先给出注解的定义

@Inherited
@Target({ ElementType.FIELD })
@Retention(RetentionPolicy.RUNTIME)
public @interface Mask {
}
@Inherited
@Target({ ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
public @interface SensitiveData {
}
@Inherited
@Target({ ElementType.FIELD })
@Retention(RetentionPolicy.RUNTIME)
public @interface SensitiveField {
}

接下来就是拦截器的写法了,mybatis给我们提供了对应的插件扩展,对于mybatis-plus同样适用。mybatis在插入的时候有一个方法叫做setParameter, 会对参数做设置, 查询的时候有一个方法叫做handleResultSet, 会对结果做操作,我们只需要拦截这两个请求,设置参数的时候,加密敏感字段;操作结果的时候,解密敏感字段即可。给出两个拦截器代码。

import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.executor.parameter.ParameterHandler;
import org.apache.ibatis.plugin.*;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.stereotype.Component;

import java.lang.reflect.Field;
import java.sql.PreparedStatement;
import java.util.Objects;

/**
 * <br>
 * .
 * <p>
 * Copyright: Copyright (c) 2021/1/18 5:09 下午
 * <p>
 * Company: zhongdian
 * <p>
 *
 * @author tianlong
 * @version 1.0.0
 * @date: 2021/1/18 5:09 下午
 * @email tianlong03@cestc.com
 * @desc
 */
@Slf4j
@Component
@Intercepts({
        @Signature(type = ParameterHandler.class, method = "setParameters", args = {PreparedStatement.class}),
})
public class EncryptInterceptor implements Interceptor {

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        try {
            ParameterHandler parameterHandler = (ParameterHandler) invocation.getTarget();
            // 获取参数对像,即 mapper 中 paramsType 的实例
            Field parameterField = parameterHandler.getClass().getDeclaredField("parameterObject");
            parameterField.setAccessible(true);
            //取出实例
            Object parameterObject = parameterField.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();
                    encrypt(declaredFields, parameterObject);
                }
            }
            return invocation.proceed();
        } catch (Exception e) {
            log.error("加密失败", e);
        }
        return invocation.proceed();
    }

    /**
     * 切记配置,否则当前拦截器不会加入拦截器链
     */
    @Override
    public Object plugin(Object o) {
        return Plugin.wrap(o, this);
    }

    public <T> T encrypt(Field[] declaredFields, T paramsObject) throws IllegalAccessException {
        for (Field field : declaredFields) {
            //取出所有被EncryptDecryptField注解的字段
            SensitiveField sensitiveField = field.getAnnotation(SensitiveField.class);
            if (!Objects.isNull(sensitiveField)) {
                field.setAccessible(true);
                Object object = field.get(paramsObject);
                //暂时只实现String类型的加密
                if (object instanceof String) {
                    String value = (String) object;
                    //加密  这里我使用自定义的AES加密工具
                    field.set(paramsObject, EncryptUtils.encrypt(value));
                }
            }
        }
        return paramsObject;
    }
}
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.ibatis.executor.resultset.ResultSetHandler;
import org.apache.ibatis.plugin.*;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.stereotype.Component;

import java.lang.reflect.Field;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Objects;

/**
 * <br>
 * .
 * <p>
 * Copyright: Copyright (c) 2021/1/18 10:51 上午
 * <p>
 * Company: zhongdian
 * <p>
 *
 * @author tianlong
 * @version 1.0.0
 * @date: 2021/1/18 10:51 上午
 * @email tianlong03@cestc.com
 * @desc
 */
@Intercepts({
        @Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class})
})
@Slf4j
@Component
public class DecryptInterceptor implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        Object resultObject = invocation.proceed();
        try {
            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) {
                        //逐一解密
                        decrypt(result);
                    }
                }
                //基于selectOne
            } else {
                if (needToDecrypt(resultObject)) {
                    EncryptUtils.decrypt((String) resultObject);
                }
            }
            return resultObject;
        } catch (Exception e) {
            log.error("解密失败", e);
        }
        return resultObject;
    }

    private boolean needToDecrypt(Object object) {
        Class<?> objectClass = object.getClass();
        SensitiveData sensitiveData = AnnotationUtils.findAnnotation(objectClass, SensitiveData.class);
        return Objects.nonNull(sensitiveData);
    }

    @Override
    public Object plugin(Object o) {
        return Plugin.wrap(o, this);
    }

    public <T> T decrypt(T result) throws IllegalAccessException {
        //取出resultType的类
        Class<?> resultClass = result.getClass();
        Field[] declaredFields = resultClass.getDeclaredFields();
        for (Field field : declaredFields) {
            //取出所有被EncryptDecryptField注解的字段
            SensitiveField sensitiveField = field.getAnnotation(SensitiveField.class);
            if (!Objects.isNull(sensitiveField)) {
                field.setAccessible(true);
                Object object = field.get(result);
                //只支持String的解密
                if (object instanceof String) {
                    String value = (String) object;
                    //对注解的字段进行逐一解密
                    field.set(result, EncryptUtils.decrypt(value).replaceAll("(\d{4})\d{10}(\w{4})", "$1*****$2"));
                }
            }
            Mask mask = field.getAnnotation(Mask.class);
            if (!Objects.isNull(mask)) {
                field.setAccessible(true);
                Object object = field.get(result);
                if (object instanceof String) {
                    String phone = (String) object;
                    field.set(result, phone.replaceAll("(\d{3})\d{4}(\d{4})", "$1****$2"));
                }
            }
        }
        return result;
    }
}

同时解密的时候,会扫描Mask,运用正则表达式,做掩码。

这个里面使用了加密的工具类: EncryptUtils,这里的加密方法可以根据自己的需求自己定义,我这里使用的是非对称加密sm2. 具体加密就不给出了,只要自己定义一个加密解密就行了。