스프링 공부/게시판 프로젝트 만들기

[스프링] 35. AuthController.reissue 테스트부터 프로젝트 코드까지

장아장 2023. 2. 20. 20:18

토큰을 재발행하기 위해서는 어떤 과정이 필요할까를 생각해보았다. 

  1. 필터체인에서 권한 검증을 받는다(여기서 토큰이 검증되지 않을 경우 예외처리된다)
  2. HttpServletRequest에서 토큰을 가져온다. 
  3. 가져온 토큰을 AuthService.reissue에 집어넣는다. 
  4. 서비스에서 나오는 TokenResponseDto을 HttpServletResponse에 담아준다. 
  5. 응답을 준다. 

정도로 생각했다. 

 

저번과 마찬가지로, 성공과 실패 경우를 생각해보려고 했다. 

하지만, 생각해보니, 이에 대한 문제를 생각하지 않아도 될 것 같은 이유가, 모든 경우에 대한 예외를 Spring Security를 만들 때 적용시켜두었다. 

그래서 간단하게, 토큰이 정상적으로 있을 때, 토큰이 없을 때, 토큰이 정상적이지 않을 때를 테스트해보았다.

@Test
@DisplayName("헤더에 Access Token, Refresh Token이 있을 때 토큰이 재발행되어 헤더에 반환된다.")
public void reissue_Success() throws Exception{
    //given
    RegisterRequestDto registerRequestDto = RegisterRequestDto.builder()
            .username("testUser" + 1)
            .nickname("test" + 1)
            .email("test"  + 1 + "@test.com")
            .password("테스트" + 1)
            .passwordCheck("테스트" + 1)
            .build();
    authService.registerNewMember(registerRequestDto);
    TokenResponseDto tokenResponseDto = authService.signIn(
            SignInRequestDto.builder().username("testUser1").password("테스트1").build());
    TimeUnit.SECONDS.sleep(3);
    //expected
    mvc.perform(MockMvcRequestBuilders.post("/api/auth/reissue")
            .header("Authorization", "Bearer ".concat(tokenResponseDto.getAccessToken()))
            .header("RefreshToken", "Bearer ".concat(tokenResponseDto.getRefreshToken())))
            .andExpect(MockMvcResultMatchers.status().isOk())
            .andDo(MockMvcResultHandlers.print());
}

@Test
@DisplayName("토큰이 유실되었을 경우, 400에러와 로그인 재요청을 반환")
public void reissue_Fail() throws Exception{
    //given
    RegisterRequestDto registerRequestDto = RegisterRequestDto.builder()
            .username("testUser" + 1)
            .nickname("test" + 1)
            .email("test"  + 1 + "@test.com")
            .password("테스트" + 1)
            .passwordCheck("테스트" + 1)
            .build();
    authService.registerNewMember(registerRequestDto);
    TokenResponseDto tokenResponseDto = authService.signIn(
            SignInRequestDto.builder().username("testUser1").password("테스트1").build());
    TimeUnit.SECONDS.sleep(3);
    //expected
    mvc.perform(MockMvcRequestBuilders.post("/api/auth/reissue")
            .header("Authorization", "Bearer ".concat(tokenResponseDto.getAccessToken())))
            .andExpect(MockMvcResultMatchers.status().isBadRequest())
            .andExpect(MockMvcResultMatchers.jsonPath("$.success").value(false))
            .andExpect(MockMvcResultMatchers.jsonPath("$.code").value(400))
            .andExpect(MockMvcResultMatchers.jsonPath("$.result.failMessage").value("로그인 후 이용해주세요."))
            .andDo(MockMvcResultHandlers.print());
}

@Test
@DisplayName("토큰이 조작되었을 때 401예외를 반환한다.")
public void reissue_Fail2() throws Exception{
    //given
    RegisterRequestDto registerRequestDto = RegisterRequestDto.builder()
            .username("testUser" + 1)
            .nickname("test" + 1)
            .email("test"  + 1 + "@test.com")
            .password("테스트" + 1)
            .passwordCheck("테스트" + 1)
            .build();
    authService.registerNewMember(registerRequestDto);
    TokenResponseDto tokenResponseDto = authService.signIn(
            SignInRequestDto.builder().username("testUser1").password("테스트1").build());
    TimeUnit.SECONDS.sleep(3);
    //expected
    mvc.perform(MockMvcRequestBuilders.post("/api/auth/reissue")
            .header("Authorization", "Bearer ".concat(tokenResponseDto.getAccessToken())+"1")
            .header("RefreshToken", "Bearer ".concat(tokenResponseDto.getRefreshToken())))
            .andExpect(MockMvcResultMatchers.status().isUnauthorized())
            .andDo(MockMvcResultHandlers.print());
}

