[Spring Security 예외 처리] 인증 및 인가에 대한 예외처리 방법

2025. 2. 25. 23:46·TIL
728x90
반응형
SMALL

스프링 시큐리티는 DispatcherServlet이라는 서블릿 단보다 앞에 위치한다. 그 말은 Spring Context와 서블릿의 영향을 받기 전에 먼저 작동하는 필터라는 말이다. 그렇기에 우리가 정의한 GlobalExceptionHandler에서 Exception이 발생하는 것을 처리하는 과정을 Filter에서는 적용할 수 없다.

그렇다면 SpringSecurity에서 발생하는 인증/인가에 대해서는 어떻게 처리해야 할까?

SpringSecurity에서는 ExceptionHandlingConfigurer라는 불변 클래스가 존재한다. 해당 클래스는 AuthenticationEntryPoint 클래스와 AccessDeniedHandler 클래스가 필드에 존재하는데 이 두 필드가 예외를 처리하는 클래스이다.
두 필드는 아래 보이는 LinkedHashMap타입의 맵에 디폴트값으로 추가가 되는데 만약 이 두 클래스를 구현한 구현체가 있다면 해당 구현체를 우선적으로 적용시킨다.


public final class ExceptionHandlingConfigurer<H extends HttpSecurityBuilder<H>> extends AbstractHttpConfigurer<ExceptionHandlingConfigurer<H>, H> {
    private AuthenticationEntryPoint authenticationEntryPoint;
    private AccessDeniedHandler accessDeniedHandler;
    private LinkedHashMap<RequestMatcher, AuthenticationEntryPoint> defaultEntryPointMappings = new LinkedHashMap();
    private LinkedHashMap<RequestMatcher, AccessDeniedHandler> defaultDeniedHandlerMappings = new LinkedHashMap();

SecurityConfig에서 전역설정

방법은 의외로 간단했다. 먼저 http에 메서드 체이닝으로 예외에 대한 핸들리을 authenticationEntryPoint와 accessDeniedHandler로 지정해주고 새로 만들 객체이름으로 적어준다.
클래스를 미리 만들고 임포트해줘도되고 여기서 이름을 지정하고 객체를 생성하는것도 상관없다.

@Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http, JwtHelper jwtHelper) throws Exception {
        // http 공통 설정
        http
                .formLogin(AbstractHttpConfigurer::disable)
                .httpBasic(AbstractHttpConfigurer::disable)
                .csrf(AbstractHttpConfigurer::disable)
                .cors(cors -> cors.configurationSource(corsConfigurationSource()))
                .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .addFilterBefore(new AuthenticationFilter(jwtHelper), UsernamePasswordAuthenticationFilter.class)
                .exceptionHandling(exception -> exception.authenticationEntryPoint(new JwtAuthenticationEntryPoint()))
                .exceptionHandling(exception -> exception.accessDeniedHandler(new CustomAccesDeniedHandler()))
        ;

AuthenticaionFilter

토큰에 대한 유효성을 검증하는 필터이다. 이 과정에서 인증이 제대로 이루어지지 않는다면 Security에서 인식할 수 있는 예외를 발생시키거나 Custom하여 만든 Exception을 발생시킨다. 나는 validateToken메서드 내부에서 토큰의 변조 및 상태검사가 이루어지며 CustomJWTException을 발생시켰다.
여기서 중요한점! CsutomException 클래스를 만든 경우 해당 예외 객체는 RuntimeException을 상속받을텐데 그렇다면 곧 만들 JwtAuthenticationEntryPoint에서 예외처리를 할 수 없다. SpringSecurity에서 내 예외 객체를 감지하지 못하기 때문이다. 그렇기에 나는 catch구문으로 직접 response에 데이터를 담아 전달해줬다. 만약 여기서 JwtAuthenticationEntryPoint로 예외를 전가시켜줘야 한다면 httpServeltRequest에 데이터를 담아 전달하면 된다.

@Slf4j
@RequiredArgsConstructor
public class AuthenticationFilter extends OncePerRequestFilter {

    private static final List<String> EXCLUDE_URLS = List.of(
            "/api/v1/auth/sign-up",
            "/api/owner/v1/auth/sign-up",
            "/api/v1/auth/sign-in",
            "/api/v1/auth/renew",
            "/api/v1/auth/sign-up/owner",
            "/api/v1/auth/sign-up/customer",
            "/api/master/v1/sign-in"
    );
    private final JwtHelper jwtHelper;

    @Override
    public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        //1.요청 경로를 가져오기
        String requestURI = request.getRequestURI();

        //2. 화이트리스트 경로 등록 후 필터에서 제외되도록 설정
        boolean isWhitelisted = EXCLUDE_URLS.stream().anyMatch(requestURI::matches);
        if (isWhitelisted) {
            filterChain.doFilter(request, response);
            return;
        }

        //3. Token 검증
        try {
            String accessToken = jwtHelper.resolveToken(request);
            if (accessToken != null) {
                jwtHelper.validateToken(accessToken);
                Authentication authentication = jwtHelper.getAuthenticationFromAccessToken(accessToken);
                if (authentication != null) {
                    SecurityContextHolder.getContext().setAuthentication(authentication);
                    filterChain.doFilter(request, response);
                    return;
                }
            } else {
                throw new CustomJwtException(ExceptionCode.NOT_FOUND_TOKEN);
            }
        } catch (CustomJwtException e) {
            log.error("JWT validation failed: {}", e.getMessage());

            // response에 바로 에러 응답을 설정하여 필터 체인 중단
            response.setContentType("application/json");
            response.setCharacterEncoding("UTF-8");
            response.setStatus(e.getHttpStatus().value());

            // ObjectMapper로 예쁘게 출력할 수 있도록 수정
            String errorResponse = String.format("{\"status\": \"%s\", \"message\": \"%s\", \"code\": \"%s\"}",
                    e.getHttpStatus(), e.getMessage(), e.getCode());

            response.getWriter().write(errorResponse);
            return;
        }

        filterChain.doFilter(request, response);
    }
}

