I faced with the following issue while using Spring MVC with ThreadLocal and WebClient from Webflux.
My task is to:
Intercept the user's request to my application and get all the headers from it and save it in ThreadLocal.
After that, when my application makes a call to another service through the WebClient, intercept this request in ExchangeFilterFunction and supplement it with the Authorization header from p.1.
When I finish processing the user's request, I clear the context.
I use my custom class "RequestContext" to store headers in ThreadLocal:
public class RequestContext {
private HttpHeaders requestHeaders;
private String jwt;
private static final String BEARER_PREFIX = "Bearer ";
public RequestContext(HttpHeaders httpHeaders) {
this.requestHeaders = httpHeaders;
if (Objects.nonNull(httpHeaders)) {
init();
}
}
private void init() {
if (Objects.nonNull(requestHeaders)) {
extractJwt();
}
}
private void extractJwt() {
var jwtHeader = requestHeaders.getFirst(HttpHeaders.AUTHORIZATION);
if (StringUtils.isNotBlank(jwtHeader) && jwtHeader.startsWith(BEARER_PREFIX)) {
jwt = jwtHeader.substring(7);
}
}
}
I use my custom clas "RequestContextService" to deal with ThreadLocal:
public class RequestContextService {
private static final ThreadLocal<RequestContext> CONTEXT = new InheritableThreadLocal<>();
public void init(RequestContext requestContext) {
if (Objects.isNull(CONTEXT.get())) {
CONTEXT.set(requestContext);
} else {
log.error("#init: Context init error");
}
}
public RequestContext get() {
return CONTEXT.get();
}
public void clear() {
CONTEXT.remove();
}
}
My app is a WebMvc app. To complete step 1, I intercept the request with an HandlerInterceptor and set all headers to Threadlocal.
public class HeaderInterceptor implements HandlerInterceptor {
private final RequestContextService requestContextService;
@Override
public boolean preHandle(@NonNull HttpServletRequest request,
@NonNull HttpServletResponse response,
@NonNull Object handler) {
if (Objects.equals(request.getDispatcherType(), DispatcherType.REQUEST)) {
var headers = new ServletServerHttpRequest(request).getHeaders();
requestContextService.init(new RequestContext(headers));
}
return true;
}
@Override
public void afterCompletion(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response,
@NonNull Object handler, Exception ex) {
requestContextService.clear();
}
}
As you can see, after every request I call "requestContextService.clear()" method to clear ThreadLocal.
To perform step two, I use the ExchangeFilterFunction, where I turn to the threadlocal and get the title from there.
public class SamlExchangeFilterFunction implements ExchangeFilterFunction {
private final RequestContextService requestContextService;
private static final ClientResponse UNAUTHORIZED_CLIENT_RESPONSE =
ClientResponse.create(HttpStatus.UNAUTHORIZED).build();
@Override
public @NotNull Mono<ClientResponse> filter(@NotNull ClientRequest request, @NotNull ExchangeFunction next) {
var jwt = requestContextService.get().getJwt();
if (StringUtils.isNoneBlank(jwt)) {
var clientRequest = ClientRequest.from(request)
.headers(httpHeaders -> httpHeaders.set(SAML_HEADER_NAME, jwt))
.build();
return next.exchange(clientRequest);
}
return Mono.just(UNAUTHORIZED_CLIENT_RESPONSE);
}
}
The problem is that the SamlExchangeFilterFunction works correctly only once.
On the first request to the application, everything works as it should. But with further requests with different authorization headers, the ExchangeFilterFunction seems to cache the value from the first request and substitutes it despite the fact that the threadlocal itself contains a completely different meaning of Authorization header.