基于SpringBoot和Mybatis实现的数据安全处理小插件

概要

基于SpringBoot和Mybatis(兼容Mybatis-Plus)的数据安全处理插件,在数据持久层对数据进行加/解密和脱敏处理。

主要功能:

1、通过注解在保存数据的时候将指定字段进行加密处理,查询时自动解密
2、通过注解在查询数据的时候对指定字段进行脱敏处理

技术架构

SpringBoot/Mybatis-plus/MySQL/HuTool/Bouncy Castle/Lombok

项目概述

提示:除了黄色标记的目录是为了测试之外,所有类上都有类的作用简介

在这里插入图片描述

技术细节

MybatisEncryptInterceptor
MyBatis数据更新拦截器,主要负责拦截MyBatis的新增和修改操作,并且调用安全处理类对带有加密注解的字段进行加密处理。

@Intercepts({
        @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})
})
public class MybatisEncryptInterceptor implements Interceptor {


    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        //获取mappedStatement对象
        MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
        // 获取Mybatis插入或更新时传入的参数对象
        Object paramEntity = invocation.getArgs()[1];
        //调用安全处理类对数据加密处理
        SecurityHandler.handlerSecurity(mappedStatement.getId(), paramEntity, false);
        return invocation.proceed();
    }


    @Override
    public Object plugin(Object target) {
        if (target instanceof Executor) {
            return Plugin.wrap(target, this);
        } else {
            return target;
        }
    }

    @Override
    public void setProperties(Properties properties) {

    }
}

MybatisDecryptInterceptor
MyBatis数据查询拦截器,主要负责拦截MyBatis的查询操作,在查询完成之后,调用安全处理类对查询返回的数据进行解密和脱敏处理。

@Intercepts({
        @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})
})
public class MybatisDecryptInterceptor implements Interceptor {


    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        //执行查询
        ArrayList<Object> result = (ArrayList) invocation.proceed();
        //查询结果解密
        if (result != null) {
            for (Object paramEntity : result) {
                //调用处理类解密
                SecurityHandler.handlerSecurity(null, paramEntity, true);
            }
        }
        return result;
    }


    @Override
    public Object plugin(Object target) {
        if (target instanceof Executor) {
            return Plugin.wrap(target, this);
        } else {
            return target;
        }
    }

    @Override
    public void setProperties(Properties properties) {

    }
}

MybatisInterceptorConfig
MyBatis拦截器配置类,把自定义的拦截器添加到MyBatis拦截器中生效。

@Component
public class MybatisInterceptorConfig {

    @Resource
    private SqlSessionFactory sqlSessionFactory;


    @PostConstruct
    public void addInterceptor() {
        Configuration sqlSessionFactoryConfiguration = sqlSessionFactory.getConfiguration();
        sqlSessionFactoryConfiguration.addInterceptor(new MybatisEncryptInterceptor());
        sqlSessionFactoryConfiguration.addInterceptor(new MybatisDecryptInterceptor());
    }
}

SecurityHandler
安全处理类,主要负责调用加密处理类和脱敏处理类

public class SecurityHandler {


    /**
     * 安全处理
     *
     * @param paramEntity
     * @param doDecrypt
     */
    public static void handlerSecurity(String methodPath, Object paramEntity, boolean doDecrypt) {
        //加/解密
        try {
            SecretHandler.doEncrypt(methodPath, paramEntity, doDecrypt);
        } catch (Exception e) {
            throw new RuntimeException("数据加/解密处理时出现异常,异常信息【" + e.getMessage() + "】");
        }
        //如果是解密,则继续进行脱敏操作
        if (doDecrypt) {
            //脱敏
            try {
                DesensitizedHandler.doDesensitized(paramEntity);
            } catch (Exception e) {
                throw new RuntimeException("数据脱敏处理时出现异常,异常信息【" + e.getMessage() + "】");
            }
        }
    }
}

SecretHandler
加/解密处理类,主要负责对配置加密注解进行校验和判断,找到需要加/解密处理的字段,调用加密工具对数据进行加密,每个代码上面都有相应的功能注释。

public class SecretHandler {


