본문 바로가기
Spring/Security

비동기 환경에서 Spring Security 로그인 정보를 유지시키는 방법

by 흑시바 2024. 9. 2.

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

https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/task/DelegatingSecurityContextAsyncTaskExecutor.html

https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/concurrent/DelegatingSecurityContextCallable.html

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

WebSecurity ignoring()에 대한 오해와 미적용 문제 해결 방법  (0) 2023.09.10
OAuth 2.0 Grant Type  (0) 2023.07.02

댓글