OpenFeign攔截器
在微服務中比較常見的場景:前端帶了JWT令牌請求服務A,在服務A中使用Feign遠程調用服務B、服務C等,A、B、C都接入了Spring Security;此時就會存在這樣的需求,如服務A調用服務B、C時不帶有JWT令牌就會出現服務調用失敗,無法通過服務B、C鑑權認證;
此時需要通過Feign提供的RequestInterceptor攔截器將A請求頭中所持有的Token在Feign發起遠程調用時繼續傳遞給服務B、服務C;
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向下傳遞的方法將出現意想不到的副作用,導致程序請求調用失敗;
上面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數據進行吞沒;
根據吞沒配置與上個請求完成後所剩餘的字節數remaining,對數據byteBuffer數據進行吞沒;
在此可看到吞沒掉的字節數爲74,爲第一個請求頭所攜帶的contentLength數據,第二個Put請求頭所丟失的數據;
如Feign調用爲Get、Put、Put請求;Get、Get、Get請求將不會有異常情況出現,上述異常情況爲:Put、Get、Put請求;之所以remaining會剩餘74是因爲Tomcat並不會對Get請求從byteBuffer讀取Content-Length所傳輸的字節數據;當請求沒有導致吞沒機制發生時就不會出現異常情況;