Tripf 프로젝트 트러블 슈팅 - JWT 로그아웃 엔드포인트 충돌 해결하기(Spring Security)

J_log·1일 전
0

UserController에 Logout 메서드를 만들고 Postman에서 API를 호출했다.

문제 발생

먼저 로그인 API를 호출해서 토큰을 받고, 로그아웃 API의 Authorization을 Bearer Token으로 설정해준 후 API를 호출하게 되면 다음과 같은 결과가 나온다.

"error": "Method Not Allowed",
"message": "Method 'GET' is not supported.",
"path": "/login"

라는 응답이 나왔는데, 먼저 path가 /login으로 되어있는 모습을 보고 의아했다. 분명 /logout이라는 엔드포인트를 입력했고 API를 호출했기 때문이다..

원인 추론

혹시 캐시 때문에 그럴수도 있겠다는 생각에 Postman 캐시를 초기화시키고 다시 시도 해봤지만 결과는 동일했다.
또 다른 원인으로는 Spring Security의 기본 설정 문제일 수도 있다. Spring Security는 기본적으로 /logout 엔드포인트를 제공하며, HTTP GET 요청으로 처리하려고 한다. 커스텀 /logout을 만들 경우, Spring Security의 기본 설정과 충돌이 날 수도 있다.

문제 해결

  1. 경로를 변경 하기
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .logout()
                .logoutUrl("/custom-logout") // 기본 경로를 변경
                .invalidateHttpSession(true)
                .deleteCookies("JSESSIONID")
                .logoutSuccessHandler((request, response, authentication) -> {
                    response.setStatus(HttpServletResponse.SC_OK);
                    response.getWriter().write("로그아웃 완료");
                })
            .and()
            .authorizeRequests()
                .anyRequest().authenticated();
    }
}
  1. /logout을 POST 요청으로만 허용하기
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http.cors(AbstractHttpConfigurer::disable)
        .csrf(AbstractHttpConfigurer::disable)
        .authorizeHttpRequests(auth -> auth
                .requestMatchers(WHITE_LIST).permitAll()
                .requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll()
                .dispatcherTypeMatchers(DispatcherType.FORWARD, DispatcherType.INCLUDE, DispatcherType.ERROR).permitAll()
                .requestMatchers("/admin/**").hasAuthority("AUTH_ADMIN")
                .anyRequest().authenticated()
        )
        .exceptionHandling(handler -> handler
                .authenticationEntryPoint(authenticationEntryPoint)
                .accessDeniedHandler(accessDeniedHandler))
        .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
        .authenticationProvider(authenticationProvider)
        .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)
        .logout(logout -> logout
                .logoutUrl("/logout") // 로그아웃 URL 설정
                .logoutRequestMatcher(new AntPathRequestMatcher("/logout", "POST")) // POST 요청만 허용
                .invalidateHttpSession(true) // 세션 무효화
                .deleteCookies("JSESSIONID") // 쿠키 삭제
                .logoutSuccessHandler((request, response, authentication) -> {
                    response.setStatus(HttpServletResponse.SC_OK);
                    response.getWriter().write("로그아웃 완료");
                })
        );

    return http.build();
}
  • "/logout" 엔드포인트를 유지 하면서도 기본으로 설정되어있던 GET 요청을 -> POST 요청으로 변경하게 된다.

하지만 프론트엔드가 붙게 되면서 기존 엔드포인트 앞에 모두 "/api"를 붙여주기로 약속했기에, 간단하게 엔드포인트를 수정하는 방법으로 해결이 되었다.

결과 확인

정상적으로 로그아웃이 진행된 모습을 확인 할 수 있다.

추가 고려 사항

지금 로그아웃을 처리하는 방식은 SecurityContext에서 인증 객체를 제거하여 현재 세션에 대한 인증 상태를 삭제한다. 하지만 JWT 자체는 여전히 유효하기 때문에 클라이언트가 토큰을 그대로 가지고 있다면, 다른 요청을 보낼 때 서버는 이를 유효한 토큰으로 간주할 수 있다.
때문에, JWT 기반 시스템에서 조금 더 완전한 로그아웃 처리를 하기 위해 Refresh Token, 블랙리스트 사용(Redis) 등을 고려해볼 필요가 있을 것 같다.

1개의 댓글

comment-user-thumbnail
1일 전

잘 보고 공감 누르고 갑니다. 오늘 눈소식이 있는 곳도 있지요? 눈 구경하시면서 행복한 하루를 보내시기 바랍니다. ^^

답글 달기