이 글은 Velog에서 이전한 글입니다. Velog 원문 보기
대학교 커뮤니티 프로젝트를 진행하던 도중 JWT 필터 관련 테스트에서 무수한 에러를 마주했다..

전체 테스트 코드
package com.Sucat.global.jwt.service;
import com.Sucat.domain.user.model.User;
import com.Sucat.domain.user.repository.UserRepository;
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.persistence.EntityManager;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.transaction.annotation.Transactional;
import java.util.HashMap;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@SpringBootTest
@AutoConfigureMockMvc
@Transactional
public class JwtFilterAuthenticationTest {
@Autowired
MockMvc mockMvc;
@Autowired
UserRepository userRepository;
@Autowired
EntityManager em;
@Autowired
JwtService jwtService;
PasswordEncoder delegatingPasswordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.access.header}")
private String accessHeader;
@Value("${jwt.refresh.header}")
private String refreshHeader;
private static String KEY_USERNAME = "email";
private static String KEY_PASSWORD = "password";
private static String USERNAME = "test@naver.com";
private static String PASSWORD = "123456789";
private static String LOGIN_URL = "/login";
private static final String ACCESS_TOKEN_SUBJECT = "AccessToken";
private static final String BEARER = "Bearer";
private ObjectMapper objectMapper = new ObjectMapper();
private void clear(){
em.flush();
em.clear();
}
@BeforeEach
private void init(){
userRepository.save(User.builder()
.email(USERNAME)
.password(delegatingPasswordEncoder.encode(PASSWORD))
.name("Member1")
.nickname("NickName1")
.build());
clear();
}
private Map getUsernamePasswordMap(String username, String password){
Map<String, String> map = new HashMap<>();
map.put(KEY_USERNAME, username);
map.put(KEY_PASSWORD, password);
return map;
}
private Map getAccessAndRefreshToken() throws Exception {
Map<String, String> map = getUsernamePasswordMap(USERNAME, PASSWORD);
MvcResult result = mockMvc.perform(
post(LOGIN_URL)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(map)))
.andReturn();
String accessToken = result.getResponse().getHeader(accessHeader);
String refreshToken = result.getResponse().getHeader(refreshHeader);
Map<String, String> tokenMap = new HashMap<>();
tokenMap.put(accessHeader,accessToken);
tokenMap.put(refreshHeader,refreshToken);
return tokenMap;
}
/**
* AccessToken : 존재하지 않음,
* RefreshToken : 존재하지 않음
*/
@Test
public void Access_Refresh_모두_존재_X() throws Exception {
//when, then
mockMvc.perform(get(LOGIN_URL+"123"))//login이 아닌 다른 임의의 주소
.andExpect(status().isForbidden());
}
/**
* AccessToken : 유효,
* RefreshToken : 존재하지 않음
*/
@Test
public void AccessToken만_보내서_인증() throws Exception {
//given
Map accessAndRefreshToken = getAccessAndRefreshToken();
String accessToken= (String) accessAndRefreshToken.get(accessHeader);
//when, then
mockMvc.perform(get(LOGIN_URL+"123").header(accessHeader,BEARER+ accessToken))//login이 아닌 다른 임의의 주소
.andExpectAll(status().isNotFound());
}
/**
* AccessToken : 유효하지 않음,
* RefreshToken : 존재하지 않음
*/
@Test
public void 안유효한AccessToken만_보내서_인증X_상태코드는_403() throws Exception {
//given
Map accessAndRefreshToken = getAccessAndRefreshToken();
String accessToken= (String) accessAndRefreshToken.get(accessHeader);
//when, then
mockMvc.perform(get(LOGIN_URL+"123").header(accessHeader,accessToken+"1"))//login이 아닌 다른 임의의 주소
.andExpectAll(status().isForbidden()); // 없는 주소로 보냈으므로 NotFound
}
/**
* AccessToken : 존재하지 않음
* RefreshToken : 유효
*/
@Test
public void 유효한RefreshToken만_보내서_AccessToken_재발급_200() throws Exception {
//given
Map accessAndRefreshToken = getAccessAndRefreshToken();
String refreshToken= (String) accessAndRefreshToken.get(refreshHeader);
// refreshToken이 null인지 확인
assertThat(refreshToken).isNotNull();
//when, then
MvcResult result = mockMvc.perform(get("/login123").header(refreshHeader, BEARER + refreshToken))
.andExpect(status().isOk())
.andReturn();
String accessToken = result.getResponse().getHeader(accessHeader);
// accessToken이 null인지 확인
assertThat(accessToken).isNotNull();
String subject = JWT.require(Algorithm.HMAC512(secret)).build().verify(accessToken).getSubject();
// subject가 예상한 값과 일치하는지 확인
assertThat(subject).isEqualTo(ACCESS_TOKEN_SUBJECT);
}
/**
* AccessToken : 존재하지 않음
* RefreshToken : 유효하지 않음
*/
@Test
public void 안유효한RefreshToken만_보내면_403() throws Exception {
//given
Map accessAndRefreshToken = getAccessAndRefreshToken();
String refreshToken= (String) accessAndRefreshToken.get(refreshHeader);
//when, then
mockMvc.perform(get(LOGIN_URL + "123").header(refreshHeader, refreshToken))//Bearer을 붙이지 않음
.andExpect(status().isForbidden());
mockMvc.perform(get(LOGIN_URL + "123").header(refreshHeader, BEARER+refreshToken+"1"))//유효하지 않은 토큰
.andExpect(status().isForbidden());
}
/**
* AccessToken : 유효
* RefreshToken : 유효
*/
@Test
public void 유효한RefreshToken이랑_유효한AccessToken_같이보냈을때_AccessToken_재발급_200() throws Exception {
//given
Map accessAndRefreshToken = getAccessAndRefreshToken();
String accessToken= (String) accessAndRefreshToken.get(accessHeader);
String refreshToken= (String) accessAndRefreshToken.get(refreshHeader);
//when, then
MvcResult result = mockMvc.perform(get(LOGIN_URL+"123")
.header(refreshHeader, BEARER + refreshToken)
.header(accessHeader, BEARER + accessToken))
.andExpect(status().isOk())
.andReturn();
String responseAccessToken = result.getResponse().getHeader(accessHeader);
String responseRefreshToken = result.getResponse().getHeader(refreshHeader);
String subject = JWT.require(Algorithm.HMAC512(secret)).build().verify(responseAccessToken).getSubject();
assertThat(subject).isEqualTo(ACCESS_TOKEN_SUBJECT);
assertThat(responseRefreshToken).isNull();//refreshToken은 재발급되지 않음
}
/**
* AccessToken : 유효하지 않음
* RefreshToken : 유효
*/
@Test
public void 유효한RefreshToken이랑_안유효한AccessToken_같이보냈을때_AccessToken_재발급_200() throws Exception {
//given
Map accessAndRefreshToken = getAccessAndRefreshToken();
String accessToken= (String) accessAndRefreshToken.get(accessHeader);
String refreshToken= (String) accessAndRefreshToken.get(refreshHeader);
//when, then
MvcResult result = mockMvc.perform(get(LOGIN_URL + "123")
.header(refreshHeader, BEARER + refreshToken)
.header(accessHeader, BEARER + accessToken + 1))
.andExpect(status().isOk())
.andReturn();
String responseAccessToken = result.getResponse().getHeader(accessHeader);
String responseRefreshToken = result.getResponse().getHeader(refreshHeader);
String subject = JWT.require(Algorithm.HMAC512(secret)).build().verify(responseAccessToken).getSubject();
assertThat(subject).isEqualTo(ACCESS_TOKEN_SUBJECT);
assertThat(responseRefreshToken).isNull();//refreshToken은 재발급되지 않음
}
/**
* AccessToken : 유효
* RefreshToken : 유효하지 않음
*/
@Test
public void 안유효한RefreshToken이랑_유효한AccessToken_같이보냈을때_상태코드200_혹은404_RefreshToken은_AccessToken모두_재발급되지않음() throws Exception {
//given
Map accessAndRefreshToken = getAccessAndRefreshToken();
String accessToken= (String) accessAndRefreshToken.get(accessHeader);
String refreshToken= (String) accessAndRefreshToken.get(refreshHeader);
//when, then
MvcResult result = mockMvc.perform(get(LOGIN_URL + "123")
.header(refreshHeader, BEARER + refreshToken+1)
.header(accessHeader, BEARER + accessToken ))
.andExpect(status().isNotFound())//없는 주소로 보냈으므로 NotFound
.andReturn();
String responseAccessToken = result.getResponse().getHeader(accessHeader);
String responseRefreshToken = result.getResponse().getHeader(refreshHeader);
assertThat(responseAccessToken).isNull();//accessToken은 재발급되지 않음
assertThat(responseRefreshToken).isNull();//refreshToken은 재발급되지 않음
}
/**
* AccessToken : 유효하지 않음
* RefreshToken : 유효하지 않음
*/
@Test
public void 안유효한RefreshToken이랑_안유효한AccessToken_같이보냈을때_403() throws Exception {
//given
Map accessAndRefreshToken = getAccessAndRefreshToken();
String accessToken= (String) accessAndRefreshToken.get(accessHeader);
String refreshToken= (String) accessAndRefreshToken.get(refreshHeader);
//when, then
MvcResult result = mockMvc.perform(get(LOGIN_URL + "123")
.header(refreshHeader, BEARER + refreshToken+1)
.header(accessHeader, BEARER + accessToken+1 ))
.andExpect(status().isForbidden())//없는 주소로 보냈으므로 NotFound
.andReturn();
String responseAccessToken = result.getResponse().getHeader(accessHeader);
String responseRefreshToken = result.getResponse().getHeader(refreshHeader);
assertThat(responseAccessToken).isNull();//accessToken은 재발급되지 않음
assertThat(responseRefreshToken).isNull();//refreshToken은 재발급되지 않음
}
@Test
public void 로그인_주소로_보내면_필터작동_X() throws Exception {
//given
Map accessAndRefreshToken = getAccessAndRefreshToken();
String accessToken= (String) accessAndRefreshToken.get(accessHeader);
String refreshToken= (String) accessAndRefreshToken.get(refreshHeader);
//when, then
MvcResult result = mockMvc.perform(post(LOGIN_URL) //get인 경우 config에서 permitAll을 했기에 notFound
.header(refreshHeader, BEARER + refreshToken)
.header(accessHeader, BEARER + accessToken))
.andExpect(status().isBadRequest())
.andReturn();
}
}
자세한 코드는 아래 깃허브 링크 참고!
https://github.com/pp8817/Sucat
우선 하나하나 해결해보기로 했다.
유효한 RefreshToken과 유효한 AccessToken을 같이 보낸 경우
@Test
public void 유효한RefreshToken이랑_유효한AccessToken_같이보냈을때_AccessToken_재발급_200() throws Exception {
//given
Map accessAndRefreshToken = getAccessAndRefreshToken();
String accessToken= (String) accessAndRefreshToken.get(accessHeader);
String refreshToken= (String) accessAndRefreshToken.get(refreshHeader);
//when, then
MvcResult result = mockMvc.perform(get(LOGIN_URL+"123")
.header(refreshHeader, BEARER + refreshToken)
.header(accessHeader, BEARER + accessToken))
.andExpect(status().isOk())
.andReturn();
String responseAccessToken = result.getResponse().getHeader(accessHeader);
String responseRefreshToken = result.getResponse().getHeader(refreshHeader);
String subject = JWT.require(Algorithm.HMAC512(secret)).build().verify(responseAccessToken).getSubject();
assertThat(subject).isEqualTo(ACCESS_TOKEN_SUBJECT);
assertThat(responseRefreshToken).isNull();//refreshToken은 재발급되지 않음
}
본래 의도한 목적은 유효한 RefreshToken, 유효한 AccessToken이 같이 넘어간 경우 RefreshToken의 유효성 검사 후 AccessToken을 재발급하도록 개발했다.
그러나 테스트 코드 결과 로그를 보면

