SPARTA 과제/TEAM | commerce

JWT 트러블슈팅

kjw81024 2026. 3. 15. 02:40

JWT 구현 미리 해보기

 

일단 지금까지 구현해서 사용했던 jwt 로직을 정리해서 보내고 어떤점이 부족한지 ai 한테 물어봤다

구조 확인할 겸 한 번 대충.... 그려봤다

정리하자면 아래와 같다 

 

1. 토큰 꺼내고 파싱, 검증, 복호화 (Claims 꺼내기) 로직은 JwtUtil 에서 처리 

2. UserDetailsService 를 만들어서 DB에서 사용자 조회 후 Authentication 객체 만들기 

3. CustomUserDetails implements UserDetails 만들기 (PK, role 등 넣기) -> 이후 AuthenticationPrincipal 로 뽑아쓰기

4. Refresh 토큰 관리하는 곳을 추가로 구현

5. AuthenticationEntryPoint 를 추가로 구현 

 

6. blackList 를 추가로 구현 (로그아웃) << 요 부분은 안 함 

 

천천히 수정하면서 구현해보기 


구현하면서 Custom error 부분도 같이 구현했다

 

https://github.com/jiwon-e-e/jwt-practice

 

GitHub - jiwon-e-e/jwt-practice

Contribute to jiwon-e-e/jwt-practice development by creating an account on GitHub.

github.com

두고두고 추가하면서 연습해보기 

 

동작 방법은 아래와 같다

 

 

오류 났던 부분

 

문제 1

분명 auth/api 요청은 FilterChain 에서 넘기기로 되어있는데 login 요청을 보내면 401 에러 발생

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
            .csrf(csrf -> csrf.disable())
            .cors(cors -> cors.configure(http))
            # 세션 인증을 비활성화
            .sessionManagement(session ->
                    session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            )
            # 인가 설정
            .authorizeHttpRequests(auth ->
                    auth
                            # api auth 는 filter 패스~
                            .requestMatchers("/api/auth/**").permitAll()
                            .anyRequest().authenticated()
            )
            .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)
            .exceptionHandling(ex ->
                    ex.authenticationEntryPoint(customAuthenticationEntryPoint)
            );

    return http.build();
}

(CommonResponse 적용도 안 됨....)

 

해결

로그인 요청(api/auth/login)이 들어오면 어차피 jwtFilter 는 통과시키기 때문에 바로 controller 를 넘어 Service 로 들어감 

# Filter 는 FilterChain 에서 설정해주더라도 일단 작동을 하기 때문에
# Token 이 비어있다면 FilterChain 에서 검사할 수 있도록 바로 넘겨버리는 로직 
if (bearerToken == null || !bearerToken.startsWith("Bearer ")){
    filterChain.doFilter(request, response);
    return;
}
@Transactional
public String login(LoginRequest request) {

	# request email 로 user 찾기, 비밀번호 맞는지 확인하는 로직 숨김

    String accessToken = jwtUtil.createAccessToken(user.getId());
    String refreshToken = jwtUtil.createRefreshToken(user.getId(), user.getRole().name());

    RefreshToken tokenEntity = refreshTokenRepository.findById(user.getId())
            .orElse(new RefreshToken(user.getId(), refreshToken));

    refreshTokenRepository.save(tokenEntity);

    return accessToken;
}

문제 2

Reissue 를 ....... 왜 발급해주는지 잘 이해가 안 갔음

client 가 어떻게 만료되는 시간을 어떻게 알고 reissue api 를 요청하는지

 

해결

몰랐는데 ...! 사용자가 만료될때쯤 직접 요청하는 형식이 아니라 자동으로 처리됨 

 

클라이언트가 다른 요청 (get.. post 등등 로그인이 필요한 모든 요청) 을 보내면 

Filter 에서 해당 토큰이 유효한지 확인 

-> Access 토큰이 만료되었네? ? -> 401 Unauthorized 로 보냄

이 때

클라이언트 (프론트 서버) 에서 만료시간을 확인하고 reissue 를 진행!!!!

-> 새로운 access token 을 발급받음

-> 원래 요청 재시도 

응답을 클라이언트한테 줌

결론!!! 사용자가 전혀 모르게 처리됨 

 

백엔드에서는 Access Token 이 만료되면 401을 던져주는 역할

프론트엔드에서는 401이 오면 reissue 를 처리해봄

 

-> 그럼 모든 401 요청을 reissue 처리하는가? 

그건 또 아님

토큰 위조나 존재하지 않는 토큰의 경우는 reissue 처리할 필요가 없음

401을 던질 때 error 이름으로 expired Token 이라고 주면 프론트에서 처리하기 편하다!

토큰의 상태에 따라 다른 에러코드를 내주는 연습을 하거나 코드를 짜보는게 좋을듯 


이후 구현해볼 부분 

 

보통 Refresh Token 은 내부 RDS 가 아니라 Redis 를 사용하여 저장 (Docker 사용)

-> 성능이 좋고 자동으로 만료 시간을 제어해줌

 

https://mysolutions.tistory.com/27 

 

[JWT] Redis TTL로 Refresh Token 만료 자동화와 예외 처리 구현하기

1. Refresh Token의 저장소에 대한 고민처음에는 Refresh Token을 데이터베이스에 저장하고, 로그아웃 시 해당 레코드를 삭제하는 방식을 고려했습니다. 클라이언트에서 로그아웃을 요청하면 Access Token

mysolutions.tistory.com