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

news2024/10/5 17:20:54

文章目录

  • 一、前言
  • 二、拦截器简介
  • 三、代码目录结构简介
  • 四、核心代码讲解
    • 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/933163.html

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

相关文章

7年经验之谈 —— 如何实现高效的Web自动化测试?

随着互联网的快速发展&#xff0c;Web应用程序的重要性也日益凸显。为了保证Web应用程序的质量和稳定性&#xff0c;Web自动化测试成为必不可少的一环。然而&#xff0c;如何实现高效的Web自动化测试却是一个值得探讨的课题。 首先&#xff0c;选择合适的测试工具是关键。市面…

低通滤波器和高通滤波器

应用于图像低通滤波器和高通滤波器的实现 需要用到傅里叶变换 #include <opencv2/opencv.hpp> #include <Eigen> #include <iostream> #include <vector> #include <cmath> #include <complex>#define M_PI 3.14159265358979323846…

五、多表查询-3.4连接查询-联合查询union

一、概述 二、演示 【例】将薪资低于5000的员工&#xff0c;和 年龄大于50岁的 员工全部查询出来 1、查询薪资低于5000的员工 2、查询年龄大于50岁的员工 3、将薪资低于5000的员工&#xff0c;和 年龄大于50岁的 员工全部查询出来&#xff08;把上面两部分的结果集直接合并起…

最新敏感信息和目录收集技术

敏感信息和目录收集 目标域名可能存在较多的敏感目录和文件&#xff0c;这些敏感信息很可能存在目录穿越漏洞、文件上传漏洞&#xff0c;攻击者能通过这些漏洞直接下载网站源码。搜集这些信息对之后的渗透环节有帮助。通常&#xff0c;扫描检测方法有手动搜寻和自动工具查找两…

requestAnimationFrame(RAF)

1、RAF介绍 requestAnimateFrame&#xff08;RAF&#xff09;动画帧&#xff0c;是一个做动画的API。 如果想要一个动画流畅&#xff0c;就需要以60帧/s来更新视图&#xff0c;那么一次视图的更新就是16.67ms。 想要达到上述目标&#xff0c;可以通过setTimeout定时器来手动控…

JSON文件教程之【jsoncpp源码编译】

目录 1 数据下载(jsoncpp源码)2 文件编译内容: JSON文件的读取与保存可以使用jsoncpp库来实现,这里介绍该库的下载及编译方法。 1 数据下载(jsoncpp源码) 数据下载:Github地址 图1 github源码示意图 2 文件编译 2.1 点击Download ZIP,下载源码。 图2 压缩包数据 2.2 将压…

在 macOS 中安装 TensorFlow 1g

tensorflow 需要多大空间 pip install tensorflow pip install tensorflow Looking in indexes: https://pypi.douban.com/simple/ Collecting tensorflowDownloading https://pypi.doubanio.com/packages/1a/c1/9c14df0625836af8ba6628585c6d3c3bf8f1e1101cafa2435eb28a7764…

2022年06月 C/C++(四级)真题解析#中国电子学会#全国青少年软件编程等级考试

第1题&#xff1a;公共子序列 我们称序列Z < z1, z2, …, zk >是序列X < x1, x2, …, xm >的子序列当且仅当存在 严格上升 的序列< i1, i2, …, ik >&#xff0c;使得对j 1, 2, … ,k, 有xij zj。比如Z < a, b, f, c > 是X < a, b, c, f, b, …

软考:中级软件设计师:关系代数:中级软件设计师:关系代数,规范化理论函数依赖,它的价值和用途,键,范式,模式分解

软考&#xff1a;中级软件设计师:关系代数 提示&#xff1a;系列被面试官问的问题&#xff0c;我自己当时不会&#xff0c;所以下来自己复盘一下&#xff0c;认真学习和总结&#xff0c;以应对未来更多的可能性 关于互联网大厂的笔试面试&#xff0c;都是需要细心准备的 &…

