MybatisPlus拦截器实战之数据的加解密和脱敏

一、前言

看完本文你将能学到什么?

  • 自定义mybatis-plus拦截器,对指定数据更新时自动加密处理
  • 自定义mybatis拦截器,打印完整sql;
  • 自定义springboot-starter;
  • 自定义注解;
  • 自定义 HandlerMethodReturnValueHandler 处理接口响应结果,我这里是使用它对需要的数据进行拦截处理,解密/脱敏;
  • mybatis-plus 基本的增删改查api操作;

文章对应的完整代码仓库:
https://gitee.com/fengsoshuai/mybatis-plus-interceptor-demo

二、拦截器简介

Mybatis Plus 的拦截器终极奥义是使用了 Mybatis 的拦截器。
只是在原先的基础上,划分的更加细致了。缺点也很明确,没有处理响应结果的钩子方法。

Mybatis Plus 中的拦截器的定义是:

@Intercepts({@Signature(
    type = StatementHandler.class,
    method = "prepare",
    args = {Connection.class, Integer.class}
), @Signature(
    type = StatementHandler.class,
    method = "getBoundSql",
    args = {}
), @Signature(
    type = Executor.class,
    method = "update",
    args = {MappedStatement.class, Object.class}
), @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 MybatisPlusInterceptor implements Interceptor {
  // 省略全部代码...
  // mybatis-plus 的拦截器集合
  private List<InnerInterceptor> interceptors = new ArrayList();

}

可以看到Mybatis Plus 拦截器的处理器, 其实现了Interceptor ,在内部遍历interceptors ,处理sql执行前的数据。
一般可以用作打印sql,或者按照某些条件拼接sql的条件(比如数据权限分离)。

三、代码目录结构简介

在这里插入图片描述

四、核心代码讲解

4.1 application.yml文件

额外定义了mybatis拦截器配置,主要是配置是否启用打印sql,或数据加密等拦截器。

# 数据源
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/mp_interceptor_db?useUnicode=true&serverTimezone=UTC
    username: root
    password: root
    type: com.alibaba.druid.pool.DruidDataSource

# mybatis plus
mybatis-plus:
  # xml扫描,多个目录用逗号或者分号分隔(告诉mapper所对应的xml文件位置)
  mapper-locations: classpath*:mapper/**Mapper.xml
  # 以下配置均有默认值
  global-config:
    db-config:
      #主键类型  auto:"数据库ID自增" 1:"用户输入ID",2:"全局唯一ID (数字类型唯一ID)", 3:"全局唯一ID UUID";
      id-type: auto
      # 全局逻辑删除的实体字段名
      logic-delete-field: deleted
      # 逻辑已删除值(默认为 1)
      logic-delete-value: 1
      # 逻辑未删除值(默认为 0)
      logic-not-delete-value: 0
  configuration:
    # 是否开启自动驼峰命名规则映射:从数据库列名到Java属性驼峰命名的类似映射
    map-underscore-to-camel-case: true
    # 如果查询结果中包含空值的列,则 MyBatis 在映射的时候,不会映射这个字段
    call-setters-on-nulls: true
    # 这个配置会将执行的sql打印出来,在开发或测试的时候可以用
    # log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  # 扫描实体
  type-aliases-package: org.feng.entity

# 自定义mybatis拦截器配置
mybatis:
  interceptor:
    property:
      enable-sensitive: true
      enable-illegal-sql: true
      enable-optimistic-locker: true
      print-sql: true

4.2 自定义注解

4.2.1 SensitiveEntity

标注一个实体类,是否包含需要加密的字段。比如User类中有属性 phone,需要加密存储,则可以在User类上使用该注解。

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface SensitiveEntity {
}

4.2.2 SensitiveData

在标注有SensitiveEntity注解的实体中,使用本注解标注某个字段,表示该字段是加密的,并且指定加密类型(以哪种加密算法加密的)。

本项目中,重点在于代码设计,加密算法就使用了最简单的 Base64转码的方式,不喜勿喷!

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface SensitiveData {

    /**
     * 指定加解密类型
     *
     * @return 类型,{@link  AbstractSensitive}
     */
    String sensitiveType() default "";
}

4.2.3 MaskedEntity

标注一个实体是需要脱敏处理的。不一定会真正执行,需要和 MaskedMethod注解搭配使用。
比如本项目中,需要对 UserVO 进行脱敏处理。

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface MaskedEntity {
}

@Data
@MaskedEntity
public class UserVO implements Response {
    /**
     * 用户名
     */
    private String username;

    /**
     * 年龄
     */
    private Integer age;

    /**
     * 邮箱
     */
    @MaskedField(type = SensitiveDataTypeEnum.EMAIL)
    private String email;

    /**
     * 电话
     */
    @MaskedField(type = SensitiveDataTypeEnum.PHONE, sensitiveType = Base64Sensitive.SENSITIVE_TYPE_CODE)
    private String phone;

    /**
     * 创建时间
     */
    private LocalDateTime createTime;

    /**
     * 更新时间
     */
    private LocalDateTime updateTime;

    public UserVO copyFieldByUser(@NonNull User user) {
        this.setUsername(user.getUsername());
        this.setAge(user.getAge());
        this.setPhone(user.getPhone());
        this.setEmail(user.getEmail());
        this.setCreateTime(user.getCreateTime());
        this.setUpdateTime(user.getUpdateTime());
        return this;
    }
}

4.2.4 MaskedField

用于在标注了MaskedEntity的实体中的单个字段上,表示该字段需要脱敏处理。
必须同时指定脱敏数据类型,比如是手机脱敏,还是邮箱脱敏等。
加密类型可以不指定,在指定时会进行解密处理,不指定则当做明文来操作,只进行脱敏数据。

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface MaskedField {

    /**
     * 指定脱敏数据类型
     *
     * @return 脱敏数据类型
     */
    SensitiveDataTypeEnum type();

    /**
     * 加密类型
     *
     * @return 加密类型编码
     */
    String sensitiveType() default "";
}

4.2.5 MaskedMethod

用于标注在Controller内的带有 ResponseBody的方法上。
表示该方法的返回值需要进行数据脱敏处理。
内部使用gson序列化为json,最终返回给调用方。

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MaskedMethod {
}

比如在Controller中定义:

@MaskedMethod
@GetMapping("/list")
public ResponseEntity<List<UserVO>> list() {
    return new ResponseEntity<>("查询用户数据成功", "200", userService.listUser());
}

4.3 Mybatis-Plus 拦截器数据自动加密

对应的类是:MybatisPlusSensitiveInterceptor
在这里插入图片描述
具体实现如下:

package org.feng.interceptor;

import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor;
import lombok.Builder;
import lombok.experimental.Accessors;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.MappedStatement;
import org.feng.annotions.SensitiveData;
import org.feng.sensitive.AbstractSensitive;
import org.feng.util.SensitiveUtil;
import org.springframework.util.StringUtils;

import java.lang.reflect.Field;
import java.util.Objects;

/**
 * 处理密文拦截器
 *
 * @version v1.0
 * @author: fengjinsong
 * @date: 2023年08月24日 23时07分
 */
@Slf4j
@Builder
@Accessors(chain = true)
public class MybatisPlusSensitiveInterceptor implements InnerInterceptor {

    /**
     * 全局的加/解密处理
     */
    private AbstractSensitive sensitive;

    @Override
    public void beforeUpdate(Executor executor, MappedStatement ms, Object parameter) {
        // 执行加密操作
        executeSensitive(parameter);
    }

    private void executeSensitive(Object parameter) {
        Class<?> entityClass = parameter.getClass();
        // 当前实体类有标注了SensitiveEntity注解
        if (SensitiveUtil.isSensitiveEntity(parameter)) {
            Field[] fields = entityClass.getDeclaredFields();
            for (Field field : fields) {
                if (field.isAnnotationPresent(SensitiveData.class)) {
                    SensitiveData sensitiveData = field.getAnnotation(SensitiveData.class);
                    String sensitiveType = sensitiveData.sensitiveType();
                    // 字段注解传的sensitiveType有值
                    if (StringUtils.hasLength(sensitiveType)) {
                        // 获取缓存中的实例
                        AbstractSensitive sensitiveByType = AbstractSensitive.getSensitiveByType(sensitiveType);
                        if (Objects.isNull(sensitiveByType)) {
                            throw new RuntimeException("加解密类型设置错误,类型不存在");
                        }
                        // 重置变量的值
                        SensitiveUtil.encryptFieldValue(field, parameter, sensitiveByType);
                        continue;
                    } else if (Objects.nonNull(sensitive)) {
                        // 重置变量的值
                        SensitiveUtil.encryptFieldValue(field, parameter, sensitive);
                        continue;
                    }
                    throw new RuntimeException("未指定加、解密类型");
                }
            }
        }
    }
}

4.4 Mybatis 打印完整sql的拦截器

拦截Executor的查询和更新的方法,拼接sql语句并打印。

package org.feng.interceptor;

import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.cache.CacheKey;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.apache.ibatis.type.TypeHandlerRegistry;
import org.feng.util.TimeUtil;
import org.springframework.util.CollectionUtils;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.Date;
import java.util.List;
import java.util.Objects;

/**
 * mybatis拦截器拦截处理查询、更新的方法,mybatis-plus拦截器见:{@link MybatisPlusInterceptor}
 *
 * @version v1.0
 * @author: fengjinsong
 * @date: 2023年08月25日 23时23分
 */

@Intercepts({
        @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
        @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}),
})
@Slf4j
public class MybatisPrintSqlInterceptor implements Interceptor {

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // 获取语句映射对象
        Object[] invocationArgs = invocation.getArgs();
        MappedStatement mappedStatement = (MappedStatement) invocationArgs[0];

