Codex Assistant
14 小时以前 0a48616045ddce1562584543a0e89e5144051fde
backend/src/main/java/com/rongyichuang/auth/filter/JwtAuthenticationFilter.java
@@ -10,7 +10,9 @@
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;
@@ -18,9 +20,12 @@
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;
/**
@@ -37,6 +42,13 @@
    @Autowired
    private UserRepository userRepository;
    
    // 允许匿名访问的GraphQL查询列表
    private static final List<String> PUBLIC_GRAPHQL_QUERIES = Arrays.asList(
        "carouselPlayList",
        "activities",
        "hello"
    );
    /**
     * 判断是否应该跳过JWT认证
     */
@@ -48,8 +60,8 @@
            "/test/",
            "/cleanup/",
            "/upload/",
            "/graphql",
            "/graphiql"
            "/graphiql"   // GraphiQL开发工具
            // 注意:/graphql 不在跳过列表中,需要由JWT过滤器处理以区分公开和私有查询
        };
        
        for (String path : skipPaths) {
@@ -59,6 +71,107 @@
        }
        
        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
@@ -83,6 +196,86 @@
            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;