Notice
Recent Posts
Recent Comments
Link
«   2025/04   »
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30
Archives
Today
Total
관리 메뉴

전공공부

Spring Security with Async 본문

Study/Spring Boot

Spring Security with Async

monitor 2024. 2. 27. 00:07

문제점


 

Spring Security와 CompletableFuture를 함께 사용 할 시 Security Context가 null이 되는 문제가 있었습니다. 이 문제는 이런 상황에서 발생 할 수 있습니다.

 

아래 코드는 spring mvc - Security Context is null with CompletableFuture - Stack Overflow 비슷한 오류 상황을 가진 코드를 가져왔습니다. 저 중 getCurrentApplicationUser() 부분이 SecurityContextHolder.getContext.getAuthentication()을 부르는데 이것이 문제가 되었습니다.

 

이걸 부르는 곳 중에 타고 들어가면 RequestContextHolder이라는 객체가 있는데 이 객체는 Request 값을 Controller 단 이외에서 Service 단과 같은 곳에서 불러서 사용 하기 위해서 씁니다.

@RequestMapping(value = {"/randomizer"}, method = RequestMethod.POST)
public CompletableFuture<String> randomizer(){
    CompletableFuture<String> someString = stringService
        .findRandomByInput("123")
        .thenCombine(stringService.findAnotherRandomByInput("321"), (result1, result2) -> {
            return applyRandom(result1, result2);
        });

    CompletableFuture<Void> computation = computingService.computeRandomByInput(RandomDto.empty(), "123");

    return someString.thenCombineAsync(computation, (result1, result2) -> {
        combineIt(result1, result2, getCurrentApplicationUser()); //SecurityContextHolder.getContext().getAuthentication()
    }, taskExecutor);
}

 

RequestContextHolder - 내부 구조


 

private static final boolean jsfPresent =
       ClassUtils.isPresent("jakarta.faces.context.FacesContext", RequestContextHolder.class.getClassLoader());

private static final ThreadLocal<RequestAttributes> requestAttributesHolder =
       new NamedThreadLocal<>("Request attributes");

private static final ThreadLocal<RequestAttributes> inheritableRequestAttributesHolder =
       new NamedInheritableThreadLocal<>("Request context");

 

이 구조는 class가 새로 생길때 마다 위 변수들이 static으로 만들어지며 특히나 attributes 들은 ThreadLocal 에서 관리가 됩니다.

 

아래 currentRequestAttributes를  호출해서 파라미터나 헤더 값을 가져오는데 ThreadSafe 하지 않거나 비동기 프로그래밍으로 인하여 Thread가 바뀌면 아래와 같이 값을 가져 올 수 없게 되는 것이죠. IllegalStateException 참조

public static RequestAttributes currentRequestAttributes() throws IllegalStateException {
    RequestAttributes attributes = getRequestAttributes();
    if (attributes == null) {
       if (jsfPresent) {
          attributes = FacesRequestAttributesFactory.getFacesRequestAttributes();
       }
       if (attributes == null) {
          throw new IllegalStateException("No thread-bound request found: " +
                "Are you referring to request attributes outside of an actual web request, " +
                "or processing a request outside of the originally receiving thread? " +
                "If you are actually operating within a web request and still receive this message, " +
                "your code is probably running outside of DispatcherServlet: " +
                "In this case, use RequestContextListener or RequestContextFilter to expose the current request.");
       }
    }
    return attributes;
}

 

 

protected final void processRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        long startTime = System.currentTimeMillis();
        Throwable failureCause = null;
        LocaleContext previousLocaleContext = LocaleContextHolder.getLocaleContext();
        LocaleContext localeContext = this.buildLocaleContext(request);
        RequestAttributes previousAttributes = RequestContextHolder.getRequestAttributes();
        ServletRequestAttributes requestAttributes = this.buildRequestAttributes(request, response, previousAttributes);
        WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
        asyncManager.registerCallableInterceptor(FrameworkServlet.class.getName(), new FrameworkServlet.RequestBindingInterceptor());
        this.initContextHolders(request, localeContext, requestAttributes);

        try {
            this.doService(request, response);
        } catch (IOException | ServletException var16) {
            failureCause = var16;
            throw var16;
        } catch (Throwable var17) {
            failureCause = var17;
            throw new NestedServletException("Request processing failed", var17);
        } finally {
            this.resetContextHolders(request, previousLocaleContext, previousAttributes);
            if (requestAttributes != null) {
                requestAttributes.requestCompleted();
            }

            this.logResult(request, response, (Throwable)failureCause, asyncManager);
            this.publishRequestHandledEvent(request, response, startTime, (Throwable)failureCause);
        }

    }

 

위는 processRequest라고 FrameworkServlet 클래스의 객체 입니다. 이 내용중 previousAttributes를 불러서 requestAttributes를 만들때 사용하고 requestAttributes로 초기화를 진행합니다.

 

 

initContextHolders 메서드는 새로운 RequestContextHolder로 초기화를 시켜버립니다. 

 

previousAttributes 메서드는 이전 요청에서 사용한 요청 어트리뷰트를 참조하여서 만일 요청이 중첩되어 발생될 시 내부 요청이 완료된 이후에 외부 요청이 계속 진행 될 수 있도록 이전의 RequestAttribute를 복원 해둡니다. 

 

resetContextHolders 메서드는 요청 처리과 완료된 이후에 정리하기 위해서 사용이 됩니다. clear()와 같은 역할 입니다.

 

이런 방식을 통해서 새로운 요청이 올때마다 ThreadLocal하게 static한 값을 사용합니다.

 

 

어쨌든 다시 돌아가서 보면  병렬 프로그래밍이 구현된 서버의 새로운 Thread에서 만일, RequestAttributes 등을 참조하게 되면 null 타입이 나오게 된다. 당연히 initContextHolders 메서드가 돌아버리니 안된다.

 

 

그래서 아래 해결 방안에 넣은 같은 bean을 등록하면 된다. 최상위 단인 DispatcherServlet에서 ThreadContextInheritable을 true로 설정해버리면 아랫단인 RequestContextHolder를 불러와서 쓸 때 기본값이 아래와 같이 false인데 true로 설정이 된다.

 

public static void setRequestAttributes(@Nullable RequestAttributes attributes) {
    setRequestAttributes(attributes, false);
}


public static void setRequestAttributes(@Nullable RequestAttributes attributes, boolean inheritable) {
		if (attributes == null) {
			resetRequestAttributes();
		}
		else {
			if (inheritable) {
				inheritableRequestAttributesHolder.set(attributes);
				requestAttributesHolder.remove();
			}
			else {
				requestAttributesHolder.set(attributes);
				inheritableRequestAttributesHolder.remove();
			}
		}
	}

 

해결방안


 

class whateverNameYouLike {
   @Bean
   DispatcherServlet dispatcherServlet() {
       DispatcherServlet srvl = new DispatcherServlet();
       srvl.setThreadContextInheritable(true);
       return srvl;
   }
}

 

 

 

 

[ 해결에 참고했던 블로그  ]

 

Using a request scoped bean outside of an actual web request

I have a web application that has a Spring Integration logic running with it in a separated thread. The problem is that at some point my Spring Integration logic tries to use a request scoped bean ...

stackoverflow.com

 

 

Spring RequestContextHolder

RequestContextHolder 개요 RequestContextHolder 는 Spring에서 전역으로 Request에 대한 정보를 가져오고자 할 때 사용하는 유틸성 클래스이다. 주로, Controller가 아닌 Business Layer 등에서 Request 객체를 참고하려

gompangs.tistory.com