🎉🎉《Spring Boot实战案例合集》目前已更新161个案例,我们将持续不断的更新。文末有电子书目录。
💪💪永久更新承诺
我们郑重承诺,所有订阅合集的粉丝都将享受永久免费的后续更新服务。
💌💌如何获取
订阅我们的合集《点我订阅》,并通过私信联系我们,我们将第一时间将电子书发送给您。
环境:SpringBoot3.4.2
1. 简介
在实际项目中,经常会遇到一些敏感字段信息,像用户的身份证号码、银行卡卡号、详细家庭住址、手机号码以及各类密码等。若这些敏感字段在接口传输或展示过程中未作脱敏处理,一旦遭遇数据泄露事件,将给用户带来严重损失,包括财产被盗刷、个人隐私曝光,甚至可能引发身份盗用等风险。
接口敏感字段脱敏,通过对关键信息进行部分隐藏、替换或加密等操作,在不影响数据正常使用和分析的前提下,最大程度降低敏感信息暴露风险。
本篇文章我们将采用5种技术方案来实现敏感字段的脱敏。
准备环境
public class User {private Long id ;private String name ;private Integer age ;private String phone ;private String idNo ;// getters, setters, constructors}// Controller接口("/users")public class UserController {public ResponseEntity<User> query() {return ResponseEntity.ok(new User(666L, "Pack_xg", 33,"13399065789", "320311198512185678")) ;}}
我们在编写一个工具类,统一都由该工具类处理敏感字段
public class MaskUtils {public static String maskString(String input) {int length = input.length();// 计算xxx、***、yyy的长度(总长度=xxx+***+yyy)int xxxLength = length / 3; // 前1/3int yyyLength = length / 3; // 后1/3// 中间剩余部分用*int starLength = length - xxxLength - yyyLength;// 确保至少各保留1个字符(避免xxx或yyy为0)xxxLength = Math.max(1, xxxLength);yyyLength = Math.max(1, yyyLength);starLength = Math.max(1, starLength);// 重新调整(防止因取整导致总和超出原长度)while (xxxLength + starLength + yyyLength > length) {if (xxxLength > 1) xxxLength--;else if (starLength > 1) starLength--;else yyyLength--;}String xxx = input.substring(0, xxxLength);String yyy = input.substring(length - yyyLength);String stars = repeat("*", starLength);return xxx + stars + yyy;}private static String repeat(String s, int count) {return new String(new char[count]).replace("\0", s);}}
2.1 自定义Json序列化
public class CommonMaskSerializer extends JsonSerializer<String> {public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException {if (value == null || value.length() < 6) {gen.writeString("***");return;}String masked = MaskUtils.maskString(value) ;gen.writeString(masked);}}
接下来,修改实体对象如下:
public class User {@JsonSerialize(using = CommonMaskSerializer.class)private String phone ;@JsonSerialize(using = CommonMaskSerializer.class)private String idNo ;}
输出结果
2.2 自定义Jackson模块
上一种方法是字段级别的,虽然精准,但若多个实体都需要相同的逻辑,则会显得冗余繁琐。为了避免将所有内容都与注解绑定,我们可以通过自定义模块方式注册自定义序列化器,这些序列化器可应用于整个类型。如下代码实现:
public class JacksonConfig {Jackson2ObjectMapperBuilderCustomizer maskingCustomizer() {return builder -> builder.modules(new MaskingModule());}public static final class MaskingModule extends SimpleModule {// 这里我们固定了处理那些字段名,你也可以将其放到配置文件中进行动态管理private static final Set<String> fields = Set.of("idNo", "phone") ;public MaskingModule() {setSerializerModifier(new BeanSerializerModifier() {public List<BeanPropertyWriter> changeProperties(SerializationConfig config, BeanDescription beanDesc,List<BeanPropertyWriter> props) {for (int i = 0; i < props.size(); i++) {BeanPropertyWriter w = props.get(i);if (w.getType().isTypeOrSubTypeOf(String.class) && fields.contains(w.getName())) {props.set(i, new MaskingWriter(w));}}return props;}});}}static final class MaskingWriter extends BeanPropertyWriter {private static final long serialVersionUID = 1L;MaskingWriter(BeanPropertyWriter base) {super(base);}public void serializeAsField(Object bean, JsonGenerator gen, com.fasterxml.jackson.databind.SerializerProvider prov)throws Exception {Object raw = get(bean);if (raw == null) {super.serializeAsField(bean, gen, prov);return;}String masked = MaskUtils.maskString(raw.toString());gen.writeStringField(getName(), masked);}}}
接下来,我们将实体类上的@JsonSerialize注解删除,一样能达到效果,并且此种方式不需要我们对实体类进行任何的修改,这能满足绝大多数的场景。
2.3 使用AOP技术
利用AOP技术,在Controller接口返回后,通过反射技术对返回值进行处理。
自定义注解,只有返回值的类型上使用了该注解才进行处理
public Mask {/**需要处理的字段*/String[] value() default {} ;}
定义切面
public class MaskAspect {private static final Map<Class<?>, List<PropMethod>> cache = new ConcurrentHashMap<>();public void applyMasking(Object response) throws Throwable {Object target = response;if (response instanceof ResponseEntity entity) {target = entity.getBody();}Class<?> clazz = target.getClass();Mask mask = clazz.getAnnotation(Mask.class);if (mask == null) {return;}List<String> fields = Arrays.asList(mask.value());if (fields.isEmpty()) {return ;}List<PropMethod> props = cache.get(clazz);if (props == null) {BeanInfo beanInfo = Introspector.getBeanInfo(clazz);for (PropertyDescriptor pd : beanInfo.getPropertyDescriptors()) {if (fields.contains(pd.getName())) {props = cache.computeIfAbsent(clazz, key -> new ArrayList<>()) ;props.add(new PropMethod(pd.getReadMethod(), pd.getWriteMethod())) ;}}}if (props != null) {for (PropMethod pm : props) {pm.setter.invoke(target, MaskUtils.maskString((String) pm.getter.invoke(target))) ;}}}static record PropMethod(Method getter, Method setter) {}}
修改User实体对象如下
({"phone", "idNo"})public class User {private Long id ;private String name ;private Integer age ;private String phone ;private String idNo ;}
调用Controller接口
2.4 使用ResponseBodyAdvice
我们可以利用Spring MVC的核心组件 ResponseBodyAdvice,该组件能够在控制器方法返回的响应体被序列化输出前,对返回数据进行拦截和处理。此种方式不是AOP技术,这里就完全使用反射技术实现脱敏。
在该方案中,我们还是利用Mask注解来进行标记处理,整体的处理与AOP基本相同。
public class MaskBodyAdvice implements ResponseBodyAdvice<Object> {private static final Map<Class<?>, List<PropMethod>> cache = new ConcurrentHashMap<>();public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {return true ;}public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request,ServerHttpResponse response) {if (body == null) {return null;}if (body instanceof ResponseEntity entity) {body = entity.getBody() ;}if (body instanceof Collection<?> coll) {for (Object o : coll) {try {maskField(o);} catch (Throwable e) {}}return body;}try {maskField(body);} catch (Throwable e) {}return body;}private void maskField(Object target) throws Throwable {// 这里的处理逻辑与上面的AOP一样}}
此种方案核心与AOP一样,只是这里不需要代理技术而已。
2.5 使用@JsonFilter
我们还可以配置 Jackson 属性过滤器,也就是在需要处理的类上添加 @JsonFilter 注解,这样我们就不需要为每个对象单独添加注解的字段级过滤。
首先,修改实体对象
public class User {// ...}
这里的名称 maskFilter 随意。
接下来,自定义ObjectMapper
public class MaskFilterConfig {private static final Set<String> fields = Set.of("idNo", "phone");Jackson2ObjectMapperBuilderCustomizer maskingFilterCustomizer() {return builder -> {SimpleFilterProvider filters = new SimpleFilterProvider().addFilter("maskFilter", new SimpleBeanPropertyFilter() {public void serializeAsField(Object pojo, JsonGenerator gen, SerializerProvider prov, PropertyWriter writer)throws Exception {if (fields.contains(writer.getName())) {Object val = (writer instanceof BeanPropertyWriter bpw) ? bpw.get(pojo) : null;if (val == null) {writer.serializeAsField(pojo, gen, prov);return;}gen.writeStringField(writer.getName(), MaskUtils.maskString(val.toString()));return;}writer.serializeAsField(pojo, gen, prov);}});builder.filters(filters);};}}
通过上面的方法也能也能达到对敏感字段的处理。
推荐文章
高级开发!Spring Boot 请求全链路10个神级扩展点
高级开发!自定义@RequestMapping深度整合熔断降级功能
高级版@ResponseBody,接口响应数据格式完全自定义
告别超卖!Spring Boot + 悲观锁:1行代码解决并发难题
Spring Boot 记录Controller接口请求日志7种方式,第六种性能极高
Spring Boot 通过@JsonComponent注解完全控制JSON数据
太神了!Spring Boot+Socket.IO 一个注解搞定实时通信


