|
|
|
@ -1,19 +1,23 @@
|
|
|
|
|
package com.supervision.livedigitalavatarmanage.filter;
|
|
|
|
|
|
|
|
|
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
|
|
|
import com.supervision.livedigitalavatarmanage.dto.R;
|
|
|
|
|
import com.supervision.livedigitalavatarmanage.dto.UserDetail;
|
|
|
|
|
import com.supervision.livedigitalavatarmanage.util.JwtUtils;
|
|
|
|
|
import jakarta.servlet.FilterChain;
|
|
|
|
|
import jakarta.servlet.ServletException;
|
|
|
|
|
import jakarta.servlet.http.HttpServletRequest;
|
|
|
|
|
import jakarta.servlet.http.HttpServletResponse;
|
|
|
|
|
import lombok.NonNull;
|
|
|
|
|
import java.io.IOException;
|
|
|
|
|
import lombok.RequiredArgsConstructor;
|
|
|
|
|
import org.springframework.http.HttpStatus;
|
|
|
|
|
import org.springframework.http.MediaType;
|
|
|
|
|
import org.springframework.security.core.context.SecurityContextHolder;
|
|
|
|
|
import org.springframework.security.core.userdetails.UserDetailsService;
|
|
|
|
|
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
|
|
|
|
import org.springframework.security.web.util.matcher.RequestMatcher;
|
|
|
|
|
import org.springframework.stereotype.Component;
|
|
|
|
|
import org.springframework.web.filter.OncePerRequestFilter;
|
|
|
|
|
import org.springframework.security.core.userdetails.UserDetails;
|
|
|
|
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
|
|
|
|
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
|
|
|
|
|
@Component
|
|
|
|
@ -21,42 +25,87 @@ import org.springframework.security.web.authentication.WebAuthenticationDetailsS
|
|
|
|
|
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 {
|
|
|
|
|
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
|
|
|
|
|
FilterChain filterChain) throws IOException, ServletException {
|
|
|
|
|
|
|
|
|
|
// 1. 检查是否在白名单中
|
|
|
|
|
if (isPermitAllRequest(request)) {
|
|
|
|
|
filterChain.doFilter(request, response);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
String authHeader = request.getHeader("Authorization");
|
|
|
|
|
if (authHeader != null && authHeader.startsWith("Bearer ")) {
|
|
|
|
|
// 提取JWT Token(去掉前缀"Bearer ")
|
|
|
|
|
String token = authHeader.substring(7);
|
|
|
|
|
String username;
|
|
|
|
|
//2:根本没有 Authorization 头
|
|
|
|
|
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
|
|
|
|
|
writeTokenErrorResponse(response, "用户未登录,请登录");
|
|
|
|
|
return; // 直接返回,不放行
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
String token = authHeader.substring(7); // 去掉 "Bearer "
|
|
|
|
|
String username;
|
|
|
|
|
// 3:解析 Token 失败(格式错误、签名无效、过期等)
|
|
|
|
|
try {
|
|
|
|
|
username = jwtUtils.getUsernameFromToken(token);
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
writeTokenErrorResponse(response, "Token 无效或已过期,请重新登录");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 4:解析出用户名,但 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) {
|
|
|
|
|
// 根据用户名从数据库加载用户信息
|
|
|
|
|
UserDetail userDetails = (UserDetail) userDetailsService.loadUserByUsername(username);
|
|
|
|
|
// 验证登录时间是否一致,如果不一致说明登录已过期
|
|
|
|
|
// 验证Token的有效性(是否未过期)
|
|
|
|
|
|
|
|
|
|
// 验证 Token 是否仍然有效(时间戳、登录时间等)
|
|
|
|
|
if (jwtUtils.validateLoginTime(token, userDetails.getLastLoginDate())
|
|
|
|
|
&& !jwtUtils.isTokenExpired(token)) {
|
|
|
|
|
// 将用户信息封装到Authentication对象中,标记为已认证
|
|
|
|
|
|
|
|
|
|
//认证成功:设置 SecurityContext
|
|
|
|
|
UsernamePasswordAuthenticationToken authToken =
|
|
|
|
|
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
|
|
|
|
|
authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
|
|
|
|
|
// 将Authentication对象放入SecurityContext,表示当前请求已通过认证
|
|
|
|
|
SecurityContextHolder.getContext().setAuthentication(authToken);
|
|
|
|
|
|
|
|
|
|
//成功后放行,进入 Controller
|
|
|
|
|
filterChain.doFilter(request, response);
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
//Token 过期 或 登录态不一致
|
|
|
|
|
writeTokenErrorResponse(response, "登录已过期,请重新登录");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} catch (UsernameNotFoundException e) {
|
|
|
|
|
writeTokenErrorResponse(response, "用户不存在");
|
|
|
|
|
return;
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
writeTokenErrorResponse(response, "用户认证异常:" + e.getMessage());
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
filterChain.doFilter(request, response);
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|