基于Mybatis-Plus拦截器实现MySQL数据加解密

news2024/10/7 20:27:20

一、背景

用户的一些敏感数据,例如手机号、邮箱、身份证等信息,在数据库以明文存储时会存在数据泄露的风险,因此需要进行加密, 但存储数据再被取出时,需要进行解密,因此加密算法需要使用对称加密算法。

常用的对称加密算法有AES、DES、RC、BASE64等等,各算法的区别与优劣请自行百度。

本案例采用AES算法对数据进行加密。

二、MybatisPlus拦截器介绍

本文基于SpringBoot+MybatisPlus(3.5.X)+MySQL8架构,Dao层与DB中间使用MP的拦截器机制,对数据存取过程进行拦截,实现数据的加解密操作。

三、使用方法

该加解密拦截器功能在wutong-base-dao包(公司内部包)已经实现,如果您的项目已经依赖了base-dao,就可以直接使用。

另外,在码云上有Demo案例,见: mybatis-plus加解密Demo

基于wutong-base-dao包的使用步骤如下。

1、添加wutong-base-dao依赖

<dependency>
    <groupId>com.talkweb</groupId>
    <artifactId>wutong-base-dao</artifactId>
    <version>请使用最新版本</version>
</dependency>

2、在yaml配置开关,启用加解密

mybatis-plus:
  wutong:
    encrypt:
      # 是否开启敏感数据加解密,默认false
      enable: true
      # AES加密秘钥,可以使用hutool的SecureUtil工具类生成
      secretKey: yourSecretKey

3、定义PO类

实体类上使用自定义注解,来标记需要进行加解密

// 必须使用@EncryptedTable注解
@EncryptedTable
@TableName(value = "wsp_user")
public class UserEntity implements Serializable {
    private static final long serialVersionUID = 1L;
    
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    private String name;
    // 使用@EncryptedColumn注解
    @EncryptedColumn
    private String mobile;
    // 使用@EncryptedColumn注解
    @EncryptedColumn
    private String email;
}

4、定义API接口

通过MP自带API、Lambda、自定义mapper接口三种方式进行测试

/**
 * 用户表控制器
 *
 * @author wangshaopeng@talkweb.com.cn
 * @Date 2023-01-11
 */
@RestController
@RequestMapping("/user")
public class UserController {

    @Resource(name = "userServiceImpl")
    private IUserService userService;
    @Resource(name = "userXmlServiceImpl")
    private IUserService userXmlService;

    /**
     * 测试解密
     */
    @GetMapping(name = "测试解密", value = "/detail")
    public UserEntity detail(Long id) {
        // 测试MP API
//        UserEntity entity = userService.getById(id);

        // 测试自定义Mapper接口
        UserEntity entity = userXmlService.getById(id);
        if (null == entity) {
            return new UserEntity();
        }
        return entity;
    }

    /**
     * 新增用户表,测试加密
     */
    @GetMapping(name = "新增用户表,测试加密", value = "/add")
    public UserEntity add(UserEntity entity) {
        // 测试MP API
//        userService.save(entity);

        // 测试自定义Mapper接口
        userXmlService.save(entity);
        return entity;
    }

    /**
     * 修改用户表
     */
    @GetMapping(name = "修改用户表", value = "/update")
    public UserEntity update(UserEntity entity) {
        // 测试MP API
//        userService.updateById(entity);

        // 测试Lambda
//        LambdaUpdateWrapper<UserEntity> wrapper = new LambdaUpdateWrapper<>();
//        wrapper.eq(UserEntity::getId, entity.getId());
//        wrapper.set(UserEntity::getMobile, entity.getMobile());
//        wrapper.set(UserEntity::getName, entity.getName());
//        wrapper.set(UserEntity::getEmail, entity.getEmail());
//        userService.update(wrapper);

        // 测试自定义Mapper接口
        userXmlService.updateById(entity);
        return entity;
    }
}

四、实现原理

1、自定义注解

根据注解进行数据拦截

/**
 * 需要加解密的实体类用这个注解
 * @author wangshaopeng@talkweb.com.cn
 * @Date 2023-05-31
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface EncryptedTable {
 
}

/**
 * 需要加解密的字段用这个注解
 * @author wangshaopeng@talkweb.com.cn
 * @Date 2023-05-31
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface EncryptedColumn {

}

2、定义拦截器

加密拦截器EncryptInterceptor


/**
 * 加密拦截器
 *
 * @author wangshaopeng@talkweb.com.cn
 * @Date 2023-05-31
 */
