이전 포스트에서는 JWT 토큰을 생성하고 검증하는 Provider를 구현했습니다. 이번에는 실제로 HTTP 요청을 처리할 때 JWT 토큰을 검증하고 인증 정보를 설정하는 필터를 구현해보겠습니다.
Spring Security의 OncePerRequestFilter를 상속받아 JWT 인증 필터를 구현합니다.
@Slf4j
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtTokenProvider jwtTokenProvider;
private final UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
try {
String token = extractToken(request);
log.debug("Extracted token: {}", token != null ?
token.substring(0, Math.min(token.length(), 20)) + "..." : "null");
if (token != null && jwtTokenProvider.validateToken(token)) {
String userId = jwtTokenProvider.getIdFromToken(token);
UserDetails userDetails = userDetailsService.loadUserByUsername(userId);
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
userDetails,
null,
userDetails.getAuthorities()
);
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
log.debug("Authentication set for user: {}", userId);
}
filterChain.doFilter(request, response);
} catch (Exception e) {
log.error("Security Context에 인증 정보를 저장할 수 없습니다", e);
handleAuthenticationException(response, e);
}
}
}
요청에서 토큰을 추출하는 방법을 구현합니다. Authorization 헤더와 URL 파라미터 두 가지 방식을 지원합니다.
private String extractToken(HttpServletRequest request) {
// 1. Authorization 헤더 확인
String bearerToken = request.getHeader("Authorization");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
// 2. URL 파라미터 확인
String paramToken = request.getParameter("token");
if (StringUtils.hasText(paramToken)) {
return paramToken;
}
return null;
}
특정 경로에 대해서만 필터를 적용하도록 설정합니다.
@Override
protected boolean shouldNotFilter(HttpServletRequest request) {
String path = request.getServletPath();
log.debug("Checking if should filter path: {}", path);
AntPathMatcher pathMatcher = new AntPathMatcher();
boolean shouldNotFilter = Arrays.asList(
"/",
"/auth/**",
"/login/**",
"/oauth2/**",
"/css/**",
"/js/**",
"/images/**",
"/*.ico",
"/error",
"/resources/**"
).stream().anyMatch(pattern -> pathMatcher.match(pattern, path));
log.debug("Path {} should {} be filtered", path, !shouldNotFilter ? "" : "not");
return shouldNotFilter;
}
인증 실패 시 적절한 에러 응답을 생성합니다.
private void handleAuthenticationException(HttpServletResponse response, Exception e)
throws IOException {
log.error("Authentication error occurred", e);
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json;charset=UTF-8");
ErrorResponse errorResponse = new ErrorResponse(
HttpServletResponse.SC_UNAUTHORIZED,
"인증이 필요합니다",
e.getMessage()
);
String jsonResponse = new ObjectMapper().writeValueAsString(errorResponse);
response.getWriter().write(jsonResponse);
}
@Getter
@AllArgsConstructor
class ErrorResponse {
private int status;
private String message;
private String detail;
}
채팅 시스템에서의 JWT 인증 처리 예시입니다.
@GetMapping("/rooms")
public String chatRooms(Model model, HttpServletRequest request) {
try {
String bearerToken = request.getHeader("Authorization");
if (bearerToken == null || !bearerToken.startsWith("Bearer ")) {
// 쿠키에서 토큰 확인
Cookie[] cookies = request.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
if ("accessToken".equals(cookie.getName())) {
bearerToken = "Bearer " + cookie.getValue();
break;
}
}
}
}
if (bearerToken == null || !bearerToken.startsWith("Bearer ")) {
log.warn("토큰이 없거나 잘못된 형식입니다");
return "redirect:/auth/login";
}
String token = bearerToken.substring(7);
if (jwtTokenProvider.validateToken(token)) {
int memberNo = jwtTokenProvider.getMemberNoFromToken(token);
model.addAttribute("memberNo", memberNo);
return "chat/roomList";
}
return "redirect:/auth/login";
} catch (Exception e) {
log.error("채팅방 목록 페이지 로드 중 오류 발생: ", e);
return "error/error";
}
}
토큰 유출 방지
토큰 갱신
로깅