Kaynağa Gözat

修复http请求体缓存问题;优化异常日志打印逻辑;

Woody 3 hafta önce
ebeveyn
işleme
444472b336

+ 4 - 4
framework-base/src/main/java/com/chelvc/framework/base/config/ApplicationConfigurer.java

@@ -5,7 +5,7 @@ import javax.servlet.Filter;
 
 import com.chelvc.framework.base.context.DefaultSessionFactory;
 import com.chelvc.framework.base.context.SessionFactory;
-import com.chelvc.framework.base.interceptor.RequestCachingInterceptor;
+import com.chelvc.framework.base.interceptor.RequestInvokeInterceptor;
 import com.chelvc.framework.base.interceptor.ResponseWrapInterceptor;
 import com.chelvc.framework.common.util.JacksonUtils;
 import com.chelvc.framework.common.util.ObjectUtils;
@@ -48,9 +48,9 @@ public class ApplicationConfigurer {
     }
 
     @Bean
-    public FilterRegistrationBean<RequestCachingInterceptor> requestCachingInterceptor() {
-        FilterRegistrationBean<RequestCachingInterceptor> registration = new FilterRegistrationBean<>();
-        registration.setFilter(new RequestCachingInterceptor());
+    public FilterRegistrationBean<RequestInvokeInterceptor> requestInvokeInterceptor() {
+        FilterRegistrationBean<RequestInvokeInterceptor> registration = new FilterRegistrationBean<>();
+        registration.setFilter(new RequestInvokeInterceptor());
         registration.setOrder(Ordered.HIGHEST_PRECEDENCE);
         return registration;
     }

+ 2 - 2
framework-base/src/main/java/com/chelvc/framework/base/context/ApplicationContextHolder.java

@@ -541,10 +541,10 @@ public class ApplicationContextHolder implements EnvironmentAware, ApplicationCo
                     log.error("Get property value failed: {}", key, e);
                 }
             }
-            value = ObjectUtils.ifNull(value, ObjectUtils.EMPTY);
+            value = ObjectUtils.ifNull(value, ObjectUtils.EMPTY_BYTE_ARRAY);
             PROPERTY_OBJECT_MAPPING.put(key, new WeakReference<>(value));
         }
