Spring Cloud實(shí)戰(zhàn)小貼士:Zuul處理Cookie和重定向
由于我們?cè)谥八械娜腴T教程中,對(duì)于HTTP請(qǐng)求都采用了簡(jiǎn)單的接口實(shí)現(xiàn)。而實(shí)際使用過程中,我們的HTTP請(qǐng)求要復(fù)雜的多,比如當(dāng)我們將Spring Cloud Zuul作為API網(wǎng)關(guān)接入網(wǎng)站類應(yīng)用時(shí),往往都會(huì)碰到下面這兩個(gè)非常常見的問題:
- 會(huì)話無法保持
- 重定向后的HOST錯(cuò)誤
本文將幫助大家分析問題原因并給出解決這兩個(gè)常見問題的方法。
一、會(huì)話保持問題
通過跟蹤一個(gè)HTTP請(qǐng)求經(jīng)過Zuul到具體服務(wù),再到返回結(jié)果的全過程。我們很容易就能發(fā)現(xiàn),在傳遞的過程中,HTTP請(qǐng)求頭信息中的Cookie和Authorization都沒有被正確地傳遞給具體服務(wù),所以最終導(dǎo)致會(huì)話狀態(tài)沒有得到保持的現(xiàn)象。
那么這些信息是在哪里丟失的呢?我們從Zuul進(jìn)行路由轉(zhuǎn)發(fā)的過濾器作為起點(diǎn),來一探究竟。下面是RibbonRoutingFilter過濾器的實(shí)現(xiàn)片段:
- public class RibbonRoutingFilter extends ZuulFilter{
- ...
- protected ProxyRequestHelper helper;
- @Override
- public Object run() {
- RequestContext context = RequestContext.getCurrentContext();
- this.helper.addIgnoredHeaders();
- try {
- RibbonCommandContext commandContext = buildCommandContext(context);
- ClientHttpResponse response = forward(commandContext);
- setResponse(response);
- return response;
- }
- ...
- return null;
- }
- protected RibbonCommandContext buildCommandContext(RequestContext context) {
- HttpServletRequest request = context.getRequest();
- MultiValueMap<String, String> headers = this.helper
- .buildZuulRequestHeaders(request);
- MultiValueMap<String, String> params = this.helper
- .buildZuulRequestQueryParams(request);
- ...
- }
- }
這里有三個(gè)重要元素:
- 過濾器的核心邏輯run函數(shù)實(shí)現(xiàn),其中調(diào)用了內(nèi)部函數(shù)buildCommandContext來構(gòu)建上下文內(nèi)容
- 而buildCommandContext中調(diào)用了helper對(duì)象的buildZuulRequestHeaders方法來處理請(qǐng)求頭信息
- helper對(duì)象是ProxyRequestHelper類的實(shí)例
接下來我們?cè)倏纯碢roxyRequestHelper的實(shí)現(xiàn):
- public class ProxyRequestHelper {
- public MultiValueMap<String, String> buildZuulRequestHeaders(
- HttpServletRequest request) {
- RequestContext context = RequestContext.getCurrentContext();
- MultiValueMap<String, String> headers = new HttpHeaders();
- Enumeration<String> headerNames = request.getHeaderNames();
- if (headerNames != null) {
- while (headerNames.hasMoreElements()) {
- String name = headerNames.nextElement();
- if (isIncludedHeader(name)) {
- Enumeration<String> values = request.getHeaders(name);
- while (values.hasMoreElements()) {
- String value = values.nextElement();
- headers.add(name, value);
- }
- }
- }
- }
- Map<String, String> zuulRequestHeaders = context.getZuulRequestHeaders();
- for (String header : zuulRequestHeaders.keySet()) {
- headers.set(header, zuulRequestHeaders.get(header));
- }
- headers.set(HttpHeaders.ACCEPT_ENCODING, "gzip");
- return headers;
- }
- public boolean isIncludedHeader(String headerName) {
- String name = headerName.toLowerCase();
- RequestContext ctx = RequestContext.getCurrentContext();
- if (ctx.containsKey(IGNORED_HEADERS)) {
- Object object = ctx.get(IGNORED_HEADERS);
- if (object instanceof Collection && ((Collection<?>) object).contains(name)) {
- return false;
- }
- }
- ...
- }
- }
從上述源碼中,我們可以看到構(gòu)建頭信息的方法buildZuulRequestHeaders通過isIncludedHeader函數(shù)來判斷當(dāng)前請(qǐng)求的各個(gè)頭信息是否在忽略的頭信息清單中,如果是的話就不組織到此次轉(zhuǎn)發(fā)的請(qǐng)求中去。那么這些需要忽略的頭信息是在哪里初始化的呢?在PRE階段的PreDecorationFilter過濾器中,我們可以找到答案:
- public class PreDecorationFilter extends ZuulFilter{
- ...
- public Object run() {
- RequestContext ctx = RequestContext.getCurrentContext();
- final String requestURI = this.urlPathHelper.getPathWithinApplication(ctx.getRequest());
- Route route = this.routeLocator.getMatchingRoute(requestURI);
- if (route != null) {
- String location = route.getLocation();
- if (location != null) {
- ctx.put("requestURI", route.getPath());
- ctx.put("proxy", route.getId());
- // 處理忽略頭信息的部分
- if (!route.isCustomSensitiveHeaders()) {
- this.proxyRequestHelper.addIgnoredHeaders(
- this.properties.getSensitiveHeaders()
- .toArray(new String[0]));
- } else {
- this.proxyRequestHelper.addIgnoredHeaders(
- route.getSensitiveHeaders()
- .toArray(new String[0]));
- }
- ...
- }
從上述源碼中,我們可以看到有一段if/else塊,通過調(diào)用ProxyRequestHelper的addIgnoredHeaders方法來添加需要忽略的信息到請(qǐng)求上下文中,供后續(xù)ROUTE階段的過濾器使用。這里的if/else塊分別用來處理全局設(shè)置的敏感頭信息和指定路由設(shè)置的敏感頭信息。而全局的敏感頭信息定義于ZuulProperties中:
- @Data
- @ConfigurationProperties("zuul")
- public class ZuulProperties{
- private Set<String> sensitiveHeaders = new LinkedHashSet<>(
- Arrays.asList("Cookie", "Set-Cookie", "Authorization"));
- ...
- }
所以解決該問題的思路也很簡(jiǎn)單,我們只需要通過設(shè)置sensitiveHeaders即可,設(shè)置方法分為兩種:
1. 全局設(shè)置:
- zuul.sensitive-headers=
2. 指定路由設(shè)置:
- zuul.routes.
.sensitive-headers= - zuul.routes.
.custom-sensitive-headers=true
二、重定向問題
在使用Spring Cloud Zuul對(duì)接Web網(wǎng)站的時(shí)候,處理完了會(huì)話控制問題之后。往往我們還會(huì)碰到如下圖所示的問題,我們?cè)跒g覽器中通過Zuul發(fā)起了登錄請(qǐng)求,該請(qǐng)求會(huì)被路由到某WebSite服務(wù),該服務(wù)在完成了登錄處理之后,會(huì)進(jìn)行重定向到某個(gè)主頁(yè)或歡迎頁(yè)面。此時(shí),仔細(xì)的開發(fā)者會(huì)發(fā)現(xiàn),在登錄完成之后,我們?yōu)g覽器中URL的HOST部分發(fā)生的改變,該地址變成了具體WebSite服務(wù)的地址了。這就是在這一節(jié),我們將分析和解決的重定向問題!
出現(xiàn)該問題的根源是Spring Cloud Zuul沒有正確的處理HTTP請(qǐng)求頭信息中的Host導(dǎo)致。在Brixton版本中,Spring Cloud Zuul的PreDecorationFilter過濾器實(shí)現(xiàn)時(shí)完全沒有考慮這一問題,它更多的定位于REST API的網(wǎng)關(guān)。所以如果要在Brixton版本中增加這一特性就相對(duì)較為復(fù)雜,不過好在Camden版本之后,Spring Cloud Netflix 1.2.x版本的Zuul增強(qiáng)了該功能,我們只需要通過配置屬性zuul.add-host-header=true就能讓原本有問題的重定向操作得到正確的處理。關(guān)于更多Host頭信息的處理,讀者可以參考本文之前的分析思路,可以通過查看PreDecorationFilter過濾器的源碼來詳細(xì)更多實(shí)現(xiàn)細(xì)節(jié)。
【本文為51CTO專欄作者“翟永超”的原創(chuàng)稿件,轉(zhuǎn)載請(qǐng)通過51CTO聯(lián)系作者獲取授權(quán)】