인증/인가 과정에서 발생한 예외를 처리하는 방법
인증 : 사용자의 신원 확인
인가 : 신원이 확인된 사용자가 리서스에 액세스할 수 있는 권한이 있는지 확인
✨ AuthenticationEntryPoint : 인증 예외
인증이 안된 익명의 사용자가 인증이 필요한 엔드포인트에 접근하게 된다면 HttpStatus 401 Unauthorized와 함께 스프링의 기본 오류 페이지를 보여준다. → 이를 처리해주는 로직이 바로 AuthenticationEntryPoint 라는 인터페이스이다.
인터페이스 내부를 살펴보면 인증 프로세스를 시작하기 위해 필요에 따라 ServletRespnse의 헤더를 수정해야한다고 나와있다. 따라서 Response의 contentType과 status를 수정해야한다.
- AuthenticationEntryPoint 구현체
@Slf4j
@RequiredArgsConstructor
@Component
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
private final ObjectMapper objectMapper;
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
log.error("가입되지 않은 사용자 접근 : {} ", request.getRequestURI());
String responseBody = objectMapper.writeValueAsString(Response.error(ErrorCode.USER_NOT_FOUND.name()));
response.setContentType("application/json");
response.setStatus(ErrorCode.USER_NOT_FOUND.getStatus().value());
response.getWriter().write(responseBody);
}
}
SpringSecurity 에서 인증되지 않은 사용자의 리소스에 대한 접근 처리는 AuthenticationEntryPoint 가 담당한다.
인증되지 않은 사용자가 인증이 필요한 요청 엔드포인트로 접근하려 할 때, 예외 핸들링을 할 수 있도록 도와준다.
기본 오류 페이지가 아닌 커스텀 오류 페이지를 보여준다거나, 특정 로직을 수행 또는 JSON 데이터 등으로 응답해야 하는 경우, AuthenticationEntryPoint 인터페이스를 구현하고 구현체를 시큐리티에 등록하여 사용할 수 있다.
위 코드는 AuthenticationEntryPoint를 구현하여 인증되지 않은 사용자가 인증이 필요한 엔드포인트에 접근하려고 할 때 발생한 예외를 잡아서 JSON 형태의 API 스펙으로 응답하도록 한다.
- Spring Security 컨텍스트 내에서 관리되어야 하므로 @Component 빈이 되어야한다.
- SecurityConfig 설정에도 HttpSecurity 정보에 exceptionHandler에 대한 정보로 추가해줘야한다.
✨ AccessDeniedHandler : 인가 예외
인증이 완료되었으나 해당 엔드포인트에 접근할 권한이 없다면, 403 Forbidden 오류가 발생한다.이 역시 스프링 시큐리티는 기본적으로 스프링의 기본 오류 페이지를 응답한다.
- AccessDeniedHandler 구현체
@Slf4j
@RequiredArgsConstructor
@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
private final ObjectMapper objectMapper;
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
log.error("권한없는 사용자 접근 : {} ", request.getRequestURI());
String responseBody = objectMapper.writeValueAsString(Response.error(ErrorCode.INVALID_PERMISSION.name()));
response.setContentType("application/json");
response.setStatus(ErrorCode.INVALID_PERMISSION.getStatus().value());
response.getWriter().write(responseBody);
}
}
Spring Security 에서 인증되었으나 권한이 없는 사용자의 리소스에 대한 접근 처리는 AccessDeniedHandler가 담당한다.
위 코드는 AccessDeniedHandler를 구현하여 인증은 완료되었으나 요청에 대한 권한을 가지고 있지 않은 사용자가 엔드포인트에 접근하려고 할 때 발생한 예외를 잡아서 JSON 형태의 API 스펙으로 응답하도록 한다.
✨ 핸들러 등록
AuthenticationEntryPoint, AccessDeniedHandler를 구현했다면 이를 스프링 시큐리티에 등록해줘야 한다.
구현한 구현체 핸들러들을 스프링 빈으로 등록해주고, 시큐리티 설정에서 다음과 같이 작성하여 등록해준다.
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class AuthenticationConfig {
private final CustomAuthenticationEntryPoint customAuthenticationEntryPoint;
private final CustomAccessDeniedHandler customAccessDeniedHandler;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
.cors().and().csrf().disable()
.authorizeHttpRequests()
/* 권한 등록 */
.requestMatchers("/api/*/posts/*").hasAnyAuthority(UserRole.USER.name(), UserRole.ADMIN.name())
.requestMatchers("/api/*/users/join", "/api/*/users/login").permitAll()
.requestMatchers("/api/**").authenticated()
.and()
.formLogin().disable()
.logout()
.logoutSuccessUrl("/")
.and()
.addFilterBefore(new JwtTokenFilter(jwtProperties.getSecretKey()), UsernamePasswordAuthenticationFilter.class)
/* 예외처리 기능 작동 */
.exceptionHandling()
.accessDeniedHandler(customAccessDeniedHandler)
.authenticationEntryPoint(customAuthenticationEntryPoint)
.and()
.build();
}
}
.exceptionHandling() 메서드는 예외 처리 및 보안 예외에 대한 처리를 구성하는 데 사용된다.
- authenticationEntiryPoint(customAuthenticationEntryPoint) : 사용자가 인증되지 않은 상태로 보호된 리소스에 액세스하려고 할 때 호출된다.
- accessDeniedHanlder(customAccessDeniedHanlder) : 사용자가 인증되었지만 특정 리소스에 대한 액세스 권한이 없을때 호출 된다.
✏️ 참고
[Spring Security] AuthenticationEntryPoint, AccessDeniedHandler를 통한 인증/인가 오류 처리