@Component
@RequiredArgsConstructor
// OncePerRequestFilter 를 상속받아 HTTP 요청당 한 번만 실행
public class JwtFilter extends OncePerRequestFilter {
private final JwtUtil jwtUtil;
// TODO - 직렬화/역직렬화 커스텀 설정 여부 확인 필요
private final RedisTemplate<String, Object> redisTemplate;
// TODO - URL 추가 필요
// WHITELIST
private static final List<String> WHITELIST = List.of(
"/auth/login",
"/users/signup",
"/auth/refresh"
);
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
// HttpServletRequest 에서 요청된 URI 경로를 문자열로 추출
String url = request.getRequestURI();
// 요청된 URL 이 인증 우회 대상인지 판단 후 JWT 인증 필터 건너뛰기/ 후 다음 필터로 넘김
if (WHITELIST.contains(url)) {
filterChain.doFilter(request, response);
return;
}
// 클라이언트가 HTTP 요청에 Authorization 헤더 포함시켰는지 확인
String bearerJwt = request.getHeader("Authorization");
if (bearerJwt == null) {
writeErrorResponse(response, 401, "Authorization 헤더가 존재하지 않습니다.");
return;
}
/*
* 토큰 파싱 및 유효성 검사
* subStringToken() : 유효성 검사 + Bearer 제거
* subStringToken() 안에서 발생한 예외 잡아서 HTTP 응답에 401 에러와 함께 메시지 JSON 으로 응답
*/
String jwt;
try {
jwt = jwtUtil.subStringToken(bearerJwt);
} catch (ResponseStatusException e) {
writeErrorResponse(response, e.getStatusCode().value(), e.getReason());
return;
}
/*
* tokenKey : Redis 에 저장된 블랙리스트 토큰의 Key 를 구성하는 부분
* jwt 는 현재 요청에서 추출한 Access Token 문자열이기 때문에
→ Redis 에 저장할 때 "BLACKLIST_" 접두어 붙여서 구분
*/
String tokenKey = "BLACKLIST_" + jwt;
/*
* Redis 에서 tokenKey 존재하는지 확인
* hasKey() : 해당 키가 Redis 에 존재하는지 여부에 따라 true/false 반환
*/
Boolean isBlackListed = redisTemplate.hasKey(tokenKey);
/*
* isBlackListed 가 true 인지 확인 → null-safe 체크
* Redis 에서 해당 토큰이 블랙리스트에 등록되어 있으면 writeErrorResponse 로직으로 진입
*/
if (Boolean.TRUE.equals(isBlackListed)) {
writeErrorResponse(response, HttpStatus.UNAUTHORIZED, "로그아웃된 토큰입니다. 다시 로그인 해주세요.");
return;
}
try {
// 내부에서 Bearer 제거 + 토큰의 유효성 검증 → Claims 객체로 반환
Claims claims = jwtUtil.getClaims(bearerJwt);
// Claims 는 JWT 내부 payload 정보들을 갖고 있어 getSubject() 로 값 추출 가능
String userId = claims.getSubject();
// TODO - 권한 부여 로직 추가 후 권한 목록 수정 → List.of()
// Spring Security 에서 사용하는 인증 객체 생성
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userId, null,
List.of());
/*
* 보안 컨텍스트에 방금 만든 authentication 객체를 저장
→ 이후 컨트롤러 단에서 @AuthenticationPrincipal 같은 방식으로 사용자 정보를 꺼낼 수 있다
→ 인증을 수동으로 완료 처리함
*/
SecurityContextHolder.getContext().setAuthentication(authentication);
filterChain.doFilter(request, response);
// jwtUtil.getClaims() 에서 토큰 만료됐거나 위조된 경우 예외 발생
} catch (ResponseStatusException e) {
writeErrorResponse(response, e.getStatusCode(), e.getReason());
}
}
// JWT 없거나 잘못된 경우 사용할 공통 메서드
private void writeErrorResponse(HttpServletResponse response, HttpStatus status, String message) throws
IOException {
response.setStatus(status.value());
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(String.format("{\"status\":%d,\"message\":\"%s\"}", status.value(), message));
}
}
Spring Security 인증 처리와 Redis 기반 로그아웃 처리까지 포함된 상태이다.
요청 URL 확인
WHITELIST)에 포함된 URL이면 JWT 검사 없이 바로 다음 필터로 진행.Authorization 헤더 확인
Authorization 헤더가 없으면 401 에러 응답 반환.JWT 파싱 및 유효성 검사
Bearer 접두어 제거 (jwtUtil.subStringToken())ResponseStatusException 처리 → 401 응답Redis 블랙리스트 확인
"BLACKLIST_" + jwt 형식으로 Redis에 저장된 토큰 키 존재 여부 확인JWT Claims에서 사용자 식별자 추출
jwtUtil.getClaims() → 내부적으로 서명 검증, 만료 검증, 구조 검증getSubject()로 사용자 ID 추출Spring Security 인증 객체 등록
UsernamePasswordAuthenticationToken을 SecurityContext에 수동 등록다음 필터로 요청 전달
filterChain.doFilter() 호출List.of() 권한 정보는 현재 비어 있으니, 추후에 실제 권한 정보를 JWT에 담거나 사용자 DB 조회로 반영할 수 있음."BLACKLIST_" + jwt 저장할 때 만료 시간도 토큰과 동일하게 맞춰주는 것이 좋음 (예: redisTemplate.expire()).