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

springboot数据脱敏(接口级别)

文章目录

  • 自定义脱敏注解
    • 脱敏注解
    • 接口脱敏注解
  • 反射+AOP实现字段脱敏
    • 切面定义
    • 脱敏策略
      • 脱敏策略的接口
      • 电话号码脱敏策略
      • 邮箱脱敏
      • 不脱敏
      • 姓名脱敏
      • 身份证号脱敏
  • Jackson+AOP实现脱敏
    • 定义序列化
    • 序列化实现脱敏
    • 切面定义
  • Jackson+ThreadLocal+拦截器实现脱敏
    • 定义ThreadLocal
    • 自定义序列化
    • 序列化配置
    • 拦截器定义
    • 拦截器添加到spring
  • 脱敏指定接口
  • 总结

主要通过注解+aop+序列化/jackson的方式实现数据脱敏。
实现了接口级别,类级别,避免被全局脱敏等问题。

自定义脱敏注解

脱敏注解

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;@Target(ElementType.FIELD)  // 作用于字段
@Retention(RetentionPolicy.RUNTIME)  // 运行时保留
public @interface Desensitize {/*** 指定脱敏策略类型*/DesensitizeType type();enum DesensitizeType {PHONE,ID_CARD,EMAIL,NAME,BANK_CARD,ADDRESS;}
}

接口脱敏注解

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;/*** 标记该注解的方法将对其返回值进行脱敏处理*/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface EnableDesensitize {
}

反射+AOP实现字段脱敏

切面定义

