Spring/Spring Security

[Spring Security] 요청 캐시 흐름 정리

제우제우 2024. 9. 13. 23:02

해당 동작을 하는 모든 필터들은 FilterChainProxy에 있는 필터들에서 동작 

1. 인증되지 않은 사용자가 리소스 요청

사용자가 인증이 필요한 리소스에 접근하려 하면, Spring Security는 이 요청을 가로채 인증 여부를 확인

2. 인증되지 않은 경우, 예외 처리

인증되지 않은 경우, ExceptionTranslationFilter에서 예외를 처리

이때 AccessDeniedException이나 AuthenticationException을 던지게된다.

AccessDeniedException은 인증은 했지만 권한이 부족한 경우

AuthenticationException은 인증이 필요한 경우 

RequestCache 인터페이스의 구현체(HttpSessionRequestCache가 일반적)에서 요청 정보를 저장

이때 SavedRequest 인터페이스의 구현체인 DefaultSavedRequest 객체가 사용되며, 해당 객체는 세션에 저장되어 나중에 인증 성공 시 복구할 수 있게 된다. 

3. 리다이렉트

인증되지 않았으므로 Spring Security는 클라이언트를 /login 페이지로 리다이렉트

 

4. 클라이언트 로그인 시도

클라이언트가 /login 페이지에서 자격 증명을 제출하면, UsernamePasswordAuthenticationFilter가 이를 처리.

인증에 성공하면, 인증된 사용자의 정보가 SecurityContext에 저장

 

5. RequestCacheAwareFilter에서 저장된 요청 확인

로그인에 성공한 후 RequestCacheAwareFilterRequestCache.getMatchingRequest 메서드를 사용해 이전에 저장된 요청이 있는지 확인 /  세션에 저장된 SavedRequest 객체가 있는지 확인 / 쿼리 스트링 확인 등등 ... 

 

확인 작업이 끝나면, 해당 요청(리소스)에 대한 정보를 가져와 원래의 요청을 복원하여 처리. 이를 통해 클라이언트는 로그인 성공 후 원래 요청했던 리소스로 자동 리다이렉트

 

RequestCacheAwareFilter

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
        throws IOException, ServletException {
    HttpServletRequest wrappedSavedRequest = this.requestCache.getMatchingRequest((HttpServletRequest) request,
            (HttpServletResponse) response);
    chain.doFilter((wrappedSavedRequest != null) ? wrappedSavedRequest : request, response);
}

 

HttpSessionRequestCache

@Override
public HttpServletRequest getMatchingRequest(HttpServletRequest request, HttpServletResponse response) {
    if (this.matchingRequestParameterName != null) {
        if (!StringUtils.hasText(request.getQueryString())
                || !UriComponentsBuilder.fromUriString(UrlUtils.buildRequestUrl(request))
                    .build()
                    .getQueryParams()
                    .containsKey(this.matchingRequestParameterName)) {
            this.logger.trace(
                    "matchingRequestParameterName is required for getMatchingRequest to lookup a value, but not provided");
            return null;
        }
    }
    SavedRequest saved = getRequest(request, response);
    if (saved == null) {
        this.logger.trace("No saved request");
        return null;
    }
    if (!matchesSavedRequest(request, saved)) {
        if (this.logger.isTraceEnabled()) {
            this.logger.trace(LogMessage.format("Did not match request %s to the saved one %s",
                    UrlUtils.buildRequestUrl(request), saved));
        }
        return null;
    }
    removeRequest(request, response);
    if (this.logger.isDebugEnabled()) {
        this.logger.debug(LogMessage.format("Loaded matching saved request %s", saved.getRedirectUrl()));
    }
    return new SavedRequestAwareWrapper(saved, request);
}

 

요약 

1. 지정한 쿼리 스트링 매치 판단 / 해당 파라미터가 없거나 설정이 틀리면 null 반환 

2. SavedRequest 가져오기 / 없으면 null 반환 

3. 현재 요청과 저장된 요청 비교 / matchesSavedRequest 사용 일치하지 않으면 null 반환 

4. 저장된 요청 제거 및 매칭된 요청 반환 

removeRequest: 세션 또는 쿠키에서 제거

SavedRequestAwareWrapper 객체를 반환

 

파라미터 

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception{
    HttpSessionRequestCache requestCache = new HttpSessionRequestCache(); // RequestCache Custom
    requestCache.setMatchingRequestParameterName("customParam=y"); // Match Parameter Custom
    http
        .authorizeHttpRequests(auth -> auth
            .anyRequest().authenticated())
        .formLogin(Customizer.withDefaults())
        }))
        .requestCache(cache -> cache.requestCache(requestCache))

    return  http.build();
}

기본값: ?continue 

굳이 custom할 필요 없어 보인다. 

6. 리다이렉트 완료

RequestCacheAwareFilter가 저장된 요청이 존재하면 해당 요청을 다시 수행하게 되고, 클라이언트는 인증에 성공한 상태로 원래 요청한 리소스로 리다이렉트

정리 

사용자가 인증 없이 [인증 필요 X] 이벤트 설명 페이지에서 [인증 필요 O] 이벤트 등록 페이지 요청하는 상상을 해봤다.

이런 상황에서 로그인 페이지로 리다이렉트하고 로그인에 성공하면 이벤트 등록 페이지로 리다이렉트 해주는 기능은 

사용자의 경험(UX)측면에서 매우 중요한 서비스 기능이라고 생각한다. 

요청 캐시에 대한 흐름을 정리하면서 스프링 시큐리티의 전체 흐름 또한 많이 이해하였고 특히 요청 캐시를 처리하는 부분에서 사용되는 필터와 다양한 객체들을 알게 되었다.