일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | ||||
4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 |
- springboot
- 프로그래머스
- swagger
- querydls
- JWT
- DevOps
- Java
- JPA
- aop
- MSA
- rabbitmq
- testcode
- 유효성 검사
- Redis
- 객체지향원칙
- algorithm
- Github Actions
- Kafka
- trouble shooting
- AWS
- Til
- Intellij
- CI/CD
- 테스트 코드
- docker
- EC2
- 어노테이션
- 멀티 모듈
- 아키텍처
- spring boot
- Today
- Total
개발노트
25.02.17 JWT 검증 필터 와 테스트 코드 any() 본문
개요
프로젝트를 진행하면서 JWT를 검증하는 필터를 구현하였다. 여기서 의문점이 든 부분을 정리하고 검색 기능에 사용할
queryDSL에 대해 공부한걸 정리해보려한다.
JWT 검증 필터 의문점
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.csrf(csrf -> csrf.disable());
http.addFilterBefore(new JwtAuthenticationFilter(jwtUtil), UsernamePasswordAuthenticationFilter.class);
http.sessionManagement((sessionManagement) ->
sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
http.authorizeHttpRequests(authorizationHttpRequest->
authorizationHttpRequest
.requestMatchers("/api/user/signup").permitAll() // 회원 가입 및 로그인은 접근 허가
.requestMatchers("/api/user/signin").permitAll()
.requestMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll()
.anyRequest().authenticated() // 그 외 모든 요청 인증처리
);
return http.build();
}
지금 코드에서는 회원가입, 로그인. 스웨거 관련 경로를 제외 모든 경로에 인증 처리를 하게 설정되어있다.
인증이 필요한 경로에서는 JWT 필터가 동작해 사용자 권한을 확인하고있다.
JwtAuthenticationFilter 에서는 인증처리를 제외한 경로에서도 필터가 동작이되어 문제가 발생하였다.
requestMatchers로 접근을 허가했는데 왜 필터에서는 해당 경로들도 검증을하게 동작될까??
requestMatchers와 JwtAuthenticationFilter (OncePerRequestFilter)의 동작 차이가 있기때문이였다.
차이점
- requestMatchers : HTTP 요청에 대한 경로 및 권한 설정을 담당
- JwtAuthenticationFilter : 모든 HTTP 요청을 처리하는 필터
즉 JwtAuthenticationFilter 는 필터 체인 내에서 동작하며, HttpSecurity 설정에서 경로에 대한 권한을 부여하거나 제한하는 것과 는 별개로 동작한다. requestMatchers 가 권한 부여에는 영향을 미치지만 필터 자체의 실행 순서에는 영향을 주지않기때문에 인증을 제외한 경로에서도 filter가 동작한 것이다.
이 문제를 해결하기위해 JwtAuthenticationFilter 에 특정 경로를 제외해주는 로직을 추가하였다.
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtUtil jwtUtil;
private final AntPathMatcher pathMatcher = new AntPathMatcher();
// Header key 식별값
public static final String AUTHORIZATION_HEADER = "Authorization";
// Token 식별자
public static final String BEARER_PREFIX = "Bearer ";
// 검사에서 제외할 경로
private static final List<String> excludeUrls = List.of(
"/api/user/signup",
"/api/user/signin",
"/swagger-ui/**",
"/v3/api-docs/**"
);
// 특정 URL이 해당하면 필터링을 적용하지않도록 검사
@Override
protected boolean shouldNotFilter(HttpServletRequest request) {
String requestPath = request.getRequestURI();
return excludeUrls.stream().anyMatch(url -> pathMatcher.match(url, requestPath));
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
if (shouldNotFilter(request)) {
filterChain.doFilter(request, response);
return;
}
..........
제외할 경로를 리스트로 저장하고 shouldNotFilter 메서드를 오버라이딩해 사용해 특정 URL이 해당되면 필터링이 적용하지않도록 검사한다. 해당 경로가 맞으면 True를 반환 아닐 시 flase를 반환한다.
매 필터는 동작할 때 마다 제외할 경로를 검사한다. 이렇게구성하면 회원가입,로그인,스웨거 경로는 필터가 동작하지않는다.
테스트 코드 any()
회원가입 실패 테스트 중 내 예상과는 다 에러가 발생했다.
UserServiceTest
@Test
@DisplayName("회원가입 실패 테스트")
void testSignupFail(){
// Given
SignupReqDto signupReqDto = new SignupReqDto("test@example.com", "testuser", "password", "testnick");
when(userRepository.existsByUsername("testuser")).thenReturn(true);
// When & Then
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> userService.signup(signupReqDto));
assertEquals("username already exists : testuser", exception.getMessage());
}
UserService
public UserResDto signup(SignupReqDto signupReqDto) {
if (userRepository.existsByUsername(signupReqDto.getUsername())){
throw new IllegalArgumentException("username already exists : " + signupReqDto.getUsername());
}
User user = User.builder()
.email(signupReqDto.getEmail())
.password(passwordEncoder.encode(signupReqDto.getPassword()))
.username(signupReqDto.getUsername())
.nickname(signupReqDto.getNickname())
.role(UserRoles.ROLE_CUSTOMER)
.build();
user.setCreatedBy(user.getEmail());
return userRepository.save(user).toResponseDto();
}
@BeforeEach 에서 미리 생성한 User와 동일한 username을 줘서 IllegalArgumentException가 발생하는걸 생각하고 테스트 코드를 작성했다. 하지만 실제로 발생한 에러는 NullPointerException 가 발생했다.
정상적이였으면 IllegalArgumentException가 발생하고 그 뒤의 코드가 동작을 멈춰야했지만 에러가 발생하지않고 끝까지 코드가 동작해 save에서 반환된 null이 toResponse() 를 호출해 NullPointerException 가 발생한것이다.
왜일까 똑같은 username을 입력했는데 왜 다른 username으로 인식한걸까?
내 예상으로는 같은 문자열이지만 주소값을 가져서 그런건가? 라고생각했다. 20분 정도 검색과 고민을하고 답이 없어서 튜터님을 찾아가 질문으로 여쭤보았다. 바쁘실텐데 곰곰히 생각해주시고 이후에도 slack으로 추가 설명까지 해주셨다
userRepository.existsByUsername은 signupReqDto.getUsername() 을 호출해 동작하게된다. 이때 getUsername()에서 나온 String과 내가 when에서 직접 지정한 "testname"(username)이 서로 다른 객체로 인식되어서 제대로 mocking 되지 않은거였다. 해결하기위해서는 테스트를 작성하는 사람이 판단하고 정확하게 맞다는 부분은 true 라고 생각이 들때는
any() 나 anystring() 을 사용해 좀 더 유연하게 처리해도 된다고 하나의 방법으로 알려주셨다.
아니면 상단에서 지정한 하나의 변수로 mocking 해줘도 될 거 같다.
@Test
@DisplayName("회원가입 실패 테스트")
void testSignupFail(){
// Given
SignupReqDto signupReqDto = new SignupReqDto("test@example.com", "testuser", "password", "testnick");
// any() 권장
when(userRepository.existsByUsername(any())).thenReturn(true);
// When & Then
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> userService.signup(signupReqDto));
assertEquals("username already exists : test@example.com", exception.getMessage());
}
existsByUsername(any()) 로 변경해주니 테스트가 정상적으로 동작한다.
내일 테스트 코드 작성에 대해 좀 더 정리해봐야겠다.
'Spring Boot' 카테고리의 다른 글
25.02.25 통합테스트 DB 환경 분리 (0) | 2025.02.25 |
---|---|
25.02.20 JWT 리팩토링 과 헥사고날 아키텍처 도입 (0) | 2025.02.20 |
25.02.19 QueryDSL (1) | 2025.02.19 |
25.02.13 Spring Boot AOP (0) | 2025.02.13 |
25.02.12 Spring Boot JPA 에서 엔티티 간의 연관관계 매핑 (0) | 2025.02.12 |