    /**
     * 对数据加/解密
     *
     * @param paramEntity 参数实体
     * @param doDecrypt   是否是解密操作
     * @throws IllegalAccessException
     */
    public static void doEncrypt(String methodPath, Object paramEntity, boolean doDecrypt) throws IllegalAccessException, ClassNotFoundException {
        //获取参数类的class
        Class<?> paramEntityClass = paramEntity.getClass();
        //如果当前是加密并且方法入参不是实体类,那么看是方法上是否有加密参数注解
        if (!doDecrypt && paramEntityClass.equals(MapperMethod.ParamMap.class)) {
            //获取类和方法名段断点索引
            int lastIndexOf = methodPath.lastIndexOf(".");
            //获取执行的类路径
            Class<?> methodClass = Class.forName(methodPath.substring(0, lastIndexOf));
            //执行的获取方法名
            String methodName = methodPath.substring(lastIndexOf + 1);
            //获取所有执行的方法,找到当前执行的方法
            Method[] declaredMethods = methodClass.getDeclaredMethods();
            //定义当前执行的加密方法
            Method currentEncryptMethod = null;
            //循环所有方法,找到当前执行的加密方法
            for (Method declaredMethod : declaredMethods) {
                //如果当前执行的方法带有加密注解,则获取执行的参数
                if (methodName.equals(declaredMethod.getName()) && declaredMethod.isAnnotationPresent(EncryptMethod.class)) {
                    //找到了当前需要加密的方法,跳出循环
                    currentEncryptMethod = declaredMethod;
                    break;
                }
            }
            if (currentEncryptMethod != null) {
                //获取所有参数
                Parameter[] parameters = currentEncryptMethod.getParameters();
                //遍历该方法的所有参数
                for (Parameter parameter : parameters) {
                    //对带有加密参数注解的参数进行加密处理
                    if (parameter.isAnnotationPresent(EncryptParam.class)) {
                        //定义加密查询的变量名
                        String paramName;
                        //定义原始参数值
                        Object paramValue;
                        //获取参数类型
                        Class<?> parameterType = parameter.getType();
                        //通过注解获取参数值
                        EncryptParam encryptParam = parameter.getAnnotation(EncryptParam.class);
                        //获取加密的参数名,要和mybatis的xml使用的sql一致
                        String encryptParamName = encryptParam.value();
                        //如果没有设置的话,尝试取默认参数名(args0、args1、args2...)
                        if (StrUtil.isBlank(encryptParamName)) {
                            //获取加密查询的变量名
                            paramName = parameter.getName();
                            //获取原始值
                            paramValue = ((MapperMethod.ParamMap) paramEntity).get(parameter.getName());
                        } else {
                            //如果传入的参数实体包含注解设置的名称,则可以获取传入的值
                            if (((MapperMethod.ParamMap) paramEntity).containsKey(encryptParamName)) {
                                //获取加密查询的变量名
                                paramName = encryptParamName;
                                //获取原始值
                                paramValue = ((MapperMethod.ParamMap) paramEntity).get(encryptParamName);
                            } else {
                                //如果不包含就抛异常
                                throw new RuntimeException("EncryptParam注解设置的参数名【" + encryptParamName + "】不在mybatis查询的参数当中,请检查!");
                            }
                        }
                        //原始值为空直接跳过
                        if (paramValue == null) {
                            continue;
                        }
                        //如果是字符串,则直接对字符串的值进行加密
                        if (String.class.equals(parameterType)) {
                            //原始值判空
                            if (StrUtil.isNotBlank(paramValue.toString())) {
                                //对值加密
                                ((MapperMethod.ParamMap) paramEntity).put(paramName, SecretUtil.encrypt(paramValue.toString()));
                            }
                        } else if (List.class.equals(parameterType)) {
                            //如果是集合,则遍历集合,看实体类的参数是否有加密注解
                            for (Object object : (List) paramValue) {
                                if (object != null) {
                                    //对实体类加密处理
                                    encryptByEntity(object, false);
                                }
                            }
                        }
                    }
                }
            }
        } else {
            //如果查询参数是实体,则进行对实体进行判定并加密
            encryptByEntity(paramEntity, doDecrypt);
        }
    }

