Ver código fonte

优化日志查询和ip查询

liuchengsen 4 semanas atrás
pai
commit
675007145a

+ 2 - 5
backend-java/src/main/java/com/pharmacopoeia/controller/AnalyticsController.java

@@ -1,6 +1,7 @@
 package com.pharmacopoeia.controller;
 
 import com.pharmacopoeia.service.AnalyticsService;
+import com.pharmacopoeia.util.IpUtils;
 import jakarta.servlet.http.HttpServletRequest;
 import org.springframework.http.ResponseEntity;
 import org.springframework.web.bind.annotation.*;
@@ -29,11 +30,7 @@ public class AnalyticsController {
         String referrer = (String) body.getOrDefault("referrer", "");
 
         String userKey = getCurrentUserKey();
-        String ip = request.getRemoteAddr();
-        String forwarded = request.getHeader("X-Forwarded-For");
-        if (forwarded != null && !forwarded.isBlank()) {
-            ip = forwarded.split(",")[0].trim();
-        }
+        String ip = IpUtils.getClientIp(request);
         String ua = request.getHeader("User-Agent");
 
         // fire-and-forget:异步写入,不等待

+ 2 - 5
backend-java/src/main/java/com/pharmacopoeia/controller/ChatController.java

@@ -6,6 +6,7 @@ import com.pharmacopoeia.dto.FeedbackRequest;
 import com.pharmacopoeia.dto.ImageChatRequest;
 import com.pharmacopoeia.dto.MultimodalChatRequest;
 import com.pharmacopoeia.service.*;
+import com.pharmacopoeia.util.IpUtils;
 import jakarta.servlet.http.HttpServletRequest;
 import lombok.extern.slf4j.Slf4j;
 import org.jetbrains.annotations.NotNull;
@@ -825,11 +826,7 @@ public class ChatController {
             return auth.getName();
         }
         // 未登录用 IP 隔离,避免不同手机会话串了
-        String ip = request.getRemoteAddr();
-        String forwarded = request.getHeader("X-Forwarded-For");
-        if (forwarded != null && !forwarded.isBlank()) {
-            ip = forwarded.split(",")[0].trim();
-        }
+        String ip = IpUtils.getClientIp(request);
         return "ip:" + ip;
     }
 

+ 69 - 58
backend-java/src/main/java/com/pharmacopoeia/security/JwtAuthFilter.java

@@ -8,6 +8,8 @@ import jakarta.servlet.http.HttpServletResponse;
 import org.jetbrains.annotations.NotNull;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
+import org.slf4j.MDC;
+import com.pharmacopoeia.util.IpUtils;
 import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
 import org.springframework.security.core.context.SecurityContextHolder;
 import org.springframework.stereotype.Component;