一篇文章带你彻底了解Java常用的设计模式

文章目录 前言1. 工厂模式使用示例代码优势 2. 单例模式说明使用示例代码优势 3. 原型模式使用示例代码优势 4. 适配器模式使用示例代码优势 5. 观察者模式使用示例代码优势 6. 策略模式使用示例代码优势 7. 装饰者模式使用示例代码优势 8. 模板方法模式使用示例代码优势 总结 …

python-数据可视化-下载数据-CSV文件格式

数据以两种常见格式存储&#xff1a;CSV和JSON CSV文件格式 comma-separated values import csv filename sitka_weather_07-2018_simple.csv with open(filename) as f:reader csv.reader(f)header_row next(reader)print(header_row) # [USW00025333, SITKA AIRPORT, A…

YOLO目标检测——皮肤检测数据集下载分享

数据集点击下载&#xff1a;YOLO皮肤检测数据集Face-Dataset.rar

springboot源码方法

利用LinkedHashSet移除List重复的数据protected final <T> List<T> removeDuplicates(List<T> list) {return new ArrayList<>(new LinkedHashSet<>(list));} SpringFactoriesLoader#loadFactoryNames 加载配置文件

常见的移动端布局

流式布局&#xff08;百分比布局&#xff09; 使用百分比、相对单位&#xff08;如 em、rem&#xff09;等来设置元素的宽度&#xff0c;使页面元素根据视口大小的变化进行调整。这种方法可以实现基本的自适应效果&#xff0c;但可能在不同设备上显示不一致。 <!DOCTYPE ht…

ctfshow-web14

0x00 前言 CTF 加解密合集CTF Web合集 0x01 题目 0x02 Write Up 首先看到这个&#xff0c;swith&#xff0c;那么直接输入4&#xff0c;则会打印$url的值 然后访问一下 查看一下&#xff0c;发现完整的请求是http://c7ff9ed6-dccd-4d01-907a-f1c61c016c15.challenge.ctf.sho…

python网络爬虫指南二:多线程网络爬虫、动态内容爬取(待续)

文章目录 一、多线程网络爬虫1.1 线程的基础内容、GIL1.2 创建线程的两种方式1.3 threading.Thread类1.4 线程常用方法和锁机制1.5 生产者-消费者模式1.5.1 生产者-消费者模式简介1.5.2 Condition 类协调线程 1.6 线程中的安全队列1.6 多线程爬取王者荣耀壁纸1.6.1 网页分析1.6…

2023-8-26 模拟散列表

题目链接&#xff1a;模拟散列表 拉链法 #include <iostream> #include <cstring>using namespace std;const int N 100010;int h[N], e[N], ne[N], idx;void insert(int x) {int k (x % N N) % N;e[idx] x;ne[idx] h[k];h[k] idx;idx ; }bool query(int …

智慧县城,乍暖还寒风起时

提起中国的区域数字化&#xff0c;我们大概率会关注两个维度。一个是北上广深为代表的超大城市&#xff0c;这里的智慧城市落地是风向标、排头兵&#xff1b;另一个是乡村的数字化、智能化&#xff0c;作为区域智能升级的最小单位&#xff0c;乡村的典型性更强&#xff0c;也符…

nlp大模型课程笔记

自然语言处理基础和应用 &#x1f446;说明之前的大模型其实有很多都是基于迁移学习的方法。 attention机制的总结&#xff0c;解决了信息瓶颈的问题。 处理词组时BPE的过程 &#x1f446;pos表示的是token所在的位置 &#x1f446;技巧是layer normalization。

数据结构(Java实现)-二叉树(上)

树型结构 树是一种非线性的数据结构&#xff0c;它是由n&#xff08;n>0&#xff09;个有限结点组成一个具有层次关系的集合。把它叫做树是因为它看起来像一棵倒挂的树&#xff0c;也就是说它是根朝上&#xff0c;而叶朝下的。 有一个特殊的结点&#xff0c;称为根结点&…