package com.ruoyi.framework.security.filter;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.ContentCachingResponseWrapper;
import org.springframework.web.util.WebUtils;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.Enumeration;

/**
 * 请求日志过滤器,用于打印所有请求的详细信息(包括POST请求体)
 */
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)  // 确保是第一个执行的过滤器
public class RequestLoggingFilter extends OncePerRequestFilter {

    private static final Logger LOG = LoggerFactory.getLogger(RequestLoggingFilter.class);

    // 最大日志长度(避免日志过大)
    private static final int MAX_PAYLOAD_LENGTH = 2000;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
                                    FilterChain filterChain) throws ServletException, IOException {

        // 包装请求和响应,用于缓存请求体和响应体
        ContentCachingRequestWrapper wrappedRequest = new ContentCachingRequestWrapper(request);
        ContentCachingResponseWrapper wrappedResponse = new ContentCachingResponseWrapper(response);

        try {
            // 继续过滤器链
            filterChain.doFilter(wrappedRequest, wrappedResponse);
        } finally {
            // 在finally块中打印日志,确保无论是否异常都会执行
            logRequestDetails(wrappedRequest);
//            logResponseDetails(wrappedResponse);
// 打印可直接运行的 OkHttp 复现代码(仅在调试时开启,生产慎用)
//            logOkhttp(wrappedRequest);
            // 必须将缓存的响应体复制回原始响应,否则客户端收不到响应
            wrappedResponse.copyBodyToResponse();
        }
    }

    private void logOkhttp(ContentCachingRequestWrapper wrappedRequest) {

        String okHttpCode = buildOkHttpRequestCode(wrappedRequest);
        System.out.println("\n【OkHttp 复现代码】(可直接复制运行):\n"
                +"```\n"
                +okHttpCode
                +"\n```");
    }

    /**
     * 打印请求详情(包括POST请求体)
     */
    private void logRequestDetails(ContentCachingRequestWrapper request) {
        try {
            StringBuilder sb = new StringBuilder();
            sb.append("\n============================================");
            sb.append("\n=============== 请求详情 START ===============");
            sb.append("\n请求URL: ").append(request.getRequestURL().toString());
            sb.append("\n请求方法: ").append(request.getMethod());
            sb.append("\n请求URI: ").append(request.getRequestURI());
//            sb.append("\n客户端IP: ").append(getClientIp(request));
            sb.append("\n查询参数: ").append(request.getQueryString());

//            // 打印请求头
//            sb.append("\n\n请求头信息:");
//            Enumeration<String> headerNames = request.getHeaderNames();
//            while (headerNames.hasMoreElements()) {
//                String headerName = headerNames.nextElement();
//                sb.append("\n  ").append(headerName).append(": ").append(request.getHeader(headerName));
//            }

//            // 打印URL参数(GET参数)
//            Map<String, String[]> parameterMap = request.getParameterMap();
//            if (!parameterMap.isEmpty()) {
//                sb.append("\n\nURL参数:");
//                for (Map.Entry<String, String[]> entry : parameterMap.entrySet()) {
//                    sb.append("\n  ").append(entry.getKey()).append(": ");
//                    String[] values = entry.getValue();
//                    if (values != null && values.length > 0) {
//                        sb.append(String.join(", ", values));
//                    }
//                }
//            }

            // 打印请求体(POST/PUT等请求)
            String requestBody = getRequestBody(request);
            if (requestBody != null && !requestBody.isEmpty()) {
                sb.append("\n请求体:");
                // 如果内容过长,截断显示
                if (requestBody.length() > MAX_PAYLOAD_LENGTH) {
                    sb.append("\n").append(requestBody.substring(0, MAX_PAYLOAD_LENGTH))
                            .append("\n...(内容过长,已截断,总长度:").append(requestBody.length()).append(")");
                } else {
                    sb.append("\n").append(requestBody);
                }
            }

            sb.append("\n=============== 请求详情 END ================");
            sb.append("\n============================================\n");

            LOG.info(sb.toString());

        } catch (Exception e) {
            LOG.error("打印请求日志失败", e);
        }
    }

    /**
     * 打印响应详情
     */
    private void logResponseDetails(ContentCachingResponseWrapper response) {
        try {
            StringBuilder sb = new StringBuilder();
            sb.append("\n============================================");
            sb.append("\n=============== 响应详情 START ===============");
            sb.append("\n响应状态码: ").append(response.getStatus());

            // 打印响应头
            sb.append("\n\n响应头信息:");
            for (String headerName : response.getHeaderNames()) {
                sb.append("\n  ").append(headerName).append(": ").append(response.getHeader(headerName));
            }

            // 打印响应体
            String responseBody = getResponseBody(response);
            if (responseBody != null && !responseBody.isEmpty()) {
                sb.append("\n\n响应体:");
                // 如果内容过长,截断显示
                if (responseBody.length() > MAX_PAYLOAD_LENGTH) {
                    sb.append("\n").append(responseBody.substring(0, MAX_PAYLOAD_LENGTH))
                            .append("\n...(内容过长,已截断,总长度:").append(responseBody.length()).append(")");
                } else {
                    sb.append("\n").append(responseBody);
                }
            }

            sb.append("\n=============== 响应详情 END ================");
            sb.append("\n============================================\n");

            LOG.info(sb.toString());

        } catch (Exception e) {
            LOG.error("打印响应日志失败", e);
        }
    }

    /**
     * 获取请求体内容(从缓存中读取,不会消耗流)
     */
    private String getRequestBody(ContentCachingRequestWrapper request) {
        ContentCachingRequestWrapper wrapper =
                WebUtils.getNativeRequest(request, ContentCachingRequestWrapper.class);

        if (wrapper != null) {
            byte[] content = wrapper.getContentAsByteArray();
            if (content.length > 0) {
                try {
                    return new String(content, 0, content.length, wrapper.getCharacterEncoding());
                } catch (UnsupportedEncodingException e) {
                    return new String(content);
                }
            }
        }

        return "";
    }

    /**
     * 获取响应体内容
     */
    private String getResponseBody(ContentCachingResponseWrapper response) {
        ContentCachingResponseWrapper wrapper =
                WebUtils.getNativeResponse(response, ContentCachingResponseWrapper.class);

        if (wrapper != null) {
            byte[] content = wrapper.getContentAsByteArray();
            if (content.length > 0) {
                try {
                    return new String(content, 0, content.length, wrapper.getCharacterEncoding());
                } catch (UnsupportedEncodingException e) {
                    return new String(content);
                }
            }
        }

        return "";
    }

    /**
     * 获取客户端真实IP地址
     */
    private String getClientIp(HttpServletRequest request) {
        String ip = request.getHeader("X-Forwarded-For");
        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("Proxy-Client-IP");
        }
        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("WL-Proxy-Client-IP");
        }
        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }
        // 如果是多个IP,取第一个
        if (ip != null && ip.contains(",")) {
            ip = ip.split(",")[0].trim();
        }
        return ip;
    }

    /**
     * 排除不需要记录日志的请求(如静态资源)
     */
    @Override
    protected boolean shouldNotFilter(HttpServletRequest request) {
        String path = request.getRequestURI();
        // 排除静态资源
        return path.contains("/static/")
                || path.contains("/css/")
                || path.contains("/js/")
                || path.contains("/images/")
                || path.contains("/fonts/")
                || path.contains("/favicon.ico")
                || path.contains("/actuator/")
                || path.contains("/swagger/")
                || path.contains("/v3/api-docs");
    }

    /**
     * 根据 HttpServletRequest 构建可直接运行的 OkHttp 请求代码(支持 GET/POST/PUT/DELETE 等,带请求头和请求体)
     * 方便开发/测试时直接复制到 Java 代码中复现请求
     */
    private String buildOkHttpRequestCode(ContentCachingRequestWrapper request) {
        String method = request.getMethod();
        String url = request.getRequestURL().toString();
        String queryString = request.getQueryString();
        if (queryString != null && !queryString.isEmpty()) {
            url += "?" + queryString;
        }

        String requestBody = getRequestBody(request);

        StringBuilder code = new StringBuilder();
        code.append("OkHttpClient client = new OkHttpClient();\n\n");

        // 构建 RequestBody
        String requestBodyCode = "";
        if ("POST".equalsIgnoreCase(method) || "PUT".equalsIgnoreCase(method) || "PATCH".equalsIgnoreCase(method)) {
            if (requestBody != null && !requestBody.isEmpty()) {
                // 尝试智能判断 Content-Type
                String contentType = request.getContentType();
                if (contentType != null && contentType.contains("application/json")) {
                    requestBodyCode = String.format("RequestBody body = RequestBody.create(\"%s\", MediaType.parse(\"application/json; charset=utf-8\"));\n",
                            escapeJavaString(requestBody));
                } else if (contentType != null && contentType.contains("x-www-form-urlencoded")) {
                    // 表单格式需要解析参数重建(这里简化处理)
                    requestBodyCode = String.format("RequestBody body = RequestBody.create(\"%s\", MediaType.parse(\"application/x-www-form-urlencoded; charset=utf-8\"));\n",
                            escapeJavaString(requestBody));
                } else {
                    requestBodyCode = String.format("RequestBody body = RequestBody.create(\"%s\", MediaType.parse(\"text/plain; charset=utf-8\"));\n",
                            escapeJavaString(requestBody));
                }
            } else {
                requestBodyCode = "RequestBody body = RequestBody.create(\"\", null);\n";
            }
        }

        // 构建 Request.Builder
        code.append("Request request = new Request.Builder()\n");
        code.append("    .url(\"").append(url).append("\")\n");

        // 添加常见请求头
        Enumeration<String> headerNames = request.getHeaderNames();
        while (headerNames.hasMoreElements()) {
            String name = headerNames.nextElement();
            String value = request.getHeader(name);

            // 过滤掉可能导致重复或无效的头
            if ("host".equalsIgnoreCase(name) ||
                    "content-length".equalsIgnoreCase(name) ||
                    "connection".equalsIgnoreCase(name)) {
                continue;
            }

            // 特殊处理 Authorization、Cookie 等敏感头(可根据需要脱敏)
            if ("authorization".equalsIgnoreCase(name)) {
//                value = maskAuthorization(value);
                value = value;
            } else if ("cookie".equalsIgnoreCase(name)) {
//                value = maskCookie(value);
                value = value;
            }

            code.append("    .addHeader(\"").append(name).append("\", \"").append(escapeJavaString(value)).append("\")\n");
        }

        // 添加方法和 body
        if ("GET".equalsIgnoreCase(method)) {
            code.append("    .get()\n");
        } else if ("POST".equalsIgnoreCase(method)) {
            code.append(requestBodyCode);
            code.append("    .post(body)\n");
        } else if ("PUT".equalsIgnoreCase(method)) {
            code.append(requestBodyCode);
            code.append("    .put(body)\n");
        } else if ("DELETE".equalsIgnoreCase(method)) {
            if (!requestBody.isEmpty()) {
                code.append(requestBodyCode);
                code.append("    .delete(body)\n");
            } else {
                code.append("    .delete()\n");
            }
        } else if ("PATCH".equalsIgnoreCase(method)) {
            code.append(requestBodyCode);
            code.append("    .patch(body)\n");
        } else {
            code.append("    .method(\"").append(method).append("\", ")
                    .append(requestBody.isEmpty() ? "null" : "body").append(")\n");
        }

        code.append("    .build();\n\n");
        code.append("Response response = client.newCall(request).execute();\n");
        code.append("System.out.println(response.body().string());");

        return code.toString();
    }

    /**
     * Java 字符串转义(处理换行、引号等)
     */
    private String escapeJavaString(String str) {
        if (str == null) return "";
        return str.replace("\\", "\\\\")
                .replace("\"", "\\\"")
                .replace("\n", "\\n")
                .replace("\r", "\\r")
                .replace("\t", "\\t");
    }

    /**
     * 简单脱敏 Authorization 头(如 Bearer token)
     */
    private String maskAuthorization(String auth) {
        if (auth == null) return auth;
        if (auth.toLowerCase().startsWith("bearer ")) {
            String token = auth.substring(7);
            if (token.length() > 10) {
                return "Bearer " + token.substring(0, 6) + "****" + token.substring(token.length() - 4);
            }
        }
        return "****";
    }

    /**
     * 简单脱敏 Cookie
     */
    private String maskCookie(String cookie) {
        return cookie != null ? "Cookie已脱敏" : "";
    }
}

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