数据安全之MySQL数据加解密的实现方案

在我们日常的业务需求中,经常会遇到需要对存储的用户敏感数据进行加密处理的场景,如用户的身份信息、住址、身份证号等等,本文我们就讨论下,业务系统(后端)如何实现数据存储(基于MySQL)的加解密功能。

技术栈:springboot、mybatis、mysql等

方案一:基于spring aop拦截mybatis mapper.

第一步:定义注解@Encrypt

@Target(ElementType.METHOD)//注解的范围是类、接口、枚举的方法上
@Retention(RetentionPolicy.RUNTIME)//被虚拟机保存,可用反射机制读取
public @interface Encrypt{

    /**
     * 入参需要加密的字段
     * @return
     */
    String[] paramFields() default {};

    /**
     * 响应参数需解密的字段
     * @return
     */
    String[] respFields() default {};
}

第二步:开发拦截器处理类

@Slf4j
@Aspect
@Component
public class EncryptAspect {

    @Pointcut("@annotation(com.xxx.annotation.Encrypt)")
    public void encryPointCut() {
    }

    @Around("encryPointCut()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        final Sm4Intercept annotation = method.getAnnotation(Encrypt.class);
        if (null == annotation) {
            return joinPoint.proceed();
        }
        //加密入参对象属性值
        encryptRequest(joinPoint, annotation);
        //执行目标方法
        Object response = joinPoint.proceed();
        //解密响应结果对象的属性值
        decryptResponse(response, annotation);
        return response;
    }

    /**
     * 加密请求入参对象
     *
     * @param joinPoint
     * @param annotation
     */
    private void encryptRequest(ProceedingJoinPoint joinPoint, Sm4Intercept annotation) {
        //获取接口的入参列表
        final Object[] params = joinPoint.getArgs();
        if (!CollectionUtil.isEmpty(params)) {
            //接口入参
            Object param = params[0];
            //接口入参对象的属性列表
            Field[] fields = param.getClass().getDeclaredFields();
            if (!CollectionUtil.isEmpty(annotation.paramFields())) {
                //遍历加密入参的属性值
                Arrays.stream(annotation.paramFields()).forEach(target -> {
                    Field field = Arrays.stream(fields)
                            .filter(f -> f.getName().equals(target))
                            .findFirst()
                            .orElse(null);
                    if (null != field) {
                        //反射获取目标属性值
                        Object fieldValue = getFieldValue(param, field.getName());
                        if (null != fieldValue) {
                            String encryFieldValue = EncryptUtil.encryptEcb(key, fieldValue.toString());
                            log.info("类{}的属性{}的值{}已被加密为{}", param.getClass().getName(), target, fieldValue, encryFieldValue);
                            setFieldValue(param, field.getName(), encryFieldValue);
                        }
                    }
                });
            }
        }
    }

    /**
     * 解密响应结果对象的属性值
     *
     * @param object
     * @param annotation
     */
    private void decryptResponse(Object object, Sm4Intercept annotation) {
        //返回结果是list时
        if (object instanceof List) {
            decryptListObject((List) object, annotation);
            return;
        }
        //返回结果为单对象时
        decryptObject(object, annotation);
    }

    /**
     * 解密list中对象的属性值
     *
     * @param list
     * @param annotation
     */
    private void decryptListObject(List list, Sm4Intercept annotation) {
        list.stream().forEach(record -> decryptObject(record, annotation));
    }

    /**
     * 解密单对象的属性值
     *
     * @param record
     * @param annotation
     */
    private void decryptObject(Object record, Sm4Intercept annotation) {
        //接口返回对象的属性列表
        Field[] fields = record.getClass().getDeclaredFields();
        if (!CollectionUtil.isEmpty(annotation.respFields())) {
            //遍历加密入参的属性值
            Arrays.stream(annotation.respFields()).forEach(target -> {
                Field field = Arrays.stream(fields)
                        .filter(f -> f.getName().equals(target))
                        .findFirst()
                        .orElse(null);
                if (null != field) {
                    //反射获取目标属性值
                    Object fieldValue = getFieldValue(record, field.getName());
                    if (null != fieldValue) {
                        String decryFieldValue = EncryptUtil.decryptEcb(key, fieldValue.toString());
                        log.info("类{}的属性{}的值{}已被解密为{}", record.getClass().getName(), target, fieldValue, decryFieldValue);
                        setFieldValue(record, field.getName(), decryFieldValue);
                    }
                }
            });
        }
    }