JwtAuthenticationEntryPoint 생성

인증에 대한 예외를 처리할 수 있는 클래스이다. AuthenticationEntryPoint 인터페이스를 상속받아 commence를 구현하여 사용한다. 인증과정에서 문제가 발생할 경우 여기서 메세지를 response로 전달할 수 있다.


@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request,
                         HttpServletResponse response,
                         AuthenticationException authException) throws IOException {

        // Get message and code from request attributes, defaulting to empty string and 0 if null
        String message = (String) request.getAttribute("message");
        int code = (request.getAttribute("code") != null) ? (int) request.getAttribute("code") : 4999;

        // If message is null, set a default message
        if (message == null) {
            message = "Invalid Authentication";
        }

        response.setStatus(HttpServletResponse.SC_BAD_REQUEST);  // 400 Bad Request
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json");

        ObjectMapper objectMapper = new ObjectMapper();
        String result = objectMapper.writeValueAsString(ExceptionResponse.builder()
                .httpStatus(HttpStatus.FORBIDDEN)
                .status(HttpStatus.FORBIDDEN)
                .message(message)
                .code(code)
                .build());

        response.getWriter().write(result);
    }
}

CustomAccessDeniedHandler

인가에 대한 예외를 발생시킬 수 있는 클래스이다. AccessDeniedHandler를 상속받고 handle메서드를 구현하여 예외메세지를 전달할 수 있다.


public class CustomAccesDeniedHandler implements org.springframework.security.web.access.AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        // 요청 속성에서 메시지와 코드 가져오기
        String message = (String) request.getAttribute("message");
        int code = (request.getAttribute("code") != null) ? (int) request.getAttribute("code") : 4999;

        // 메시지가 없으면 기본 메시지 설정
        if (message == null) {
            message = "You do not have permission to access this resource";
        }

        response.setStatus(HttpServletResponse.SC_FORBIDDEN);  // 403 Forbidden
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json");

        ObjectMapper objectMapper = new ObjectMapper();
        String result = objectMapper.writeValueAsString(ExceptionResponse.builder()
                .httpStatus(HttpStatus.FORBIDDEN)
                .status(HttpStatus.FORBIDDEN)
                .message(message)
                .code(code)
                .build());

        response.getWriter().write(result);
    }
}