        // 获取参数(条件)
        Object paramObject = null;
        // 2个以上的入参,也就是有额外的查询或更新条件
        if (invocationArgs.length > 1) {
            paramObject = invocationArgs[1];
        }

        BoundSql boundSql = mappedStatement.getBoundSql(paramObject);
        Configuration configuration = mappedStatement.getConfiguration();
        String mappedStatementId = mappedStatement.getId();
        // 开始执行时间
        long start = System.currentTimeMillis();
        // 执行方法
        Object returnValue = invocation.proceed();
        // 执行耗时
        long executeTime = System.currentTimeMillis() - start;
        // 拼接sql,参数注入
        String sql = concatSql(configuration, boundSql);
        // 打印sql
        logs(executeTime, sql, mappedStatementId);
        return returnValue;
    }


    private String concatSql(Configuration configuration, BoundSql boundSql) {
        Object parameterObject = boundSql.getParameterObject();
        List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
        //替换空格、换行、tab缩进等
        String sql = boundSql.getSql().replaceAll("[\\s]+", " ");
        if (!CollectionUtils.isEmpty(parameterMappings) && Objects.nonNull(parameterObject)) {
            TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
            if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
                sql = sql.replaceFirst("\\?", getParameterValue(parameterObject));
            } else {
                MetaObject metaObject = configuration.newMetaObject(parameterObject);
                for (ParameterMapping parameterMapping : parameterMappings) {
                    String propertyName = parameterMapping.getProperty();
                    if (metaObject.hasGetter(propertyName)) {
                        Object obj = metaObject.getValue(propertyName);
                        sql = sql.replaceFirst("\\?", getParameterValue(obj));
                    } else if (boundSql.hasAdditionalParameter(propertyName)) {
                        Object obj = boundSql.getAdditionalParameter(propertyName);
                        sql = sql.replaceFirst("\\?", getParameterValue(obj));
                    }
                }
            }
        }
        return sql;
    }

    private String getParameterValue(Object obj) {
        String value;
        if (obj instanceof String) {
            value = "'" + obj + "'";
        } else if (obj instanceof Date) {
            value = "'" + TimeUtil.defaultFormat(((Date) obj).toInstant()) + "'";
        } else if (obj instanceof LocalDateTime) {
            value = "'" + TimeUtil.defaultFormat((LocalDateTime) obj) + "'";
        } else if (obj instanceof LocalDate) {
            value = "'" + TimeUtil.defaultFormat((LocalDate) obj) + "'";
        } else {
            if (obj != null) {
                value = obj.toString();
            } else {
                value = "";
            }
        }
        return value.replace("$", "\\$");
    }

    private void logs(long time, String sql, String sqlId) {
        log.info("\r\n执行SQL:{} \r\n执行耗时:{}ms, 执行方法:{}", sql, time, sqlId);
    }

    @Override
    public Object plugin(Object target) {
        // 如果是Executor(执行增删改查操作),则拦截下来
        if (target instanceof Executor) {
            return Plugin.wrap(target, this);
        }
        return target;
    }
}

