刘帅
2026-09-28 2914eea299ec5a1d6bfdf40b2743226ef9cbc5af
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
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
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.Arrays;
import java.util.HashSet;
import java.util.Set;
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*\\\")([^\\\"]*)(\\\")");
    /**
     * APP has to receive these values so it can apply its own display masking.
     * Keep this list exact: all other routes continue to be protected at the
     * gateway boundary.
     */
    private static final Set<String> APP_UNMASKED_PATHS = new HashSet<String>(Arrays.asList(
            "/other/api/site/queryMySiteById",
            "/account/base/exploreWP/storeDetail",
            "/other/base/store/queryStoreFreeBenefit",
            "/other/base/site/querySiteInfo",
            "/activity/huimin/getStudentListByCarId",
            "/account/base/exploreWP/customerList"
    ));
 
    @Override public int getOrder() { return -2; }
 
    @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String path = exchange.getRequest().getURI().getPath();
        // /system/sensitive enforces its own admin check and audit.  The APP
        // paths below deliberately return their original fields for client-side
        // display masking; every other response remains gateway-masked.
        if (path.startsWith("/system/sensitive/") || APP_UNMASKED_PATHS.contains(path)) 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(); }
}