-        return value == ObjectUtils.EMPTY ? null : (T) value;
+        return value == ObjectUtils.EMPTY_BYTE_ARRAY ? null : (T) value;
     }
 
     /**

+ 17 - 13
framework-base/src/main/java/com/chelvc/framework/base/context/LoggingContextHolder.java

@@ -1,7 +1,5 @@
 package com.chelvc.framework.base.context;
 
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
 import javax.servlet.ServletRequest;
 import javax.servlet.http.HttpServletRequest;
 
@@ -115,19 +113,25 @@ public final class LoggingContextHolder {
      * @return 消息内容
      */
     public static String message(ServletRequest request, @NonNull Object... messages) {
-        String uri = null, method = null;
-        if (request instanceof HttpServletRequest) {
-            uri = ((HttpServletRequest) request).getRequestURI();
-            method = ((HttpServletRequest) request).getMethod();
+        if (!(request instanceof HttpServletRequest)) {
+            return message((String) null, messages);
         }
-        String form = null, body = null;
-        if (request != null) {
-            form = HttpUtils.serialize(request.getParameterMap());
-            body = StringUtils.replace(HttpUtils.getBody(request), '\n', '\0');
+        StringBuilder endpoint = new StringBuilder();
+        endpoint.append(((HttpServletRequest) request).getMethod());
+        endpoint.append(StringUtils.SPACE);
+        endpoint.append(((HttpServletRequest) request).getRequestURI());
+        String query = ((HttpServletRequest) request).getQueryString();
+        if (StringUtils.notEmpty(query)) {
+            endpoint.append("?").append(query);
         }
-        String endpoint = Stream.of(method, uri, form, body).filter(StringUtils::notEmpty)
-                .collect(Collectors.joining(StringUtils.SPACE));
-        return message(endpoint, messages);
+        if (HttpUtils.isBodyMethod(request) && !HttpUtils.isMultipartBody(request)) {
+            byte[] body = HttpUtils.getBody(request);
+            if (ObjectUtils.notEmpty(body)) {
+                endpoint.append(StringUtils.SPACE);
+                endpoint.append(StringUtils.replace(new String(body), '\n', '\0'));
+            }
+        }
+        return message(endpoint.toString(), messages);
     }
 
     /**

+ 1 - 22
framework-base/src/main/java/com/chelvc/framework/base/interceptor/GlobalExceptionInterceptor.java

@@ -1,16 +1,11 @@
 package com.chelvc.framework.base.interceptor;
 
-import java.io.IOException;
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
 import java.util.stream.Collectors;
-import javax.servlet.Filter;
-import javax.servlet.FilterChain;
-import javax.servlet.ServletRequest;
-import javax.servlet.ServletResponse;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import javax.validation.ConstraintViolation;
@@ -20,7 +15,6 @@ import javax.validation.ValidationException;
 import com.chelvc.framework.base.context.ApplicationContextHolder;
 import com.chelvc.framework.base.context.LoggingContextHolder;
 import com.chelvc.framework.base.context.Result;
-import com.chelvc.framework.base.context.SessionContextHolder;
 import com.chelvc.framework.common.exception.FrameworkException;
 import com.chelvc.framework.common.exception.ParameterInvalidException;
 import com.chelvc.framework.common.util.ErrorUtils;
@@ -34,8 +28,6 @@ import org.springframework.beans.TypeMismatchException;
 import org.springframework.boot.autoconfigure.web.servlet.error.AbstractErrorController;
 import org.springframework.boot.web.error.ErrorAttributeOptions;
 import org.springframework.boot.web.servlet.error.DefaultErrorAttributes;
-import org.springframework.core.Ordered;
-import org.springframework.core.annotation.Order;
 import org.springframework.http.HttpStatus;
 import org.springframework.http.converter.HttpMessageConversionException;
 import org.springframework.http.converter.HttpMessageNotReadableException;
@@ -67,8 +59,7 @@ import org.springframework.web.multipart.support.MissingServletRequestPartExcept
 @Slf4j
 @Controller
 @RestControllerAdvice
-@Order(Ordered.HIGHEST_PRECEDENCE)
-public class GlobalExceptionInterceptor extends AbstractErrorController implements Filter {
+public class GlobalExceptionInterceptor extends AbstractErrorController {
     /**
      * 客户端中断异常
      */
@@ -85,18 +76,6 @@ public class GlobalExceptionInterceptor extends AbstractErrorController implemen
         return null;
     }
 
-    @Override
-    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException {
-        try {
-            chain.doFilter(request, response);
-        } catch (Exception e) {
-            LoggingContextHolder.warn(log, request, e);
-            String message = ApplicationContextHolder.getMessage("Request.Invalid");
-            Result<?> result = Result.of(HttpStatus.BAD_REQUEST.name(), null, message);
-            SessionContextHolder.response((HttpServletResponse) response, result);
-        }
-    }
-
     /**
      * 参数绑定结果格式化
      *

+ 0 - 30
framework-base/src/main/java/com/chelvc/framework/base/interceptor/RequestContextInterceptor.java

@@ -1,30 +0,0 @@
-package com.chelvc.framework.base.interceptor;
-
-import java.io.IOException;
-import javax.servlet.Filter;
-import javax.servlet.FilterChain;
-import javax.servlet.ServletException;
-import javax.servlet.ServletRequest;
-import javax.servlet.ServletResponse;
-
-import org.springframework.core.annotation.Order;
-import org.springframework.stereotype.Component;
-import org.springframework.web.context.request.RequestContextHolder;
-
-/**
- * 请求上下文拦截器
- *
- * @author Woody
- * @date 2024/1/30
- */
-@Order
-@Component
-public class RequestContextInterceptor implements Filter {
-    @Override
-    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
-            throws IOException, ServletException {
-        // 重置请求上下文,用于线程间传递
-        RequestContextHolder.setRequestAttributes(RequestContextHolder.getRequestAttributes(), true);
-        chain.doFilter(request, response);
-    }
-}

+ 71 - 37
framework-base/src/main/java/com/chelvc/framework/base/interceptor/RequestCachingInterceptor.java → framework-base/src/main/java/com/chelvc/framework/base/interceptor/RequestInvokeInterceptor.java

@@ -5,36 +5,43 @@ import java.io.ByteArrayInputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
+import java.util.Collections;
 import java.util.Enumeration;
 import java.util.Iterator;
 import java.util.Map;
-import javax.servlet.Filter;
 import javax.servlet.FilterChain;
 import javax.servlet.ReadListener;
 import javax.servlet.ServletException;
 import javax.servlet.ServletInputStream;
-import javax.servlet.ServletRequest;
-import javax.servlet.ServletResponse;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletRequestWrapper;
+import javax.servlet.http.HttpServletResponse;
 
-import com.chelvc.framework.common.util.FileUtils;
+import com.chelvc.framework.base.context.ApplicationContextHolder;
+import com.chelvc.framework.base.context.LoggingContextHolder;
+import com.chelvc.framework.base.context.Result;
+import com.chelvc.framework.base.context.SessionContextHolder;
+import com.chelvc.framework.base.util.HttpUtils;
 import com.chelvc.framework.common.util.ObjectUtils;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.HttpStatus;
+import org.springframework.web.filter.OncePerRequestFilter;
 
 /**
- * 请求参数缓存拦截器
+ * 请求调用拦截器
  *
  * @author Woody
- * @date 2025/6/25
+ * @date 2024/4/3
  */
-public class RequestCachingInterceptor implements Filter {
+@Slf4j
+public class RequestInvokeInterceptor extends OncePerRequestFilter {
     /**
      * 缓存数据流对象
      */
-    static class Stream extends ServletInputStream {
+    static class CachingInputStream extends ServletInputStream {
         private final InputStream delegate;
 
-        private Stream(byte[] bytes) {
+        private CachingInputStream(byte[] bytes) {
             this.delegate = new ByteArrayInputStream(bytes);
         }
 
@@ -48,11 +55,41 @@ public class RequestCachingInterceptor implements Filter {
             return this.delegate.read(b, off, len);
         }
 
+        @Override
+        public int read(byte[] b) throws IOException {
+            return this.delegate.read(b);
+        }
+
+        @Override
+        public long skip(long n) throws IOException {
+            return this.delegate.skip(n);
+        }
+
         @Override
         public int available() throws IOException {
             return this.delegate.available();
         }
 
+        @Override
+        public void close() throws IOException {
+            this.delegate.close();
+        }
+
+        @Override
+        public synchronized void mark(int limit) {
+            this.delegate.mark(limit);
+        }
+
+        @Override
+        public synchronized void reset() throws IOException {
+            this.delegate.reset();
+        }
+
+        @Override
+        public boolean markSupported() {
+            return this.delegate.markSupported();
+        }
+
         @Override
         public boolean isReady() {
             try {
@@ -80,32 +117,23 @@ public class RequestCachingInterceptor implements Filter {
     /**
      * 请求缓存包装对象
      */
-    static class Wrapper extends HttpServletRequestWrapper {
+    static class CachingRequestWrapper extends HttpServletRequestWrapper {
         private final byte[] body;
-        private final String query;
         private final Map<String, String[]> parameters;
 
-        private Wrapper(HttpServletRequest request) throws IOException {
+        private CachingRequestWrapper(HttpServletRequest request) {
             super(request);
-            if (request instanceof Wrapper) {
-                Wrapper wrapper = (Wrapper) request;
-                this.body = wrapper.body;
-                this.query = wrapper.query;
-                this.parameters = wrapper.parameters;
+            if (request instanceof CachingRequestWrapper) {
+                this.body = ((CachingRequestWrapper) request).body;
+                this.parameters = ((CachingRequestWrapper) request).parameters;
             } else {
-                try (InputStream input = request.getInputStream()) {
-                    this.body = FileUtils.getBytes(input);
-                }
-                this.query = request.getQueryString();
+                // Servlet 规范规定如果调用了getInputStream()或getReader()方法,则getParameter*()方法的行为是未定义的
+                // Tomcat、Jetty、Undertow容器均支持在调用getParameter*()方法后能正常调用getInputStream()方法
                 this.parameters = request.getParameterMap();
+                this.body = HttpUtils.getBody(request);
             }
         }
 
-        @Override
-        public String getQueryString() {
-            return this.query;
-        }
-
         @Override
         public String getParameter(String name) {
             String[] values = this.parameters.get(name);
@@ -114,22 +142,21 @@ public class RequestCachingInterceptor implements Filter {
 
         @Override
         public Map<String, String[]> getParameterMap() {
-            return this.parameters;
+            return Collections.unmodifiableMap(this.parameters);
         }
 
         @Override
         public Enumeration<String> getParameterNames() {
+            Iterator<String> iterator = this.parameters.keySet().iterator();
             return new Enumeration<String>() {
-                private final Iterator<String> iterator = parameters.keySet().iterator();
-
                 @Override
                 public boolean hasMoreElements() {
-                    return this.iterator.hasNext();
+                    return iterator.hasNext();
                 }
 
                 @Override
                 public String nextElement() {
-                    return this.iterator.next();
+                    return iterator.next();
                 }
             };
         }
@@ -146,17 +173,24 @@ public class RequestCachingInterceptor implements Filter {
 
         @Override
         public ServletInputStream getInputStream() throws IOException {
-            return new Stream(this.body);
+            return new CachingInputStream(this.body);
         }
     }
 
     @Override
-    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
-            throws IOException, ServletException {
-        if (request instanceof HttpServletRequest) {
-            chain.doFilter(new Wrapper((HttpServletRequest) request), response);
-        } else {
+    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
+            throws ServletException, IOException {
+        if (HttpUtils.isBodyMethod(request) && !HttpUtils.isMultipartBody(request)) {
+            request = new CachingRequestWrapper(request);
+        }
+
+        try {
             chain.doFilter(request, response);
+        } catch (Exception e) {
+            LoggingContextHolder.warn(log, request, e);
+            String message = ApplicationContextHolder.getMessage("Request.Invalid");
+            Result<?> result = Result.of(HttpStatus.BAD_REQUEST.name(), null, message);
+            SessionContextHolder.response(response, result);
         }
     }
 }

+ 68 - 60
framework-base/src/main/java/com/chelvc/framework/base/util/HttpUtils.java

@@ -15,7 +15,6 @@ import java.util.Collections;
 import java.util.Comparator;
 import java.util.List;
 import java.util.Map;
-import java.util.function.BiFunction;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 import java.util.stream.Collectors;
@@ -31,6 +30,7 @@ import com.chelvc.framework.common.util.StringUtils;
 import lombok.NonNull;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.poi.ss.usermodel.Workbook;
+import org.springframework.http.HttpMethod;
 import org.springframework.http.MediaType;
 import org.springframework.web.multipart.MultipartFile;
 
@@ -160,22 +160,6 @@ public final class HttpUtils {
                 .distinct().collect(Collectors.toList());
     }
 
-    /**
-     * 获取Body请求参数
-     *
-     * @param request Http请求对象
-     * @return 请求体参数
-     */
-    public static String getBody(@NonNull ServletRequest request) {
-        byte[] bytes;
-        try (InputStream input = request.getInputStream()) {
-            bytes = FileUtils.getBytes(input);
-        } catch (IOException e) {
-            throw new RuntimeException(e);
-        }
-        return new String(bytes);
-    }
-
     /**
      * 获取请求标识
      *
@@ -238,6 +222,56 @@ public final class HttpUtils {
         return HostUtils.DEFAULT_LOCAL_ADDRESS_IPV6.equals(host) ? HostUtils.LOCAL_ADDRESS : host;
     }
 
+    /**
+     * 获取请求体
+     *
+     * @param request 请求对象
+     * @return 请求体字节数组
+     */
+    public static byte[] getBody(@NonNull ServletRequest request) {
+        try (InputStream input = request.getInputStream()) {
+            return FileUtils.getBytes(input);
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    /**
+     * 判断是否是请求体方式
+     *
+     * @param request 请求对象
+     * @return true/false
+     */
+    public static boolean isBodyMethod(@NonNull ServletRequest request) {
+        if (!(request instanceof HttpServletRequest)) {
+            return false;
+        }
+        String method = ((HttpServletRequest) request).getMethod();
+        return HttpMethod.PUT.matches(method) || HttpMethod.POST.matches(method) || HttpMethod.PATCH.matches(method);
+    }
+
+    /**
+     * 判断是否是JSON请求体
+     *
+     * @param request 请求对象
+     * @return true/false
+     */
+    public static boolean isJsonBody(@NonNull ServletRequest request) {
+        String type = request.getContentType();
+        return StringUtils.notEmpty(type) && type.contains(MediaType.APPLICATION_JSON_VALUE);
+    }
+
+    /**
+     * 判断是否是文件流请求体
+     *
+     * @param request 请求对象
+     * @return true/false
+     */
+    public static boolean isMultipartBody(@NonNull ServletRequest request) {
+        String type = request.getContentType();
+        return StringUtils.notEmpty(type) && type.startsWith("multipart/");
+    }
+
     /**
      * 初始化文件下载相应头
      *
@@ -355,6 +389,22 @@ public final class HttpUtils {
         }
     }
 
+    /**
+     * Http请求参数序列化
+     *
+     * @param request Http请求对象
+     * @return 请求参数
+     */
+    public static String serialize(@NonNull ServletRequest request) {
+        if (isBodyMethod(request) && isJsonBody(request)) {
+            // JSON参数
+            return new String(getBody(request));
+        }
+
+        // 表单参数 + URL参数
+        return serialize(request.getParameterMap());
+    }
+
     /**
      * Http请求参数序列化
      *
@@ -387,49 +437,7 @@ public final class HttpUtils {
      * @return 请求参数
      */
     public static String serialize(@NonNull Map<String, String[]> parameters) {
-        return serialize(parameters, false);
-    }
-
-    /**
-     * Http请求参数序列化
-     *
-     * @param parameters 参数名称/值映射表
-     * @param ordering   参数是否排序
-     * @return 请求参数
-     */
-    public static String serialize(@NonNull Map<String, String[]> parameters, boolean ordering) {
-        if (ordering) {
-            return StringUtils.join(parameters, "&", HttpUtils::serialize, Comparator.naturalOrder());
-        }
-        return StringUtils.join(parameters, "&", (BiFunction<String, String[], String>) HttpUtils::serialize);
-    }
-
-    /**
-     * Http请求参数序列化
-     *
-     * @param request Http请求对象
-     * @return 请求参数
-     */
-    public static String serialize(@NonNull ServletRequest request) {
-        return serialize(request, false);
-    }
-
-    /**
-     * Http请求参数序列化
-     *
-     * @param request  Http请求对象
-     * @param ordering 参数是否排序
-     * @return 请求参数
-     */
-    public static String serialize(@NonNull ServletRequest request, boolean ordering) {
-        String type = request.getContentType();
-        if (StringUtils.notEmpty(type) && type.contains(MediaType.APPLICATION_JSON_VALUE)) {
-            // Body参数
-            return getBody(request);
-        }
-
-        // 表单参数 + URL参数
-        return serialize(request.getParameterMap(), ordering);
+        return StringUtils.join(parameters, "&", HttpUtils::serialize, Comparator.naturalOrder());
     }
 
     /**

+ 2 - 2
framework-common/src/main/java/com/chelvc/framework/common/util/ObjectUtils.java

@@ -56,9 +56,9 @@ import org.apache.commons.lang3.reflect.TypeUtils;
  */
 public final class ObjectUtils {
     /**
-     * 空对象标识(空字节数组
+     * 空字节数组
      */
-    public static final byte[] EMPTY = new byte[0];
+    public static final byte[] EMPTY_BYTE_ARRAY = new byte[0];
 
     /**
      * CPU数量

+ 2 - 2
framework-redis/src/main/java/com/chelvc/framework/redis/context/RedisContextHolder.java

@@ -776,7 +776,7 @@ public final class RedisContextHolder {
             String value = StringUtils.rjust(String.valueOf(random.nextInt(100000)), 5, '0');
             long identity = Long.parseLong(time + value);
             byte[] key = (SEQUENCE_REDIS_PREFIX + identity).getBytes(StandardCharsets.UTF_8);
-            Boolean success = execute(factory, connection -> connection.set(key, ObjectUtils.EMPTY,
+            Boolean success = execute(factory, connection -> connection.set(key, ObjectUtils.EMPTY_BYTE_ARRAY,
                     Expiration.from(duration), RedisStringCommands.SetOption.SET_IF_ABSENT));
             if (Boolean.TRUE.equals(success)) {
                 return identity;
@@ -1297,7 +1297,7 @@ public final class RedisContextHolder {
         for (int i = 0; i < 3; i++) {
             String value = supplier.get();
             byte[] key = (RANDOM_REDIS_PREFIX + value).getBytes(StandardCharsets.UTF_8);
-            Boolean success = execute(factory, connection -> connection.set(key, ObjectUtils.EMPTY,
+            Boolean success = execute(factory, connection -> connection.set(key, ObjectUtils.EMPTY_BYTE_ARRAY,
                     Expiration.from(duration), RedisStringCommands.SetOption.SET_IF_ABSENT));
             if (Boolean.TRUE.equals(success)) {
                 return value;

+ 3 - 0
framework-security/src/main/java/com/chelvc/framework/security/firewall/InterceptAction.java

@@ -2,6 +2,7 @@ package com.chelvc.framework.security.firewall;
 
 import com.chelvc.framework.base.context.ApplicationContextHolder;
 import com.chelvc.framework.common.exception.FrameworkException;
+import lombok.extern.slf4j.Slf4j;
 import org.springframework.http.HttpStatus;
 import org.springframework.stereotype.Component;
 
@@ -11,6 +12,7 @@ import org.springframework.stereotype.Component;
  * @author Woody
  * @date 2025/3/14
  */
+@Slf4j
 @Component
 public class InterceptAction implements Action {
     /**
@@ -25,6 +27,7 @@ public class InterceptAction implements Action {
 
     @Override
     public void execute(String principal, Rule rule) {
+        log.warn("Firewall rule triggered: {}, principal: {}, action: {}", rule.getId(), principal, this.getName());
         String message = ApplicationContextHolder.getMessage("Forbidden");
         throw new FrameworkException(HttpStatus.FORBIDDEN.name(), null, message);
     }

+ 1 - 1
framework-security/src/main/java/com/chelvc/framework/security/firewall/ObserveAction.java

@@ -24,6 +24,6 @@ public class ObserveAction implements Action {
 
     @Override
     public void execute(String principal, Rule rule) {
-        log.warn("Firewall rule triggered: {}, principal: {}", rule.getId(), principal);
+        log.warn("Firewall rule triggered: {}, principal: {}, action: {}", rule.getId(), principal, this.getName());
     }
 }

+ 1 - 1
framework-security/src/main/java/com/chelvc/framework/security/interceptor/SecurityValidateInterceptor.java

@@ -158,7 +158,7 @@ public class SecurityValidateInterceptor implements HandlerInterceptor, WebMvcCo
 
         // 数字签名校验
         if (security != null && security.sign()) {
-            String payload = HttpUtils.serialize(request, true);
+            String payload = HttpUtils.serialize(request);
             String signature = request.getHeader(SecurityContextHolder.SIGNATURE);
             String ciphertext = SecurityContextHolder.getSecurityCipherHandler().sign(session, payload);
             if (!Objects.equals(ciphertext, signature)) {