文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

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

2023-09-01 06:30

关注

一、背景

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

常用的对称加密算法有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依赖

    com.talkweb    wutong-base-dao    请使用最新版本

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接口三种方式进行测试

@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 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、自定义注解

根据注解进行数据拦截

@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.TYPE)public @interface EncryptedTable { }@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.FIELD)public @interface EncryptedColumn {}

2、定义拦截器

加密拦截器EncryptInterceptor

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 beforeQuery(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {        if (Objects.isNull(parameterObject)) {            return;        }        if (!(parameterObject instanceof Map)) {            return;        }        Map paramMap = (Map) parameterObject;        // 参数去重,否则多次加密会导致查询失败        Set set = (Set) paramMap.values().stream().collect(Collectors.toSet());        for (Object param : set) {                        if (param instanceof AbstractWrapper || param instanceof String) {                // Wrapper、String类型查询参数,无法获取参数变量上的注解,无法确认是否需要加密,因此不做判断                continue;            }            if (annotateWithEncrypt(param.getClass())) {                encryptEntity(param);            }        }    }        @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 (annotateWithEncrypt(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 (annotateWithEncrypt(param.getClass())) {                encryptEntity(param);            }            return;        }        // 通过在mapper.xml中自定义API修改数据库时        if (paramMap.containsKey("entity") && null != (param = paramMap.get("entity"))) {            if (annotateWithEncrypt(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 (annotateWithEncrypt(entityClass)) {                    encryptWrapper(entityClass, param);                }            }            return;        }    }        private boolean annotateWithEncrypt(Class objectClass) {        EncryptedTable sensitiveData = AnnotationUtils.findAnnotation(objectClass, EncryptedTable.class);        return Objects.nonNull(sensitiveData);    }        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;                    }                }            }        }    }        private void encryptWrapper(Class entityClass, Object ewParam) {        AbstractWrapper updateWrapper = (AbstractWrapper) ewParam;        String sqlSet = updateWrapper.getSqlSet();        String[] elArr = sqlSet.split(",");        Map 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()));            }        }        Method[] declaredMethods = entityClass.getDeclaredMethods();        for (Method method : declaredMethods) {            //取出所有被EncryptedColumn注解的字段            EncryptedColumn sensitiveField = method.getAnnotation(EncryptedColumn.class);            if (Objects.isNull(sensitiveField)) {                continue;            }            String el = propMap.get(method.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()));            }        }    }}

解密拦截器

@Intercepts({        @Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class})})@Componentpublic 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;    }        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 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更新数据时加密的场景。

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

public class EncryptTypeHandler extends BaseTypeHandler {    @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接口更新数据时加密的场景,不支持解密的需求。

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

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;}

来源地址:https://blog.csdn.net/tianmaxingkonger/article/details/130986784

阅读原文内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     807人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     351人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     314人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     433人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     221人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-数据库
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