본문 바로가기
Spring/Security

[Spring] Spring Security SecurityFilter란?

by 기몬식 2023. 12. 3.

SecurityFilterChain

출처


웹 요청이 발생하면 서블릿은 DelegatatingFilterProxy를 통해 FilterChainProxy에 설정 된 필터들을 실행시킵니다.
하지만 각 요청에 적용 될 필터 체인에 대한 유효성 검증이 필요한데 이를 관리하기 위해서는 SecurityFilterChain을 통해 해당 요청에 적용되는 필터 체인이 있는지 결정할 수 있게끔 서포트합니다.(in order to decide whether it applies to that request.)


이를 위해 SecurityFilterChain의 표준 구현체로 DefaultSecurityFilterChain을 사용하며 발생한 요청이 RequestMatcher에 일치하면 Spring Security에 정의된 필터를 반환합니다.

FilterChainProxy

실제로 서블릿 컨테이너의 DelegatingFilterProxy에 의해 연결되는 필터 체인으로 실제 등록된 필터들을 동작합니다.


DefaultSecurityFilterChain을 순회하면서 matches() 를 통해 일치하는 RequestMatcher에 대해서만 Filter를 등록합니다.


등록된 필터들을 통해 필터링을 진행합니다.

SecurityFilter


FilterOrderRegistration은 애플리케이션 구동 시 Spring Security에서 기본적으로 제공하는 Security Filter Instances를 정해진 순서에 따라 등록합니다.


그 중에서 HttpSecurity에서 기본적으로 사용하는 필터의 갯수는 16개로 주요 필터는 다음과 같습니다.

SecurityContextPersistenceFilter

SecurityContextPersistenceFilter는 SecurityContext 객체를 생성, 저장, 조회 하는 필터로 Session 까지도 강제로 등록할 수 있기 때문에 기본 값은 false입니다.
먼저 요청이 들어오면 SecurityContextConfigurer를 통한 SecurityContext를 생성해야 되는데 요청의 인증 상태에 따라 SecurityContextRepository가 달라집니다.


  1. 익명 사용자가 요청한 경우 새로운 SecurityContext를 생성하여 반환합니다.

     @Override
     public SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder) {
         return SecurityContextHolder.createEmptyContext();
     }
  2. 인증 사용자가 요청한 경우 저장된 세션을 기반으로 SecurityContext를 생성하여 반환합니다.

     public SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder) {
         HttpServletRequest request = requestResponseHolder.getRequest();
         HttpServletResponse response = requestResponseHolder.getResponse();
         HttpSession httpSession = request.getSession(false);
         SecurityContext context = readSecurityContextFromSession(httpSession);
         if (context == null) {
             context = generateNewContext();
             if (this.logger.isTraceEnabled()) {
                 this.logger.trace(LogMessage.format("Created %s", context));
             }
         }
         if (response != null) {
             SaveToSessionResponseWrapper wrappedResponse = new SaveToSessionResponseWrapper(response, request,
                     httpSession != null, context);
             requestResponseHolder.setResponse(wrappedResponse);
             requestResponseHolder.setRequest(new SaveToSessionRequestWrapper(request, wrappedResponse));
         }
         return context;
     } 

각자의 전략으로 SecurityContext을 생성한 후 SecurityContextRepository에 SecurityContext를 저장하며 종료됩니다.

UsernamePasswordAuthenticationFilter

사용자가 요청한 로그인 정보를 처리하고 인증을 시도하는 필터입니다.


실제 인증 시도는 부모 클래스인 AbstractAuthenticationProcessingFilter에 진행합니다.


해당 필터는 RequestMatcher 일치 여부에 따라 동작하기 때문에 상위 부모 클래스를 가집니다.

    protected boolean requiresAuthentication(HttpServletRequest request, HttpServletResponse response) {
        if (this.requiresAuthenticationRequestMatcher.matches(request)) {
            return true;
        }
        if (this.logger.isTraceEnabled()) {
            this.logger
                    .trace(LogMessage.format("Did not match request to %s", this.requiresAuthenticationRequestMatcher));
        }
        return false;
    }

만약 RequestMatcher에 일치하지 않는다면 해당 필터는 추가적인 인증 작업 없이 바로 다음 필터를 호출합니다.

LogoutFilter


logOutRequestMatcher에 등록된 URI라면 LogoutHandler에게 로그 아웃 처리를 위임시킵니다.
기본으로는 /logout을 사용하지만 아래와 같이 커스터마이징할 수 있습니다.

    private void logout(HttpSecurity http) throws Exception {
        http.logout()
                .logoutRequestMatcher(new AntPathRequestMatcher("custom", POST.name()))
                .addLogoutHandler(new SecurityLogoutHandler())
                .logoutSuccessHandler(new SecurityLogoutSuccessHandler(objectMapper, messageSupport))
                .clearAuthentication(true);
    }

CorsFilter


CorsConfigurationSource를 통해 Cors 정책에 대해 유효한지 확인하는 필터입니다.