이처럼 403 에러가 발생한다.
403(Forbidden) 에러?
작동중인 서버에 클라이언트의 요청이 도달했지만, 서버가 클라이언트의 접근을 거부할 때 발생하는 에러 코드이다. 즉, 사용자의 권한 검증 단계에서 문제가 생긴것이다.
코드의 흐름을 보면 '/login123'으로 요청이 가서 우선 JWTAuthenticationProcessingFilter로 넘어가서 JWT 토큰에 대한 검증을 한다.
JWTAuthenticationProcessingFilter
/**
* OncePerRequestFilter: 모든 서블릿 컨테이너에서 요청 디스패치당 단일 실행을 보장하는 것을 목표로 하는 필터 기본 클래스
*/
@RequiredArgsConstructor
public class JwtAuthenticationProcessingFilter extends OncePerRequestFilter {
private final JwtService jwtService;
private final UserRepository userRepository;
private GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper();
private static final String NO_CHECK_URL = "/login";
/**
* 1. RefreshToken이 오는 경우 -> 유효하면 AccessToken 재발급후, 필터 진행 X
* 2. RefreshToken은 없고 AccessToken만 있는 경우 -> 유저정보 저장 후 필터 진행, RefreshToken 재발급X
*/
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
System.out.println("Received request URI: " + request.getRequestURI());
if (request.getRequestURI().equals(NO_CHECK_URL)) {
filterChain.doFilter(request, response);
return;
}
String refreshToken = jwtService
.extractRefreshToken(request)
.filter(jwtService::isTokenValid)
.orElse(null);
if (refreshToken != null) { // Request 요청으로 온 사용자가 refreshToken을 가지고 있다면 AccessToken을 재발급
checkRefreshTokenAndReIssueAccessToken(response, refreshToken);
return;
}
checkAccessTokenAndAuthentication(request, response, filterChain);
}
private void checkAccessTokenAndAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
jwtService.extractAccessToken(request).filter(jwtService::isTokenValid).ifPresent(
accessToken -> jwtService.extractEmail(accessToken).ifPresent(
email -> userRepository.findByEmail(email).ifPresent(
user -> saveAuthentication(user)
)
)
);
filterChain.doFilter(request,response);
}
private void saveAuthentication(User users) {
UserDetails user = org.springframework.security.core.userdetails.User.builder()
.username(users.getEmail())
.password(users.getPassword())
.roles(users.getRole().name())
.build();
Authentication authentication = new UsernamePasswordAuthenticationToken(user, null, authoritiesMapper.mapAuthorities(user.getAuthorities()));
SecurityContext context = SecurityContextHolder.createEmptyContext();//5
context.setAuthentication(authentication);
SecurityContextHolder.setContext(context);
}
private void checkRefreshTokenAndReIssueAccessToken(HttpServletResponse response, String refreshToken) {
userRepository.findByRefreshToken(refreshToken).ifPresent(
user -> jwtService.sendAccessToken(response, jwtService.createAccessToken(user.getEmail()))
);
}
}
우선 무식하게 디버깅을 찍어봤다.

