미니 프로젝트 - JWT 필터 구현

Zyoon·2025년 6월 1일

미니프로젝트

목록 보기
18/36
post-thumbnail

📘JWT 사용하여 로그아웃 기능 구현


JWT 기능 구현

📗JWT 로그인 기능 구현

📗JWT 로그아웃 기능 구현

📗JWT 토큰 재발급 기능 구현

필터 설계

[필터 흐름]

0. HTTP 요청 / doFilterInternal 진입
  
1. 인증이 필요 없는 URI인지 확인 → 필요 없으면 바로 다음 필터로 이동
   
2. 요청 헤더에서 Access Token 추출
   
3. 추출한 토큰으로 로그인 상태 확인
   
4. 화이트리스트 조건 검사 → 조건 불충족 시 종료 (컨트롤러로 미진입)
   
5. 로그인 상태에 따라 SecurityContext에 인증 정보 설정 또는 클리어
   
6. 다음 필터로 넘김 → 컨트롤러로 진입

필터 클래스

@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
    private final JwtTokenProvider jwtTokenProvider;
    private final WhiteListManager whiteListManager;
    private final TokenExtractor tokenExtractor;
    private final JwtAuthenticationProvider jwtAuthenticationProvider;

    @Override
    protected void doFilterInternal(
            HttpServletRequest request,
            HttpServletResponse response,
            FilterChain filterChain
    ) throws ServletException, IOException {

        //토큰 검증 무시 리스트 확인
        if(whiteListManager.isNoAuthRequiredUris(request)) {
            filterChain.doFilter(request,response);
            return;
        }

        //헤더에서 토큰 생성
        String accessToken = tokenExtractor.
                extractAccessTokenFromHeader(request).orElse(null);

        //로그인 상태 확인
        boolean isLoggedIn = jwtTokenProvider.isLoggedIn(accessToken);

        //로그인 상태와 화이트리스트 판별하여 조건에 따라 예외처리.
        if(!whiteListManager.isWhiteList(isLoggedIn, request, response)) return;

        //로그인 상태라면 access 토큰 security 저장. 로그아웃 상태라면 clear
        jwtAuthenticationProvider
                .setTokenToSecurityContextOrClear(accessToken, isLoggedIn, response);

        // 다음 필터로 넘김
        filterChain.doFilter(request,response);
    }

}
  • 요청당 한 번만 실행되어야 하므로 OncePerRequestFilter를 상속한다.

검증 무시 리스트 판별

//토큰 유효성 검사 무시하는 URI
private static final String[] NO_AUTH_REQUIRED_URIS = {
        "/api/reissue"
};

//입력 받은 URI 와 인증 필요없는 URI 판별
public boolean isNoAuthRequiredUri(HttpServletRequest request) {
    String requestUri = request.getRequestURI();
    return org.springframework.util
		    .PatternMatchUtils
		    .simpleMatch(NO_AUTH_REQUIRED_URIS, requestUri);
}
  • 로그아웃 기능과 토큰 재발급 기능은 Access 토큰의 유효성 검증이 필요없으므로 해당 URI 일 경우 doFilter() 생성 후 리턴한다. (다음 필터로 넘긴다)

헤더에서 토큰 생성

// [Headear 예시]
// Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6...

// Header 에서 access token 값을 가져온다.
public Optional<String> extractAccessTokenFromHeader(HttpServletRequest request) {
    return Optional.ofNullable(request.getHeader("Authorization")) // Authorization 헤더의 값을 Optional로 감싼다.
            .filter(header -> header.startsWith("Bearer ")) // 값이 "Bearer "로 시작하는지 확인 - Bearer : 토큰 종류
            .map(header -> header.substring(7)); // "Bearer " 길이만큼 잘라서 리턴
}
  • HTTP 요청 헤더에서 "Authorization" 값을 가져온다.
  • 헤더 값이 "Bearer "로 시작하는지 확인한다.
  • "Bearer " 부분을 제외한 나머지 토큰 값만 추출한다.
  • Null 방지를 위해 Optional 타입으로 반환한다.

로그인 상태 확인

//로그인 상태 확인
public boolean isLoggedIn(String accessToken) {
    if(accessToken == null) return false; // null 체크
    if(!jwtTokenUtils.isValidToken(accessToken)) return false; // 유효성 검사
    return !tokenBlacklistService.isTokenInBlackList(accessToken); // 블랙리스트 판별
}

//토큰 유효성 검증(key, 만료, 유효 시작, 형식)
//보안상 예외 처리는 최소화
public boolean isValidToken(String token) {
    try {
        Jwts.parserBuilder()
                .setSigningKey(key)
                .build()
                .parseClaimsJws(token);
        return true;
    } catch (JwtException | IllegalArgumentException e) {
        return false;
    }
}

//토큰 블랙리스트인지 확인
public boolean isTokenInBlackList(String accessToken){
    return tokenBlackListRepository.existsByAccessToken(accessToken);
}
  • 로그인 상태는 Access 토큰의 유효성 여부와 블랙리스트 판별로 확인한다.
  • 토큰의 유효성 검사를 한 후, 결과 값을 리턴해준다. 이 때 유효성검사의 에러메세지는 보안 관계상 최소화 해준다.
  • tokenBlackListRepository 에서 해당 토큰이 블랙리스트 인지 확인한다.

화이트 리스트 판별

  • 화이트 리스트 판별에 필요한 배열 생성
//Get 방식일때만 로그아웃 허용 -> URI 추가 고려하여 배열로 생성
private static final String[] ONLY_GET_PUBLIC_URI = {
        "/api/posts"
};

