刘帅
2026-09-29 b55cd9aad0835a96a790e61f9f89edc2fae1be9f
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
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
package com.dsh.privacy;
 
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
 
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.nio.file.Path;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.Map;
 
/** 确保所有常规管理端 JSON 响应均遵循统一的隐私保护规则。 */
@ControllerAdvice
public class SensitiveDataMaskingAdvice implements ResponseBodyAdvice<Object> {
    private final HttpServletRequest request;
    public SensitiveDataMaskingAdvice(HttpServletRequest request) { this.request = request; }
 
    @Override public boolean supports(MethodParameter type, Class<? extends HttpMessageConverter<?>> converter) {
        // 仅明确限定、仅管理员可访问且已审计的接口返回原始数据。
        return !request.getRequestURI().startsWith("/system/sensitive/");
    }
    @Override public Object beforeBodyWrite(Object body, MethodParameter type, MediaType mediaType,
            Class<? extends HttpMessageConverter<?>> converter, org.springframework.http.server.ServerHttpRequest req,
            org.springframework.http.server.ServerHttpResponse response) {
        mask(body, new IdentityHashMap<Object, Boolean>()); return body;
    }
    private void mask(Object value, IdentityHashMap<Object, Boolean> seen) {
        if (value == null || value instanceof String || value.getClass().isEnum() || seen.put(value, Boolean.TRUE) != null) return;
        // 响应式流及其内部队列属于运行时基础设施,并非响应 DTO。特别是 Reactor 的
        // MpscLinkedQueue 虽实现了 Iterable,却会在 iterator() 中主动抛出
        // UnsupportedOperationException;不得反射遍历它或其他响应式基础设施。
        if (isReactiveInfrastructure(value.getClass())) return;
        // UnixPath 实现了 Iterable,其迭代器会为每个路径段创建新的 Path 对象,
        // 因此基于对象标识的循环检测无法终止递归。
        if (value instanceof Path) return;
        if (value instanceof Map) {
            for (Object item : ((Map<?, ?>) value).entrySet()) {
                Map.Entry entry = (Map.Entry) item; Object child = entry.getValue(); int kind = kind(String.valueOf(entry.getKey()));
                if (child instanceof String && kind != 0) entry.setValue(maskText((String) child, kind)); else mask(child, seen);
            }
            return;
        }
        if (value instanceof Iterable) {
            maskIterable((Iterable<?>) value, seen);
            return;
        }
        if (value.getClass().isArray()) { for (int i = 0; i < Array.getLength(value); i++) mask(Array.get(value, i), seen); return; }
        if (isJdkType(value.getClass())) return;
        for (Class<?> type = value.getClass(); type != null && type != Object.class; type = type.getSuperclass()) for (Field field : type.getDeclaredFields()) {
            if (Modifier.isStatic(field.getModifiers()) || field.isSynthetic()) continue;
            try { field.setAccessible(true); Object child = field.get(value); int kind = kind(field.getName());
                if (child instanceof String && kind != 0) field.set(value, maskText((String) child, kind)); else mask(child, seen);
            } catch (IllegalAccessException | RuntimeException ignored) { }
        }
    }
 
    /**
     * 部分第三方容器仅为 API 兼容而实现 Iterable,并不支持实际迭代。
     * 响应隐私过滤器不得因这类实现细节导致接口返回 500 错误。
     */
    private void maskIterable(Iterable<?> values, IdentityHashMap<Object, Boolean> seen) {
        try {
            Iterator<?> iterator = values.iterator();
            while (iterator.hasNext()) mask(iterator.next(), seen);
        } catch (UnsupportedOperationException ignored) {
            // 没有可安全访问并进行脱敏的元素。
        }
    }
 
    private boolean isReactiveInfrastructure(Class<?> type) {
        String name = type.getName();
        return name.startsWith("reactor.")
                || name.startsWith("org.reactivestreams.")
                || name.startsWith("io.netty.");
    }
    private boolean isJdkType(Class<?> type) {
        String name = type.getName();
        return name.startsWith("java.") || name.startsWith("javax.") || name.startsWith("sun.");
    }
    private int kind(String name) {
        String key = name == null ? "" : name.replace("_", "").replace("-", "").toLowerCase();
        if (key.equals("idcard") || key.equals("identitycard") || key.equals("identitynumber") || key.equals("idnumber")) return 2;
        return key.equals("phone") || key.equals("mobile") || key.equals("telephone") || key.equals("tel") || key.endsWith("phone") || key.endsWith("mobile") ? 1 : 0;
    }
    private String maskText(String text, int kind) {
        if (text == null) return null;
        return kind == 1 ? (text.length() <= 7 ? stars(text.length()) : text.substring(0, 3) + "****" + text.substring(7))
                : (text.length() <= 10 ? stars(text.length()) : text.substring(0, 6) + stars(text.length() - 10) + text.substring(text.length() - 4));
    }
    private String stars(int count) { StringBuilder result = new StringBuilder(); for (int i = 0; i < count; i++) result.append('*'); return result.toString(); }
}