새소식

framework/spring-security

[Spring Security] AuthenticationEntryPoint, AccessDeniedHandler를 통한 인증/인가 오류 처리

  • -

인증/인가 오류 처리

우리는 Spring Security를 통해 사용자의 권한을 처리하게 된다.
Security 설정을 통해 특정 엔드포인트로의 요청에 필요한 권한 등을 설정할 수 있다.

@Bean
public SecurityFilterChain oauth2SecurityFilterChain(HttpSecurity http) throws Exception {
    http
        // ...
        .and()
        .authorizeRequests()
        .antMatchers("/end-point1").hasRole(Role.USER.name())
        .antMatchers("/end-point2").hasRole(Role.ADMIN.name())
        .antMatchers("/end-point3").permitAll()
        // ...

    return http.build();
}

위 설정은 아래 설명과 같다.

  • /end-point1로의 요청은 USER 권한이 있다면 접근
  • /end-point2로의 요청은 ADMIN 권한이 있다면 접근
  • /end-point3로의 요청은 모든 인증된 사용자를 허용

만약 여기서 인증이 안된 익명의 사용자가 해당 엔드 포인트로 접근하려하면 어떻게 될까?
인증은 되었으나 해당 요청에 대한 권한이 없는 유저는 어떻게 될까?

인증과 인가 과정에서 발생한 예외를 처리하는 방법에 대해서 알아보도록 한다.

AuthenticationEntryPoint

인증이 안된 익명의 사용자가 인증이 필요한 엔드포인트로 접근하게 된다면 Spring Security의 기본 설정으로는 HttpStatus 401과 함께 스프링의 기본 오류페이지를 보여준다.

HttpStatus 401 Unauthorized는 사용자가 인증되지 않았거나 유효한 인증 정보가 부족하여 요청이 거부된 것을 말한다.

기본 오류 페이지가 아닌 커스텀 오류 페이지를 보여준다거나, 특정 로직을 수행 또는 JSON 데이터 등으로 응답해야 하는 경우, 우리는 AuthenticationEntryPoint 인터페이스를 구현하고 구현체를 시큐리티에 등록하여 사용할 수 있다.

AuthenticationEntryPoint 인터페이스는 인증되지 않은 사용자가 인증이 필요한 요청 엔드포인트로 접근하려 할 때, 예외를 핸들링 할 수 있도록 도와준다.

public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {

    private final ObjectMapper objectMapper;

    @Override
    public void commence(HttpServletRequest request,
                         HttpServletResponse response,
                         AuthenticationException authException) throws IOException, ServletException {
        log.error("Not Authenticated Request", authException);
        log.error("Request Uri : {}", request.getRequestURI());

        ApiResponse<ErrorResponse> apiResponse = ApiResponse.createAuthenticationError();
        String responseBody = objectMapper.writeValueAsString(apiResponse);

        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        response.setStatus(HttpStatus.UNAUTHORIZED.value());
        response.setCharacterEncoding("UTF-8");
        response.getWriter().write(responseBody);
    }
}

위 코드는 AuthenticationEntryPoint를 구현하여 인증되지 않은 사용자가 인증이 필요한 엔드포인트에 접근하려고 할 때 발생한 예외를 잡아서 JSON 형태의 API 스펙으로 응답하도록 한다.

AccessDeniedHandler

인증이 완료되었으나 해당 엔드포인트에 접근할 권한이 없다면, 403 Forbidden 오류가 발생한다.
이 역시 스프링 시큐리티는 기본적으로 스프링의 기본 오류 페이지를 응답한다.

HttpStatus 403 Forbidden은 서버가 해당 요청을 이해하였으나, 사용자의 권한이 부족하여 요청이 거부된 상태를 말한다.

이를 커스텀하기 위해서는 AccessDeniedHandler 인터페이스를 구현하면 된다.

public class CustomAccessDeniedHandler implements AccessDeniedHandler {

    private final ObjectMapper objectMapper;

    @Override
    public void handle(HttpServletRequest request,
                       HttpServletResponse response,
                       AccessDeniedException accessDeniedException) throws IOException, ServletException {
        log.error("No Authorities", accessDeniedException);
        log.error("Request Uri : {}", request.getRequestURI());

        ApiResponse<ErrorResponse> apiResponse = ApiResponse.createAuthoritiesError();
        String responseBody = objectMapper.writeValueAsString(apiResponse);

        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        response.setStatus(HttpStatus.FORBIDDEN.value());
        response.setCharacterEncoding("UTF-8");
        response.getWriter().write(responseBody);
    }
}

위 코드는 AccessDeniedHandler를 구현하여 인증은 완료되었으나 요청에 대한 권한을 가지고 있지 않은 사용자가 엔드포인트에 접근하려고 할 때 발생한 예외를 잡아서 JSON 형태의 API 스펙으로 응답하도록 한다.

핸들러 등록

AuthenticationEntryPoint, AccessDeniedHandler를 구현했다면 이를 스프링 시큐리티에 등록해줘야 한다.

구현한 핸들러들을 스프링 빈으로 등록해주고, 시큐리티 설정에서 다음과 같이 작성하여 등록해준다.

@Bean
public SecurityFilterChain oauth2SecurityFilterChain(HttpSecurity http) throws Exception {
    http
        // ...
        .and()
        .authorizeRequests()
        .antMatchers("/api/v1/user-profile").hasRole(Role.TEMP.name())
        .antMatchers("/api/v1/blog/**").hasAnyRole(Role.USER.name(), Role.ADMIN.name())
        .antMatchers("/api/v1/auth/**").permitAll()
        .antMatchers("/api/v1/users/**").permitAll()
        // ...
        .and()
        .exceptionHandling()
        // AuthenticationEntryPoint 등록
        .authenticationEntryPoint(customAuthenticationEntryPoint)
        // AccessDeniedHandler 등록
        .accessDeniedHandler(customAccessDeniedHandler);

    return http.build();
}
[Spring Security] AuthenticationEntryPoint, AccessDeniedHandler를 통한 인증/인가 오류 처리

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.