4.4.1 打印结果示例

2023-08-26 17:11:40.192  INFO 21248 --- [nio-8080-exec-1] o.f.i.MybatisPrintSqlInterceptor         : 
执行SQL:SELECT id,username,age,email,phone,create_time,update_time,deleted FROM mp_user WHERE deleted=0 AND (username = '牛大山') 
执行耗时:221ms, 执行方法:org.feng.mapper.UserMapper.selectList

4.5 Mybatis-Plus 配置类

添加拦截器。

package org.feng.config;

import com.baomidou.mybatisplus.autoconfigure.ConfigurationCustomizer;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.IllegalSQLInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
import lombok.extern.slf4j.Slf4j;
import org.feng.interceptor.MybatisPlusSensitiveInterceptor;
import org.feng.interceptor.MybatisPrintSqlInterceptor;
import org.feng.properties.MybatisInterceptorProperties;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.context.annotation.Bean;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;

/**
 * Mybatis-Plus插件之拦截器的自动配置类<br>
 * <a href="https://baomidou.com/pages/2976a3/#mybatisplusinterceptor">插件官网链接</a>
 *
 * @version v1.0
 * @author: fengjinsong
 * @date: 2023年08月24日 22时01分
 */
@Slf4j
@AutoConfiguration
public class MybatisInterceptorConfiguration {