@SuppressWarnings({"rawtypes"})
public class EncryptInterceptor extends JsqlParserSupport implements InnerInterceptor {
    /**
     * 变量占位符正则
     */
    private static final Pattern PARAM_PAIRS_RE = Pattern.compile("#\\{ew\\.paramNameValuePairs\\.(" + Constants.WRAPPER_PARAM + "\\d+)\\}");

    @Override
    public void beforeUpdate(Executor executor, MappedStatement mappedStatement, Object parameterObject) throws SQLException {
        if (Objects.isNull(parameterObject)) {
            return;
        }
        // 通过MybatisPlus自带API(save、insert等)新增数据库时
        if (!(parameterObject instanceof Map)) {
            if (needToDecrypt(parameterObject.getClass())) {
                encryptEntity(parameterObject);
            }
            return;
        }
        Map paramMap = (Map) parameterObject;
        Object param;
        // 通过MybatisPlus自带API(update、updateById等)修改数据库时
        if (paramMap.containsKey(Constants.ENTITY) && null != (param = paramMap.get(Constants.ENTITY))) {
            if (needToDecrypt(param.getClass())) {
                encryptEntity(param);
            }
            return;
        }
        // 通过在mapper.xml中自定义API修改数据库时
        if (paramMap.containsKey("entity") && null != (param = paramMap.get("entity"))) {
            if (needToDecrypt(param.getClass())) {
                encryptEntity(param);
            }
            return;
        }
        // 通过UpdateWrapper、LambdaUpdateWrapper修改数据库时
        if (paramMap.containsKey(Constants.WRAPPER) && null != (param = paramMap.get(Constants.WRAPPER))) {
            if (param instanceof Update && param instanceof AbstractWrapper) {
                Class<?> entityClass = mappedStatement.getParameterMap().getType();
                if (needToDecrypt(entityClass)) {
                    encryptWrapper(entityClass, param);
                }
            }
            return;
        }
    }

    /**
     * 校验该实例的类是否被@EncryptedTable所注解
     */
    private boolean needToDecrypt(Class<?> objectClass) {
        EncryptedTable sensitiveData = AnnotationUtils.findAnnotation(objectClass, EncryptedTable.class);
        return Objects.nonNull(sensitiveData);
    }

    /**
     * 通过API(save、updateById等)修改数据库时
     *
     * @param parameter
     */
    private void encryptEntity(Object parameter) {
        //取出parameterType的类
        Class<?> resultClass = parameter.getClass();
        Field[] declaredFields = resultClass.getDeclaredFields();
        for (Field field : declaredFields) {
            //取出所有被EncryptedColumn注解的字段
            EncryptedColumn sensitiveField = field.getAnnotation(EncryptedColumn.class);
            if (!Objects.isNull(sensitiveField)) {
                field.setAccessible(true);
                Object object = null;
                try {
                    object = field.get(parameter);
                } catch (IllegalAccessException e) {
                    continue;
                }
                //只支持String的解密
                if (object instanceof String) {
                    String value = (String) object;
                    //对注解的字段进行逐一加密
                    try {
                        field.set(parameter, AESUtils.encrypt(value));
                    } catch (IllegalAccessException e) {
                        continue;
                    }
                }
            }
        }
    }

    /**
     * 通过UpdateWrapper、LambdaUpdateWrapper修改数据库时
     *
     * @param entityClass
     * @param ewParam
     */
    private void encryptWrapper(Class<?> entityClass, Object ewParam) {
        AbstractWrapper updateWrapper = (AbstractWrapper) ewParam;
        String sqlSet = updateWrapper.getSqlSet();
        String[] elArr = sqlSet.split(",");
        Map<String, String> propMap = new HashMap<>(elArr.length);
        Arrays.stream(elArr).forEach(el -> {
            String[] elPart = el.split("=");
            propMap.put(elPart[0], elPart[1]);
        });

        //取出parameterType的类
        Field[] declaredFields = entityClass.getDeclaredFields();
        for (Field field : declaredFields) {
            //取出所有被EncryptedColumn注解的字段
            EncryptedColumn sensitiveField = field.getAnnotation(EncryptedColumn.class);
            if (Objects.isNull(sensitiveField)) {
                continue;
            }
            String el = propMap.get(field.getName());
            Matcher matcher = PARAM_PAIRS_RE.matcher(el);
            if (matcher.matches()) {
                String valueKey = matcher.group(1);
                Object value = updateWrapper.getParamNameValuePairs().get(valueKey);
                updateWrapper.getParamNameValuePairs().put(valueKey, AESUtils.encrypt(value.toString()));
            }
        }
    }
}

