文章目录
- 一、前言
- 二、拦截器简介
- 三、代码目录结构简介
- 四、核心代码讲解
- 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"
}