    /**
     * 实体类加密处理
     *
     * @param paramEntity 加密实体类
     * @param doDecrypt   是否是解密
     * @throws IllegalAccessException
     */
    private static void encryptByEntity(Object paramEntity, boolean doDecrypt) throws IllegalAccessException {
        //获取实体类的class对象
        Class<?> paramEntityClass = paramEntity.getClass();
        //如果是实体类,判断该实体类是否添加EncryptEntity注解,如果没有EncryptEntity注解,则不需要加密操作
        boolean needEncryptEntity = paramEntityClass.isAnnotationPresent(EncryptEntity.class);
        //不需要加密,直接返回
        if (!needEncryptEntity) {
            return;
        }
        //是否自动标记字段
        boolean autoMark = paramEntityClass.getAnnotation(EncryptEntity.class).autoMark();
        //获取实体类的所有字段值
        Field[] declaredFields = paramEntityClass.getDeclaredFields();
        //遍历字段加密,如果自动标记为true则除了忽略的所有字段全部需要加密
        if (autoMark) {
            for (Field declaredField : declaredFields) {
                //判断字段类型是否是字符串,不是字符串直接跳过
                Class<?> fieldType = declaredField.getType();
                if (!String.class.equals(fieldType)) {
                    continue;
                }
                //判断字段是否有忽略加密注解
                boolean ignoreEncrypt = declaredField.isAnnotationPresent(IgnoreEncryptField.class);
                if (ignoreEncrypt) {
                    continue;
                }
                encryptField(paramEntity, doDecrypt, declaredField);
            }
        } else {
            //否则只对带有EncryptField字段的字段加密
            for (Field declaredField : declaredFields) {
                //判断字段类型是否是字符串,不是字符串直接跳过
                Class<?> fieldType = declaredField.getType();
                if (!String.class.equals(fieldType)) {
                    continue;
                }
                //判断是否有加密注解
                boolean needEncryptField = declaredField.isAnnotationPresent(EncryptField.class);
                if (!needEncryptField) {
                    continue;
                }
                encryptField(paramEntity, doDecrypt, declaredField);
            }
        }
    }

    /**
     * 加密/解密字段
     *
     * @param paramEntity   参数实体
     * @param doDecrypt     是否是解密操作
     * @param declaredField 待处理字段
     * @throws IllegalAccessException
     */
    private static void encryptField(Object paramEntity, boolean doDecrypt, Field declaredField) throws IllegalAccessException {
        //如果自动标记字段则除了忽略的所有字段全部需要加密
        declaredField.setAccessible(true);
        //获取原始值
        Object paramValue = declaredField.get(paramEntity);
        //加解密操作
        if (paramValue != null && StrUtil.isNotBlank(paramValue.toString())) {
            if (doDecrypt) {
                //设置解密值
                try {
                    declaredField.set(paramEntity, SecretUtil.decrypt(paramValue.toString()));
                } catch (Exception e) {
                    throw new RuntimeException("解密失败,异常信息为【" + e.getMessage() + "】,请联系管理员");
                }
            } else {
                //设置加密值
                declaredField.set(paramEntity, SecretUtil.encrypt(paramValue.toString()));
            }
            declaredField.setAccessible(false);
        }
    }
}

DesensitizedHandler
脱敏处理类,主要负责对配置的脱敏注解进行校验和判断,找到需要脱敏处理的字段,调用脱敏的工具对数据脱敏,同样每个代码上面都有相应的功能注释。

public class DesensitizedHandler {


