|
|
|
@ -1,5 +1,7 @@
|
|
|
|
|
package com.supervision.filter;
|
|
|
|
|
|
|
|
|
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
|
|
|
import com.supervision.dto.R;
|
|
|
|
|
import com.supervision.util.JwtUtils;
|
|
|
|
|
import jakarta.servlet.FilterChain;
|
|
|
|
|
import jakarta.servlet.ServletException;
|
|
|
|
@ -7,14 +9,17 @@ import jakarta.servlet.http.HttpServletRequest;
|
|
|
|
|
import jakarta.servlet.http.HttpServletResponse;
|
|
|
|
|
import lombok.NonNull;
|
|
|
|
|
import lombok.RequiredArgsConstructor;
|
|
|
|
|
import org.springframework.http.HttpStatus;
|
|
|
|
|
import org.springframework.http.MediaType;
|
|
|
|
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
|
|
|
|
import org.springframework.security.core.context.SecurityContextHolder;
|
|
|
|
|
import org.springframework.security.core.userdetails.UserDetails;
|
|
|
|
|
import org.springframework.security.core.userdetails.UserDetailsService;
|
|
|
|
|
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
|
|
|
|
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
|
|
|
|
|
import org.springframework.security.web.util.matcher.RequestMatcher;
|
|
|
|
|
import org.springframework.stereotype.Component;
|
|
|
|
|
import org.springframework.web.filter.OncePerRequestFilter;
|
|
|
|
|
|
|
|
|
|
import java.io.IOException;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@ -23,40 +28,88 @@ import java.io.IOException;
|
|
|
|
|
public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
|
|
|
|
|
|
|
|
|
private final JwtUtils jwtUtils;
|
|
|
|
|
|
|
|
|
|
private final UserDetailsService userDetailsService;
|
|
|
|
|
|
|
|
|
|
private final ObjectMapper objectMapper;
|
|
|
|
|
|
|
|
|
|
private final RequestMatcher[] permitAllRequestMatchers;
|
|
|
|
|
@Override
|
|
|
|
|
protected void doFilterInternal(HttpServletRequest request, @NonNull HttpServletResponse response,
|
|
|
|
|
@NonNull FilterChain filterChain) throws ServletException, IOException {
|
|
|
|
|
|
|
|
|
|
// 1. 检查是否在白名单中
|
|
|
|
|
if (isPermitAllRequest(request)) {
|
|
|
|
|
filterChain.doFilter(request, response);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
// 获取 Authorization 头
|
|
|
|
|
String authHeader = request.getHeader("Authorization");
|
|
|
|
|
if (authHeader != null && authHeader.startsWith("Bearer ")) {
|
|
|
|
|
// 提取JWT Token(去掉前缀"Bearer ")
|
|
|
|
|
String token = authHeader.substring(7);
|
|
|
|
|
String username;
|
|
|
|
|
// 提取 token(去掉 "Bearer " 前缀)
|
|
|
|
|
String token = authHeader.substring(7);
|
|
|
|
|
String username;
|
|
|
|
|
|
|
|
|
|
// 2:解析 token 失败(格式错误、签名无效、过期等)
|
|
|
|
|
try {
|
|
|
|
|
username = jwtUtils.getUsernameFromToken(token);
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
writeTokenErrorResponse(response, "Token 无效或已过期,请重新登录");
|
|
|
|
|
return; // 中断
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 3:成功解析出用户名,且当前 SecurityContext 未认证
|
|
|
|
|
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
|
|
|
|
|
try {
|
|
|
|
|
// 从JWT中解析用户名
|
|
|
|
|
username = jwtUtils.getUsernameFromToken(token);
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
// 如果JWT格式不正确或过期,直接放行(后续的过滤器会处理认证失败)
|
|
|
|
|
filterChain.doFilter(request, response);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
// 如果成功提取到用户名,并且当前没有已认证的用户
|
|
|
|
|
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
|
|
|
|
|
// 根据用户名从数据库加载用户信息
|
|
|
|
|
// 从 UserDetailsService 加载用户信息
|
|
|
|
|
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
|
|
|
|
|
// 验证Token的有效性(是否未过期)
|
|
|
|
|
|
|
|
|
|
// 验证 Token 是否未过期(你可以根据需要添加更多验证,如登录时间)
|
|
|
|
|
if (!jwtUtils.isTokenExpired(token)) {
|
|
|
|
|
// 将用户信息封装到Authentication对象中,标记为已认证
|
|
|
|
|
// 创建认证对象
|
|
|
|
|
UsernamePasswordAuthenticationToken authToken =
|
|
|
|
|
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
|
|
|
|
|
authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
|
|
|
|
|
// 将Authentication对象放入SecurityContext,表示当前请求已通过认证
|
|
|
|
|
|
|
|
|
|
// 设置到 SecurityContext
|
|
|
|
|
SecurityContextHolder.getContext().setAuthentication(authToken);
|
|
|
|
|
|
|
|
|
|
// 认证成功!放行到 Controller
|
|
|
|
|
filterChain.doFilter(request, response);
|
|
|
|
|
return;
|
|
|
|
|
} else {
|
|
|
|
|
// Token 已过期
|
|
|
|
|
writeTokenErrorResponse(response, "Token 已过期,请重新登录");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} catch (UsernameNotFoundException e) {
|
|
|
|
|
// 用户不存在
|
|
|
|
|
writeTokenErrorResponse(response, "用户不存在或已被删除");
|
|
|
|
|
return;
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
// 其他加载异常
|
|
|
|
|
writeTokenErrorResponse(response, "用户信息加载失败:" + e.getMessage());
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
filterChain.doFilter(request, response);
|
|
|
|
|
|
|
|
|
|
// 特殊情况兜底:比如 token 解析成功但 userDetails 为 null,或已认证但不符合预期
|
|
|
|
|
// 根据你的“核心逻辑”,只要没成功放行,就视为失败
|
|
|
|
|
writeTokenErrorResponse(response, "认证失败,请重新登录");
|
|
|
|
|
}
|
|
|
|
|
private void writeTokenErrorResponse(HttpServletResponse response, String message) throws IOException {
|
|
|
|
|
response.setStatus(HttpStatus.OK.value()); // 设置为 200
|
|
|
|
|
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
|
|
|
|
|
response.setCharacterEncoding("UTF-8");
|
|
|
|
|
objectMapper.writeValue(response.getWriter(), R.fail(401, message));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private boolean isPermitAllRequest(HttpServletRequest request) {
|
|
|
|
|
for (RequestMatcher matcher : permitAllRequestMatchers) {
|
|
|
|
|
if (matcher.matches(request)) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|