OpenFeign引起的HTTP Status 400與Tomcat吞沒數據

OpenFeign攔截器

  在微服務中比較常見的場景:前端帶了JWT令牌請求服務A,在服務A中使用Feign遠程調用服務B、服務C等,A、B、C都接入了Spring Security;此時就會存在這樣的需求,如服務A調用服務B、C時不帶有JWT令牌就會出現服務調用失敗,無法通過服務B、C鑑權認證;
image.png

  此時需要通過Feign提供的RequestInterceptor攔截器將A請求頭中所持有的Token在Feign發起遠程調用時繼續傳遞給服務B、服務C;
image.png

Demo示例代碼:

public class DemoRequestInterceptor implements RequestInterceptor {

private final BearerTokenResolver tokenResolver;
@Override
public void apply(RequestTemplate template) {
    ServletRequestAttributes  attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
    HttpServletRequest request= attributes.getRequest();
    //獲取當前請求header
    Enumeration<String> headerNames = request.getHeaderNames();
    if (headerNames != null) {
        while (headerNames.hasMoreElements()) {
            String name = headerNames.nextElement();
            String values = request.getHeader(name);
            //將header沿Feign調用鏈傳遞
            template.header(name, values);
        }
    }
 }
}

導致的問題

  這種簡單粗暴全部將Header向下傳遞的方法將出現意想不到的副作用,導致程序請求調用失敗;
image.png

  上面Demo代碼,如發生如上圖所示的服務調用,將產生服務調用失敗,HTTP 400異常;
  以下分析環境爲Spring Boot2.7,使用內置tomcat 9.0.65;

  1、客戶端Put請求,content-length等於74
  2、全部拷貝UpdateA請求所攜帶的Header頭,服務A發起feign調用服務B,Get請求;
  3、服務A發起feign調用服務C,Put請求;
  服務A調用服務B成功完成請求,在服務A繼續發起的feign調用服務C出現如下異常,Tomcat無法解析該請求,拋出異常,此時請求頭已經被破壞:

java.lang.IllegalArgumentException: Invalid character found in method name [late, ]. HTTP method names must be tokens
at org.apache.coyote.http11.Http11InputBuffer.parseRequestLine(Http11InputBuffer.java:421)

  從異常中明顯可以看出Tomcat在解析請求行時出現異常,但具體什麼原因引起的還將要debug Tomcat源碼才能發現問題所在;

問題跟蹤

  在Tomcat中從拋出異常的類處添加日誌輸出,跟蹤問題;
  1、在Http11InputBuffer類的parseRequestLine方法中添加打印日誌:
可看到兩個請求的請求頭信息:

  服務A對服務B發起的Get請求信息,如上所述,Header頭全部帶上了:

2022-09-10 15:50:21.642 DEBUG 12028 --- [nio-8085-exec-5] 
o.a.coyote.http11.Http11InputBuffer 
:2:parsingRequestLinePhase--Received [GET /infos? 
pageSize=10&pageNo=1 HTTP/1.1
accept: */*
accept-encoding: gzip, deflate, br
authorization: Bearer admin11::ab5050d3-3542-4ada-80e9- 
6241132de5e5
cache-control: no-cache
connection: keep-alive
content-length: 74
content-type: application/json
cookie: JSESSIONID=3A1FD18830F43E295F07E8F77C0FA9FF
host: 127.0.0.1:7730
postman-token: fe8cfff4-e10b-4286-98da-d6d7bc05c006
user-agent: PostmanRuntime/7.29.2

  服務A對服務C發起的Put請求信息,但請求頭並不完整;請求行已不見,請求頭也只剩部分,請求體完整,這也是爲什麼拋出請求行解析失敗的原因;

2022-09-10 15:50:39.126 DEBUG 12028 --- [nio-8085-exec-5] o.a.coyote.http11.Http11InputBuffer      
:2:parsingRequestLinePhase--Received [late, br
authorization: Bearer admin11::ab5050d3-3542-4ada-80e9- 
6241132de5e5
cache-control: no-cache
connection: keep-alive
cookie: JSESSIONID=3A1FD18830F43E295F07E8F77C0FA9FF
host: 127.0.0.1:7730
postman-token: fe8cfff4-e10b-4286-98da-d6d7bc05c006
user-agent: PostmanRuntime/7.29.2
Content-Type: application/json
Content-Length: 34

{"aaabbbbId":123,"varray":["123"]}]

  第一個Get請求,雖然附加了content-length: 74,但並不影響請求;第二個Put請求,請求頭直接被破壞;通過private boolean fill(boolean block)方法的日誌發現,Put請求接收時HTTP請求頭還是完整的;
  在Parameters類的processParameters方法中並沒有對Get請求讀取Content-Length長度的數據,目前只能從Get請求完成之後、Put頭解析之前的代碼進行跟蹤分析,還是在Http11InputBuffer類中,在每個請求結束之後都會執行如下方法:

/**
* 結束請求,消耗剩餘字節
**/
void endRequest() throws IOException {
   //吞沒機制是否開啓
    if (swallowInput && (lastActiveFilter != -1)) {
        int extraBytes = (int) activeFilters[lastActiveFilter].end();
        byteBuffer.position(byteBuffer.position() - extraBytes);
    }
}

  此方法將會根據吞沒機制配置與remaining字節數對byteBuffer數據進行吞沒;
image.png
  根據吞沒配置與上個請求完成後所剩餘的字節數remaining,對數據byteBuffer數據進行吞沒;
image.png

  在此可看到吞沒掉的字節數爲74,爲第一個請求頭所攜帶的contentLength數據,第二個Put請求頭所丟失的數據;
image.png

  如Feign調用爲Get、Put、Put請求;Get、Get、Get請求將不會有異常情況出現,上述異常情況爲:Put、Get、Put請求;之所以remaining會剩餘74是因爲Tomcat並不會對Get請求從byteBuffer讀取Content-Length所傳輸的字節數據;當請求沒有導致吞沒機制發生時就不會出現異常情況;

文章首發地址:https://mp.weixin.qq.com/s/COfUWUshQge-i2UOST8QUw

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