解密拦截器

/**
 * 解密拦截器
 *
 * @author wangshaopeng@talkweb.com.cn
 * @Date 2023-05-31
 */
@Intercepts({
        @Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class})
})
@Component
public class DecryptInterceptor implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        Object resultObject = invocation.proceed();
        if (Objects.isNull(resultObject)) {
            return null;
        }
        if (resultObject instanceof ArrayList) {
            //基于selectList
            ArrayList resultList = (ArrayList) resultObject;
            if (!resultList.isEmpty() && needToDecrypt(resultList.get(0))) {
                for (Object result : resultList) {
                    //逐一解密
                    decrypt(result);
                }
            }
        } else if (needToDecrypt(resultObject)) {
            //基于selectOne
            decrypt(resultObject);
        }
        return resultObject;
    }

    /**
     * 校验该实例的类是否被@EncryptedTable所注解
     */
    private boolean needToDecrypt(Object object) {
        Class<?> objectClass = object.getClass();
        EncryptedTable sensitiveData = AnnotationUtils.findAnnotation(objectClass, EncryptedTable.class);
        return Objects.nonNull(sensitiveData);
    }

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

    private <T> T decrypt(T result) throws Exception {
        //取出resultType的类
        Class<?> resultClass = result.getClass();
        Field[] declaredFields = resultClass.getDeclaredFields();
        for (Field field : declaredFields) {
            //取出所有被EncryptedColumn注解的字段
            EncryptedColumn sensitiveField = field.getAnnotation(EncryptedColumn.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, AESUtils.decrypt(value));
                }
            }
        }
        return result;
    }
}

四、其他实现方案

在技术调研过程中,还测试了另外两种便宜实现方案,由于无法覆盖MP自带API、Lambda、自定义API等多种场景,因此未采用。

1、使用字段类型处理器

字段类型处理器的[官方文档点这里],不能处理LambdaUpdateWrapper更新数据时加密的场景。

自定义类型处理器,实现加解密:


/**
 * @author wangshaopeng@talkweb.com.cn
 * @desccription 加密类型字段处理器
 * @date 2023/5/31
 */
public class EncryptTypeHandler extends BaseTypeHandler<String> {
    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException {
        ps.setString(i, AESUtils.encrypt(parameter));
    }

    @Override
    public String getNullableResult(ResultSet rs, String columnName) throws SQLException {
        final String value = rs.getString(columnName);
        return AESUtils.decrypt(value);
    }

    @Override
    public String getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
        final String value = rs.getString(columnIndex);
        return AESUtils.decrypt(value);
    }

    @Override
    public String getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
        final String value = cs.getString(columnIndex);
        return AESUtils.decrypt(value);
    }
}

在实体属性上进行指定

// @TableName注解必须指定autoResultMap = true
@EncryptedTable
@TableName(value = "wsp_user", autoResultMap = true)
public class UserEntity implements Serializable {
    private static final long serialVersionUID = 1L;
    
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    private String name;
    @TableField(typeHandler = EncryptTypeHandler.class)
    private String mobile;
    @TableField(typeHandler = EncryptTypeHandler.class)
    private String email;
}

2、自动填充功能

自动填充功能的[官方文档点这里],不能处理LambdaUpdateWrapper、自定义mapper接口更新数据时加密的场景,不支持解密的需求。

自定义类型处理器,实现加解密:


/**
 * Mybatis元数据填充处理类,仅能处理MP的函数,不能处理mapper.xml中自定义的insert、update
 *
 * @author wangshaopeng@talkweb.com.cn
 * @Date 2023-01-11
 */
public class DBMetaObjectHandler implements MetaObjectHandler {

    @Override
    public void insertFill(MetaObject metaObject) {
        String mobile = (String) metaObject.getValue("mobile");
        this.strictInsertFill(metaObject, "mobile", String.class, AESUtils.encrypt(mobile));
        String email = (String) metaObject.getValue("email");
        this.strictInsertFill(metaObject, "email", String.class, AESUtils.encrypt(email));
    }

    @Override
    public void updateFill(MetaObject metaObject) {
        String mobile = (String) metaObject.getValue("mobile");
        this.strictUpdateFill(metaObject, "mobile", String.class, AESUtils.encrypt(mobile));
        String email = (String) metaObject.getValue("email");
        this.strictUpdateFill(metaObject, "email", String.class, AESUtils.encrypt(email));
    }
}