로그인을 해서 토큰을 받은 후, 토큰을 헤더에 넣어서 요청을 넣어보았다. 

 

이렇게 만든 헤더에, 

@PostMapping("/reissue")
public Response reissue(HttpServletRequest request, HttpServletResponse response){
    TokenResponseDto tokenResponseDto = authService.reissue(tokenPath.getReissueResponseDtoFromHeader(request));
    tokenPath.putTokensOnHeader(response, tokenResponseDto);
    return Response.success("재발행 성공");
}

이런식으로 컨트롤러 실제 로직을 만들어주었다. 

추가로, SecurityConfig에 해당 주소에 대한 설정을 해주어야 한다.

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
    http.httpBasic(withDefaults())
            .authorizeRequests().requestMatchers(CorsUtils::isPreFlightRequest).permitAll();

    http.addFilter(config.corsFilter())
            .csrf().disable()
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
            .formLogin().disable()
            .httpBasic().disable();

    http.exceptionHandling()
            .authenticationEntryPoint(jwtAuthenticationEntryPoint)
            .accessDeniedHandler(jwtAccessDenialHandler)
            .and()
            .authorizeRequests()
            .antMatchers("/api/auth/sign_in", "/api/auth/join").permitAll()
            .antMatchers("/api/auth/reissue").access("hasAuthority('USER') or hasAuthority('MANAGER') or hasAuthority('ADMIN')")
            .antMatchers("/api/members/**").access("hasAuthority('USER') or hasAuthority('MANAGER') or hasAuthority('ADMIN')")
            .and()
            .apply(new JwtSecurityConfig(tokenProvider));
    return http.build();
}

전체적으로 설정을 조금 추가해주었다. 

개인적으로 추천하는 바는, 하나씩 모르겠는 부분을 cmd + 클릭 (맥 기준) 해서 주석을 보는 것을 추천한다.

(는 사실 내가 설명하는 것 보다, 그걸 찾아서 읽어보는게 기능적으로 더 잘 설명될 것 같기 때문이다)

 

일단 필요한 부분을 이야기 해보자면, 

authorizeRequests()에서 이제 각 url에 대한 권한설정을 할 수 있다. 

antMatchers에 권한을 설정할 url요청 주소를 쓰고, .access에 어떤 권한이 있는지를 설정할 수 있다. 

이에 대한 설정을 해주기 위해서는, 

public enum Role {

    USER, MANAGER, ADMIN;
}

여기에서 우리가 권한을 무슨 이름으로 설정했는지 확인해야 하고, 

여기에서 토큰을 만들 때, 이름/비밀번호/authority를 넣었다. 

이를 토대로, 우리는 hasAuthority라는 메서드로 안에 enum타입의 role를 넣어야 한다는 것을 찾아냈다.

(사실 여기서 예전에 공부할 때 썼던 hasRole이라고 써둬서 나도 찾아보면서 왜이런가 했다)

 

이후, 테스트를 실행시켰다. 

이렇게 AuthController에 대한 테스트들을 완성시켰다. 

개인적으로 힘들었던 부분은, 

hasAuthority라는 것을 어디서 찾아서, 어떻게 만들어져 있는지 파악하는데 아직 미숙했다는 것과, 

토큰이 바로 reissue되면 바뀌지 않았던 것이다. 

토큰을 다 뜯어보면서, 어떤 부분이 있던 건지 직접 파악했다. 

거기서 Authority라고 되어있는 것을 보았고, 추가로 LocalDateTime이 있었다. 

여기서 time delay를 넣어야 토큰이 진정으로 바뀐다는 것을 파악할 수 있었다. 

내가 만들지만 나도 몰랐던 것을 배우니까 할 맛이 난다.