当前位置: 首页 > news >正文

SpringBoot中实现接口查询数据动态脱敏

有两个页面,调用同一个查询接口,一个页面要数据脱敏,另一个页面不脱敏。
目前情况是给字段加自定义注解,序列化的时候有注解的都脱敏了。如何不copy一份代码实现动态脱敏呢?

解决方案:ThreadLocal + Filter

序列化依赖

 <dependency><groupId>com.alibaba.fastjson2</groupId><artifactId>fastjson2</artifactId><version>2.0.58</version></dependency>

代码

@Data
@Builder
public class DesensitizeContext {private boolean skipSensitive;
}
public class DesensitizeManager {private static final ThreadLocal<DesensitizeContext> THREAD_LOCAL = new ThreadLocal<>();public static void set(DesensitizeContext skip) {THREAD_LOCAL.set(skip);}public static DesensitizeContext get() {return THREAD_LOCAL.get();}public static void clear() {THREAD_LOCAL.remove();}
}
自定义脱敏注解
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@JacksonAnnotationsInside
@JsonSerialize(using = SensitiveJsonSerializer.class)
public @interface Sensitive {SensitiveType type();
}
实体类
@Data
@NoArgsConstructor
@AllArgsConstructor
public class UserDTO {private String name;@Sensitive(type = SensitiveType.ID_CARD)private String idCard;@Sensitive(type = SensitiveType.PHONE)private String phone;
}
脱敏规则枚举
public enum SensitiveType {PHONE {@Overridepublic String sensitize(String value) {if (value == null || value.length() < 8) return value;return value.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2");}},ID_CARD {@Overridepublic String sensitize(String value) {if (value == null || value.length() < 15) return value;// 保留前6位和后4位,中间用 * 代替int length = value.length();String prefix = value.substring(0, 6);String suffix = value.substring(length - 4);return prefix + "********" + suffix;}},EMAIL {@Overridepublic String sensitize(String value) {if (value == null || !value.contains("@")) return value;int atIndex = value.indexOf('@');String username = value.substring(0, Math.max(1, atIndex));return username + "****" + value.substring(atIndex);}};/*** 抽象方法:每个枚举值必须实现** @param value 传入要脱敏的值* @return -*/public abstract String sensitize(String value);
}
}
过滤器
public class DesensitizeFilter implements Filter {private static final String BYPASS_DESENSITIZE_HEADER = "X-No-Desensitize";@Overridepublic void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)throws IOException, ServletException {/*从参数判断是否跳过脱敏*/// String desensitize = request.getParameter("desensitize");// boolean skipSensitive = "true".equalsIgnoreCase(desensitize);/*从 header判断是否跳过脱敏*/HttpServletRequest httpRequest = (HttpServletRequest) request;String headerValue = httpRequest.getHeader(BYPASS_DESENSITIZE_HEADER);boolean skipDesensitize = "true".equalsIgnoreCase(headerValue);try {DesensitizeManager.set(DesensitizeContext.builder().skipSensitive(skipDesensitize).build());// 放行chain.doFilter(request, response);} finally {// 必须清理!DesensitizeManager.clear();}}
}
注册过滤器
@Configuration
public class DesensitizeFilterConfiguration {@Beanpublic FilterRegistrationBean<DesensitizeFilter> desensitizeFilter() {FilterRegistrationBean<DesensitizeFilter> registrationBean = new FilterRegistrationBean<>();registrationBean.setFilter(new DesensitizeFilter());// 需要拦截的请求registrationBean.addUrlPatterns("/user/users");registrationBean.setOrder(1);return registrationBean;}
}
字段脱敏序列化器
public class SensitiveJsonSerializer extends JsonSerializer<String> implements ContextualSerializer {private SensitiveType sensitiveType;/*** 序列化时执行,调用 N 次(N = 对象数量 × 字段数量)** @param value              值* @param gen                生成器* @param serializerProvider -* @throws IOException -*/@Overridepublic void serialize(String value, JsonGenerator gen, SerializerProvider serializerProvider) throws IOException {String masked = value;DesensitizeContext context = DesensitizeManager.get();// 未被拦截的请求、拦截的请求并跳过脱敏的请求if (context == null || !context.isSkipSensitive()) {// 执行脱敏逻辑masked = sensitiveType.sensitize(value);}// 明文gen.writeString(masked);}/*** 此方法相当于给需要脱敏的字段打脱敏类型标记(使用哪种脱敏规则)* * 第一次请求 / 应用启动后首次序列化,只调用一次 per field** @param serializerProvider -* @param property           属性* @return -*/@Overridepublic JsonSerializer<?> createContextual(SerializerProvider serializerProvider, BeanProperty property) {Sensitive sensitive = property.getAnnotation(Sensitive.class);// 有脱敏注解并且为字符串if (sensitive != null && String.class.equals(property.getType().getRawClass())) {SensitiveJsonSerializer serializer = new SensitiveJsonSerializer();serializer.sensitiveType = sensitive.type();return serializer;}return this;}
}

列化器中serialize、createContextual这两个方法的执行流程:
假设有如下3条数据(两个字段):
[
{ “phone”: “1385678", “idCard”: “110101********XXXX” },
{ “phone”: "139
4321”, “idCard”: “110101YYYY" },
{ “phone”: “150****8888”, “idCard”: "110101
ZZZZ” }
]
┌─────────────────────────────┐
│ createContextual(phone) │ → 返回一个 type=PHONE 的 SensitiveJsonSerializer 实例
└─────────────────────────────┘
┌─────────────────────────────┐
│ createContextual(idCard) │ → 返回一个 type=ID_CARD 的 SensitiveJsonSerializer 实例
└─────────────────────────────┘
┌─────────────────────────────────────────┐
│ serialize(“13812345678”, gen, provider) │ → 使用 PHONE 实例脱敏
└─────────────────────────────────────────┘
┌─────────────────────────────────────────┐
│ serialize(“110101…XXXX”,gen, provider)│ → 使用 ID_CARD 实例脱敏
└─────────────────────────────────────────┘
┌─────────────────────────────────────────┐
│ serialize(“13987654321”, gen, provider) │ → 再次使用 PHONE 实例
└─────────────────────────────────────────┘
集合中是同一个对象类型,有几个字段就调用几次createContextual
以上流程共执行2次createContextual, 6次serialize(对象数量 × 字段数量)

controller测试
@RestController
public class UserController {@GetMapping("/user/users")public List<UserDTO> getUsers() {return Arrays.asList(new UserDTO("张三", "11010119900307XXXX", "13812345678"),new UserDTO("李四", "11010119910408XXXX", "13987654321"));}@GetMapping("/user1/users1")public List<UserDTO> getUsers1() {return Arrays.asList(new UserDTO("张三", "11010119900307XXXX", "13812345678"),new UserDTO("李四", "11010119910408XXXX", "13987654321"));}
}
测试结果

根据请求头进行动态脱敏

/user1/users1:
加不加X-No-Desensitize请求头都是脱敏的数据在这里插入图片描述
在这里插入图片描述
过滤器拦截的/user/users:
不加X-No-Desensitize请求头
在这里插入图片描述
X-No-Desensitize请求头
在这里插入图片描述

建议

虽然这种方式可以实现不更改接口即可实现动态脱敏,但是通过外部传参来决定是否脱敏具有一定的安全问题。应该在controller复制一份接口,把映射的url改一下,过滤器针对这个url进行拦截,前端换接口。

http://www.dtcms.com/a/346677.html

相关文章:

  • 倍福下的EC-A10020-P2-24电机调试说明
  • NVIDIA Nsight Systems性能分析工具
  • ISO 22341 及ISO 22341-2:2025安全与韧性——防护安全——通过环境设计预防犯罪(CPTED)
  • 武大智能与集成导航小组!i2Nav-Robot:用于的室内外机器人导航与建图的大规模多传感器融合数据集
  • 【字母异位分组】
  • 火车头使用Post方法采集Ajax页面教程
  • 量子计算驱动的Python医疗诊断编程前沿展望(中)
  • kubernetes-dashboard使用http不登录
  • 快速了解命令行界面(CLI)的行编辑模式
  • PyTorch框架之图像识别模型与训练策略
  • 一键部署开源 Coze Studio
  • 蓝牙链路层状态机精解:从待机到连接的状态跃迁与功耗控制
  • 全面解析了Java微服务架构的设计模式
  • 新疆地州市1米分辨率土地覆盖图
  • GOLANG 接口
  • 可自定义的BMS管理系统
  • 论文阅读:Inner Monologue: Embodied Reasoning through Planning with Language Models
  • SpringBoot 自动配置深度解析:从注解原理到自定义启动器​
  • 【JVM】JVM的内存结构是怎样的?
  • 调味品生产过程优化中Ethernet/IP转ProfiNet协议下施耐德 PLC 与欧姆龙 PLC 的关键通信协同案例
  • 字符串的大小写字母转换
  • linux中文本文件操作之grep命令
  • Linux-常用文件IO函数
  • Java:类及方法常见规约
  • UE5多人MOBA+GAS 53、测试专属服务器打包和连接,以及配置EOS
  • linux编程----网络通信(TCP)
  • 利用Prometheus监控服务器相关数据
  • SpringBoot自动配置原理解析
  • 本地文件夹即时变身 Web 服务器(文件服务器)
  • Linux问答题:归档和传输文件