package cn.mb.cloud.gateway.filter; import org.reactivestreams.Publisher; import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.cloud.gateway.filter.GlobalFilter; import org.springframework.core.Ordered; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.core.io.buffer.DataBufferUtils; import org.springframework.http.MediaType; import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.http.server.reactive.ServerHttpResponseDecorator; import org.springframework.stereotype.Component; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; import java.nio.charset.StandardCharsets; import java.util.regex.Matcher; import java.util.regex.Pattern; /** Final API boundary for APP traffic, including responses returned by every service. */ @Component public class SensitiveDataMaskingGlobalFilter implements GlobalFilter, Ordered { private static final Pattern FIELD = Pattern.compile("(?i)(\\\"(?:[a-z]*phone|[a-z]*mobile|telephone|tel)\\\"\\s*:\\s*\\\")([^\\\"]*)(\\\")"); private static final Pattern IDCARD = Pattern.compile("(?i)(\\\"(?:[a-z]*idcard|[a-z]*identitycard|identitynumber|idnumber)\\\"\\s*:\\s*\\\")([^\\\"]*)(\\\")"); @Override public int getOrder() { return -2; } @Override public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { // This route itself enforces the admin check and writes an audit record. if (exchange.getRequest().getURI().getPath().startsWith("/system/sensitive/")) return chain.filter(exchange); ServerHttpResponseDecorator decorated = new ServerHttpResponseDecorator(exchange.getResponse()) { @Override public Mono writeWith(Publisher body) { MediaType type = getHeaders().getContentType(); if (type == null || !MediaType.APPLICATION_JSON.isCompatibleWith(type)) return super.writeWith(body); return DataBufferUtils.join(body).flatMap(buffer -> { byte[] bytes = new byte[buffer.readableByteCount()]; buffer.read(bytes); DataBufferUtils.release(buffer); String json = new String(bytes, StandardCharsets.UTF_8); byte[] masked = mask(json).getBytes(StandardCharsets.UTF_8); getHeaders().setContentLength(masked.length); DataBufferFactory factory = getDelegate().bufferFactory(); return super.writeWith(Mono.just(factory.wrap(masked))); }); } }; return chain.filter(exchange.mutate().response(decorated).build()); } private String mask(String json) { return replace(replace(json, FIELD, false), IDCARD, true); } private String replace(String json, Pattern pattern, boolean idCard) { Matcher matcher = pattern.matcher(json); StringBuffer output = new StringBuffer(); while (matcher.find()) matcher.appendReplacement(output, Matcher.quoteReplacement(matcher.group(1) + maskValue(matcher.group(2), idCard) + matcher.group(3))); matcher.appendTail(output); return output.toString(); } private String maskValue(String value, boolean idCard) { if (idCard) return value.length() <= 10 ? stars(value.length()) : value.substring(0, 6) + stars(value.length() - 10) + value.substring(value.length() - 4); return value.length() <= 7 ? stars(value.length()) : value.substring(0, 3) + "****" + value.substring(7); } private String stars(int count) { StringBuilder result = new StringBuilder(); for (int i = 0; i < count; i++) result.append('*'); return result.toString(); } }