前言
最近團隊的網關日誌發現有不少響應結果記錄,出現形如下的亂碼
�V*.I,IU�JK�)N�Q�M-.NL�^�m�?��(�釷/�,}�����]O7L|���ŲƧ�MϦnP�Q*K�)*�+���QJ-*�/r�O���{�@8� ��
一開始感覺是不是中文亂碼,但是後面發現有些日誌不是中文,也是亂碼,而有些記錄的日誌又能正常顯示。於是搜索了一圈,在https://blog.csdn.net/u013506626/article/details/134487673
在這篇文章找到的思路以及解決答案。
如何解決
根據上面博文介紹是因為請求的headers中加了有"Accept-Encoding"屬性,值為"gzip, deflate, br",導致響應結果亂碼。解決思路就是將Accept-Encoding置為空“”就可以解決,按他的思路,我就寫了一個過濾器
@RequiredArgsConstructor
public class RemoveGzipHeaderGlobalFilter implements Ordered, GlobalFilter {
private final GwCommonProperty gwCommonProperty;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
if (isSkipRemoveGzipHeaderEnabled(exchange)) {
return chain.filter(exchange);
} else {
ServerHttpRequest request = exchange.getRequest().mutate()
.header(HttpHeaders.ACCEPT_ENCODING, "").build();
return chain.filter(exchange.mutate().request(request).build());
}
}
private boolean isSkipRemoveGzipHeaderEnabled(ServerWebExchange exchange){
if(!gwCommonProperty.isRemoveGzipHeaderEnabled()){
return true;
}
String gzipHeader = exchange.getRequest().getHeaders().getFirst(HttpHeaders.ACCEPT_ENCODING);
if(!StringUtils.hasText(gzipHeader)){
return true;
}
return !gzipHeader.contains("gzip");
}
@Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE;
}
}
後面果然不再出現亂碼。既然是Accept-Encoding引起的亂碼問題,我們就來聊下Accept-Encoding
Accept-Encoding
Accept-Encoding 是 HTTP 協議中的一個頭部字段,其主要作用在於告知服務器,客户端能夠理解的內容編碼方式。這個字段對於網絡傳輸效率的優化非常重要,因為它允許服務器根據客户端的能力來壓縮響應數據,從而減少傳輸的數據量,加快網頁加載速度。
1、常見編碼方式:
gzip: 使用 Lempel-Ziv 編碼(LZ77)和 Huffman 編碼進行壓縮的算法。
deflate: 使用 zlib 庫和 deflate 壓縮算法進行壓縮。
br(Brotli): Google 開發的一種新的數據壓縮算法,旨在提供比 gzip 和 deflate 更高的壓縮率。
2、字段格式:
Accept-Encoding 字段的值是一個由逗號分隔的列表,其中包含了客户端支持的內容編碼方式。例如:Accept-Encoding: gzip, deflate, br
3、工作流程:
客户端在發送 HTTP 請求時,會在請求頭部中包含 Accept-Encoding 字段,列出它支持的內容編碼方式。
服務器在收到請求後,會檢查 Accept-Encoding 字段,並根據客户端支持的內容編碼方式來選擇合適的壓縮算法來壓縮響應數據。
如果服務器選擇了一種內容編碼方式,它會在響應頭部的 Content-Encoding 字段中指定所使用的編碼方式。
網關日誌記錄響應結果亂碼原因
介紹完Accept-Encoding,我們繼續探討一下為啥Accept-Encoding會引起網關日誌響應結果亂碼,因為設置了Accept-Encoding: gzip,deflate,所以服務器就會根據客户端支持的內容編碼方式來選擇合適的壓縮算法來壓縮響應,而網關層數據沒對數據進行解壓縮,因此就亂碼
因此解決亂碼的思路理論上會有2種,一種是上述博文介紹的,去掉Accept-Encoding: gzip,deflate這個頭信息。去掉這個頭信息就是告訴服務器,客户端不支持壓縮,要求不壓縮直接返回數據
另外一種思路是如果服務器選擇了一種內容編碼方式,它會在響應頭部的 Content-Encoding 字段中指定所使用的編碼方式。因此我們就可以根據Content-Encoding來判斷是否要對數據進行解壓縮
網關日誌記錄過濾器核心改造的示例如下
/**
* 記錄響應日誌
* 通過 DataBufferFactory 解決響應體分段傳輸問題。
*/
private ServerHttpResponseDecorator recordResponseLog(ServerWebExchange exchange, AccessLog accessLog) {
ServerHttpResponse response = exchange.getResponse();
DataBufferFactory bufferFactory = response.bufferFactory();
return new ServerHttpResponseDecorator(response) {
@Override
public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
if (body instanceof Flux) {
LogUtils.setReponse(accessLog,exchange);
Flux<? extends DataBuffer> fluxBody = Flux.from(body);
return super.writeWith(fluxBody.buffer().map(dataBuffers -> {
// 合併多個流集合,解決返回體分段傳輸
DataBufferFactory dataBufferFactory = new DefaultDataBufferFactory();
DataBuffer join = dataBufferFactory.join(dataBuffers);
byte[] content = new byte[join.readableByteCount()];
join.read(content);
content = isGzip(response) ? gzipMessageBodyResolver.decode(content) : content;
// 釋放掉內存
DataBufferUtils.release(join);
String responseResult = new String(content, StandardCharsets.UTF_8);
accessLog.setResponseData(responseResult);
return bufferFactory.wrap(content);
}));
}
// if body is not a flux. never got there.
return super.writeWith(body);
}
};
}
public boolean isGzip(ServerHttpResponse serverHttpResponse) {
HttpHeaders headers = serverHttpResponse.getHeaders();
if (headers.containsKey(HttpHeaders.CONTENT_ENCODING)) {
List<String> encodingList = headers.get(HttpHeaders.CONTENT_ENCODING);
return CollectionUtil.isNotEmpty(encodingList) && encodingList.contains(GZIP);
}
return false;
}
注: 特別提醒,因為要獲取服務端header響應Content-Encoding,用的是ServerHttpResponse,而不是ServerHttpRequest
總結
綜上解決因Accept-Encoding引起的亂碼方式有2種,一種是直接移除Accept-Encoding,告訴服務端不要對響應數據進行壓縮,直接返回未壓縮數據。另外一種如果不移除Accept-Encoding,就得根據Content-Encoding來對服務端響應的數據進行解壓縮