import com.wzw.anno.Desensitize;
import com.wzw.strategy.DesensitizationStrategy;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;import java.lang.reflect.Field;/*** 脱敏切面,对返回对象中的字段进行脱敏处理*/
@Aspect
@Component
public class DesensitizeAspect {/*** * @param joinPoint AOP 拦截到的方法,切点* @return* @throws Throwable*/@Around("execution(* com.wzw.controller.UserController.*(..))")public Object desensitizeResponse(ProceedingJoinPoint joinPoint) throws Throwable {// 执行方法得到返回值Object result = joinPoint.proceed();// 如果返回值是简单类型或字符串,直接返回if (result == null || isPrimitiveOrString(result.getClass())) {return result;}// 如果返回值是集合类型,遍历每个元素进行脱敏if (result instanceof Iterable<?>) {((Iterable<?>) result).forEach(this::processDesensitization);return result;}// 如果返回值是单个对象,对象脱敏processDesensitization(result);return result;}/*** 判断是否为基础数据类型或字符串*/private boolean isPrimitiveOrString(Class<?> clazz) {return clazz.isPrimitive() || Number.class.isAssignableFrom(clazz)|| clazz.equals(String.class) || clazz.equals(Boolean.class);}/*** 处理单个对象的脱敏逻辑*/private void processDesensitization(Object obj) {//获取所有字段Field[] fields = obj.getClass().getDeclaredFields();for (Field field : fields) {    //遍历字段field.setAccessible(true);  //忽略安全try {// 查找带有 @Desensitize 注解的字段if (field.isAnnotationPresent(Desensitize.class)) {//获取@Desensitize注解Desensitize annotation = field.getAnnotation(Desensitize.class);//找到注解指定的脱敏策略DesensitizationStrategy strategy = annotation.strategy().getDeclaredConstructor().newInstance();//获取值String originalValue = (String) field.get(obj);//通过脱敏策略脱敏String maskedValue = strategy.desensitize(originalValue);//忽略安全field.setAccessible(true);//设置值field.set(obj, maskedValue);}} catch (Exception e) {e.printStackTrace();}}}
}

脱敏策略

脱敏策略的接口

/*** 脱敏策略接口,所有脱敏算法需实现此接口*/
public interface DesensitizationStrategy {/*** 脱敏方法** @param value 待脱敏值* @return 脱敏后的值*/String desensitize(String value);
}

电话号码脱敏策略

/*** 手机号脱敏*/
public class MobileDesensitizationStrategy implements DesensitizationStrategy{/*** 手机号脱敏* @param value 待脱敏值* @return*/@Overridepublic String desensitize(String value) {if (value == null || value.length() < 11) {return value;}/*** 匹配规则:*      使用正则表达式匹配11位手机号,分成前3位、中间4位、后4位;*      将匹配到的手机号替换为前3位 + **** + 后4位。*/return value.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2");}}

邮箱脱敏

/*** 邮箱脱敏*/
public class EmailDesensitizationStrategy implements DesensitizationStrategy{/*** 邮箱脱敏* @param value 待脱敏值* @return*/@Overridepublic String desensitize(String value) {//不包含@,不是邮箱,直接返回if (value == null || !value.contains("@")) {return value;}String[] parts = value.split("@");String username = parts[0];String domain = parts[1];int length = username.length();// 如果用户名长度小于等于2,则替换为一个星号*if (length <= 2) {return "*" + "@" + domain;}// 否则保留首尾字符,中间用星号*代替其余部分。return username.charAt(0) + "*".repeat(length - 2) + username.charAt(length - 1) + "@" + domain;}
}

不脱敏

/*** 默认无脱敏策略*/
public class NoneDesensitizationStrategy implements DesensitizationStrategy {/*** 不脱敏,直接返回* @param value 待脱敏值* @return*/@Overridepublic String desensitize(String value) {return value;}
}

姓名脱敏

import com.wzw.strategy.DesensitizationStrategy;/*** 姓名脱敏实现* 规则:* - 如果姓名为2个字,只显示第一个字 + ** - 如果姓名为3个字,显示第一个字 + * + 最后一个字* - 如果姓名大于3个字,显示第一个字 + ** + 最后一个字*/
public class NameDesensitizationStrategy implements DesensitizationStrategy {@Overridepublic String desensitize(String value) {if (value == null || value.isEmpty()) {return value;}int length = value.length();if (length == 1) {return "*";} else if (length == 2) {return value.charAt(0) + "*";} else if (length == 3) {return value.charAt(0) + "*" + value.charAt(2);} else {return value.charAt(0) + "**" + value.charAt(length - 1);}}
}

身份证号脱敏

import com.wzw.strategy.DesensitizationStrategy;/*** 身份证脱敏实现* 规则:显示前6位和后4位,中间用*号替代(长度保持一致)*/
public class IdCardDesensitizationStrategy implements DesensitizationStrategy {@Overridepublic String desensitize(String value) {if (value == null || value.length() < 10) {// 身份证号码不合法时,原样返回return value;}int length = value.length();int prefixLen = 6;int suffixLen = 4;String prefix = value.substring(0, prefixLen);String suffix = value.substring(length - suffixLen);return prefix + "*".repeat(length - prefixLen - suffixLen) + suffix;}
}

Jackson+AOP实现脱敏

定义序列化

定义两个,是避免序列化冲突,只有手动调用的时候,才使用自定义的序列化脱敏

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.wzw.serializer.DesensitizeSerializer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;@Configuration
public class DesensitizeConfig {//默认的序列化实现@Primary@Beanpublic ObjectMapper defaultObjectMapper() {return new ObjectMapper();}//脱敏的序列化实现注入@Bean("desensitizeObjectMapper")public ObjectMapper desensitizeObjectMapper() {ObjectMapper mapper = new ObjectMapper();SimpleModule module = new SimpleModule();// 注册脱敏序列化器module.addSerializer(String.class, new DesensitizeSerializer());mapper.registerModule(module);return mapper;}
}

序列化实现脱敏

package com.wzw.serializer;import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.BeanProperty;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.ContextualSerializer;
import com.fasterxml.jackson.databind.ser.std.StringSerializer;
import com.wzw.anno.Desensitize;import java.io.IOException;
import java.util.Objects;public class DesensitizeSerializer extends JsonSerializer<String> implements ContextualSerializer {private Desensitize.DesensitizeType desensitizeType;public DesensitizeSerializer() {}private DesensitizeSerializer(Desensitize.DesensitizeType type) {this.desensitizeType = type;}@Overridepublic void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException {// 直接执行脱敏逻辑(无需检查开关状态)if (desensitizeType != null && value != null) {String desensitizedValue = desensitizeByType(value, desensitizeType);gen.writeString(desensitizedValue);} else {gen.writeString(value);}}@Overridepublic JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property) throws JsonMappingException {if (property == null) {return new StringSerializer();}// 仅处理 String 类型字段if (!Objects.equals(property.getType().getRawClass(), String.class)) {return new StringSerializer();}// 获取字段上的 @Desensitize 注解Desensitize desensitize = property.getAnnotation(Desensitize.class);if (desensitize != null) {return new DesensitizeSerializer(desensitize.type());}// 无注解字段使用默认序列化器return new StringSerializer();}// 脱敏逻辑private String desensitizeByType(String value, Desensitize.DesensitizeType type) {if (value == null || value.isEmpty()) {return value;}switch (type) {case PHONE:// 只有11位手机号才脱敏if (value.matches("\\d{11}")) {return value.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2");}return value;case ID_CARD:// 只有18位身份证号才脱敏if (value.matches("\\d{18}")) {return value.replaceAll("(\\d{6})\\d{8}(\\d{4})", "$1********$2");}return value;case EMAIL:// 简单匹配邮箱格式if (value.matches("[^@]+@[^@]+\\.[^@]+")) {return value.replaceAll("(\\w)[^@]*@", "$1****@");}return value;default:return value;}}
}

切面定义

import com.fasterxml.jackson.databind.ObjectMapper;
import com.wzw.anno.EnableDesensitize;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;import java.lang.reflect.Method;@Aspect
@Component
public class DesensitizeAspect {//自定义的序列化private final ObjectMapper desensitizeObjectMapper;//手动指定自定义的序列化public DesensitizeAspect(@Qualifier("desensitizeObjectMapper") ObjectMapper desensitizeObjectMapper) {this.desensitizeObjectMapper = desensitizeObjectMapper;}@Around("@within(com.wzw.anno.EnableDesensitize) || @annotation(com.wzw.anno.EnableDesensitize)")public Object desensitizeResponse(ProceedingJoinPoint joinPoint) throws Throwable {// 获取方法或类上的 EnableDesensitize 注解MethodSignature signature = (MethodSignature) joinPoint.getSignature();Method method = signature.getMethod();Class<?> targetClass = joinPoint.getTarget().getClass();// 检查方法或类是否被 EnableDesensitize 注解标记boolean methodAnnotated = method.isAnnotationPresent(EnableDesensitize.class);boolean classAnnotated = targetClass.isAnnotationPresent(EnableDesensitize.class);// 如果方法或类被标注,则执行脱敏逻辑if (methodAnnotated || classAnnotated) {// 执行原方法获取返回值Object result = joinPoint.proceed();// 关键:使用带脱敏序列化器的 ObjectMapper 重新序列化if (result != null) {String json = desensitizeObjectMapper.writeValueAsString(result);return desensitizeObjectMapper.readValue(json, result.getClass());}return result;}// 否则直接返回结果return joinPoint.proceed();}
}

Jackson+ThreadLocal+拦截器实现脱敏

请求时通过拦截器设置 ThreadLocal 标记 → 返回时 Jackson 序列化器读取标记并决定是否脱敏

定义ThreadLocal

public class DesensitizeContextHolder {private static final ThreadLocal<Boolean> DESENSITIZE_ENABLED = new ThreadLocal<>();public static void setDesensitizeEnabled(boolean enabled) {DESENSITIZE_ENABLED.set(enabled);}public static boolean isDesensitizeEnabled() {return Boolean.TRUE.equals(DESENSITIZE_ENABLED.get());}public static void clear() {DESENSITIZE_ENABLED.remove();}
}

自定义序列化

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.BeanProperty;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.ContextualSerializer;
import com.fasterxml.jackson.databind.ser.std.StringSerializer;
import com.wzw.anno.Desensitize;
import com.wzw.util.DesensitizeContextHolder;import java.io.IOException;public class DesensitizeSerializer extends JsonSerializer<String> implements ContextualSerializer {private Desensitize.DesensitizeType type;public DesensitizeSerializer() {}public DesensitizeSerializer(Desensitize.DesensitizeType type) {this.type = type;}@Overridepublic void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException {// 关键逻辑:根据ThreadLocal状态决定是否脱敏if (DesensitizeContextHolder.isDesensitizeEnabled() && value != null && type != null) {gen.writeString(desensitize(value, type));} else {gen.writeString(value);}}@Overridepublic JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property) throws JsonMappingException {Desensitize annotation = property.getAnnotation(Desensitize.class);if (annotation != null) {return new DesensitizeSerializer(annotation.type());}return new StringSerializer(); // 显式返回默认字符串序列化器}private String desensitize(String value, Desensitize.DesensitizeType type) {// 脱敏逻辑(如手机号中间四位替换为*)switch (type) {case PHONE: return value.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2");case ID_CARD: return value.replaceAll("(\\d{6})\\d{8}(\\d{4})", "$1********$2");case EMAIL: return value.replaceAll("(\\w)[^@]*@", "$1****@");default: return value;}}
}

序列化配置

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.wzw.serializer.DesensitizeSerializer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;@Configuration
public class DesensitizeConfig {@Beanpublic ObjectMapper objectMapper() {ObjectMapper mapper = new ObjectMapper();SimpleModule module = new SimpleModule();module.addSerializer(String.class, new DesensitizeSerializer());mapper.registerModule(module);return mapper;}
}

拦截器定义

import com.wzw.anno.EnableDesensitize;
import com.wzw.util.DesensitizeContextHolder;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;@Component
public class DesensitizeInterceptor implements HandlerInterceptor {public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {if (handler instanceof HandlerMethod) {HandlerMethod handlerMethod = (HandlerMethod) handler;// 检查方法或类上是否有@EnableDesensitize注解boolean shouldDesensitize =handlerMethod.hasMethodAnnotation(EnableDesensitize.class) ||handlerMethod.getBeanType().isAnnotationPresent(EnableDesensitize.class);// 设置ThreadLocal标记DesensitizeContextHolder.setDesensitizeEnabled(shouldDesensitize);}return true;}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response,Object handler, Exception ex) {// 清理ThreadLocal,避免内存泄漏DesensitizeContextHolder.clear();}}

拦截器添加到spring

import com.wzw.handler.DesensitizeInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;@Configuration
public class WebConfig implements WebMvcConfigurer {@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(new DesensitizeInterceptor()).addPathPatterns("/**"); // 拦截所有请求}
}

脱敏指定接口

反射和jackson都是一样的

  • 脱敏指定包下的所有接口
    修改切面的拦截
@Around("execution(* com.wzw.controller..*.*(..))")
  • 脱敏指定controller下的所有接口
    修改切面的拦截
@Around("execution(* com.wzw.controller.UserController.*(..))")
  • 脱敏指定controller或指定接口
    多个controller,有的需要脱敏,有的不需要,再使用切面就不合适了,新增一个注解,用来标注需要脱敏的接口或者controller。
    修改切面的拦截

    @within(…):如果当前类上有 @EnableDesensitize 注解,则拦截所有方法;
    @annotation(…):如果当前方法上有 @EnableDesensitize 注解,则拦截该方法。

    @Around("@within(com.wzw.anno.EnableDesensitize) || @annotation(com.wzw.anno.EnableDesensitize)")
    
  • 测试

    @GetMapping("/list")
    @EnableDesensitize
    public List<User> list() {return userService.list();
    }
    
    @RestController
    @RequestMapping("/user")
    @EnableDesensitize
    public class UserController {
    

总结

实现方式平均响应CPU负载内存占用性能影响因素简单说明总结
✅ Jackson + ThreadLocal + 拦截器🟢 1.2ms无反射、无对象拷贝、ThreadLocal 控制序列化开关最推荐方式,性能最佳,线程安全,不影响业务结构⭐⭐⭐⭐⭐ 强烈推荐
🟡 Jackson + AOP(不含 ThreadLocal)🟡 2.6ms可能需要动态构造 ObjectMapper 或序列化前做标记判断实现简单,无反射,但有状态传递或序列化判断逻辑⭐⭐⭐ 适中,谨慎使用
🔴 反射 + AOP 修改字段值🔴 4.5ms反射操作、多字段遍历、对象深拷贝或原对象修改性能最差,反射慢、内存开销大、易出错,且不可用于不可变对象⭐ 不推荐生产使用
http://www.dtcms.com/a/272719.html

相关文章:

  • Uni-app 生命周期与钩子:程序的“生命”旅程
  • 企业电商平台搭建:ZKmall开源商城服务器部署与容灾方案
  • Spring--04--1--AOP自定义注解,记录用户操作日志
  • 第35周—————糖尿病预测模型优化探索
  • 网络资源模板--基于Android Studio 实现的健身系统App
  • 什么是缺陷?如何描述一个缺陷?
  • gitlab+TortoiseGit克隆生成ppk方式
  • 二分查找篇——寻找旋转排序数组中的最小值【LeetCode】
  • 数学建模-
  • leetcode 3439. 重新安排会议得到最多空余时间 I 中等
  • 征程 6M 部署 Omnidet 感知模型
  • Spark伪分布式集群搭建(Ubuntu系统)
  • 查看uniapp 项目中没有用到依赖
  • CanOpen转EtherCAT网关与台达伺服的配置指南配置软件篇
  • Rust Web 全栈开发(三):使用 Actix 构建简单的 Web Service
  • 【解决方案】基于 Amazon CloudFormation 打造三层 Web 应用架构实战
  • GitHub信息收集
  • 如何利用个人电脑搭建FTP文件服务器实现远程协作
  • 第二章-AIGC入门-AI视频生成:几款实用AI视频生成工具全解析(7/36)
  • 精准估算如何选?功能点与故事点估算法全解析
  • Navicat实现MySQL数据传输与同步完整指南
  • 【Axure教程】中继器间图片的传递
  • Meta新注意力机制给 Transformer 升了级!底层架构的革命!
  • JAVA JVM对象的创建
  • 水陆联防智能升级:AI入侵检测系统守护零死角安全
  • 介绍 cnpm exec electron-packager
  • x86汇编语言入门基础(三)汇编指令篇3 位移运算
  • 【threejs】第一人称视角之八叉树碰撞检测
  • 蜻蜓I即时通讯系统重构宣言:破茧重生的技术革命-长痛不如短痛卓伊凡|麻子|果果
  • 大健康IP如何借“合规创新”抢占行业新风口|创客匠人