Security를 알면 알수록 보안적인 측면이나 예외적인 것들을 처리할 수 있는 방법을 더 많이 알게되는것같다. 앞으로 또 좋은 공부가 된 내용이 있다면 추가하고자 한다!

728x90
반응형
SMALL

'TIL' 카테고리의 다른 글

[DDD 아키텍처/잡담] 도메인 주도 설계를 왜 해야 할까?  (0) 2025.02.28
[TIL] 모놀리딕 스프링 부트 프로젝트를 수행하고 내가 공부해야 할 것들  (0) 2025.02.26
[TIL] Stream 활용하기, CI/CD 파이프라인 개발, 엔티티 개발 협업  (0) 2025.02.20
[TIL] Filter예외 처리, 서버 배포  (0) 2025.02.19
[TIL] 팀원간 코드 리뷰 진행 및 인덱스가 많아지면 생기는 문제, JPA의 flush 발생하는 조건  (0) 2025.02.18
'TIL' 카테고리의 다른 글
  • [DDD 아키텍처/잡담] 도메인 주도 설계를 왜 해야 할까?
  • [TIL] 모놀리딕 스프링 부트 프로젝트를 수행하고 내가 공부해야 할 것들
  • [TIL] Stream 활용하기, CI/CD 파이프라인 개발, 엔티티 개발 협업
  • [TIL] Filter예외 처리, 서버 배포
공부하고 기억하는 공간
공부하고 기억하는 공간
IT 비전공자로 시작하여 훌륭한 개발자가 되기 위해 공부하고 있는 공간입니다. 틀린 내용이나 부족한 부분이 있으면 댓글로 알려주세요 바로 수정하겠습니다.
    250x250
  • 공부하고 기억하는 공간
    IT - railroad
    공부하고 기억하는 공간
  • 전체
    오늘
    어제
    • 분류 전체보기 (314) N
      • 면접 준비 (36) N
        • OS (6)
        • Spring Security (0)
        • Java (2) N
        • DB (9)
        • Network (3)
      • ElasticSearch (2)
      • Kafka (4)
      • Spring (22)
        • Spring Cloud (7)
        • Security6 (5)
        • JPA (12)
        • 프로젝트 리팩토링 회고록 (4)
        • Logging (8)
        • Batch (2)
      • Redis (17)
        • Redis 개념 (8)
        • Redis 채팅 (5)
        • Redis 읽기쓰기 전략 (1)
      • AWS (11)
      • 리눅스 (29)
        • 리눅스 마스터 2급 (5)
        • 네트워크(기초) (7)
        • 리눅스의 이해 (6)
        • 리눅스의 설치 (2)
        • 리눅스 운영 및 관리 (6)
      • JAVA-기초 (16)
        • JAVA기본 (11)
        • Design Pattern (5)
      • JSP (27)
        • JSP 기본 개념 (10)
        • JSP (1)
      • SQL (1)
      • TIL (36)
      • 문제 풀이 (2)
        • Programmers (9)
        • 백준 문제풀이 (28)
      • JavaScript (10)
      • HTML (17)
      • Ngrinder (1)
        • Ngrinder 문서 정리 (1)
  • 블로그 메뉴

    • 링크

    • 공지사항

    • 인기 글

    • 태그

      JavaScript
      java
      스프링프레임워크
      spring redis
      JS
      리눅스
      jsp기초
      Spring
      리눅스마스터2급정리
      프로그래머스
      redis
      jsp request
      Springframework
      redis 채팅
      백준
      리눅스마스터2급
      Til
      자바 알고리즘
      자바기초
      CSS
      Spring Data Redis
      springsecurity
      레디스
      자바 면접
      자바 면접질문
      자바 반복문
      HTML
      JSP
      자바스크립트
      자바
    • 최근 댓글

    • 최근 글

    • hELLO· Designed By정상우.v4.10.3
    공부하고 기억하는 공간
    [Spring Security 예외 처리] 인증 및 인가에 대한 예외처리 방법
    상단으로

    티스토리툴바