///
Search
📢

getWriter() has already been called for this response

문제 정의

사실 수집
유저 회원가입 테스트 중 getWriter() has already been called for this response 출력
ExceptionHandeler에서 문제가 났다고 하길래, Exception을 안 내면 되겠다 생각
Exception을 안 내도 getWriter 오류 발생
원인 추론
response를 반환해야하는데, 이미 getWriter()가 쓰였기 때문에 안 된다는 의미 같은데
JwtAuthorizationFilter에서 오류가 난다고 한다
이 부분에서 난다.
@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); } }
Java
복사
내가 작성한 코드를 보면 토큰이 유효하지 않을 시, getWriter()를 쓰는데
생각해보니 왜 회원가입을 테스트하는데 Authorization에 들어가지?
@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(); } }
Java
복사
Security Config에서 requestMatcher 에서도 /users 에 대해 인가를 해야한다 하는 부분은 적혀있지 않다.
이미 anyRequest()를 permitAll을 해주는데…
http.addFilterBefore(jwtAuthorizationFilter(), UsernamePasswordAuthenticationFilter.class);
http.addFilterBefore(jwtAuthenticationFilter(), JwtAuthorizationFilter.class);
이 부분이 문제 인 걸까?
코드 상으로는 인가 전에 인증 필터를 먼저 거쳐야한다고 적었다
계속 헷갈려서.. 전에 테스트할 때는 멀쩡해서 놔둔 부분인데
자료를 찾아봐야겠다

조치 방안 검토

관련 자료 조사
AuthenticationManager가 UsernamePasswordAuthenticationFilter 후에 적용되는 것을 볼 수 있다
현재 내가 작성한 코드에서는 AuthenticationManager 역할을 jwtAuthenticationFilter 이 대신 해주고 있는데 이렇게 보면 AuthenticationFilter 를 UsernamePasswordAuthenticationFilter 뒤에 넣으면 되지 않을까?
황원욱 튜터님께 질문
AuthorizationFilter에서 Response만들고 writer를 하는게 일반적인 방법이 아니다.
Filter 맨 앞단에 Exception Handler를 넣는게 좋을 거 같다

조치 방안 구현

1.
filter 순서 수정
a.
http.addFilterBefore(jwtAuthorizationFilter(), UsernamePasswordAuthenticationFilter.class);
http.addFilterAfter(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
순서 수정 후 재 테스트를 해봤지만 그대로다..
내가 잘못 이해했나보다 다시 되돌렸다
2.
Filter 앞단에 예외 핸들러 추가하기
a.
필터 예외 핸들러 추가
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; } }
Java
복사
b.
WebSecurityConfig 코드 추가
http.addFilterBefore(new ExceptionHandlerFilter(),JwtAuthenticationFilter.class);
Java
복사

해결 방안

해결방법
기존에 작성됐던 AuthorizationFilter에서 getWriter()를 쓰는 코드가 일반적인 방법이 아니었다
그래서 필터에서 예외가 발생하면 Filter 앞단에 필터 예외 핸들러를 추가해서 문제를 해결했다.
public void validateTokenAndThrow(String token) { try { Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token); } catch (SecurityException | MalformedJwtException | SignatureException e) { throw e; } catch (ExpiredJwtException e) { throw e; } catch (UnsupportedJwtException e) { throw e; } catch (IllegalArgumentException e) { throw e; } catch (NullPointerException e) { throw e; } }
Java
복사
@RequiredArgsConstructor public class JwtAuthorizationFilter extends OncePerRequestFilter { private final JwtUtil jwtUtil; 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); jwtUtil.validateTokenAndThrow(accessToken); } } else if (accessToken != null) { jwtUtil.validateTokenAndThrow(accessToken); } filterChain.doFilter(request, response); } }
Java
복사
public class ExceptionHandlerFilter extends OncePerRequestFilter { @Override protected void doFilterInternal( HttpServletRequest request, HttpServletResponse response, FilterChain filterChain ) throws ServletException, IOException { try { filterChain.doFilter(request, response); } catch (SecurityException | MalformedJwtException | SignatureException e) { // 유효하지 않은 토큰 setErrorResponse(response, ExceptionCode.UNAUTHORIZED_TOKEN_INVALID_SIGNATURE); } catch (ExpiredJwtException e) { // 유효기간 만료이 만료된 토큰 setErrorResponse(response, ExceptionCode.UNAUTHORIZED_TOKEN_EXPIRED); } catch (UnsupportedJwtException e) { // 지원되지 않는 토큰 setErrorResponse(response, ExceptionCode.UNAUTHORIZED_TOKEN_UNSUPPORTED); } catch (IllegalArgumentException e) { // 잘못된 토큰 setErrorResponse(response, ExceptionCode.UNAUTHORIZED_TOKEN_INVALID); } catch (NullPointerException e) { // 토큰이 없음 setErrorResponse(response, ExceptionCode.UNAUTHORIZED_TOKEN_IS_NULL); } catch (JwtException e) { // 토큰 처리 중 오류 발생 (포괄적) setErrorResponse(response, ExceptionCode.UNAUTHORIZED_TOKEN_GENERIC_ERROR); } } private void setErrorResponse( HttpServletResponse response, ExceptionCode exceptionCode ) { ObjectMapper objectMapper = new ObjectMapper(); response.setStatus(exceptionCode.getHttpStatus().value()); response.setContentType(MediaType.APPLICATION_JSON_VALUE + ";charset=UTF-8"); 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 status; private final String msg; } }
Java
복사