본문 바로가기
Develop Study/Spring 2025. 4. 30.

(25.04.30) Spring Security 인가 Authorization Filter 의 인가 실패 AccessDeniedHandler & AuthenticationEntryPoint

 

2025.04.28 - [Develop Study/Spring] - (25.04.28) Spring Security 인증 Authentication Filter 의 Success & Failure 핸들러

 

(25.04.28) Spring Security 인증 Authentication Filter 의 Success & Failure 핸들러

2025.04.22 - [Develop Study/Spring] - (25.04.22) Spring Security 인증 인가를 통한 Login 기능 - Filter & JWT에 대해 (25.04.22) Spring Security 인증 인가를 통한 Login 기능 - Filter & JWT에 대해2025.03.20 - [[Gena Co.] Internship Proje

andrew75313.tistory.com

앞선 Authentication 즉 인증 필터 다음으로 작동하는 Authorization 인가 필터에 대해서도 정리했다.

JwtAuthenticationFilter는 UsernamePasswordAuthenticationFilter를 상속받아 커스텀했다고 생각한다면,

JwtAuthorizationFilter는 검증을 담당하는 모든 필터의 로직을 직접 작성하기 때문에, 직접 지정을 해서 지정을 해야했다.

따라서 JWT 검증 에서의 예외, 인가에 대한 예외를 핸들링 할 수 있도록 이 역할을 클래스로 분리해서 구현했다.


Spring Security의 Authorization Filter

@RequiredArgsConstructor
public class JwtAuthorizationFilter extends OncePerRequestFilter {

    ...
    
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws IOException {

        try {
            String token = jwtUtil.getJwtFromHeader(request);

            if (!StringUtils.hasText(token)) {
                token = jwtUtil.getRefreshJwtFromHeader(request);
            }

            if (StringUtils.hasText(token) && jwtUtil.validateToken(token)) {
                Claims claims = jwtUtil.getClaimFromToken(token);
                setAuthentication(claims.getSubject());
            }

            filterChain.doFilter(request, response);
        } catch (JwtException e) {
            authenticationEntryPoint.commence(request, response, new AuthenticationException("JWT Authentication failed") {
            });
        } catch (AccessDeniedException e) {
            accessDeniedHandler.handle(request, response, new AccessDeniedException("Access Denied"));
        } catch (Exception e) {
            throw new RuntimeException("Request Error");
        }
    }
    
    ...
}
  • 직접 커스텀하여 만든 JWT 인가를 위한 AuthorizationFilter
  • JWT 검증 (유효한지 인증) 과 사용자 권한 확인 이 주요 역할

OncePerRequestFilter

  • Request 요청 한 번에 딱 한 번만 실행되는 필터를 커스텀할 수 있도록 상속
  • 인증/인가 관련 필터에 사용되는 기능을 위한 필터로 많이 사용

doFilterInternal

  • 요청에 실행되는 필터의 로직을 구현할 수 있는 메서드
    • OncePerRequestFilter 에 의해서 요청 1회에 한번씩 실행이 되는 형태
  • 완료가 된 로직(예외가 없이 진행된 로직) 은 Security Config에서 설정된 FitlerChain 에 의해 다음 Filter로 진행
  • 이 메서드 안에서 Header로 담겨온 JWT 토큰 검증, 사용자 인가 가 진행
    • JwtUtil 클래스에서 이 로직을 담당하여 분리되어 있기 때문에, 간단하게 메서드로 진행하게 했음
    • JWT를 통해 얻어진 Claims = 사용자 정보를 토대로 사용자 Authentication 을 통해 인가가 진행
@RequiredArgsConstructor
public class JwtAuthorizationFilter extends OncePerRequestFilter {

    // doFilterInternal 메서드
    ...
    
    private void setAuthentication(String userId) {
        SecurityContext context = SecurityContextHolder.createEmptyContext();
        Authentication authentication = createAuthentication(userId);
        context.setAuthentication(authentication);
        SecurityContextHolder.setContext(context);
    }
    
    private Authentication createAuthentication(String userId) {
        UserDetails userDetails = userDetailsService.loadUserByUserId(userId);
        return new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
    }
}

setAuthentication

  • createAuthentication 을 통해서 앞선 글에서 처럼 UserDetails을 포함한 Authentication을 생성 하는 메서드
  • (이전 글 참고)

AccessDeniedHandler & AuthenticationEntryPoint

처리 클래스 설명 발생 시점
AuthenticationEntryPoint 사용자가 인증되지 않은 상태에서 보호된 리소스에 접근하려 할 때 인증 필터 전
(예: JWT 없음, 만료 등)
AccessDeniedHandler 토큰으로 인증은 되었지만,
요청한 API에 대한
권한이 없을 때
인가 필터에서
(예: ROLE 불일치)
  • AuthenticationEntryPoint 는 필터단에서 발생하지 않는 점이므로 분리
  • AccessDeniedHandler 도 ****역시 JwtAuthenticationFilter 의 성공 실패 Handler처럼 작동 될 수 있음
    • 요청 리소스에 대한 권한은 역시 Security Config에서 지정이 되어있는 부분
  • Spring Security 내부의 다른 컴포넌트에서 호출되기 때문에, 명시적으로 따로 구분을 하려고 함
    • JwtAuthenticationFilte에서의 Handler는 구분을 위해 내부 작동 요소를 분리하는 형태였지만, 여기서는 역할을 분리하는 역할

Custom 한 Authorization Filter 작동 Flow

  1. 요청(Request) 수신
    • 클라이언트 요청이 필터 체인을 통해 들어옴
  2. JWT 추출
    • Authorization 헤더나 커스텀 헤더에서 JWT를 가져옴
  3. JWT 유효성 검사
    • 토큰이 존재하는지, 서명(Signature)이 유효한지, 만료되지 않았는지 검증
  4. 인증 성공
    • JWT에서 사용자 정보(subject 등)를 추출
    • **UserDetailsService**로 사용자 정보를 조회
    • Authentication 객체를 생성하여 **SecurityContext**에 등록
  5. 인증 실패 시 (토큰 없음, 만료, 위조 등)
    • **AuthenticationEntryPoint**로 위임하여 인증 실패 처리
  6. 인가 실패 시 (권한 부족)
    • 예외 발생 시 **AccessDeniedHandler**로 위임하여 인가 실패 처리
  7. 정상 처리 시
    • **filterChain.doFilter(request, response)**를 호출하여 다음 필터로 요청 전달
      • Security Config 에서 설정

AccessDeniedHandler & AuthenticationEntryPoint 구현

AuthenticationEntryPoint

@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        response.setContentType("application/json;charset=UTF-8");

        String jsonResponse = new ObjectMapper().writeValueAsString(Map.of(
                "statusCode", HttpServletResponse.SC_UNAUTHORIZED,
                "msg", authException.getMessage()
        ));

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

  • Handler와 마찬가지로 커스텀을 했기 때문에 Bean 등록을 위한 @Component 로 등록

commence

  • AuthenticationEntryPoint 인터페이스에 선언된 추상 메서드
  • JWT 검증이 실패 시 호출되어 작동되는 메서드로 설정 Override해서 만들어서 활용할 수 있음
    • JwtAuthorizationFilter 에서 authenticationEntryPoint.commence(request, response, new AuthenticationException("JWT Authentication failed") {}); 형태로 호출
    • 위에서는 JSON 타입으로 Response 지정

AccessDeniedHandler

@Component
public class JwtAccessDeniedHandler implements AccessDeniedHandler {

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException {
        response.setStatus(HttpServletResponse.SC_FORBIDDEN);
        response.setContentType("application/json;charset=UTF-8");

        String jsonResponse = new ObjectMapper().writeValueAsString(Map.of(
                "statusCode", HttpServletResponse.SC_FORBIDDEN,
                "msg", accessDeniedException.getMessage()
        ));

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

  • Spring Security가 인가 실패를 감지하면 자동으로 이 handle() 메서드를 호출되기 때문에, @Override 로 내용을 AuthenticationFilter의 Handler처럼 일관적으로 작성

인증 필터의 AuthenticationFailureHandler 와 비교

핸들러 AuthenticationFailureHandler AccessDeniedHandler
주요 목적 로그인 인증 실패 처리
(비밀번호 틀림 등)
인증된 사용자의 권한 부족 처리
예외 시점 로그인 폼에서 로그인 시도 (/login 등)에서 실패 (커스텀포함) 보호된 리소스 접근 중 권한이 없는 경우
필터 위치 UsernamePasswordAuthenticationFilter 또는 CustomAuthenticationFilter 내부에서 사용 FilterSecurityInterceptor 이후에 작동

발생 예외 타입 BadCredentialsExceptionInternalAuthenticationServiceException 등 AuthenticationException 하위 예외 AccessDeniedException
HTTP 상태코드 보통 401 Unauthorized 또는 400 Bad Request 403 Forbidden

FilterSecurityInterceptor

  • Spring Security의 필터 체인에서 인가(Authorization)를 최종적으로 검사하는 필터
  • Security Config 에서 미리 지정된 사용자가 요청한 URL, 메서드, 리소스에 접근할 권한이 있는지 판단하는 인터셉터
    • 인터셉터는 필터 다음에 작동되기 때문에
  • 작동 FLOW
    1. 사용자 인증됨 (로그인 성공 / 유효한 JWT 확인)
    2. FilterSecurityInterceptor를 통해 리소스 접근 권한 없음 파악 → AccessDeniedException 발생
    3. AccessDeniedHandler 이 예외(**AccessDeniedException)**를 잡아 처리

Security Config 에서의 설정**

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

    private final JwtAuthenticationEntryPoint authenticationEntryPoint;

   ...
   
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
        return configuration.getAuthenticationManager();
    }

    @Bean
    public JwtAuthorizationFilter jwtAuthorizationFilter() {
        return new JwtAuthorizationFilter(jwtUtil, userDetailsService, authenticationEntryPoint, accessDeniedHandler);
    }

    @Bean
    public AccessDeniedHandler accessDeniedHandler() {
        return new JwtAccessDeniedHandler();
    }
    
    ...
  • 인가 과정에서 발생하는 예외 처리(AccessDeniedException, AuthenticationException)는 Spring Security가 자동으로 필터 체인 내에 포함시키지 않기 때문에 해당 부분인 JwtAuthorizationFilter(jwtUtil, userDetailsService, authenticationEntryPoint, accessDeniedHandler) 처럼 명시해야함
  • **@Component**를 사용하면 JwtAccessDeniedHandler가 스프링 빈으로 자동 등록은 되지만, Spring Security에 기능적으로 적용은 아니기 때문에 Config 에서 빈등록이 필수
    • AuthenticationFailureHandler, AuthenticationSuccessHandler은 내부필터에서 작동
    • 해당 부분은 자동으로 작동되어야하기 때문에
  • AuthenticationEntryPoint 는 JwtAuthorizationFilter 내에서 생성자 주입을 해서 직접적으로 commence를 실행시키기 때문에 굳이 빈등록을 Spring Security에서 지정할 필요없이 @Component 만 하면 가능

참고자료

 

 

 

OncePerRequestFilter (Spring Framework 6.2.6 API)

Filter base class that aims to guarantee a single execution per request dispatch, on any servlet container. It provides a doFilterInternal(jakarta.servlet.http.HttpServletRequest, jakarta.servlet.http.HttpServletResponse, jakarta.servlet.FilterChain) metho

docs.spring.io

 

 

AccessDeniedHandler (spring-security-docs 6.4.5 API)

Handles an access denied failure.

docs.spring.io

 

 

Architecture :: Spring Security

The Security Filters are inserted into the FilterChainProxy with the SecurityFilterChain API. Those filters can be used for a number of different purposes, like exploit protection,authentication, authorization, and more. The filters are executed in a speci

docs.spring.io