| | |
| | | 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; |
| | | |
| | | /** |
| | |
| | | |
| | | @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 = |
| | |
| | | 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); |