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.
Yet another approach is a replacement of Spring Security's stock SecurityContextHolderFilter
with a custom one which uses ScopedSecurityContextHolderStrategy
, discussed above:
public class ScopedSecurityContextHolderFilter extends SecurityContextHolderFilter {
...
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
doFilter((HttpServletRequest) request, (HttpServletResponse) response, chain);
}
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
if (request.getAttribute(FILTER_APPLIED) != null) {
chain.doFilter(request, response);
return;
}
request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
DeferredSecurityContext deferredContext = securityContextRepository.loadDeferredContext(request);
try {
ScopedSecurityContextHolderStrategy.runWhere(deferredContext, () -> {
securityContextHolderStrategy.setDeferredContext(deferredContext);
try {
chain.doFilter(request, response);
} catch (IOException | ServletException e) {
throw new RuntimeException(e);
}
});
} catch (RuntimeException e) {
final Throwable cause = e.getCause();
if (cause instanceof ServletException)
throw (ServletException)cause;
if (cause instanceof IOException)
throw (IOException)cause;
throw e;
} finally {
request.removeAttribute(FILTER_APPLIED);
}
}
...
}
In this scenario, a ScopedValue
is bound to a thread not at the point of its initiation in Web Server/Servlet Container (Tomcat), but at arbitrary point up-stack of such initiation.
This allows to avoid the dependency of Web Server/Servlet Container (Tomcat), but brings another issues.
First, String Security Filter Chain implementation, FilterChainProxy
, invokes Security Context clearing, SecurityContextHolderStrategy.clearContext()
method, at the end of Security Filter Chain executing, thus employing a free, unrestricted ThreadLocal
design. Evidently, more restrictive ScopedValue
design comes into conflict with the Spring Security design and Security Filters, that execute after ScopedValue
gets unbound from the thread, appear top be SecurityContext
-less, and whether this might be an issue is difficult to say in general.
Second issue is associated with the replacement of SecurityContextHolderFilter
itself. Spring Security Filters configuration uses the correspondent SecurityContextConfigurer
instance directly, as a source of SecurityContextRepository
. Therefore, certain tricks are necessary to implement this replacement. One of such solutions is brought and discussed in the POC example, mentioned above, it is probably as "hacky" as its equivalents.
Finally, the implementation of ScopedSecurityContextHolderFilter
is bound to be a shameless copy-paste job from SecurityContextHolderFilter
, which also compromises upgradability and maintainability of the solution.
All in all, the approach of custom Spring Security Filter turns out to be even more questionable than the one that involves Tomcat Executor service customization.
SecurityContext
(as with any other traditionally TL-based context) at any time, whileScopedValue
s have to be defined at the time of thread initiation. This entices a lot of refactoring of ideology which is based on thread pools. – BroekerScopedValue
at thread creation time. The only requirement is to have a well defined scope, rather than set and reset calls which may be accidentally unpaired. – Draughtcreation
, I saidinitiation
, and it is by intent. Your statement, being theoretically correct, is not related to the context of discussed topic. To understand the OP's, mine and any Spring Security user' issues with SV you have to be in our boots. My answer below discusses those and if you have some thoughts on that, you are more than welcome. Also, please next time include a reference to comment's author, otherwise, not only he cannot answer but the readers won't be able to understand whom you are addressing your comment three months later. – BroekerScopedValue
a to a thread way up-stack of its creation. This hopefully explains the worries of Spring men about the point of the binding. Having said that, binding at thread initiation might be defined as an attempt to bind as down-stack of the point of thread creation as possible to avoid the issues, discussed in the second part of my answer. I excuse myself for the awkward language, we might look for a better wording. – Broeker