Spring에서는 비동기 처리를 위해 @Async 어노테이션이나 CompletableFuture를 자주 사용한다. 하지만 비동기적으로 실행되는 코드에서는 SecurityContextHolder에 저장된 인증 정보(Authentication)가 기본적으로 전파되지 않기 때문에 보안 컨텍스트를 유지하는 것이 문제가 될 수 있다. 해당 포스트에서는 이 문제를 해결하는 방법을 예제와 함께 설명한다.
왜 비동기 환경에서 SecurityContext가 유지되지 않을까?
Spring Security는 현재 스레드에 보안 컨텍스트를 저장한다. 하지만 @Async나 CompletableFuture를 사용하면 새로운 스레드가 생성되어 비동기 작업이 수행된다. 이때, 기본적으로 새로운 스레드에는 원래 스레드의 보안 컨텍스트가 전파되지 않는다. 따라서, 비동기 메서드 내에서 SecurityContextHolder.getContext().getAuthentication()을 호출해도 null이 반환되거나 인증 정보가 존재하지 않는 문제를 겪게 된다.
해결 방법
비동기 메서드에서도 SecurityContextHolder의 인증 정보를 유지하기 위해 몇 가지 방법을 사용할 수 있다.
1. @Async와 DelegatingSecurityContextAsyncTaskExecutor
Spring은 @Async를 사용할 때 보안 컨텍스트를 전파하기 위한 도우미 클래스인 DelegatingSecurityContextAsyncTaskExecutor를 제공한다. 이 클래스를 사용하면 비동기 작업에서도 현재 보안 컨텍스트가 자동으로 전파된다.
설정
먼저 DelegatingSecurityContextAsyncTaskExecutor를 사용하도록 AsyncConfigurer를 구현하여 설정을 수정한다.
@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {
@Override
public TaskExecutor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(10);
executor.setQueueCapacity(25);
executor.initialize();
return new DelegatingSecurityContextAsyncTaskExecutor(executor);
}
}
사용 예제
@Service
public class ShibaHolicAsyncService {
@Async
public void asyncMethod() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
System.out.println("LoggedIn User: " + authentication.getName());
}
}
이 방법으로 비동기 메서드에서도 SecurityContextHolder에 접근할 수 있으며, 인증 정보가 올바르게 유지된다.
테스트 코드
@SpringBootTest
class ShibaHolicAsyncServiceTest {
@Autowired
ShibaHolicAsyncService shibaHolicAsyncService;
@Test
void asyncTest() {
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken((Principal) () -> "ShibaHolic", "ShibaHolic");
SecurityContextHolder.getContext().setAuthentication(token);
shibaHolicAsyncService.asyncMethod();
}
}
테스트 코드를 작성하여 DelegatingSecurityContextAsyncTaskExecutor를 사용하지 않은 경우와 사용한 경우의 결과를 확인해 본다.
1. 사용하지 않은 경우
위 이미지와 같이 NPE가 발생하는 것을 볼 수 있다.
2. 사용한 경우
위 이미지와 같이 테스트 성공 결과를 얻을 수 있다.
2. CompletableFuture와 DelegatingSecurityContextCallable
CompletableFuture를 사용할 때도 비슷한 문제를 겪을 수 있다. 이를 해결하기 위해 Spring은 DelegatingSecurityContextCallable을 제공한다. 이 클래스는 기존의 Callable을 래핑하여 보안 컨텍스트를 전파한다.
사용 예제
@Service
public class ShibaHolicCompletableFutureService {
private final Executor executor = Executors.newFixedThreadPool(10);
public CompletableFuture<String> asyncMethod() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
// DelegatingSecurityContextCallable로 감싸서 보안 컨텍스트 전파
Callable<String> callable = new DelegatingSecurityContextCallable<>(authentication::getName);
return CompletableFuture.supplyAsync(() -> {
try {
return callable.call();
} catch (Exception e) {
throw new RuntimeException(e);
}
}, executor);
}
}
이 예제에서는 DelegatingSecurityContextCallable을 사용해 비동기적으로 CompletableFuture를 실행하면서도 보안 컨텍스트를 유지한다.
테스트 코드
@SpringBootTest
class ShibaHolicCompletableFutureServiceTest {
@Autowired
ShibaHolicCompletableFutureService shibaHolicCompletableFutureService;
@Test
void asnycTest() {
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken((Principal) () -> "ShibaHolic", "ShibaHolic");
SecurityContextHolder.getContext().setAuthentication(token);
CompletableFuture<String> future = shibaHolicCompletableFutureService.asyncMethod();
assertThat(future.join()).isEqualTo("ShibaHolic");
}
}
테스트 코드를 작성하여DelegatingSecurityContextCallable를 사용하지 않은 경우와 사용한 경우의 결과를 확인해 본다.
1. 사용하지 않은 경우
위 이미지와 같이 동일하게 NPE가 발생하는 것을 볼 수 있다.
2. 사용한 경우
위 이미지와 같이 테스트 성공 결과를 얻을 수 있다.
결론
비동기 처리 시 SecurityContext를 유지하는 것은 중요한 문제다. Spring에서는 DelegatingSecurityContextAsyncTaskExecutor와 DelegatingSecurityContextCallable과 같은 도우미 클래스를 제공하여 이를 쉽게 해결할 수 있도록 지원한다. 이 방법들을 사용하면 비동기 작업에서도 현재 사용자의 인증 정보를 안전하게 유지할 수 있다.
REFERENCE
'Spring > Security' 카테고리의 다른 글
WebSecurity ignoring()에 대한 오해와 미적용 문제 해결 방법 (0) | 2023.09.10 |
---|---|
OAuth 2.0 Grant Type (0) | 2023.07.02 |
댓글