//로그아웃 상태 진입 URI - 게시글 보기는 로그아웃 상태에서도 진입 가능
private static final String[] PUBLIC_URIS = {
        "/api/users/signup",
        "/api/login"

};

//로그인 상태에서 진입 불가 URI - 회원가입, 로그인 기능은 로그아웃 상태에서만 진입 가능
private static final String[] LOGOUT_ONLY_URIS = {
        "/api/users/signup",
        "/api/login"
};
  • 화이트 리스트 판단
//화이트 리스트 판단
public boolean isWhiteList(boolean isLoggedIn, HttpServletRequest request, HttpServletResponse response) throws IOException {
    String requestUri = request.getRequestURI();
    String requestMethod = request.getMethod();

    boolean isAllowed;
    if (isLoggedIn) {
        isAllowed = handleLoggedInUser(requestUri);
    } else {
        isAllowed = handleGuestUser(requestUri, requestMethod);
    }

    if (!isAllowed) {
        filterException.writeExceptionResponse(response);
    }

    return isAllowed;
}
// 로그인 상태 처리
private boolean handleLoggedInUser(String requestUri) {
    return !isLogoutOnlyUris(requestUri);
}

// 비로그인 상태 처리
private boolean handleGuestUser(String requestUri, String requestMethod) {
    return isPublicUris(requestUri) || isPublicGetUris(requestUri, requestMethod);
}

//로그아웃 상태에서만 집입가능한 uri
private boolean isLogoutOnlyUris(String requestUri){
    return isWhitelistedUri(LOGOUT_ONLY_URIS, requestUri);
}

//로그인 없이 진입가능한 uri -> 추후 사용
private boolean isPublicUris(String requestUri){
    return isWhitelistedUri(PUBLIC_URIS, requestUri);
}

//로그인 없이 진입가능한 Get uri (post)
private boolean isPublicGetUris(String requestUri,String requestMethod ){
    boolean isWhiteList = isStartsWithWhitelistedUri(ONLY_GET_PUBLIC_URI, requestUri);
    return isWhiteList && "GET".equalsIgnoreCase(requestMethod);
}

//화이트 리스트 인지 확인
private boolean isWhitelistedUri(String[] URLS_LIST, String requestURI) {
    return PatternMatchUtils.simpleMatch(URLS_LIST, requestURI);
}

//하위 경로 포함하여 화이트 리스트 확인
private boolean isStartsWithWhitelistedUri (String[] URLS_LIST, String requestURI){
    return Arrays.stream(URLS_LIST)
            .anyMatch(requestURI::startsWith);
}
  • 조건에 따라 URI 예외처리를 해준다.
    1. 로그인 상태지만 "로그아웃 상태 전용 URI"가 아닌 경우
    2. 누구나 접근 가능한 URI일 때
    3. 누구나 접근 가능한 GET URI인 경우 (/api/posts)
  • 그 외는 예외 처리를 해준다. (필터에서 Return)

토큰 저장 (Security Context)

public void setTokenToSecurityContextOrClear(String accessToken, boolean isLoggedIn) throws IOException {
    //로그인 상태가 아닐경우 ContextHolder 초기화
    if(!isLoggedIn) {
        SecurityContextHolder.clearContext();
    }
    //토큰 유효성 검증 후 SecurityContext 저장 (Access token)
    else{
        saveAuthenticationFromToken(accessToken);
    }
}

//토큰에서 인증 정보 설정
public void saveAuthenticationFromToken(String accessToken){
    // 토큰으로부터 유저 정보 받기
    Authentication authentication = getAuthentication(accessToken);
    // SecurityContext 에 Authentication 객체를 저장
    SecurityContextHolder.getContext().setAuthentication(authentication);
}
  • 로그아웃 상태
    • 인증 정보 클리어 → 이후 요청에서 인증된 사용자 없음으로 처리한다.
  • 로그인 상태
    • 토큰을 바탕으로 사용자 인증 정보를 SecurityContext에 담는다.

Security Config (필터 Bean 등록)

@Configuration
@RequiredArgsConstructor
public class SecurityConfig {

    private final JwtTokenProvider jwtTokenProvider;
    private final WhiteListManager whiteListManager;
    private final TokenExtractor tokenExtractor;
    private final JwtAuthenticationProvider jwtAuthenticationProvider;

    @Bean
    public JwtAuthenticationFilter jwtAuthenticationFilter() {
        return new JwtAuthenticationFilter(jwtTokenProvider,whiteListManager,tokenExtractor,jwtAuthenticationProvider);
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                .csrf(csrf -> csrf.disable()) //보호 기능 삭제
                .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) //세션 사용하지 않음
                .authorizeHttpRequests(auth -> auth
                        .requestMatchers("/**").permitAll() //특정 경로는 접근 허용
                        .anyRequest().authenticated() // 나머지 요청은 인증 필요
                )
                // JwtAuthenticationFilterUsernamePasswordAuthenticationFilter 보다 먼저 실행되도록 등록
                .addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);

        return http.build(); //필터 객체 반환
    }

    // 로그인 등 인증 처리 시 필요한 AuthenticationManager 빈 등록
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
        return config.getAuthenticationManager();
    }
}
  • JwtAuthenticationFilter를 통해 JWT 토큰 기반 인증을 처리하고 SecurityContext에 유저 정보를 저장한다.
  • 세션 없이 작동하도록 설정하고, 기본 필터 체인에 JWT 필터를 먼저 등록한다.
  • 현재 모든 요청은 permitAll로 허용하고, 필터 내부의 화이트리스트 로직으로 접근 제어한다.
profile
기어 올라가는 백엔드 개발

0개의 댓글