[스프링/Spring] JWT 인증 구현하기 (2) - JWT 인증 필터 구현과 적용

dongbrown·2024년 10월 28일

Spring

목록 보기
12/23

이전 포스트에서는 JWT 토큰을 생성하고 검증하는 Provider를 구현했습니다. 이번에는 실제로 HTTP 요청을 처리할 때 JWT 토큰을 검증하고 인증 정보를 설정하는 필터를 구현해보겠습니다.

1. JwtAuthenticationFilter 구현

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);
        }
    }
}

2. 토큰 추출 로직 구현

요청에서 토큰을 추출하는 방법을 구현합니다. 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;
}

3. 필터 적용 경로 설정

특정 경로에 대해서만 필터를 적용하도록 설정합니다.

@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;
}

4. 에러 처리

인증 실패 시 적절한 에러 응답을 생성합니다.

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;
}

5. 실제 사용 예시

채팅 시스템에서의 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";
    }
}

6. 보안 고려사항

  1. 토큰 유출 방지

    • HTTPS 사용 필수
    • 토큰을 안전하게 저장 (localStorage나 sessionStorage 대신 HttpOnly 쿠키 사용 고려)
  2. 토큰 갱신

    • Refresh Token을 이용한 Access Token 갱신 메커니즘 구현
  3. 로깅

    • 중요 보안 이벤트에 대한 로깅 구현
    • 디버그 로그에 토큰 전체가 노출되지 않도록 주의

0개의 댓글