[2] WebAsyncManagerIntegrationFilter

WebAsyncManagerIntegrationFilter

介紹

    字面意思這是一個Web異步管理集成過濾器,翻譯成中文依然很抽象。我們從ThreadLocal講起,ThreadLocal可以理解爲一個與當前線程綁定的Map, key是當前線程,value是要存儲的object。當我們在同一個線程中,可以通過get()方法獲取到value。如果在A線程set(object),然後在B線程中調用get(),由於線程已切換,key是A,拿B自然取不出key爲A的value。對於一個SSO系統,我們常常把當前登錄的用戶相關信息放到ThreadLocal中,避免每次使用的時候都去調RPC接口或者DB接口去查詢。Spring Security的SecurityContextHolder就是通過ThreadLocal實現的。
WebAsyncManager,Spring文檔有這麼一句話:The central class for managing asynchronous request processing, mainly intended as an SPI and not typically used directly by application classes. 它主要用於異步請求,筆者發現這個東西在spring mvc請求分發處理中用到很多,時間有限,筆者也沒有深入的瞭解,我們的主要關注點是WebAsyncManagerIntegrationFilter。

代碼分析

步驟1

    WebAsyncManagerIntegrationFilter#doFilterInternal()關鍵代碼如下:

@Override
protected void doFilterInternal(HttpServletRequest request,
        HttpServletResponse response, FilterChain filterChain)
        throws ServletException, IOException {
    WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);

    SecurityContextCallableProcessingInterceptor securityProcessingInterceptor = (SecurityContextCallableProcessingInterceptor) asyncManager
            .getCallableInterceptor(CALLABLE_INTERCEPTOR_KEY);
    if (securityProcessingInterceptor == null) {
        asyncManager.registerCallableInterceptor(CALLABLE_INTERCEPTOR_KEY,
                new SecurityContextCallableProcessingInterceptor());
    }

    filterChain.doFilter(request, response);
}

步驟2

    SecurityContextCallableProcessingInterceptor,這個攔截器的關鍵作用,就是執行前後存儲和清空SecurityContext,以便在WebAsyncTask異步調用中獲取到SecurityContext,核心代碼如下:

@Override
public <T> void beforeConcurrentHandling(NativeWebRequest request, Callable<T> task) {
    if (securityContext == null) {
        setSecurityContext(SecurityContextHolder.getContext());
    }
}

@Override
public <T> void preProcess(NativeWebRequest request, Callable<T> task) {
    SecurityContextHolder.setContext(securityContext);
}

@Override
public <T> void postProcess(NativeWebRequest request, Callable<T> task,
        Object concurrentResult) {
    SecurityContextHolder.clearContext();
}

private void setSecurityContext(SecurityContext securityContext) {
    this.securityContext = securityContext;
}

步驟3

    筆者做了一個實踐,分別一個Runnable和WebAsyncTask中嘗試從SecurityContextHolder獲取當前登錄用戶,AuthHolder只是對SecurityContextHolder進行封裝,本質上還是調用的SecurityContextHolder.get()獲取線程上下文信息,代碼如下:

@ApiOperation("[WebAsyncTask]異步獲取上下文用戶信息")
@GetMapping("/web_async_task")
public WebAsyncTask getContextByWebAsyncTask() {
    log.info("1:main線程開始[{}]", Thread.currentThread());
    WebAsyncTask<Oauth2User> task = new WebAsyncTask<>(300L, () -> {
        log.info("1:WebAsyncTask異步線程[{}], 開始執行異步任務", Thread.currentThread().getName());
        Oauth2User user = AuthHolder.user();
        log.info("2:WebAsyncTask異步線程[{}], 任務執行完成, 結果:{}",
            Thread.currentThread().getName(), JSON.toJSONString(user));
        return user;
    });

    log.info("2:main線程結束[{}]", Thread.currentThread());
    return task;
}

@ApiOperation("[Runnable]異步獲取上下文用戶信息")
@GetMapping("/runnable")
public R<Oauth2User> getContextByRunnable() throws Exception {
    log.info("1:main線程開始[{}]", Thread.currentThread());
    final List<Oauth2User> list = new ArrayList<>();
    CountDownLatch latch = new CountDownLatch(1);
    Thread async = new Thread(() -> {
        try {
            log.info("1:Runnable異步線程[{}], 開始執行異步任務", Thread.currentThread().getName());
            Oauth2User user = AuthHolder.user();
            list.add(user);
            log.info("2:Runnable異步線程[{}], 任務執行完成, 結果:{}",
                Thread.currentThread().getName(), JSON.toJSONString(user));
        } finally {
            latch.countDown();
        }
    });
    async.start();
    latch.await();
    log.info("2:main線程結束[{}]", Thread.currentThread());
    return R.success(list.isEmpty() ? null : list.get(0));
}

執行結果如下:
image.png    可以看出,WebAsyncTask從SecurityContextHolder獲取到了身份認證信息,但Runnable卻不可以。WebAsyncTask之所以能夠在異步線程中從SecurityContextHolder中獲取上下文信息,與WebAsyncManagerIntegrationFilter的作用密不可分。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章