package com.rongyichuang.auth.filter;
|
|
import com.rongyichuang.auth.util.JwtUtil;
|
import com.rongyichuang.user.entity.User;
|
import com.rongyichuang.user.repository.UserRepository;
|
import jakarta.servlet.FilterChain;
|
import jakarta.servlet.ServletException;
|
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletResponse;
|
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;
|
|
/**
|
* JWT认证过滤器
|
*/
|
@Component
|
public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
|
private static final Logger logger = LoggerFactory.getLogger(JwtAuthenticationFilter.class);
|
|
@Autowired
|
private JwtUtil jwtUtil;
|
|
@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_FORBIDDEN);
|
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 {
|
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查询,允许匿名访问");
|
|
// 设置匿名认证,让Spring Security知道这是一个已认证的匿名用户
|
AnonymousAuthenticationToken anonymousAuth = new AnonymousAuthenticationToken(
|
"anonymous",
|
"anonymous",
|
Arrays.asList(new SimpleGrantedAuthority("ROLE_ANONYMOUS"))
|
);
|
SecurityContextHolder.getContext().setAuthentication(anonymousAuth);
|
logger.debug("为公开GraphQL查询设置匿名认证");
|
|
filterChain.doFilter(wrappedRequest, response);
|
return;
|
}
|
|
logger.warn("GraphQL请求缺少有效的Authorization头且不是公开查询");
|
sendUnauthorizedResponse(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.error("JWT token解析失败: {}", e.getMessage(), e);
|
}
|
} else {
|
logger.debug("没有找到Authorization头或格式不正确");
|
}
|
|
// 如果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 =
|
new UsernamePasswordAuthenticationToken(
|
user.getId().toString(),
|
null,
|
new ArrayList<>() // 暂时不设置权限,后续可以根据角色设置
|
);
|
|
authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
|
SecurityContextHolder.getContext().setAuthentication(authToken);
|
|
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);
|
}
|
}
|