    /**
     * 通过反射,用属性名称获得属性值
     *
     * @param thisClass 需要获取属性值的类
     * @param fieldName 该类的属性名称
     * @return
     */
    private Object getFieldValue(Object thisClass, String fieldName) {
        Object value = new Object();
        try {
            Method method = thisClass.getClass().getMethod(getMethodName(fieldName, "get"));
            value = method.invoke(thisClass);
        } catch (Exception e) {
        }
        return value;
    }

    /**
     * 通过反射,设置属性值
     *
     * @param thisClass
     * @param fieldName
     * @param fieldValue
     */
    private void setFieldValue(Object thisClass, String fieldName, Object fieldValue) {
        try {
            Method method = thisClass.getClass().getMethod(getMethodName(fieldName, "set"), String.class);
            method.invoke(thisClass, fieldValue);
        } catch (Exception e) {
        }
    }

    /**
     * 获取方法名称(getXXX,setXXX)
     *
     * @param fieldName
     * @param methodPrefix
     * @return
     */
    private String getMethodName(String fieldName, String methodPrefix) {
        return methodPrefix.concat(fieldName.substring(0, 1).toUpperCase() + fieldName.substring(1));
    }
}

第三步:mapper中定义注解@Encrypt进行数据拦截

@Mapper
public interface UserInfoMapper extends BaseMapper {


    /**
     * 如果查询条件中包含username,则在mapper执行前进行加密
     * 如果返回数据中包含username及address等信息,则进行解密  
     * @param vo
     * @return
     */
    @Encrypt(paramFields = {"userName"}, respFields = {"userName", "address"})
    List findUserInfo(UserSearchVo vo);
}

这样,便实现了在数据查询(或更新、插入等)时,完成入参及返回数据的加解密操作。不过这种处理方式仅限于数据操作是通过Dao的mapper接口调用时,如果想处理更多场景,如通过mybatis-plus的Wraper方式进行数据处理时,则考虑用后面的第二种处理方式。

方案二:基于mybatis自带的扩展插件(plugins)实现

MyBatis 允许你在映射语句执行过程中的某一点进行拦截调用。默认情况下,MyBatis 允许使用插件来拦截的方法调用包括:

  • Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
  • ParameterHandler (getParameterObject, setParameters)
  • ResultSetHandler (handleResultSets, handleOutputParameters)
  • StatementHandler (prepare, parameterize, batch, update, query)

这些类中方法的细节可以通过查看每个方法的签名来发现,或者直接查看 MyBatis 发行包中的源代码。 如果你想做的不仅仅是监控方法的调用,那么你最好相当了解要重写的方法的行为。 因为在试图修改或重写已有方法的行为时,很可能会破坏 MyBatis 的核心模块。 这些都是更底层的类和方法,所以使用插件的时候要特别当心。

通过 MyBatis 提供的强大机制,使用插件是非常简单的,只需实现 Interceptor 接口,并指定想要拦截的方法签名即可。

 插件的使用参考实现如下(mybatis官方文档)

@Slf4j
@Component
@Intercepts({@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class,
        RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class
})})
public class ExamplePlugin implements Interceptor {
  private Properties properties = new Properties();
  public Object intercept(Invocation invocation) throws Throwable {
    // implement pre processing if need
    Object returnObject = invocation.proceed();
    // implement post processing if need
    return returnObject;
  }
  public void setProperties(Properties properties) {
    this.properties = properties;
  }
}

即在mybatis的Executor的query方法执行前后进行拦截,如事先定义好需要加解密的“配置规则”(如“对哪个表的哪些字段需要加解密”、“方法的出入参需加解密的字段”等等),然后拦截sql的请求参数及执行的返回结果,对其进行相应的数据加解密操作

本文的核心实现思路都是围绕spring aop进行实现的,足以说明aop思想的强大之处,大家平时学习工作中一定要勤学、多用、多练!希望本文可以帮助到有需要的朋友们!

你可能感兴趣的:(知识沉淀,java,mybatis,spring,反射,aop)