首页 > 解决方案 > WebClient ExchangeFilterFunction 缓存来自 ThreadLocal 的值

问题描述

在将 Spring MVC 与 Webflux 的 ThreadLocal 和 WebClient 一起使用时,我遇到了以下问题。

我的任务是:

  1. 拦截用户对我的应用程序的请求,并从中获取所有标头并将其保存在 ThreadLocal 中。

  2. 之后,当我的应用程序通过 WebClient 调用另一个服务时,在 ExchangeFilterFunction 中拦截此请求,并用 p.1 中的 Authorization 标头补充它。

当我完成处理用户的请求时,我会清除上下文。

我使用我的自定义类“RequestContext”将标头存储在 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);
        }
    }

}

我使用我的自定义类“RequestContextService”来处理 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();
    }

}

我的应用是一个 WebMvc 应用。为了完成第 1 步,我使用 HandlerInterceptor 拦截请求并将所有标头设置为 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();
    }

}

如您所见,在每次请求后,我都会调用“requestContextService.clear()”方法来清除 ThreadLocal。

为了执行第二步,我使用了 ExchangeFilterFunction,在这里我转向 threadlocal 并从那里获取标题。

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);
    }

}

问题是 SamlExchangeFilterFunction 只能正常工作一次。

在对应用程序的第一次请求时,一切正常。但是对于具有不同授权标头的进一步请求,ExchangeFilterFunction 似乎缓存了第一个请求的值并替换它,尽管 threadlocal 本身包含授权标头的完全不同的含义。

标签: javaspringwebclientwebflux

解决方案


推荐阅读