@@ -22,7 +24,9 @@ public class JwtAuthFilter extends OncePerRequestFilter {
 
     private static final Logger log = LoggerFactory.getLogger(JwtAuthFilter.class);
 
-    /** 始终公开的路径(不需要 token) */
+    /**
+     * 始终公开的路径(不需要 token)
+     */
     private static final Set<String> PUBLIC_PREFIXES = Set.of(
             "/health",
             "/api/v1/auth/",
@@ -51,74 +55,81 @@ public class JwtAuthFilter extends OncePerRequestFilter {
     }
 
     @Override
-    protected void doFilterInternal(HttpServletRequest request,
+    protected void doFilterInternal(@NotNull HttpServletRequest request,
                                     @NotNull HttpServletResponse response,
                                     @NotNull FilterChain filterChain) throws ServletException, IOException {
-        String uri = request.getRequestURI();
-        String method = request.getMethod();
+        String ip = IpUtils.getClientIp(request);
+        MDC.put("ip", ip);
+        try {
+            String uri = request.getRequestURI();
+            String method = request.getMethod();
 
-        // 公开路径:不要求 token,但如果带来了有效 token 仍解析出用户身份
-        if (isPublicPath(uri)) {
-            String header = request.getHeader("Authorization");
-            if (header != null && header.startsWith("Bearer ")) {
-                try {
-                    String token = header.substring(7);
-                    if (jwtUtil.validateToken(token)) {
-                        String subject = jwtUtil.getSubject(token);
-                        SecurityContextHolder.getContext().setAuthentication(
-                                new UsernamePasswordAuthenticationToken(subject, null, Collections.emptyList()));
+            // 公开路径:不要求 token,但如果带来了有效 token 仍解析出用户身份
+            if (isPublicPath(uri)) {
+                String header = request.getHeader("Authorization");
+                if (header != null && header.startsWith("Bearer ")) {
+                    try {
+                        String token = header.substring(7);
+                        if (jwtUtil.validateToken(token)) {
+                            String subject = jwtUtil.getSubject(token);
+                            SecurityContextHolder.getContext().setAuthentication(
+                                    new UsernamePasswordAuthenticationToken(subject, null, Collections.emptyList()));
+                        }
+                    } catch (Exception ignored) {
                     }
-                } catch (Exception ignored) {}
-            }
-            filterChain.doFilter(request, response);
-            return;
-        }
-
-        String header = request.getHeader("Authorization");
-
-        if (header != null && header.startsWith("Bearer ")) {
-            String token = header.substring(7);
-            log.info("JWT 认证请求: {} {} | token={}...", method, uri, token.substring(0, Math.min(20, token.length())));
-            if (jwtUtil.validateToken(token)) {
-                String subject = jwtUtil.getSubject(token);
-                var auth = new UsernamePasswordAuthenticationToken(subject, null, Collections.emptyList());
-                SecurityContextHolder.getContext().setAuthentication(auth);
-                log.info("JWT 认证通过: subject={}, uri={} {}", subject, method, uri);
+                }
                 filterChain.doFilter(request, response);
                 return;
             }
-            // token 无效
-            log.warn("JWT 认证拒绝(token无效): {} {}", method, uri);
-            if (authProperties.isEnabled()) {
-                send401(response, "token 无效或已过期");
-                return;
-            }
-        } else {
-            // 无 token
-            log.warn("JWT 认证拒绝(无token): {} {}", method, uri);
-            if (authProperties.isEnabled()) {
-                send401(response, "未提供认证 token");
-                return;
-            }
-        }
 
-        // 鉴权未启用,放行
-        log.debug("鉴权未启用,放行: {} {}", method, uri);
-        filterChain.doFilter(request, response);
-    }
+            String header = request.getHeader("Authorization");
 
-    private boolean isPublicPath(String uri) {
-        for (String prefix : PUBLIC_PREFIXES) {
-            if (uri.startsWith(prefix)) {
-                return true;
+            if (header != null && header.startsWith("Bearer ")) {
+                String token = header.substring(7);
+                log.info("JWT 认证请求: {} {} | token={}...", method, uri, token.substring(0, Math.min(20, token.length())));
+                if (jwtUtil.validateToken(token)) {
+                    String subject = jwtUtil.getSubject(token);
+                    var auth = new UsernamePasswordAuthenticationToken(subject, null, Collections.emptyList());
+                    SecurityContextHolder.getContext().setAuthentication(auth);
+                    log.info("JWT 认证通过: subject={}, uri={} {}", subject, method, uri);
+                    filterChain.doFilter(request, response);
+                    return;
+                }
+                // token 无效
+                log.warn("JWT 认证拒绝(token无效): {} {}", method, uri);
+                if (authProperties.isEnabled()) {
+                    send401(response, "token 无效或已过期");
+                    return;
+                }
+            } else {
+                // 无 token
+                log.warn("JWT 认证拒绝(无token): {} {}", method, uri);
+                if (authProperties.isEnabled()) {
+                    send401(response, "未提供认证 token");
+                    return;
+                }
             }
+
+            // 鉴权未启用,放行
+            log.debug("鉴权未启用,放行: {} {}", method, uri);
+            filterChain.doFilter(request, response);
+        }finally {
+            MDC.clear();
         }
-        return "/".equals(uri) || uri.endsWith(".html") || uri.endsWith(".js") || uri.endsWith(".css");
-    }
+}
 
-    private void send401(HttpServletResponse response, String msg) throws IOException {
-        response.setStatus(401);
-        response.setContentType("application/json;charset=UTF-8");
-        response.getWriter().write("{\"error\":\"" + msg + "\",\"code\":401}");
+private boolean isPublicPath(String uri) {
+    for (String prefix : PUBLIC_PREFIXES) {
+        if (uri.startsWith(prefix)) {
+            return true;
+        }
     }
+    return "/".equals(uri) || uri.endsWith(".html") || uri.endsWith(".js") || uri.endsWith(".css");
+}
+
+private void send401(HttpServletResponse response, String msg) throws IOException {
+    response.setStatus(401);
+    response.setContentType("application/json;charset=UTF-8");
+    response.getWriter().write("{\"error\":\"" + msg + "\",\"code\":401}");
+}
 }

+ 25 - 18
backend-java/src/main/java/com/pharmacopoeia/security/RateLimitFilter.java

@@ -2,6 +2,9 @@ package com.pharmacopoeia.security;
 
 import com.pharmacopoeia.config.RateLimitProperties;
 import jakarta.servlet.FilterChain;
+import org.jetbrains.annotations.NotNull;
+import org.slf4j.MDC;
+import com.pharmacopoeia.util.IpUtils;
 import jakarta.servlet.ServletException;
 import jakarta.servlet.http.HttpServletRequest;
 import jakarta.servlet.http.HttpServletResponse;
@@ -28,31 +31,35 @@ public class RateLimitFilter extends OncePerRequestFilter {
     }
 
     @Override
-    protected void doFilterInternal(HttpServletRequest request,
-                                    HttpServletResponse response,
-                                    FilterChain filterChain) throws ServletException, IOException {
-        boolean isChatApi = request.getRequestURI().contains("/chat/")
-                && request.getMethod().equalsIgnoreCase("POST");
-
-        if (isChatApi) {
-            String ip = request.getRemoteAddr();
-            var ipCount = minuteCounter.computeIfAbsent(ip, k -> new AtomicInteger(0));
-            int count = ipCount.incrementAndGet();
-            if (count > props.getPerMinute()) {
-                ipCount.decrementAndGet(); // 超限不计
-                writeRateLimitError(response, "请求过于频繁,请稍后再试");
-                return;
+    protected void doFilterInternal(@NotNull HttpServletRequest request,
+                                    @NotNull HttpServletResponse response,
+                                    @NotNull FilterChain filterChain) throws ServletException, IOException {
+        String ip = IpUtils.getClientIp(request);
+        MDC.put("ip", ip);
+        try {
+            boolean isChatApi = request.getRequestURI().contains("/chat/")
+                    && "POST".equalsIgnoreCase(request.getMethod());
+            if (isChatApi) {
+                var ipCount = minuteCounter.computeIfAbsent(ip, k -> new AtomicInteger(0));
+                int count = ipCount.incrementAndGet();
+                if (count > props.getPerMinute()) {
+                    ipCount.decrementAndGet(); // 超限不计
+                    writeRateLimitError(response);
+                    return;
+                }
             }
-        }
 
-        filterChain.doFilter(request, response);
+            filterChain.doFilter(request, response);
+        } finally {
+            MDC.clear();
+        }
     }
 
-    private static void writeRateLimitError(HttpServletResponse response, String message) throws IOException {
+    private static void writeRateLimitError(HttpServletResponse response) throws IOException {
         response.setStatus(429);
         response.setContentType("application/json;charset=UTF-8");
         response.getWriter().write(
-            "{\"error\":\"" + message + "\",\"detail\":\"" + message + "\",\"code\":429}"
+                "{\"error\":\"" + "请求过于频繁,请稍后再试" + "\",\"detail\":\"" + "请求过于频繁,请稍后再试" + "\",\"code\":429}"
         );
     }
 }

+ 28 - 0
backend-java/src/main/java/com/pharmacopoeia/util/IpUtils.java

@@ -0,0 +1,28 @@
+package com.pharmacopoeia.util;
+
+import jakarta.servlet.http.HttpServletRequest;
+
+public final class IpUtils {
+
+    private IpUtils() {}
+
+   public static String getClientIp(HttpServletRequest request) {
+        // X-Forwarded-For: client, proxy1, proxy2, ...
+        String xff = request.getHeader("X-Forwarded-For");
+        if (xff != null && !xff.isBlank()) {
+            for (String part : xff.split(",")) {
+                String ip = part.trim();
+                if (!ip.isEmpty() && !"unknown".equalsIgnoreCase(ip)) {
+                    return ip;
+                }
+            }
+        }
+        for (String header : new String[]{"X-Real-IP", "Proxy-Client-IP", "WL-Proxy-Client-IP"}) {
+            String val = request.getHeader(header);
+            if (val != null && !val.isBlank() && !"unknown".equalsIgnoreCase(val.trim())) {
+                return val.trim();
+            }
+        }
+        return request.getRemoteAddr();
+   }
+}

+ 3 - 3
backend-java/src/main/resources/logback-spring.xml

@@ -12,7 +12,7 @@
     <!-- 控制台输出(开发调试用) -->
     <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
         <encoder>
-            <pattern>%d{HH:mm:ss.SSS} %-5level %logger{36}.%M\(%line\) - %msg%n</pattern>
+            <pattern>%d{HH:mm:ss.SSS} %-5level [%thread] [%X{ip}] %logger{20}.%M\(%line\) - %msg%n</pattern>
             <charset>UTF-8</charset>
         </encoder>
     </appender>
@@ -27,7 +27,7 @@
             <totalSizeCap>${TOTAL_SIZE}</totalSizeCap>
         </rollingPolicy>
         <encoder>
-            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level %logger{36}.%M\(%line\) - %msg%n</pattern>
+            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%thread] [%X{ip}] %logger{20}.%M\(%line\) - %msg%n</pattern>
             <charset>UTF-8</charset>
             <immediateFlush>true</immediateFlush>
         </encoder>
@@ -46,7 +46,7 @@
             <totalSizeCap>512MB</totalSizeCap>
         </rollingPolicy>
         <encoder>
-            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level %logger{36}.%M\(%line\) - %msg%n</pattern>
+            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%thread] [%X{ip}] %logger{20}.%M\(%line\) - %msg%n</pattern>
             <charset>UTF-8</charset>
             <immediateFlush>true</immediateFlush>
         </encoder>