Spring JPA, SecurityContext사용시 ThreadLocal 사용범위 관련 #1
<현상>
- HHH000346: Error during managed flush [null]
- Spring JPA 사용중 Transcation 처리가 정상적으로 되지 않는 현상
<원인>
- 사용자 ID를 @CreatedBy 를 사용해서 관리중 (@EnableJpaAuditing)
- AuditAware에서 SecurityContext를 꺼내보면 auth가 null로 되어있다.
- 그 이유는 Spring Security내에서 ThreadLocal로 정보를 관리하고 있으며 새로 생성된 Thread내에는 해당값이 전파되지 않기 때문.
- 현재 로직
1. 여러 API를 호출한 뒤 결과값을 바탕으로 DB 에 update 해야함
2. 해당 로직은 오랜시간이 필요하기 때문에 다음과 같이 구성함
- 외부 API call하는 메소드 : B서비스 메소드#B + @Async
- 메타정보에 따라서 외부 API Call 여부를 결정하는 로직 : A서비스 메소드#A
- 서비스 메소드를 CompletableFutuer.runAsync로 호출
CompletableFuture.runAsync(() -> aService.aMethod());
public void aMethod(){
for ( : list) {
if ( ) {
CompletableFuture<Map<String, Integer>> completableFutureForRule;
synchronized (this){
waitBeforeCall();
completableFutureForRule = bService.bMethod(filterDto);
resultCount.incrementAndGet();
}
completableFutureForRule.thenAccept(retMap -> {
synchronized (this){
notifyAfterCall();
}
updateDB(...);
});
@Async("executorBean")
public CompletableFuture<Map<String, Integer>> bMethod(...) {
...
contents = webClient
.post()
...
return CompletableFuture.completedFuture(map);
-Spring SecurityContext의 경우 ThreadLocal로 처리하기 때문에 CompletableFuture 비동기 처리시 인증정보가 전달되지 않는다.
<해결방안>
https://www.baeldung.com/spring-security-async-principal-propagation
https://stackoverflow.com/questions/40345643/java-future-spring-authentication-is-null-into-auditoraware
1. DelegatingSecurityContextAsyncTaskExecutor 적용
@Bean
public DelegatingSecurityContextAsyncTaskExecutor taskExecutor(ThreadPoolTaskExecutor delegate) {
return new DelegatingSecurityContextAsyncTaskExecutor(delegate);
}
2. DelegatingSecurityContextAsyncTaskExecutor 소스를 살펴보면 다음과 같다.
최상위 추상 클래스로 AbstractDelegatingSecurityContextSupport 가 구현되어 있으며 Runnable, Callable에 대하여 wrapping처리를 하게 되며,
abstract class AbstractDelegatingSecurityContextSupport {
private final SecurityContext securityContext;
/**
* Creates a new {@link AbstractDelegatingSecurityContextSupport} that uses the
* specified {@link SecurityContext}.
*
* @param securityContext the {@link SecurityContext} to use for each
* {@link DelegatingSecurityContextRunnable} and each
* {@link DelegatingSecurityContextCallable} or null to default to the current
* {@link SecurityContext}.
*/
AbstractDelegatingSecurityContextSupport(SecurityContext securityContext) {
this.securityContext = securityContext;
}
protected final Runnable wrap(Runnable delegate) {
return DelegatingSecurityContextRunnable.create(delegate, securityContext);
}
protected final <T> Callable<T> wrap(Callable<T> delegate) {
return DelegatingSecurityContextCallable.create(delegate, securityContext);
}
}
DelegatingSecurityContextRunnable 의 run 메소드를 수행할때 delegateSecurityContext를 세팅하여 수행하도록 되어있다.
수행이 완료된 뒤에는 originalSecurityContext로 원복시키고, 현재 Runnable에 세팅되어 있던 값은 null 처리 해야 ThreadPool사용시 문제가 발생하지 않는다.
public final class DelegatingSecurityContextRunnable implements Runnable {
...
@Override
public void run() {
this.originalSecurityContext = SecurityContextHolder.getContext();
try {
SecurityContextHolder.setContext(delegateSecurityContext);
delegate.run();
}
finally {
SecurityContext emptyContext = SecurityContextHolder.createEmptyContext();
if (emptyContext.equals(originalSecurityContext)) {
SecurityContextHolder.clearContext();
} else {
SecurityContextHolder.setContext(originalSecurityContext);
}
this.originalSecurityContext = null;
}
}
...
<추가 문제상황>
- 해당 작업을 @Scheduled 를 통해서 수행할 경우에도 SecurityContext는 비어있게 된다. 인증정보를 가지고 수행된 것이 아니기 때문!
- 다음 글에서 추가로 확인해보도록 한다.