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(); }
|
}
|