簡介 ZuulFilter


在〈使用 Zuul〉中談過,在採用閘道之後,如果想要知道,哪個客戶端的使用程度等資訊,或者是對某些客戶端加以控管,這類橫切的需求,也可以在閘道上實現,具體來說,可以透過過濾器來實現。

有關於 Zuul 過濾器的撰寫,在〈Router and Filter: Zuul〉有著詳細的說明,範例主要放在〈Custom Zuul Filter Examples〉該節。

簡單來說,可以繼承 ZuulFilter 類別來實現過濾器,ZuulFilter 是個抽象類別,實作了 IZuulFilter 介面(定義了 shouldFilterrun 方法),並定義了抽象方法 filterOrderfilterType,因此在繼承之後,必須實作的方法有四個:

  • filterOrder:傳回數字,用來判斷過濾器執行的順序,可以與其他過濾器具有相同數字,也不一定要連續。
  • filterType:過濾器的類型,可以是 FilterConstants.PRE_TYPE"pre")、FilterConstants.ROUTE_TYPE"route")、FilterConstants.POST_TYPE"post")或 FilterConstants.ERROR_TYPE"error")。
  • shouldFilter:是否執行過濾器的 run 方法。
  • run:過濾的實現流程要定義在這個方法之中。

過濾器的執行時間點依序會是前置過濾器(Pre Filter)、路由過濾器(Route Filter)、目標服務、後置過濾器(Post Filter),Zuul 過濾器生命週期間若拋出例外,會由錯誤過濾器(Error Filter)處理,這個過程可以在 ZuulServlet.java 中看到:

@Override
public void service(javax.servlet.ServletRequest servletRequest, javax.servlet.ServletResponse servletResponse) throws ServletException, IOException {
    try {
        init((HttpServletRequest) servletRequest, (HttpServletResponse) servletResponse);

        // Marks this request as having passed through the "Zuul engine", as opposed to servlets
        // explicitly bound in web.xml, for which requests will not have the same data attached
        RequestContext context = RequestContext.getCurrentContext();
        context.setZuulEngineRan();

        try {
            preRoute();
        } catch (ZuulException e) {
            error(e);
            postRoute();
            return;
        }
        try {
            route();
        } catch (ZuulException e) {
            error(e);
            postRoute();
            return;
        }
        try {
            postRoute();
        } catch (ZuulException e) {
            error(e);
            return;
        }

    } catch (Throwable e) {
        error(new ZuulException(e, 500, "UNHANDLED_EXCEPTION_" + e.getClass().getName()));
    } finally {
        RequestContext.getCurrentContext().unset();
    }
}

來看看如何定義一個前置過濾器,若請求參數中沒有 token 參數,就不進行路由:

package cc.openhome;

import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;

import javax.servlet.http.HttpServletRequest;

import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.stereotype.Component;

@Component
public class TokenParamPreFilter extends ZuulFilter {

    @Override
    public boolean shouldFilter() {
        return true;
    }

    @Override
    public Object run() throws ZuulException {
        RequestContext ctx = RequestContext.getCurrentContext();
        HttpServletRequest request = ctx.getRequest();
        if (request.getParameter("token") == null) {
            ctx.setSendZuulResponse(false);
            ctx.setResponseStatusCode(403);
        }
        return null;
    }

    @Override
    public String filterType() {
        return FilterConstants.PRE_TYPE;
    }

    @Override
    public int filterOrder() {
        return FilterConstants.PRE_DECORATION_FILTER_ORDER - 1;
    }

}

Spring 如果可以在管理的 Bean 中發現 Zuul 過濾器,就會自動安裝,因此在這邊加註了 @Component

Zuul 使用 RequestContext 在過濾器執行期間傳遞訊息,個別請求的資訊是放在 ThreadLocal 中,在上例中,取得了 HttpServletRequest 以便檢查是否有 token 參數,若無就設置 setSendZuulResponse(false),這會使得 RequetContextsendZuulResponse 傳回 false

還記得在〈使用 Zuul〉中加註了 @EnableZuulProxy 嗎?在〈@EnableZuulProxy Filters〉中可以看到,Spring Cloud 會據此而載入一些過濾器,其中包含了 RibbonRoutingFilter,而 RibbonRoutingFiltershouldFilter 是這麼實現的:

@Override
public boolean shouldFilter() {
    RequestContext ctx = RequestContext.getCurrentContext();
    return (ctx.getRouteHost() == null && ctx.get(SERVICE_ID_KEY) != null
            && ctx.sendZuulResponse());
}

RibbonRoutingFilter 會判斷服務 ID,使用 HttpClient 來請求目標服務,在它 shouldFilter 中可以看到 ctx.sendZuulResponse() 是決定要不要執行 run 方法的依據之一,因此在 @EnableZuulProxy 下,想要拒絕路由,就是透過 setSendZuulResponse(false)

不過,要注意的是,過濾器是否執行 run 方法,完全是由 shouldFilter 來決定,而不是 setSendZuulResponse(false)(令 sendZuulResponse 傳回 false),如果你的過濾器 shouldFilter 並沒有依據 sendZuulResponse 來決定,setSendZuulResponse(false) 之後,過濾器還是會執行。

在設置 @EnableZuulProxy 中,Spring Cloud 會安裝 PreDecorationFilter,用來確定路由的位置與方式,在上頭的 TokenParamPreFilter 中,filterOrder 傳回 FilterConstants.PRE_DECORATION_FILTER_ORDER - 1,這表示 TokenParamPreFilter 會在 PreDecorationFilter 前執行。

在這邊就只簡介一些必須注意的地方,像是 shouldFiltersetSendZuulResponse(false) 真正的作用,由於〈Router and Filter: Zuul〉寫得蠻詳細的,真想實現 Zuul 過濾器,可以仔細閱讀一下。