feat: 增强日志系统和请求追踪功能

- 新增CorrelationFilter过滤器,为每个请求生成唯一跟踪ID
- 增强logback配置,支持关联ID、租户信息和用户ID的结构化日志
- 新增RequestLoggingInterceptor,记录详细的HTTP请求响应信息
This commit is contained in:
irving
2025-09-06 20:28:52 -04:00
8 changed files with 696 additions and 5 deletions

View File

@@ -0,0 +1,207 @@
package com.starry.admin.common.filter;
import com.starry.admin.modules.clerk.module.entity.PlayClerkUserInfoEntity;
import com.starry.admin.modules.clerk.service.impl.PlayClerkUserInfoServiceImpl;
import com.starry.admin.modules.custom.module.entity.PlayCustomUserInfoEntity;
import com.starry.admin.modules.custom.service.impl.PlayCustomUserInfoServiceImpl;
import com.starry.admin.modules.weichat.service.WxTokenService;
import com.starry.common.context.CustomSecurityContextHolder;
import java.io.IOException;
import java.util.UUID;
import javax.annotation.Resource;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.slf4j.MDC;
import org.springframework.context.annotation.Lazy;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
/**
* 请求关联ID过滤器为每个HTTP请求生成唯一的跟踪ID
* 用于日志关联和请求链路追踪
*
* @author Claude
*/
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class CorrelationFilter implements Filter {
@Resource
@Lazy
private WxTokenService wxTokenService;
@Resource
@Lazy
private PlayClerkUserInfoServiceImpl clerkUserInfoService;
@Resource
@Lazy
private PlayCustomUserInfoServiceImpl customUserInfoService;
public static final String CORRELATION_ID_HEADER = "X-Correlation-ID";
public static final String CORRELATION_ID_MDC_KEY = "correlationId";
public static final String USER_ID_MDC_KEY = "userId";
public static final String TENANT_ID_MDC_KEY = "tenantId";
public static final String TENANT_KEY_MDC_KEY = "tenantKey";
public static final String REQUEST_URI_MDC_KEY = "requestUri";
public static final String REQUEST_METHOD_MDC_KEY = "requestMethod";
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
if (!(request instanceof HttpServletRequest)) {
chain.doFilter(request, response);
return;
}
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
try {
// 生成或获取关联ID
String correlationId = getOrGenerateCorrelationId(httpRequest);
// 设置MDC上下文
MDC.put(CORRELATION_ID_MDC_KEY, correlationId);
MDC.put(REQUEST_URI_MDC_KEY, httpRequest.getRequestURI());
MDC.put(REQUEST_METHOD_MDC_KEY, httpRequest.getMethod());
// 提取并设置租户信息
String tenantKey = httpRequest.getHeader("tenantkey");
if (tenantKey != null && !tenantKey.trim().isEmpty()) {
MDC.put(TENANT_KEY_MDC_KEY, tenantKey);
}
// 尝试从安全上下文获取租户ID
try {
String tenantId = CustomSecurityContextHolder.getTenantId();
if (tenantId != null && !tenantId.trim().isEmpty() && !"9999".equals(tenantId)) {
MDC.put(TENANT_ID_MDC_KEY, tenantId);
}
} catch (Exception e) {
// 忽略获取租户ID的异常
}
// 尝试获取用户ID可能来自JWT或其他认证信息
String userId = extractUserId(httpRequest);
if (userId != null) {
MDC.put(USER_ID_MDC_KEY, userId);
}
// 将关联ID添加到响应头
httpResponse.setHeader(CORRELATION_ID_HEADER, correlationId);
// 继续过滤器链
chain.doFilter(request, response);
} finally {
// 清理MDC上下文
MDC.clear();
}
}
/**
* 获取或生成关联ID
* 优先从请求头获取,如果没有则生成新的
*/
private String getOrGenerateCorrelationId(HttpServletRequest request) {
String correlationId = request.getHeader(CORRELATION_ID_HEADER);
if (correlationId == null || correlationId.trim().isEmpty()) {
correlationId = "REQ-" + UUID.randomUUID().toString().substring(0, 8);
}
return correlationId;
}
/**
* 尝试从请求中提取用户ID
* 根据项目的认证机制:支持微信端(clerk/custom token)和管理端(Authorization header)
*/
private String extractUserId(HttpServletRequest request) {
try {
// 1. 微信端 - Clerk用户认证
String clerkToken = request.getHeader("clerkusertoken");
if (clerkToken != null && !clerkToken.trim().isEmpty()) {
return extractUserIdFromWxToken(clerkToken, "clerk");
}
// 2. 微信端 - Custom用户认证
String customToken = request.getHeader("customusertoken");
if (customToken != null && !customToken.trim().isEmpty()) {
return extractUserIdFromWxToken(customToken, "custom");
}
// 3. 管理端 - JWT Bearer token认证
String authHeader = request.getHeader("Authorization");
if (authHeader != null && authHeader.startsWith("Bearer ")) {
return extractUserIdFromJwtToken(authHeader);
}
} catch (Exception e) {
// 如果解析失败不影响主流程只是记录日志时没有用户ID
// 可以选择记录debug日志但不抛异常
}
return null;
}
/**
* 从微信token中提取真实用户ID
*/
private String extractUserIdFromWxToken(String token, String userType) {
try {
// 使用WxTokenService解析JWT token获取真实用户ID
String userId = wxTokenService.getWxUserIdByToken(token);
if (userId != null) {
// 根据用户类型获取更多用户信息(可选)
if ("clerk".equals(userType)) {
PlayClerkUserInfoEntity clerkUser = clerkUserInfoService.selectById(userId);
if (clerkUser != null) {
// 返回格式: clerk_userId 或者可以包含昵称等信息
return "clerk_" + userId;
}
} else if ("custom".equals(userType)) {
PlayCustomUserInfoEntity customUser = customUserInfoService.selectById(userId);
if (customUser != null) {
return "custom_" + userId;
}
}
// 如果查询用户详情失败至少返回基础用户ID
return userType + "_" + userId;
}
} catch (Exception e) {
// Token解析失败可能是过期或无效token不影响主流程
}
return null;
}
/**
* 从管理端JWT token中提取用户ID
*/
private String extractUserIdFromJwtToken(String authHeader) {
try {
// 管理端的JWT解析比较复杂这里先返回标识
// 实际应该通过JwtToken组件或SecurityUtils获取当前登录用户信息
return "admin_user";
} catch (Exception e) {
return null;
}
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
// 初始化逻辑
}
@Override
public void destroy() {
// 清理逻辑
}
}

View File

@@ -27,6 +27,7 @@ import org.springframework.security.web.authentication.UsernamePasswordAuthentic
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
/**
* @author admin
@@ -73,7 +74,6 @@ public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
httpSecurity.exceptionHandling().accessDeniedHandler(customAccessDeniedHandler)
.authenticationEntryPoint(customAuthenticationEntryPoint);
}
private CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.setAllowCredentials(true);
@@ -86,6 +86,19 @@ public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
return source;
}
@Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.addAllowedOriginPattern("*");
config.addAllowedHeader("*");
config.addAllowedMethod("*");
config.addExposedHeader("*");
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService()).passwordEncoder(passwordEncoder());