문제 정의
- 사실 수집
유저 회원가입 테스트 중 getWriter() has already been called for this response 출력
ExceptionHandeler에서 문제가 났다고 하길래, Exception을 안 내면 되겠다고 생각했다.
Exception을 안 내도 getWriter() has already 오류가 발생했다
- 원인 추론
response를 반환해야하는데, 이미 getWriter() 가 쓰였기 때문에 안 된다는 의미 같았다.
@RequiredArgsConstructor
public class JwtAuthorizationFilter extends OncePerRequestFilter {
private final JwtUtil jwtUtil;
private final ObjectMapper objectMapper;
private final UserDetailsServiceImpl userDetailsServiceImpl;
private final RefreshTokenRepository refreshTokenRepository;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String accessToken = jwtUtil.resolveAccessToken(request);
String refreshToken = jwtUtil.resolveRefreshToken(request);
if (accessToken != null && jwtUtil.validateToken(accessToken)) {
Claims info = jwtUtil.getUserInfoFromToken(accessToken);
// 인증정보에 유저정보 넣기
String email = info.getSubject();
SecurityContext context = SecurityContextHolder.createEmptyContext();
UserDetails userDetails = userDetailsServiceImpl.loadUserByUsername(email);
Authentication authentication = new UsernamePasswordAuthenticationToken(userDetails,
null, userDetails.getAuthorities());
context.setAuthentication(authentication);
SecurityContextHolder.setContext(context);
} else if (refreshToken != null && jwtUtil.validateToken(refreshToken)) {
Claims info = jwtUtil.getUserInfoFromToken(refreshToken);
String username = info.getSubject();
UserRoleEnum role = UserRoleEnum.valueOf(
info.get(JwtUtil.AUTHORIZATION_KEY).toString());
if (refreshTokenRepository.existsByUsername(username)) {
String newAccessToken = jwtUtil.createAccessToken(username, role);
String currentRefreshToken = JwtUtil.BEARER_PREFIX + refreshToken;
jwtUtil.addJwtToHeader(newAccessToken, currentRefreshToken, response);
}
} else {
CommonResponseDto commonResponseDto = new CommonResponseDto(
HttpStatus.BAD_REQUEST.value(), "토큰이 유효하지 않습니다.");
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
response.setContentType("application/json; charset=UTF-8");
response.getWriter() // 이 부분이 문제인 거 같다..
.write(objectMapper.writeValueAsString(commonResponseDto));
}
filterChain.doFilter(request, response);
}
}
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class WebSecurityConfig {
private final JwtUtil jwtUtil;
private final ObjectMapper objectMapper;
private final UserDetailsServiceImpl userDetailsServiceImpl;
private final AuthenticationConfiguration authenticationConfiguration;
private final RefreshTokenRepository refreshTokenRepository;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration)
throws Exception {
return configuration.getAuthenticationManager();
}
@Bean
public JwtAuthorizationFilter jwtAuthorizationFilter() {
return new JwtAuthorizationFilter(jwtUtil, objectMapper, userDetailsServiceImpl,
refreshTokenRepository);
}
@Bean
public JwtAuthenticationFilter jwtAuthenticationFilter() throws Exception {
JwtAuthenticationFilter filter = new JwtAuthenticationFilter(jwtUtil);
filter.setAuthenticationManager(authenticationManager(authenticationConfiguration));
return filter;
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.csrf((csrf) -> csrf.disable());
http.sessionManagement((sessionManagement) ->
sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
);
http.authorizeHttpRequests((authorizeHttpRequests) ->
authorizeHttpRequests
.requestMatchers("/reviews/**").authenticated()
.requestMatchers("/orders/**").authenticated()
.anyRequest().permitAll()
);
http.addFilterBefore(jwtAuthorizationFilter(), UsernamePasswordAuthenticationFilter.class);
http.addFilterBefore(jwtAuthenticationFilter(), JwtAuthorizationFilter.class);
return http.build();
}
}
addFilterBefore 부분이 문제인 건가?
싶어서 자료를 찾아봤다.
조치 방안 검토
1. 위 블로그에서는 AuthenticationManager가 UsernamePasswordAuthenticationFilter 후에 적용이 된다는데, jwtAuthenticationFilter을 UsernamePasswordAuthenticationFilter 뒤에 넣어줘야하나?
2.
else {
CommonResponseDto commonResponseDto = new CommonResponseDto(
HttpStatus.BAD_REQUEST.value(), "토큰이 유효하지 않습니다.");
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
response.setContentType("application/json; charset=UTF-8");
response.getWriter()
.write(objectMapper.writeValueAsString(commonResponseDto));
}
내 Authorization Filter 코드에서 토큰이 유효하지 않으면 사용자에게 오류 메세지와 오류 코드를 반환하고 싶어서 이렇게 코드를 짰는데 이 방법이 문제인가?
찾아보니 내가 작성한 방법은 일반적인 방법이 아니라고 한다.
조치 방안 구현
1. Filter 순서 수정
http.addFilterBefore(jwtAuthorizationFilter(), UsernamePasswordAuthenticationFilter.class);
http.addFilterAfter(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
순서 수정 후 재 테스트를 해봤지만 그대로다
내가 이해하고 있던 부분이 맞았던 거 같다
2. Filter 앞단에 예외 핸들러 추가
2-1. 필터 예외 핸들러 추가
public class ExceptionHandlerFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain
) throws ServletException, IOException {
try{
filterChain.doFilter(request, response);
}catch (ExpiredJwtException e){
//토큰의 유효기간 만료
setErrorResponse(response, ExceptionCode.UNAUTHORIZED_TOKEN_EXPIRED);
}catch (JwtException | IllegalArgumentException e){
//유효하지 않은 토큰
setErrorResponse(response, ExceptionCode.UNAUTHORIZED_TOKEN_INVALID);
}
}
private void setErrorResponse(
HttpServletResponse response,
ExceptionCode exceptionCode
){
ObjectMapper objectMapper = new ObjectMapper();
response.setStatus(exceptionCode.getHttpStatus().value());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
ErrorResponse errorResponse = new ErrorResponse(exceptionCode.getHttpStatus().value(), exceptionCode.getMessage());
try{
response.getWriter().write(objectMapper.writeValueAsString(errorResponse));
}catch (IOException e){
e.printStackTrace();
}
}
@Data
public static class ErrorResponse{
private final Integer code;
private final String message;
}
}
2-2. WebSecurityConfig에 코드 추가
http.addFilterBefore(new ExceptionHandlerFilter(),JwtAuthenticationFilter.class);
이 방법으로 하니 내가 원래 의도 했던 방식대로 잘 작동하고 오류도 해결됐다.
Filter 앞에다 컨트롤러 처럼 예외 핸들러를 추가하는 방식이 있다는 것이 있다고는 들었었는데
어떻게 구현해야하는지 감이 안잡혔었다
이번에 이런 오류를 만나면서 직접 구현해보니 아 이런 느낌이구나 싶었다
'TIL' 카테고리의 다른 글
TIL 2023-12-12 왜 Service를 인터페이스와 구현체로 나눌까? (0) | 2023.12.12 |
---|---|
TIL 2023-12-11 명시적 삭제 컬럼 (0) | 2023.12.11 |
TIL 2023-12-07 Refresh Token 쓰는 이유 및 DB로 Redis를 쓰는 이유 (0) | 2023.12.07 |
TIL 2023-12-06 jwt 로그인 시 unsuccessfulAuthentication로 계속 빠진다면? (0) | 2023.12.06 |
TIL 2023-12-05 Entity에 대한 JPA 테스트는 어떻게 할까? (0) | 2023.12.05 |