刘帅
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
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
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
package cn.mb.cloud.gateway.filter;
 
import cn.mb.cloud.gateway.util.RedisUtil;
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;
 
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
 
/**
 * 所有经网关进入系统的请求统一写入审计服务。
 * 使用服务发现直连,避免审计请求再次经过网关产生递归;写入异常不会影响业务响应。
 */
@Slf4j
@Component
public class AccountAuditGlobalFilter implements GlobalFilter, Ordered {
    private static final String AUDIT_SERVICE = "mb-cloud-other";
 
    @Autowired private DiscoveryClient discoveryClient;
    @Autowired private RedisUtil redisUtil;
 
    @Override public int getOrder() { return 1000; }
 
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        String uri = request.getURI().getRawPath();
        if (uri.startsWith("/internal/audit-logs")) return chain.filter(exchange);
        long startedAt = System.currentTimeMillis();
        return chain.filter(exchange).then(Mono.<Void>fromRunnable(() -> writeAudit(exchange, startedAt))
                .subscribeOn(Schedulers.boundedElastic()));
    }
 
    private void writeAudit(ServerWebExchange exchange, long startedAt) {
        try {
            ServerHttpRequest request = exchange.getRequest();
            String uri = request.getURI().getRawPath();
            Map<String, Object> audit = new LinkedHashMap<String, Object>();
            audit.put("traceId", request.getHeaders().getFirst("X-Audit-Trace-Id") == null
                    ? UUID.randomUUID().toString().replace("-", "") : request.getHeaders().getFirst("X-Audit-Trace-Id"));
            audit.put("sourceService", "mb-cloud-gateway");
            audit.put("sourceType", "HTTP");
            audit.put("operationType", operationType(request.getMethodValue(), uri));
            audit.put("riskLevel", riskLevel(uri, (String) audit.get("operationType")));
            audit.put("operationName", request.getMethodValue() + " " + uri);
            audit.put("requestMethod", request.getMethodValue());
            audit.put("requestUri", uri);
            audit.put("requestParams", maskedQuery(request));
            // 审计账号用于追溯操作人,必须保留原始账号;手机号、证件号等请求参数仍按规则脱敏。
            audit.put("userAccount", username(request));
            audit.put("clientIp", clientIp(request));
            int status = exchange.getResponse().getStatusCode() == null ? 200 : exchange.getResponse().getStatusCode().value();
            audit.put("result", status < 400 ? "SUCCESS" : "FAIL");
            audit.put("errorMessage", status < 400 ? null : "HTTP " + status);
            audit.put("createdAt", new Date());
            audit.put("durationMs", System.currentTimeMillis() - startedAt);
            post(audit);
        } catch (Exception e) {
            log.warn("统一审计写入失败", e);
        }
    }
 
    private void post(Map<String, Object> audit) throws Exception {
        List<ServiceInstance> instances = discoveryClient.getInstances(AUDIT_SERVICE);
        if (instances == null || instances.isEmpty()) return;
        ServiceInstance instance = instances.get(0);
        URL url = new URL(instance.getUri().toString() + "/internal/audit-logs");
        HttpURLConnection connection = (HttpURLConnection) url.openConnection();
        connection.setConnectTimeout(1000);
        connection.setReadTimeout(1500);
        connection.setRequestMethod("POST");
        connection.setDoOutput(true);
        connection.setRequestProperty("Content-Type", "application/json;charset=UTF-8");
        byte[] bytes = JSON.toJSONString(audit).getBytes(StandardCharsets.UTF_8);
        try (OutputStream output = connection.getOutputStream()) { output.write(bytes); }
        connection.getResponseCode();
        connection.disconnect();
    }
 
    private String username(ServerHttpRequest request) {
        String authorization = request.getHeaders().getFirst("Authorization");
        if (StringUtils.isBlank(authorization)) return null;
        String token = authorization.replaceFirst("(?i)^Bearer\\s+", "").trim();
        return token.length() < 32 ? null : redisUtil.getValue(token.substring(token.length() - 32));
    }
    private String operationType(String method, String uri) {
        String lower = uri.toLowerCase();
        if (lower.contains("login") || lower.contains("token")) return "LOGIN";
        if (lower.matches(".*(status|state|ticket|payment|finance|refund|withdraw|balance).*")) return "STATUS_CHANGE";
        if ("POST".equalsIgnoreCase(method)) return "CREATE";
        if ("PUT".equalsIgnoreCase(method) || "PATCH".equalsIgnoreCase(method)) return "UPDATE";
        if ("DELETE".equalsIgnoreCase(method)) return "DELETE";
        if (lower.matches(".*(phone|mobile|idcard|identity|bank|wallet).*")) return "VIEW_SENSITIVE";
        return "READ";
    }
    private String riskLevel(String uri, String type) {
        String lower = uri.toLowerCase();
        if ("STATUS_CHANGE".equals(type) || lower.matches(".*(finance|payment|refund|withdraw|balance|ticket).*")) return "CRITICAL";
        return lower.matches(".*(phone|mobile|idcard|identity|bank|wallet).*") ? "SENSITIVE" : "NORMAL";
    }
    private String maskedQuery(ServerHttpRequest request) {
        Map<String, Object> values = new LinkedHashMap<String, Object>();
        request.getQueryParams().forEach((key, value) -> values.put(key,
                key.toLowerCase().matches(".*(password|token|secret|idcard|bank|phone|mobile).*") ? "***" : value));
        return JSON.toJSONString(values);
    }
    private String clientIp(ServerHttpRequest request) { String value = request.getHeaders().getFirst("X-Forwarded-For"); return StringUtils.isBlank(value) ? String.valueOf(request.getRemoteAddress()) : value.split(",")[0].trim(); }
}