문제가 발생한 지점을 찾았다. 분명 테스트코드에서 의도한 바에 따르면 RefreshToken이 유효한 상태로 넘어가게 되는데 RefreshToken을 추출하는 과정에서 null이 나왔다.
RefreshToken 추출
@Override
public Optional<String> extractRefreshToken(HttpServletRequest request) {
return Optional.ofNullable(request.getHeader(refreshHeader)).filter(
refreshToken -> refreshToken.startsWith(BEARER)
).map(refreshToken -> refreshToken.replace(BEARER, ""));
}
이제 이 코드를 디버깅 해보면 오류의 원인을 찾을 수 있을거 같다.

음... 이제 답은 2개 중 하나이다.
- refreshToken -> refreshToken.startsWith(BEARER) 과정에 문제가 있다.
- request.getHeader(refreshHeader)에서 값을 가져오지 못한다.
1번 먼저 확인해보자.

아.. 보이는 것처럼 상수는 Constants라는 파일을 만들어 따로 보관하는데 BEARER에서 오류가 있는 것을 확일할 수 있다. 꽤나 허무하지만 사소한 실수 때문에 큰 문제가 생기는 것을 잘 알고있기 때문에 일찍 발견해서 다행이라고 생각한다.
오류를 수정하고 다시 테스트 코드를 실행해보자.

문제 해결
솔직히 단순히 띄어쓰기를 잘못한 사소한 실수인데 굳이 글로 정리해야 되나? 라는 생각이 들었다.
하지만 모든 문제는 사소한 문제에서 시작한다는 것을 알고 있기 때문에 경각심을 가지자는 의미와 이또한 소중한 경험이라고 생각해 글로 정리해봤다.
'Troubleshooting' 카테고리의 다른 글
| [시행착오] AWS 프리티어 비용 과금 이슈, 해결 방법 (0) | 2026.07.29 |
|---|---|
| 💥 아! Get 요청시에는 Message body를 사용하면 안되는구나!! (1) | 2026.07.29 |
| 💥 [Error] Dirty Checking이 안되는 오류 (0) | 2026.07.24 |
| 💥 Springboot Multipart 파일 업로드 용량 초과 에러 (0) | 2026.07.21 |
| 💥 Web server failed to start. Port 8080 was already in use (0) | 2026.07.20 |