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

news2024/9/25 1:26:00

文章目录

  • 一、前言
  • 二、拦截器简介
  • 三、代码目录结构简介
  • 四、核心代码讲解
    • 4.1 application.yml文件
    • 4.2 自定义注解
      • 4.2.1 SensitiveEntity
      • 4.2.2 SensitiveData
      • 4.2.3 MaskedEntity
      • 4.2.4 MaskedField
      • 4.2.5 MaskedMethod
    • 4.3 Mybatis-Plus 拦截器数据自动加密
    • 4.4 Mybatis 打印完整sql的拦截器
      • 4.4.1 打印结果示例
    • 4.5 Mybatis-Plus 配置类
    • 4.6 CustomHandlerMethodReturnValueHandler
    • 4.7 敏感数据类型枚举
    • 4.8 Mvc拦截器配置
  • 五、测试结果
    • 5.1 数据库中
    • 5.2 查询结果脱敏

一、前言

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

  • 自定义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"
}

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

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

相关文章

浅析Linux SCSI子系统:设备管理

文章目录 概述设备管理数据结构scsi_host_template&#xff1a;SCSI主机适配器模板scsi_host&#xff1a;SCSI主机适配器主机适配器支持DIF scsi_target&#xff1a;SCSI目标节点scsi_device&#xff1a;SCSI设备 添加主机适配器构建sysfs目录 添加SCSI设备挂载Lun 相关参考 概…

FTHR-G0001 新手小白的第一块keil开发板

前言 作为从未接触过这类板子的新手&#xff0c;从申请起就十分忐忑&#xff0c;拿到板子的第一印象就是小而又特别&#xff0c;既可以整块板使用&#xff0c;也可以掰开用杜邦线连接的形式具备了灵活与小巧的特点&#xff0c;而核心板的把排针围成一圈的设计就足以让它在树莓…

HRS--人力资源系统(Springboot+vue)--打基础升级--(五)编辑当条记录

今天开发第一步&#xff1a;当前条记录&#xff0c;点击编辑&#xff0c;可以修改数据 1. 首先点击编辑&#xff0c;获取到了当前条的数据&#xff0c;弹出了一个小窗口 这个窗口是不是很熟悉&#xff0c;&#xff0c;没错。。这个窗口跟新增按钮弹出的窗口是同一个的 bug1&am…

使用 Next.js、Langchain 和 OpenAI 构建 AI 聊天机器人

在当今时代&#xff0c;将 AI 体验集成到您的 Web 应用程序中变得越来越重要。LangChain 与 Next.js 的强大功能相结合&#xff0c;提供了一种无缝的方式来将 AI 驱动的功能引入您的应用程序。 在本指南中&#xff0c;我们将学习如何使用Next.js&#xff0c;LangChain&#xf…

【MongoDB系列】1.MongoDB 6.x 在 Windows 和 Linux 下的安装教程(详细)

本文主要介绍 MongoDB 最新版本 6.x 在Windows 和 Linux 操作系统下的安装方式&#xff0c;和过去 4.x 、5.x 有些许不同之处&#xff0c;供大家参考。 Windows 安装 进入官网下载 Mongodb 安装包&#xff0c;点此跳转&#xff0c;网站会自动检测当前操作系统提供最新的版本&…

线性代数的学习和整理14: 线性方程组求解

目录 1 线性方程组 2 有解&#xff0c;无解 3 解的个数 1 线性方程组 A*xy 3根直线的交点&#xff0c;就是解 无解的情况 无解&#xff1a; 三线平行无解&#xff1a;三线不相交 有解 有唯一解&#xff1a;三线相交于一点有无数解&#xff1a;三条线重叠 2 齐次线性方程组…

vector quantized diffusion model for text-to-image synthesis

CVPR 2022论文分享会 - 基于VQ-Diffusion的文本到图像合成_哔哩哔哩_bilibiliCVPR 2022论文分享会 - 基于VQ-Diffusion的文本到图像合成, 视频播放量 1438、弹幕量 2、点赞数 38、投硬币枚数 12、收藏人数 40、转发人数 13, 视频作者 微软科技, 作者简介 大家好我是田老师&…

【电源专题】单节锂离子电池的保护的基本原理

为什么需要保护 锂离子电池在使用中最重要的是要确保它不会被过度充电和放电,这两种行为对它的伤害都是不可修复的,甚至可能还是危险的。因为它的内部材料结构被破坏了,就什么问题都可能表现出来。 因此使用中首先要做的就是要给它加上保护电路,确保过度充放电的行为不会…

系统报错msvcp120.dll丢失的解决方法,常见的三种解决方法