    @Resource
    private MybatisInterceptorProperties mybatisInterceptorProperties;

    /**
     * 配置拦截器
     *
     * @return 拦截器
     */
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        // 自定义拦截:密文处理
        if (mybatisInterceptorProperties.isEnableSensitive()) {
            log.info("mybatis注册拦截器:密文处理");
            interceptor.addInnerInterceptor(MybatisPlusSensitiveInterceptor.builder().build());
        }

        // sql性能规范
        if (mybatisInterceptorProperties.isEnableIllegalSql()) {
            log.info("mybatis注册拦截器:SQL性能规范检查");
            interceptor.addInnerInterceptor(new IllegalSQLInnerInterceptor());
        }

        // 乐观锁
        if (mybatisInterceptorProperties.isEnableOptimisticLocker()) {
            log.info("mybatis注册拦截器:乐观锁");
            interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
        }
        return interceptor;
    }

    @Bean
    public ConfigurationCustomizer configurationCustomizer() {
        return configuration -> {
            // SQL 打印拦截
            if (mybatisInterceptorProperties.isPrintSql()) {
                log.info("mybatis注册拦截器:打印sql");
                configuration.addInterceptor(new MybatisPrintSqlInterceptor());
            }
        };
    }


    @PostConstruct
    private void init() {
        log.info("Mybatis-Plus插件之拦截器的自动配置类 init");
    }
}

4.6 CustomHandlerMethodReturnValueHandler

自定义方法返回值处理器,用做数据脱敏处理。

package org.feng.common;

import lombok.extern.slf4j.Slf4j;
import org.springframework.core.MethodParameter;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.AsyncHandlerMethodReturnValueHandler;
import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
import org.springframework.web.method.support.ModelAndViewContainer;

import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Field;
import java.util.List;
import java.util.Objects;

/**
 * 自定义方法返回结果处理器
 *
 * @version v1.0
 * @author: fengjinsong
 * @date: 2023年08月26日 14时15分
 */
@Slf4j
public class CustomHandlerMethodReturnValueHandler implements HandlerMethodReturnValueHandler, AsyncHandlerMethodReturnValueHandler {
    @Override
    public boolean supportsReturnType(MethodParameter returnType) {
        // 方法上标注了ResponseBody,就处理
        return (AnnotatedElementUtils.hasAnnotation(returnType.getContainingClass(), ResponseBody.class) || returnType.hasMethodAnnotation(ResponseBody.class))
                // 当前方法返回值需要数据脱敏
                && (AnnotatedElementUtils.hasAnnotation(returnType.getContainingClass(), MaskedMethod.class) || returnType.hasMethodAnnotation(MaskedMethod.class));
    }

