Codex Assistant
15 小时以前 0a48616045ddce1562584543a0e89e5144051fde
backend/src/main/java/com/rongyichuang/auth/filter/JwtAuthenticationFilter.java
@@ -10,16 +10,22 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.ContentCachingRequestWrapper;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
/**
@@ -35,35 +41,276 @@
    @Autowired
    private UserRepository userRepository;
    // 允许匿名访问的GraphQL查询列表
    private static final List<String> PUBLIC_GRAPHQL_QUERIES = Arrays.asList(
        "carouselPlayList",
        "activities",
        "hello"
    );
    /**
     * 判断是否应该跳过JWT认证
     */
    private boolean shouldSkipAuthentication(String requestURI) {
        // 这些路径不需要JWT认证(已去掉context path)
        String[] skipPaths = {
            "/auth/",
            "/actuator/",
            "/test/",
            "/cleanup/",
            "/upload/",
            "/graphiql"   // GraphiQL开发工具
            // 注意:/graphql 不在跳过列表中,需要由JWT过滤器处理以区分公开和私有查询
        };
        for (String path : skipPaths) {
            if (requestURI.startsWith(path)) {
                return true;
            }
        }
        return false;
    }
    /**
     * 检查是否是GraphQL请求
     */
    private boolean isGraphQLRequest(String requestURI) {
        return "/graphql".equals(requestURI);
    }
    /**
     * 检查GraphQL请求是否为公开查询(不需要认证)
     * 只有明确标记为公开的查询才允许匿名访问
     */
    private boolean isPublicGraphQLQuery(HttpServletRequest request) {
        try {
            // 检查GET参数中的query
            String query = request.getParameter("query");
            if (query != null && !query.trim().isEmpty()) {
                logger.debug("从参数获取GraphQL查询: {}", query);
                return containsPublicQuery(query);
            }
            // 对于POST请求,尝试读取请求体
            if ("POST".equalsIgnoreCase(request.getMethod())) {
                try {
                    // 使用CachedBodyHttpServletRequest来读取请求体
                    String body = getRequestBody(request);
                    if (body != null && !body.trim().isEmpty()) {
                        logger.debug("从请求体获取GraphQL查询: {}", body);
                        // 检查Content-Type
                        String contentType = request.getContentType();
                        if (contentType != null && contentType.contains("application/graphql")) {
                            // 对于application/graphql,请求体直接是GraphQL查询
                            return containsPublicQuery(body);
                        } else {
                            // 对于application/json,简单解析JSON,查找query字段
                            if (body.contains("\"query\"")) {
                                return containsPublicQuery(body);
                            }
                        }
                    }
                } catch (Exception e) {
                    logger.warn("读取POST请求体失败", e);
                }
            }
            return false;
        } catch (Exception e) {
            logger.error("解析GraphQL请求失败", e);
            return false;
        }
    }
    /**
     * 读取请求体内容
     */
    private String getRequestBody(HttpServletRequest request) {
        try {
            if (request instanceof ContentCachingRequestWrapper) {
                ContentCachingRequestWrapper wrapper = (ContentCachingRequestWrapper) request;
                byte[] content = wrapper.getContentAsByteArray();
                if (content.length > 0) {
                    String encoding = wrapper.getCharacterEncoding() != null ? wrapper.getCharacterEncoding() : "UTF-8";
                    return new String(content, encoding);
                }
            }
            // 不从原始请求流读取,避免下游组件拿不到请求体导致 400
            return null;
        } catch (Exception e) {
            logger.warn("读取请求体失败", e);
            return null;
        }
    }
    /**
     * 检查查询字符串是否包含公开查询
     */
    private boolean containsPublicQuery(String queryString) {
        if (queryString == null || queryString.trim().isEmpty()) {
            return false;
        }
        // 检查是否包含公开查询
        for (String publicQuery : PUBLIC_GRAPHQL_QUERIES) {
            if (queryString.contains(publicQuery)) {
                logger.debug("检测到公开GraphQL查询: {}", publicQuery);
                return true;
            }
        }
        return false;
    }
    /**
     * 返回权限错误响应
     */
    private void sendUnauthorizedResponse(HttpServletResponse response) throws IOException {
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().write("{\"errors\":[{\"message\":\"没有权限访问,请先登录\",\"extensions\":{\"code\":\"UNAUTHORIZED\"}}]}");
    }
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, 
                                  FilterChain filterChain) throws ServletException, IOException {
                                    FilterChain filterChain) throws ServletException, IOException {
        String requestURI = request.getRequestURI();
        String contextPath = request.getContextPath();
        // 去掉context path,与Spring Security的行为保持一致
        String pathWithoutContext = requestURI;
        if (contextPath != null && !contextPath.isEmpty() && requestURI.startsWith(contextPath)) {
            pathWithoutContext = requestURI.substring(contextPath.length());
        }
        System.out.println("=== JWT过滤器被调用 === 原始URI: " + requestURI + ", 去掉context path后: " + pathWithoutContext);
        logger.debug("JWT过滤器开始处理请求: {}", pathWithoutContext);
        // 跳过不需要认证的路径
        if (shouldSkipAuthentication(pathWithoutContext)) {
            logger.debug("跳过JWT认证,路径: {}", pathWithoutContext);
            filterChain.doFilter(request, response);
            return;
        }
        // 对GraphQL请求进行特殊处理
        if (isGraphQLRequest(pathWithoutContext)) {
            logger.debug("检测到GraphQL请求");
            // 为POST请求包装请求以支持重复读取请求体
            HttpServletRequest wrappedRequest = request;
            if ("POST".equalsIgnoreCase(request.getMethod())) {
                wrappedRequest = new ContentCachingRequestWrapper(request);
            }
            // 先检查Authorization头,如果没有token,再检查是否为公开查询
            String authHeader = request.getHeader("Authorization");
            if (authHeader == null || !authHeader.startsWith("Bearer ")) {
                logger.debug("GraphQL请求没有Authorization头,尝试判定是否为公开查询");
                // 尝试判定公开查询;如果能确定是公开查询则放行
                if (isPublicGraphQLQuery(wrappedRequest)) {
                    logger.debug("检测到公开GraphQL查询,允许匿名访问");
                    AnonymousAuthenticationToken anonymousAuth = new AnonymousAuthenticationToken(
                        "anonymous",
                        "anonymous",
                        Arrays.asList(new SimpleGrantedAuthority("ROLE_ANONYMOUS"))
                    );
                    SecurityContextHolder.getContext().setAuthentication(anonymousAuth);
                    filterChain.doFilter(wrappedRequest, response);
                    return;
                }
                // 无法可靠读取/判定请求体时,默认以匿名身份放行到GraphQL层,由各Resolver自行进行权限校验
                logger.debug("无法可靠判定是否为公开查询,设置匿名认证并交由GraphQL层处理");
                AnonymousAuthenticationToken anonymousAuth = new AnonymousAuthenticationToken(
                    "anonymous",
                    "anonymous",
                    Arrays.asList(new SimpleGrantedAuthority("ROLE_ANONYMOUS"))
                );
                SecurityContextHolder.getContext().setAuthentication(anonymousAuth);
                filterChain.doFilter(wrappedRequest, response);
                return;
            }
            logger.debug("检测到需要认证的GraphQL请求,开始验证JWT");
            String token = authHeader.substring(7);
            try {
                Long userId = jwtUtil.getUserIdFromToken(token);
                if (userId == null || !jwtUtil.validateToken(token)) {
                    logger.warn("GraphQL请求的JWT token无效");
                    sendUnauthorizedResponse(response);
                    return;
                }
                // 查找用户信息并设置认证
                Optional<User> userOpt = userRepository.findById(userId);
                if (userOpt.isPresent()) {
                    User user = userOpt.get();
                    UsernamePasswordAuthenticationToken authToken =
                        new UsernamePasswordAuthenticationToken(
                            user.getId().toString(),
                            null,
                            new ArrayList<>()
                        );
                    authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                    SecurityContextHolder.getContext().setAuthentication(authToken);
                    logger.debug("GraphQL请求认证成功: userId={}", user.getId());
                } else {
                    logger.warn("GraphQL请求的用户不存在: userId={}", userId);
                    sendUnauthorizedResponse(response);
                    return;
                }
            } catch (Exception e) {
                logger.error("GraphQL请求JWT验证失败: {}", e.getMessage());
                sendUnauthorizedResponse(response);
                return;
            }
            // 继续处理请求
            filterChain.doFilter(wrappedRequest, response);
            return;
        }
        
        String authHeader = request.getHeader("Authorization");
        String token = null;
        Long userId = null;
        logger.debug("Authorization头: {}", authHeader);
        // 从请求头中提取JWT token
        if (authHeader != null && authHeader.startsWith("Bearer ")) {
            token = authHeader.substring(7);
            logger.debug("提取到JWT token: {}", token.substring(0, Math.min(20, token.length())) + "...");
            try {
                userId = jwtUtil.getUserIdFromToken(token);
                logger.debug("从token中解析到用户ID: {}", userId);
            } catch (Exception e) {
                logger.debug("JWT token解析失败: {}", e.getMessage());
                logger.error("JWT token解析失败: {}", e.getMessage(), e);
            }
        } else {
            logger.debug("没有找到Authorization头或格式不正确");
        }
        // 如果token有效且当前没有认证信息
        if (userId != null && SecurityContextHolder.getContext().getAuthentication() == null) {
        // 如果token有效且当前是匿名或无认证,则进行认证
        Authentication existingAuth = SecurityContextHolder.getContext().getAuthentication();
        boolean isAnonymous = (existingAuth == null) || ("anonymousUser".equals(String.valueOf(existingAuth.getPrincipal())));
        if (userId != null && isAnonymous) {
            logger.debug("开始验证token有效性");
            
            // 验证token是否有效
            if (jwtUtil.validateToken(token)) {
                logger.debug("Token验证成功,查找用户信息");
                
                // 查找用户信息
                Optional<User> userOpt = userRepository.findById(userId);
                if (userOpt.isPresent()) {
                    User user = userOpt.get();
                    logger.debug("找到用户: userId={}, phone={}", user.getId(), user.getPhone());
                    
                    // 创建认证对象
                    UsernamePasswordAuthenticationToken authToken = 
@@ -76,9 +323,17 @@
                    authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                    SecurityContextHolder.getContext().setAuthentication(authToken);
                    
                    logger.debug("用户认证成功: userId={}, phone={}", user.getId(), user.getPhone());
                    logger.info("用户认证成功: userId={}, phone={}", user.getId(), user.getPhone());
                } else {
                    logger.warn("用户不存在: userId={}", userId);
                }
            } else {
                logger.warn("Token验证失败");
            }
        } else if (userId == null) {
            logger.debug("没有解析到用户ID");
        } else {
            logger.debug("已存在非匿名认证信息,跳过JWT认证");
        }
        filterChain.doFilter(request, response);