
개요
AuthenticationEntryPoint를 통해 인증되지 않은 사용자가 접근할 때에 대한 예외 처리를 하는 것이 목적이었다. 하지만 인증 예외 처리가 일어나지 말아야 할 상황에서도 인증 에러 메시지가 호출 되었다.
해당 과정에서 있었던 트러블 슈팅을 기록하려 한다.
코드
CustomAuthenticationEntryPoint
@Component
@Slf4j
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
private final ObjectMapper mapper = new ObjectMapper();
private String toJson(ErrorResponse response) throws JsonProcessingException {
return mapper.writeValueAsString(response);
}
private void sendJson(HttpServletResponse response, String resultJson) throws IOException {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.setContentType("application/json;charset=UTF-8");
response.setContentLength(resultJson.getBytes().length);
response.getWriter().write(resultJson);
}
//인증 예외 발생 시, JSON으로 변환하여 응답한다
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
AuthErrorCode errorCode = AuthErrorCode.AUTH_REQUIRED;
log.info("{}", errorCode.getMessage());
sendJson(response, toJson(new ErrorResponse(errorCode.getMessage(), errorCode.getCode())));
}
}
인증되지 않은 사용자가 인증 필요 API에 접근할 때, 해당 AuthenticationEntryPoint에서 인증 예외를 잡아 직접 JSON 메시지를 던져주기를 기대했다.
SecurityConfig
@Configuration
@EnableWebSecurity
@Slf4j
@RequiredArgsConstructor
public class SecurityConfig {
private final TokenProvider tokenProvider;
private final AuthenticationSuccessHandler authenticationSuccessHandler;
private final UserRepository userRepository;
//직접 구현한 AuthenticationEntryPoint
private final AuthenticationEntryPoint authenticationEntryPoint;
.
.
.
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.
.
.
// 직접 구현한 AuthenticationEntryPoint 설정
.exceptionHandling(configurer -> configurer
.authenticationEntryPoint(authenticationEntryPoint))
)
.addFilterBefore(
new CustomAuthenticationFilter(tokenProvider, userRepository),
UsernamePasswordAuthenticationFilter.class)
.oauth2Login(customizer -> customizer.successHandler(authenticationSuccessHandler))
.build();
}
}
하지만 생각처럼 동작하지 않았고, 해당 AuthenticationEntryPoint에서 명확하지 않은 기준으로 예외를 잡아먹어 처리하였다..!
검색을 해도 비슷한 케이스가 없었고, 로직 상의 문제도 없어보였다...
팀원들에게 해당 트러블에 대해 말하니, AuthenticationEntryPoint는 예외를 처리하기보다는 예외를 던져주고 ExceptionFilter를 구현하여 처리하게 하는게 낫다 라고 조언하여 주었다!
해당 팀원도 비슷한 트러블을 겪고, 디버깅해보았지만 답을 찾지 못하였다고 한다....
개선
그래서 위에서 조언받은데로 AuthenticationEntryPoint를 예외 던지기의 용도로 사용하였다.
SecurityConfig
@Configuration
@EnableWebSecurity
@Slf4j
@RequiredArgsConstructor
public class SecurityConfig {
private final TokenProvider tokenProvider;
private final AuthenticationSuccessHandler authenticationSuccessHandler;
private final UserRepository userRepository;
// 예외 처리 필터
private final JwtExceptionHandlerFilter jwtExceptionHandlerFilter;
.
.
.
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.
.
.
//인증 예외 시 예외 변환하여 throw
.exceptionHandling(configurer -> configurer
.authenticationEntryPoint(
(request, response, exception)
-> {
throw new ValidationException(AuthErrorCode.AUTH_REQUIRED);
}
)
)
//예외 처리 필터 추가
.addFilterBefore(
jwtExceptionHandlerFilter,
UsernamePasswordAuthenticationFilter.class
)
.addFilterBefore(
new CustomAuthenticationFilter(tokenProvider, userRepository),
UsernamePasswordAuthenticationFilter.class)
.oauth2Login(customizer -> customizer.successHandler(authenticationSuccessHandler))
.build();
}
}
직접 구현할 필요 없이 인증 예외 발생 시 람다식을 이용하여 예외를 변환하여 던져주도록 하였다.
그리고 해당 예외를 처리할 수 있도록 CustomExceptionFilter를 구현하였다
JwtExceptionHandlerFilter
@Component
@Slf4j
public class JwtExceptionHandlerFilter extends GenericFilter {
private final ObjectMapper mapper = new ObjectMapper();
private String toJson(ErrorResponse response) throws JsonProcessingException {
return mapper.writeValueAsString(response);
}
private void sendJson(HttpServletResponse response, String resultJson) throws IOException {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.setContentType("application/json;charset=UTF-8");
response.setContentLength(resultJson.getBytes().length);
response.getWriter().write(resultJson);
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
try {
chain.doFilter(request, response);
} catch (ValidationException ve) {
log.info("{}", ve.getMessage());
sendJson(httpServletResponse, toJson(new ErrorResponse(ve.getMessage(), ve.getCode())));
}
}
}
커스텀하게 구현한 ValidationException이 넘어온다면 catch 후, json으로 변환하여 응답한다.
개선 후, 어처구니 없는 인증 예외가 날아오는 일은 거의 없었다!!
정리
사실 로직이 크게 변한 느낌은 아니다.
개선 전과 후는, 예외를 잡아서 처리를 해주냐, 그저 예외를 변환해서 던져주냐의 차이 밖에 없어보인다...
하지만 개선 후, 이슈가 더이상 발생하지 않는 것을 보면, AuthenticationEntryPoint의 어떠한 비밀이 있는 것처럼 보인다
디버깅을 해봐도 모르겠고, Security가 너무 방대해서 더 깊게 파보다가는 다른 것을 못할 것 같다
비슷한 경험이 있거나, 해당 로직에 대해 잘 알고 계신 분이라면, 댓글로 남겨주시면 너무너무 감사드리겠습니다~ :-)
'Spring > Security' 카테고리의 다른 글
| [Spring Security] Security OAuth2.0 디버깅 & 커스텀 여행기 (0) | 2023.12.28 |
|---|