今天为大家讲述关于系统报错msvcp120.dll丢失的解决方法。在这个信息爆炸的时代&#xff0c;我们每个人都可能遇到各种各样的问题&#xff0c;而这些问题往往需要我们去探索、去解决。今天&#xff0c;我将带领大家走进这个神秘的世界&#xff0c;一起寻找解决msvcp120.dll丢失…

LONG-TAILED RECOGNITION 精读

BackGround 解决类别不平衡问题一般的思路&#xff1a; re-sample the data 重采样design specific loss functions that better facilitate learning with imbalanced data 设计针对不平衡数据的损失函数enhance recognition performance of the tail classes by transferri…

【NVIDIA CUDA】2023 CUDA夏令营编程模型(二)

博主未授权任何人或组织机构转载博主任何原创文章&#xff0c;感谢各位对原创的支持&#xff01; 博主链接 本人就职于国际知名终端厂商&#xff0c;负责modem芯片研发。 在5G早期负责终端数据业务层、核心网相关的开发工作&#xff0c;目前牵头6G算力网络技术标准研究。 博客…

为啥外卖小哥宁愿600一月租电动车,也不花2、3千买一辆送外卖!背后的原因......

大家好&#xff01;我是菜哥&#xff01; 又到周末了&#xff0c;我们聊点非技术的东西。最近知乎“为何那些穿梭于城市大街小巷的外卖小哥&#xff0c;宁愿每月掏出600块租一辆电动车&#xff0c;也不愿意掏出2、3千买一辆呢&#xff1f;” 冲上热榜&#xff01; 听起来有点“…

redis学习笔记 - 进阶部分

文章目录 redis单线程如何处理并发的客户端&#xff0c;以及为何单线程快&#xff1f;redis的发展历程&#xff1a;redis单线程和多线程的体现&#xff1a;redis3.x单线程时代但性能很快的主要原因&#xff1a;redis4.x开始引入多线程&#xff1a;redis6/redis7引入多线程IO&am…

UDP通信、本地套接字

#include <sys/types.h> #include <sys/socket > ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen);- 参数&#xff1a;- sockfd : 通信的fd- buf : 要发送的数据- len : 发送数据的长度…

leetcode 567. 字符串的排列(滑动窗口-java)

滑动窗口 字符串的排列滑动窗口代码演示进阶优化版 上期经典 字符串的排列 难度 -中等 leetcode567. 字符串的排列 给你两个字符串 s1 和 s2 &#xff0c;写一个函数来判断 s2 是否包含 s1 的排列。如果是&#xff0c;返回 true &#xff1b;否则&#xff0c;返回 false 。 换句…

curl --resolve参数的作用

之所以会有这样的操作&#xff0c;是因为域名一般对应的都是一个反向代理&#xff0c;直接请求域名&#xff0c;反向代理会将流量随机选一台机器打过去&#xff0c;而无法确保所有的机器都可用。所以直接用ip。 在 curl 命令中&#xff0c;--resolve 参数用于指定自定义的主机名…

【LeetCode-中等题】2. 两数相加

文章目录 题目方法一&#xff1a;借助一个进制位&#xff0c;以及更新尾结点方法一改进&#xff1a;相比较第一种&#xff0c;给head一个临时头节点&#xff08;开始节点&#xff09;&#xff0c;最后返回的时候返回head.next&#xff0c;这样可以省去第一次的判断 题目 方法一…

Java-继承和多态(上)

面向对象思想中提出了继承的概念&#xff0c;专门用来进行共性抽取&#xff0c;实现代码复用。 继承(inheritance)机制&#xff1a;继承主要解决的问题是&#xff1a;共性的抽取&#xff0c;实现代码复用。 继承的语法 在Java中如果要表示类之间的继承关系&#xff0c;需要借助…

2 hadoop的目录

1. 目录结构&#xff1a; 其中比较的重要的路径有&#xff1a; hdfs,mapred,yarn &#xff08;1&#xff09;bin目录&#xff1a;存放对Hadoop相关服务&#xff08;hdfs&#xff0c;yarn&#xff0c;mapred&#xff09;进行操作的脚本 &#xff08;2&#xff09;etc目录&#x…

docker之 Consul(注册与发现)

目录 一、什么是服务注册与发现&#xff1f; 二、什么是consul 三、consul 部署 3.1建立Consul服务 3.1.1查看集群状态 3.1.2通过 http api 获取集群信息 3.2registrator服务器 3.2.1安装 Gliderlabs/Registrator 3.2.2测试服务发现功能是否正常 3.2.3验证 http 和 ng…