    /**
     * 脱敏处理
     *
     * @param paramEntity 参数实体
     * @throws IllegalAccessException
     */
    public static void doDesensitized(Object paramEntity) throws IllegalAccessException {
        //获取参数类的class
        Class<?> paramEntityClass = paramEntity.getClass();
        //获取实体类的所有字段值
        Field[] declaredFields = paramEntityClass.getDeclaredFields();
        //遍历字段脱敏
        for (Field declaredField : declaredFields) {
            //判断字段类型是否是字符串,不是字符串直接跳过
            Class<?> fieldType = declaredField.getType();
            if (!String.class.equals(fieldType)) {
                continue;
            }
            //判断字段是否有脱敏注解
            boolean needDesensitized = declaredField.isAnnotationPresent(DesensitizedField.class);
            //不需要脱敏直接跳过
            if (!needDesensitized) {
                continue;
            }
            declaredField.setAccessible(true);
            Object paramValue = declaredField.get(paramEntity);
            //脱敏
            if (paramValue != null && StrUtil.isNotBlank(paramValue.toString())) {
                String paramValueStr = paramValue.toString();
                //获取脱敏注解
                DesensitizedField desensitizedField = declaredField.getAnnotation(DesensitizedField.class);
                //获取脱敏配置的模式
                DesensitizedMode desensitizedMode = desensitizedField.mode();
                //如果是默认模式,根据front和tail的值脱敏
                if (DesensitizedMode.defaultMode == desensitizedMode) {
                    //否则根据配置的脱敏模式脱敏
                    int front = desensitizedField.front();
                    int tail = desensitizedField.tail();
                    //如果头尾位置都大于0,根据脱敏的头尾值脱敏,否则不进行脱敏处理
                    if (front >= 0 && tail >= 0) {
                        declaredField.set(paramEntity, DesensitizeUtil.desensitized(paramValueStr, front, tail));
                    }
                } else {
                    //如果不是默认模式,则根据配置的模式脱敏
                    declaredField.set(paramEntity, DesensitizeUtil.desensitizedByMode(desensitizedMode, paramValueStr));
                }
                declaredField.setAccessible(false);
            }
        }
    }
}

小结

上面只贴了MyBatis过滤器的配置以及加密脱敏判断的处理类代码,核心注解类以及加密和脱敏的工具类的代码就不贴了,下面是源码地址,里面有一份插件的使用文档,如果有不清楚怎么使用的读者,或者是项目有错误以及不足之处欢迎一起讨论。

为了方便读者测试,小编保留了简单的测试接口,下载源码后,先执行里面的数据库脚本(table.sql),然后就可以调用项目里开发好的接口进行测试。

另外,不足之处还请见谅,谢谢 ~

链接: 源码地址

Spring Boot是一个用于构建独立的、生产级的Spring应用程序的框架,它简化了基于Spring的应用程序的开发过程。它具有以下特点: 1. 简化配置:Spring Boot通过自动配置和约定大于配置的原则,大大减少了开发人员在项目中进行繁琐的配置工作。它提供了一些默认配置,可以快速启动和运行应用程序。 2. 内嵌服务器:Spring Boot可以使用内嵌的Tomcat、Jetty或Undertow服务器,无需部署WAR文件,简化了项目的部署和发布过程。 3. 自动化依赖管理:Spring Boot通过使用Maven或Gradle等构建工具,可以自动解决项目中的依赖关系。它根据项目中所使用的库和框架,自动导入所需的依赖。 4. 开箱即用的功能:Spring Boot提供了许多开箱即用的功能,例如安全认证、监控、日志记录等。开发人员可以通过简单的配置即可启用这些功能,而无需手动编写大量的代码。 MyBatis Plus是一款基于MyBatis的ORM框架,它提供了一系列增强的功能和特性,简化了数据访问层的开发。它具有以下特点: 1. 简化CRUD操作:MyBatis Plus提供了一系列的内置方法和注解,可以简化常见的CRUD操作。例如,通过继承BaseMapper接口,可以直接使用通用的增删改查方法,无需手动编写SQL语句。 2. 代码生成器:MyBatis Plus提供了一个代码生成器,可以根据数据库表结构自动生成实体类、Mapper接口和XML映射文件。这样可以减少手写重复的代码,提高开发效率。 3. 分页插件MyBatis Plus内置了一个强大的分页插件,可以方便地进行分页查询。开发人员只需在查询方法中添加分页参数,即可自动进行分页查询,并返回分页结果。 4. 乐观锁插件MyBatis Plus提供了一个乐观锁插件,可以在并发环境下解决数据更新的冲突问题。通过使用乐观锁注解,并在更新操作时进行版本号比对,可以确保数据的一致性。 总结来说,Spring Boot简化了Spring应用程序的开发和部署,而MyBatis Plus简化了数据访问层的开发。它们的结合可以提高开发效率,减少开发人员的工作量。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值