문제 정의
•
사실 수집
유저 회원가입 테스트 중 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
복사