    @Override
    public void handleReturnValue(Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
        mavContainer.setRequestHandled(true);
        HttpServletResponse response = webRequest.getNativeResponse(HttpServletResponse.class);
        assert response != null;
        response.setContentType("application/json;charset=utf-8");

        if (returnValue instanceof ResponseEntity) {
            ResponseEntity<?> responseEntity = (ResponseEntity<?>) returnValue;
            Object data = responseEntity.getData();
            // 响应结果是集合
            if (data instanceof List) {
                List<?> dataList = (List<?>) data;
                // 集合为空,直接返回
                if (CollectionUtils.isEmpty(dataList)) {
                    response.getWriter().write(GsonUtil.toJsonWithNull(returnValue));
                    return;
                }
                // 处理集合结果集
                Class<?> singleDataClass = dataList.get(0).getClass();
                // 是否标注为脱敏数据实体
                boolean maskedEntity = singleDataClass.isAnnotationPresent(MaskedEntity.class);
                // 不是脱敏实体,不用处理
                if (!maskedEntity) {
                    response.getWriter().write(GsonUtil.toJsonWithNull(returnValue));
                    return;
                }

                // 处理数据脱敏
                for (Object singleData : dataList) {
                    Field[] fields = singleDataClass.getDeclaredFields();
                    for (Field field : fields) {
                        doMaskedField(field, singleData);
                    }
                }
                response.getWriter().write(GsonUtil.toJsonWithNull(returnValue));
                return;
            } else {
                // 非集合响应结果处理
                Class<?> dataClass = data.getClass();
                // 是否标注为脱敏数据实体
                boolean maskedEntity = dataClass.isAnnotationPresent(MaskedEntity.class);
                // 不是脱敏实体,不用处理
                if (!maskedEntity) {
                    response.getWriter().write(GsonUtil.toJsonWithNull(returnValue));
                    return;
                }
                // 脱敏数据
                Field[] fields = dataClass.getDeclaredFields();
                for (Field field : fields) {
                    doMaskedField(field, data);
                }
            }
        }

        // 序列化响应
        response.getWriter().write(GsonUtil.toJsonWithNull(returnValue));
    }

    /**
     * 脱敏数据处理:暂不支持复杂对象(嵌套)
     *
     * @param field  属性对象
     * @param object 对象本身
     */
    private void doMaskedField(Field field, Object object) {
        // 标注了MaskedField
        boolean maskedField = field.isAnnotationPresent(MaskedField.class);
        if (maskedField) {
            field.setAccessible(true);
            MaskedField maskedFieldAnnotation = field.getAnnotation(MaskedField.class);
            // 脱敏数据
            try {
                SensitiveDataTypeEnum dataTypeEnum = maskedFieldAnnotation.type();
                Object fieldValue = field.get(object);
                if (Objects.nonNull(fieldValue)) {
                    field.set(object, dataTypeEnum.doDecryptAndMaskedField(fieldValue.toString(), maskedFieldAnnotation.sensitiveType()));
                }
            } catch (IllegalAccessException e) {
                log.error("脱敏失败", e);
                throw new RuntimeException("脱敏失败");
            }
            field.setAccessible(false);
        }
    }

    @Override
    public boolean isAsyncReturnValue(Object returnValue, MethodParameter returnType) {
        return supportsReturnType(returnType);
    }
}


4.7 敏感数据类型枚举

将对应的类型和脱敏方法,加解密规则进行绑定。

package org.feng.common;

import lombok.AllArgsConstructor;
import lombok.Getter;
import org.feng.sensitive.AbstractSensitive;
import org.feng.util.SensitiveUtil;

import java.util.Objects;

/**
 * 敏感数据类型
 *
 * @version v1.0
 * @author: fengjinsong
 * @date: 2023年08月26日 00时20分
 */
@Getter
@AllArgsConstructor
public enum SensitiveDataTypeEnum {
    /**
     * 手机类型
     */
    PHONE() {
        @Override
        String doSensitive(String originalData) {
            return SensitiveUtil.maskedPhone(originalData);
        }

        @Override
        String decrypt(String encryptData, String sensitiveType) {
            AbstractSensitive sensitive = AbstractSensitive.getSensitiveByType(sensitiveType);
            if (Objects.isNull(sensitive)) {
                return encryptData;
            }
            return sensitive.decrypt(encryptData);
        }
    },
    ID_CARD,
    EMAIL(){
        @Override
        String doSensitive(String originalData) {
            return SensitiveUtil.maskedEmail(originalData);
        }

        @Override
        String decrypt(String encryptData, String sensitiveType) {
            AbstractSensitive sensitive = AbstractSensitive.getSensitiveByType(sensitiveType);
            if (Objects.isNull(sensitive)) {
                return encryptData;
            }
            return sensitive.decrypt(encryptData);
        }
    },
    BANK_CARD,
    ADDRESS,
    CUSTOM;

    /**
     * 脱敏数据
     *
     * @param originalData 原数据
     * @return 脱敏后的数据
     */
    String doSensitive(String originalData) {
        throw new RuntimeException(this.name() + " 暂不支持脱敏数据");
    }

    String decrypt(String encryptData, String sensitiveType) {
        throw new RuntimeException(this.name() + " 暂不支持解密数据");
    }

