| | |
| | | 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.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认证 |
| | | */ |
| | |
| | | "/test/", |
| | | "/cleanup/", |
| | | "/upload/", |
| | | "/graphql", |
| | | "/graphiql" |
| | | "/graphiql" // GraphiQL开发工具 |
| | | // 注意:/graphql 不在跳过列表中,需要由JWT过滤器处理以区分公开和私有查询 |
| | | }; |
| | | |
| | | for (String path : skipPaths) { |
| | |
| | | } |
| | | |
| | | 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 |
| | |
| | | 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; |