CORS

CORS는 Cross-Origin Resource Sharing의 약자로 다른 도메인(Origin) 간에 자원(Resource) 공유를 허용하기 위한 보안 기술입니다.
웹 브라우저의 Same-Origin Policy 에 따라 일반적으로 다른 출처의 자원에 접근하는 것이 금지되어 있지만 필요에 따라서 다른 도메인에서 제공되는 자원에 접근할 필요가 있을 때 CORS 허용 여부를 브라우저에게 알려주는 헤더를 사용하여 설정합니다.

CsrfFilter


CsrfTokenRepository를 통해 세션이나 쿠키의 Csrf Token과 요청의 헤더의 Csrf Token을 비교하여 같다면 같은 사용자로 판단하고 다음 필터를 실행합니다.
만약 일치하지 않다면 AccessDeniedHandler로 요청을 거절합니다.

CSRF

CSRF는 Cross-Site Request Forgery의 약자로 공격자가 사용자의 권한을 이용하여 사용자가 의도하지 않은 요청을 보내는 악의적 공격 형태입니다.
세션 서버는 브라우저에 저장된 값을 신뢰하는데 사용자의 인증된 상태의 브라우저를 해커 사이트로 이동 시킨 후 해커 사이트에서 사용자 정보를 임의로 조작하여 서버에 요청하는 방식입니다.
CSRF 공격을 방지하기 위해 일반적으로 다음과 같은 방법들이 사용됩니다.

  • CSRF 토큰
  • SameSite 쿠키 속성
  • Referrer 검증

RememberMeAuthenticationFilter


세션이나 토큰이 만료 된 후에도 서버에서 클라이언트의 인증 유무를 기억하는 기능으로 로그인이 성공하게 되면 쿠키에 발행 되는 Remember-me Token을 통해 유효성 검증을 합니다.
앞선 필터에서 SecurityContext의 Authentication이 채워진 상태라면 동작하지 않고 다음 필터를 실행시킵니다(populated with remember-me token, as it already contained).

그렇지 않고 remember-me 토큰이 존재한다면 해당 쿠키에 있는 Token 값을 통해 RememberMeAuthentication을 반환합니다.


ExceptionTranslationFilter


필터 체인 내에서 발생한 모든 AccessDeniedException 및 AuthenticationException 을 처리합니다.

AuthenticationException 는 인증 관련 예외이며 사용자를 최초 인증 시점 상태로 되돌려 보냅니다.
이는 AuthenticationEntryPoint에 의해 로그인 페이지 또는 최초 요청 상태가 될 수 있습니다.

    private void handleAuthenticationException(HttpServletRequest request, HttpServletResponse response,
            FilterChain chain, AuthenticationException exception) throws ServletException, IOException {
        this.logger.trace("Sending to authentication entry point since authentication failed", exception);
        sendStartAuthentication(request, response, chain, exception);
    }

AccessDeniedException 는 AccessDecisionManager 에 의해 접근 거부가 발생했을 때 AccessDeniedHandler에 의해 접근 거부 페이지를 보여주거나 최초 인증 시점 상태로 보냅니다.
    public void handle(HttpServletRequest request, HttpServletResponse response,
            AccessDeniedException accessDeniedException) throws IOException, ServletException {
        if (response.isCommitted()) {
            logger.trace("Did not write to response since already committed");
            return;
        }
        if (this.errorPage == null) {
            logger.debug("Responding with 403 status code");
            response.sendError(HttpStatus.FORBIDDEN.value(), HttpStatus.FORBIDDEN.getReasonPhrase());
            return;
        }
        // Put exception into request scope (perhaps of use to a view)
        request.setAttribute(WebAttributes.ACCESS_DENIED_403, accessDeniedException);
        // Set the 403 status code.
        response.setStatus(HttpStatus.FORBIDDEN.value());
        // forward to error page.
        if (logger.isDebugEnabled()) {
            logger.debug(LogMessage.format("Forwarding to %s with status code 403", this.errorPage));
        }
        request.getRequestDispatcher(this.errorPage).forward(request, response);
    }

AuthorizationFilter


AuthorizationDecision 전략은 AuthorizationFilter 생성시 주입 받는 AuthorizationManager의 구현체에 따라 인가 여부를 판별합니다.
기본적으로 URL 기반 권한 부여 처리는 RequestMatcherDelegatingAuthorizationManager 구현체를 사용하며 check() 메소드에 인증 결정을 내리는데 필요한 모든 관련 정보를 전달합니다.
그 후 권한이 부여되지 않은 요청이라면 AccessDeniedException 예외를 발생 시킵니다.


오탈자 및 오류 내용을 댓글 또는 메일로 알려주시면, 검토 후 조치하겠습니다.

'Spring > Security' 카테고리의 다른 글

[Spring] Spring Security Authentication이란?  (0) 2023.12.10
[Spring] Spring Security 구성 요소  (0) 2023.09.20
[Spring] Spring Security란?  (1) 2023.09.14