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

一、背景

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

常用的对称加密算法有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
 */
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) {
            /**
             *  仅支持类型是自定义Entity的参数,不支持mapper的参数是QueryWrapper、String等,例如:
             *
             *  支持:findList(@Param(value = "query") UserEntity query);
             *  支持:findPage(@Param(value = "query") UserEntity query, Page<UserEntity> page);
             *
             *  不支持:findOne(@Param(value = "mobile") String mobile);
             *  不支持:findList(QueryWrapper wrapper);
             */
            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;
        }
    }

    /**
     * 校验该实例的类是否被@EncryptedTable所注解
     */
    private boolean annotateWithEncrypt(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()));
            }
        }

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

解密拦截器

/**
 * 解密拦截器
 *
 * @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;
}
  • 8
    点赞
  • 44
    收藏
    觉得还不错? 一键收藏
  • 15
    评论
Mybatis-plus分页查询无数据的问题可能有多种原因。首先,需要确保你的分页查询方法正确地配置了分页拦截器。在MybatisPlusConfig类中,你需要添加PaginationInnerInterceptor分页插件,并设置数据库类型为mysql。这样才能使分页查询功能生效。\[2\] 另外,你需要检查你的查询接口代码是否正确。在你的查询接口中,你需要传入正确的分页参数,并使用page方法进行分页查询。确保你的查询条件正确,并且与数据库中的数据匹配。\[3\] 如果你的分页查询仍然无数据,可能是因为数据库中没有符合查询条件的数据。你可以检查数据库中的数据是否正确,并且与你的查询条件匹配。如果数据库中确实没有符合条件的数据,那么分页查询将返回空结果。 总结起来,要解决mybatis-plus分页查询无数据的问题,你需要确保正确配置了分页拦截器,并且传入正确的分页参数和查询条件。同时,还需要检查数据库中是否有符合条件的数据。 #### 引用[.reference_title] - *1* *2* [mybatis-plus分页查询详解](https://blog.csdn.net/w1014074794/article/details/125787908)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] - *3* [mybatis-plus 分页后没有数据问题(超过当前数据量的页)](https://blog.csdn.net/jj89929665/article/details/126590180)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 15
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值