    /**
     * 解密数据并脱敏
     *
     * @param encryptData   密文数据
     * @param sensitiveType 加密类型
     * @return 解密并脱敏后的数据
     */
    String doDecryptAndMaskedField(String encryptData, String sensitiveType) {
        String decryptText = decrypt(encryptData, sensitiveType);
        return doSensitive(decryptText);
    }
}


4.8 Mvc拦截器配置

package org.feng.config;

import org.feng.common.CustomHandlerMethodReturnValueHandler;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import java.util.List;

/**
 * MVC配置
 *
 * @version v1.0
 * @author: fengjinsong
 * @date: 2023年08月26日 14时06分
 */
@Configuration
public class WebMvcConfiguration implements WebMvcConfigurer {


    @Override
    public void addReturnValueHandlers(List<HandlerMethodReturnValueHandler> returnValueHandlers) {
        returnValueHandlers.add(new CustomHandlerMethodReturnValueHandler());
    }
}

五、测试结果

5.1 数据库中

在这里插入图片描述
插入数据时,没有手动调用base64转码,手机号自动调用加密方法,转为base64

5.2 查询结果脱敏

在UserVO中使用注解(这里省略其他字段):

@Data
@MaskedEntity
public class UserVO implements Response {
    /**
     * 邮箱
     */
    @MaskedField(type = SensitiveDataTypeEnum.EMAIL)
    private String email;

    /**
     * 电话
     */
    @MaskedField(type = SensitiveDataTypeEnum.PHONE, sensitiveType = Base64Sensitive.SENSITIVE_TYPE_CODE)
    private String phone;
}

以上注解表示对该类对象进行脱敏数据处理,并指定了email字段的脱敏规则,phone的解密规则和脱敏规则。

可以看到响应结果中,手机号是解密了的,并且进行了数据脱敏。
邮箱因为没有加密存储,使用注解标注后,也进行了数据脱敏。

{
  "data": [
    {
      "username": "小冯",
      "age": 27,
      "email": null,
      "phone": null,
      "createTime": "2023-08-24 23:45:59",
      "updateTime": "2023-08-24 23:45:59"
    },
    {
      "username": "小李",
      "age": 25,
      "email": null,
      "phone": null,
      "createTime": "2023-08-24 23:46:16",
      "updateTime": "2023-08-24 23:46:16"
    },
    {
      "username": "小刘",
      "age": 32,
      "email": null,
      "phone": null,
      "createTime": "2023-08-24 23:46:26",
      "updateTime": "2023-08-24 23:46:26"
    },
    {
      "username": "牛山",
      "age": 22,
      "email": "f***g@163.com",
      "phone": "181****5213",
      "createTime": "2023-08-25 21:22:02",
      "updateTime": "2023-08-25 21:22:02"
    },
    {
      "username": "牛大山",
      "age": 23,
      "email": "f***g@163.com",
      "phone": "181****5213",
      "createTime": "2023-08-25 22:49:02",
      "updateTime": "2023-08-25 22:49:02"
    }
  ],
  "message": "查询用户数据成功",
  "code": "200"
}

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
MybatisPlus拦截器是用来增强MyBatis功能的工具,它可以在执行SQL语句之前或之后进行一些额外的处理。在MybatisPlus中,可以通过自定义拦截器实现对SQL语句的拦截和修改。在你提供的代码中,可以看到通过自定义拦截器的方式来添加MybatisPlus拦截器。\[1\] MybatisPlus拦截器的作用有很多,比如可以实现自定义的SQL拦截、分页查询拦截、打印SQL日志等功能。通过自定义拦截器,你可以在执行SQL语句之前或之后进行一些自定义的操作,以满足特定的需求。\[2\] 在你提供的代码中,可以看到添加了一个自定义拦截器LizzMybatisIntercepts,并且还添加了MybatisPlus自带的分页拦截器PaginationInnerInterceptor。这样就可以实现分页查询的功能。\[1\] 总结来说,MybatisPlus拦截器是用来增强MyBatis功能的工具,可以通过自定义拦截器实现对SQL语句的拦截和修改,以满足特定的需求,比如实现自定义的SQL拦截、分页查询拦截、打印SQL日志等功能。\[2\] #### 引用[.reference_title] - *1* *3* [MyBatis-plus拦截器](https://blog.csdn.net/winerpro/article/details/126053599)[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^insert_down1,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* [Mybatis Plus 配置以及Mybatis Plus分页查询的拦截器](https://blog.csdn.net/weixin_72637752/article/details/130547235)[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^insert_down1,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

你家宝宝

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值