Search code examples
javaspringspring-bootspring-securityvirtual-threads

Spring Security Virtual Threads and ThreadLocal


As I was reading up about virtual threads and their pitfalls I found this mention :

Don't Cache Expensive Reusable Objects in Thread-Local Variables

Virtual threads support thread-local variables just as platform threads do. See Thread-Local Variables for more information. Usually, thread-local variables are used to associate some context-specific information with the currently running code, such as the current transaction and user ID. This use of thread-local variables is perfectly reasonable with virtual threads. However, consider using the safer and more efficient scoped values. See Scoped Values for more information.

Here : https://docs.oracle.com/en/java/javase/21/core/virtual-threads.html#GUID-68216B85-7B43-423E-91BA-11489B1ACA61

But i also remembered that Spring Security uses ThreadLocal to save the SecurityContext of a given request:

By default, SecurityContextHolder uses a ThreadLocal to store these details, which means that the SecurityContext is always available to methods in the same thread, even if the SecurityContext is not explicitly passed around as an argument to those methods. Using a ThreadLocal in this way is quite safe if you take care to clear the thread after the present principal’s request is processed. Spring Security’s FilterChainProxy ensures that the SecurityContext is always cleared.

Docs : https://docs.spring.io/spring-security/reference/servlet/authentication/architecture.html

So the question is : is it safe to use virtual threads in a Spring Boot REST Application with endpoints that do require authentication and authorization and therefor have a SecurityContext ? Is this considered a pitfall ?

Thanks !


Solution

  • While it is possible 1) to implement a custom SecurityContextHolderStrategy which retrieves SecurityContext from a ScopedValue and saves it there:

    public class ScopedSecurityContextHolderStrategy implements SecurityContextHolderStrategy {
        private static final ScopedValue<SecurityContextScopedValueHolder> SECURITY_CONTEXT = ScopedValue.newInstance();
    
        private static class SecurityContextScopedValueHolder {
            
            private SecurityContext securityContext;
    
            public SecurityContext getSecurityContext() {
                return securityContext;
            }
    
            public void setSecurityContext(SecurityContext securityContext) {
                this.securityContext = securityContext;
            }
    
        }
        
        @Override
        public void clearContext() {
            retrieveSecurityContextScopedValueHolder().setSecurityContext(null);
        }
    
        @Override
        public SecurityContext getContext() {
            return retrieveSecurityContextScopedValueHolder().getSecurityContext();
        }
    
        @Override
        public void setContext(SecurityContext context) {
            retrieveSecurityContextScopedValueHolder().setSecurityContext(context);
        }
    
        @Override
        public SecurityContext createEmptyContext() {
            return new SecurityContextImpl();
        }
        
        private SecurityContextScopedValueHolder retrieveSecurityContextScopedValueHolder() {
            if (SECURITY_CONTEXT.isBound()) {
                return SECURITY_CONTEXT.get();
            } else {
                throw new IllegalStateException("Security Context Scoped Value not bound");
            }
        }
        
        public static ScopedValue.Carrier getSecuriyContextCarrier() {
            return ScopedValue.where(SECURITY_CONTEXT, new SecurityContextScopedValueHolder());
        }
    
    }  
    

    and 2) configure Tomcat to start a virtual thread with the ScopedValue, bound to it:

    @Component
    public class TomcatVirtualThreadExecutorCustomizer 
            implements WebServerFactoryCustomizer<TomcatServletWebServerFactory> {
    
        private static class ScopedVirtualThreadExecutor extends VirtualThreadExecutor {
    
            public ScopedVirtualThreadExecutor(String namePrefix) {
                super(namePrefix);
            }
    
            @Override
            public void execute(Runnable command) {
                super.execute(() -> ScopedSecurityContextHolderStrategy.getSecuriyContextCarrier().run(command));
            }
    
        }
    
        @Override
        public void customize(TomcatServletWebServerFactory factory) {
            factory.addProtocolHandlerCustomizers((protocolHandler) -> protocolHandler
                    .setExecutor(new ScopedVirtualThreadExecutor("tomcat-handler-")));
        }
    
    }
    

    it is easy to see, however, a substantial awkwardness in such approach.

    First, the approach is tightly bound to type of web server/servlet container, Tomcat in our case. The solution for other servers, like Undertow or Jetty, might be different if at all possible.

    Second, Spring Security is a ubiquitous thing that SecurityContext is meant to be used everywhere, not only on server's worker threads. For example, there might be a need to setup a SecurityContext on a cron/scheduler thread or just on a thread, managed by a standalone Executor. ScopeValue-based approach will require similar binding of it to such thread, while with a standard ThreadLocal-bound SecurityContextHolderStrategy the context can be set without any thread tweaking.

    All in all, this technique introduces some not-very-welcome coupling between the code which creates a thread and the code which sets/retrieves SecurityContext.

    From conceptual standpoint, I'd daresay that the concepts of Structured Programming and Spring Security don't get along with each other very well - at least in current versions of both.

    The small POC Spring Boot project is available here.

    Note that the example works for Spring Boot 3.2.2, its applicability to earlier and later versions is not guaranteed as things with Loom are rather volatile at the moment.