刘帅
2026-09-17 12732229b53d2280074cea3f91f3aa67b578ac7f
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
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<Void> 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<Void> writeWith(Publisher<? extends DataBuffer> 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(); }
}