在实体类上指定自动填充策略

@EncryptedTable
@TableName(value = "wsp_user")
public class UserEntity implements Serializable {
    private static final long serialVersionUID = 1L;
    
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    private String name;
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private String mobile;
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private String email;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/608521.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Unreal5 第三人称射击游戏 射击功能实现1

状态机的缓存 状态机缓存功能相当于我们只需要实现一次&#xff0c;可以在多个地方引用&#xff0c;也可以在别的状态机里面使用&#xff0c;而不是在里面再重新写一遍相应的功能。 我们可以在基础状态机的链接拉出一条线&#xff0c;搜索“缓存” 第一个就是新保存的缓存姿势…

Qt OpenGL(四十二)——Qt OpenGL 核心模式-GLSL(二)

提示:本系列文章的索引目录在下面文章的链接里(点击下面可以跳转查看): Qt OpenGL 核心模式版本文章目录 Qt OpenGL(四十二)——Qt OpenGL 核心模式-GLSL(二) 冯一川注:GLSL其实也是不断迭代的,比如像3.3版本中,基本数据类型浮点型只支持float型,而GLSL4.0版本开始就…

chatgpt赋能python:Python单位换算—让编程更精确的工具

Python单位换算—让编程更精确的工具 作为一名10年经验的Python工程师&#xff0c;我深深认识到在各种计算机领域的重要性。它可以帮助我们进行大量数据的计算和转换&#xff0c;其中的单位换算是其中一个重要的子领域。 单位换算对编程的重要性 无论是在科学研究还是工业生…

序贯最小二乘平差 VS 卡尔曼滤波

文章目录 Part.I IntroductionPart.II 概念比较Chap.I 序贯最小二乘平差Chap.II 卡尔曼滤波Chap.III 比较 Reference Part.I Introduction 序贯最小二乘平差和卡尔曼滤波有些相似&#xff0c;但是还是有一些区别&#xff1a; 序贯最小二乘平差用来处理静态数据&#xff0c;也…

层次多尺度注意力用于语义分割

层次多尺度注意力用于语义分割 HIERARCHICAL MULTI-SCALE ATTENTION FOR SEMANTIC SEGMENTATION https://arxiv.org/pdf/2005.10821.pdf 摘要 多尺度推断通常用于提高语义分割的结果。多个图像尺度通过网络传递&#xff0c;然后使用平均或最大池化方法将结果组合起来。在本文…

活用Excel高级筛选解决实际问题

一朋友遇到一个难题&#xff1a;一个电子行业文档&#xff0c;数据中有一项为输出电压&#xff0c;有固定值比如24V、48V等&#xff1b;也有范围值比如10V-60V或者40V-50V&#xff0c;数据不规则&#xff0c;且数量庞大。需要对数据进行分析&#xff0c;筛选出有效数据&#xf…

Spring Bean生命周期之(1)BeanDefinition

文章目录 1 BeanDefinition1.1 Spring Bean 读取解析配置信息1.1.1 XML 配置的处理主要使用的事例1.1.2 注解 BeanDefinition 解析示例 1.2 spring bean 注册阶段1.3 Spring BeanDefinition 合并阶段1.3.1 BeanDefinition 合并示例1.3.2 源码分析 1 BeanDefinition 在spring b…

机器学习——线性回归篇

基本概念 什么是回归预测&#xff1f;什么是分类预测&#xff1f; 模型输入变量预测结果应用回归预测实值离散一个连续值域上的任意值预测值的分布情况分类预测实值离散两个或多个分类值将输入变量分类到不同类别 思考一个问题&#xff1a;分类问题是否可以转变为回归问题&am…

百度商业AI技术创新大赛启动-63万元奖金!

随着生成式AI在全球范围的热议&#xff0c;AIGC前沿技术也在快速迭代&#xff0c;正如百度CEO李彦宏所说“人工智能发生了方向性改变&#xff0c;从辨别式AI走向生成式AI&#xff0c;生成式AI会带来极大的效率提升”。而这一领域的发展&#xff0c;将推动AI产品应用深化&#x…

两个震子如何调整发送信号时间差来调整波束

ΔΦ 2π/λdsinθ 这个式子中 d是振子的距离不变 λ是波长可以根据频率知道...所以要控制θ 就要控制两个振子的相位差 也就是控制振子a与振子b的时间差就能控制θ 30mhz的带宽与相位偏差的影响 ΔΦ 2π/λdsinθ 频率900hmz 波长300/9000.333 频率930hmz 波长300/9300…

t-SNE(t-stochastic neighourhood embedding) 数据降维及可视化

文章目录 算法原理示例一示例二 算法原理 t-SNE 的基本思想是将高维数据映射到低维空间&#xff0c;同时保留数据间的局部结构。具体而言&#xff0c;给定一个高维数据集 X { x 1 , … , x n } \mathbf{X}\{\mathbf{x}_1,\dots,\mathbf{x}_n\} X{x1​,…,xn​}&#xff0c;其…

macm1环境下IDEA项目切换jdk版本

macm1环境下IDEA项目切换jdk版本 本文目录 macm1环境下IDEA项目切换jdk版本背景需求下载对应版本jdk配置IDEA中指定项目的JDK版本配置 Project Structure配置 Settings 可能报错同时安装JDK8和JDK17并切换 背景需求 项目基于 springboot3.0.7Nacos2.2.3seata1.6.1 由spring官…

TensorFlow2安装备忘

系统配置 Ubuntu18.04 Anaconda4.9.2 Cuda10.0Cudnn7.6.5 CPU版本 执行pip install tensorflow默认安装的是CPU版本&#xff0c;并且安装后会有较多import依赖问题&#xff0c;部分记录如下&#xff0c;基本都属于"No module named xxxx"。 1.找不到wrapt 通过…

【23】SCI易中期刊推荐——神经网络科学及机器人学(中科院3区)

💖💖>>>加勒比海带,QQ2479200884<<<💖💖 🍀🍀>>>【YOLO魔法搭配&论文投稿咨询】<<<🍀🍀 ✨✨>>>学习交流 | 温澜潮生 | 合作共赢 | 共同进步<<<✨✨ 📚📚>>>人工智能 | 计算机视觉…

面试官:useEffect和useLayoutEffect有什么区别?

您好&#xff0c;如果喜欢我的文章&#xff0c;可以关注我的公众号「量子前端」&#xff0c;将不定期关注推送前端好文~ Effect数据结构 顾名思义&#xff0c;React底层在函数式组件的Fiber节点设计中带入了hooks链表的概念&#xff08;memorizedState&#xff09;&#xff0…

博弈论——巴什博弈(C++)

博弈论&#xff08;C&#xff09; 前言例题&#xff1a;拍卖会题目描述输入输出格式输入格式&#xff1a;输出格式&#xff1a; 输入输出样例输入样例#1&#xff1a;输出样例#1&#xff1a; 例题的解&#xff1a;巴什博奕&#xff08;Bash Game&#xff09;&#xff1a;代码&am…

CEPH部署

//存储类型 块存储 一对一&#xff0c;只能被一个主机挂载使用&#xff0c;数据以块为单位进行存储&#xff0c;典型代表&#xff1a;硬盘 文件存储 一对多&#xff0c;能被多个主机同时挂载使用&#xff0c;数据以文件的形式存储的&#xff08;元数据和实际数据是分开存储…

【30】核心易中期刊推荐——人工智能图像处理

🚀🚀🚀NEW!!!核心易中期刊推荐栏目来啦 ~ 📚🍀 核心期刊在国内的应用范围非常广,核心期刊发表论文是国内很多作者晋升的硬性要求,并且在国内属于顶尖论文发表,具有很高的学术价值。在中文核心目录体系中,权威代表有CSSCI、CSCD和北大核心。其中,中文期刊的数…

javascript基础二十三:说说 JavaScript 中内存泄漏的几种情况

一、是什么 内存泄漏&#xff08;Memory leak&#xff09;是在计算机科学中&#xff0c;由于疏忽或错误造成程序未能释放已经不再使用的内存 并非指内存在物理上的消失&#xff0c;而是应用程序分配某段内存后&#xff0c;由于设计错误&#xff0c;导致在释放该段内存之前就失…

【数学建模】期末样题(2021年真题)

一、&#xff08;10分&#xff09;某乳制品厂计划生产A、B、C三种酸奶。已知生产单位重量的A需要加工设备3小时&#xff0c;原料甲1千克&#xff0c;原料乙0.2千克&#xff1b;生产单位重量的B需要加工设备5小时&#xff0c;原料甲1千克&#xff0c;原料乙0.3千克&#xff1b;生…