import com.alibaba.fastjson.JSON;
import com.mk.gateway.event.GatewayLogEvent;
import com.mk.gateway.util.IpUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.support.CachedBodyOutputMessage;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.util.ObjectUtils;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.nio.charset.StandardCharsets;
import java.util.*;
import com.mk.common.beans.GatewayLog;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.reactivestreams.Publisher;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.cloud.gateway.route.Route;
import org.springframework.cloud.gateway.support.BodyInserterContext;
import org.springframework.cloud.gateway.support.ServerWebExchangeUtils;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.codec.HttpMessageReader;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpRequestDecorator;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.http.server.reactive.ServerHttpResponseDecorator;
import org.springframework.stereotype.Component;
import org.springframework.util.MultiValueMap;
import org.springframework.web.reactive.function.BodyInserter;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.server.HandlerStrategies;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.server.ServerWebExchange;
@Slf4j
@Component
public class GatewayLogFilter implements GlobalFilter, Ordered {
@Autowired
private ApplicationEventPublisher applicationEventPublisher;
private final List
> messageReaders = HandlerStrategies.withDefaults().messageReaders();
private static final String CONTENT_TYPE = "application/json";
// 请求来源应用
private static final String REQUEST_ORIGIN_APP = "Request-Origin-App";
// 自定义请求头,转发之前删除自定义请求头
private static final List CUSTOM_HEADERS = Arrays.asList("sign", "timestamp", "random", "Request-Origin-App");
@Override
public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
// 请求路径
String requestPath = request.getPath().pathWithinApplication().value();
// 获取路由信息
Route route = getGatewayRoute(exchange);
String ipAddress = IpUtil.getIpAddr(request);
GatewayLog gatewayLog = new GatewayLog();
gatewayLog.setOrigin(request.getHeaders().getFirst(REQUEST_ORIGIN_APP));
gatewayLog.setSchema(request.getURI().getScheme());
gatewayLog.setRequestMethod(request.getMethodValue());
gatewayLog.setRequestPath(requestPath);
gatewayLog.setTargetServer(route.getUri().toString());
gatewayLog.setStartTime(new Date().getTime());
gatewayLog.setIp(ipAddress);
gatewayLog.setRouteConfig(JSON.toJSONString(route));
Map headers = new HashMap<>();
for (String key : request.getHeaders().keySet()) {
headers.put(key, request.getHeaders().getFirst(key));
gatewayLog.setHeaders(JSON.toJSONString(headers));
MediaType mediaType = request.getHeaders().getContentType();
if (request.getHeaders().getContentType() != null) {
gatewayLog.setRequestContentType(request.getHeaders().getContentType().toString());
if(mediaType != null && (MediaType.APPLICATION_FORM_URLENCODED.isCompatibleWith(mediaType) || MediaType.APPLICATION_JSON.isCompatibleWith(mediaType))){
return writeBodyLog(exchange, chain, gatewayLog);
}else{
return writeBasicLog(exchange, chain, gatewayLog);
@Override
public int getOrder() {
// 必须小于等于-2,否则无法获取相应结果
return -2;
* 获取路由信息
* @param exchange
* @return
private Route getGatewayRoute(ServerWebExchange exchange) {
return exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR);
private Mono writeBasicLog(ServerWebExchange exchange, GatewayFilterChain chain, GatewayLog gatewayLog) {
StringBuilder builder = new StringBuilder();
MultiValueMap queryParams = exchange.getRequest().getQueryParams();
gatewayLog.setRequestBody(getUrlParamsByMap(queryParams));
// 修改Header
ServerHttpRequest mutableReq = exchange.getRequest().mutate().headers(httpHeaders -> {
// 删除自定义header
for (String customHeader : CUSTOM_HEADERS) {
httpHeaders.remove(customHeader);
}).build();
//获取响应体
ServerHttpResponseDecorator decoratedResponse = recordResponseLog(exchange, gatewayLog);
return chain.filter(exchange.mutate().request(mutableReq).response(decoratedResponse).build())
.then(Mono.fromRunnable(() -> {
// 打印日志
writeAccessLog(gatewayLog);
* 解决 request body 只能读取一次问题,
* 参考: org.springframework.cloud.gateway.filter.factory.rewrite.ModifyRequestBodyGatewayFilterFactory
* @param exchange
* @param chain
* @param gatewayLog
* @return
private Mono writeBodyLog(ServerWebExchange exchange, GatewayFilterChain chain, GatewayLog gatewayLog) {
ServerRequest serverRequest = ServerRequest.create(exchange,messageReaders);
Mono modifiedBody = serverRequest.bodyToMono(String.class)
.flatMap(body ->{
gatewayLog.setRequestBody(body);
return Mono.just(body);
// 通过 BodyInserter 插入 body(支持修改body), 避免 request body 只能获取一次
BodyInserter bodyInserter = BodyInserters.fromPublisher(modifiedBody, String.class);
HttpHeaders headers = new HttpHeaders();
headers.putAll(exchange.getRequest().getHeaders());
// the new content type will be computed by bodyInserter
// and then set in the request decorator
headers.remove(HttpHeaders.CONTENT_LENGTH);
CachedBodyOutputMessage outputMessage = new CachedBodyOutputMessage(exchange, headers);
return bodyInserter.insert(outputMessage,new BodyInserterContext())
.then(Mono.defer(() -> {
// 重新封装请求
ServerHttpRequest decoratedRequest = requestDecorate(exchange, headers, outputMessage);
// 记录响应日志
ServerHttpResponseDecorator decoratedResponse = recordResponseLog(exchange, gatewayLog);
// 记录普通的
return chain.filter(exchange.mutate().request(decoratedRequest).response(decoratedResponse).build())
.then(Mono.fromRunnable(() -> {
// 打印日志
writeAccessLog(gatewayLog);
* 打印日志
* @param gatewayLog 网关日志
private void writeAccessLog(GatewayLog gatewayLog) {
applicationEventPublisher.publishEvent(new GatewayLogEvent(this, gatewayLog));
* 请求装饰器,重新计算 headers
* @param exchange
* @param headers
* @param outputMessage
* @return
private ServerHttpRequestDecorator requestDecorate(ServerWebExchange exchange, HttpHeaders headers,
CachedBodyOutputMessage outputMessage) {
return new ServerHttpRequestDecorator(exchange.getRequest()) {
@Override
public HttpHeaders getHeaders() {
long contentLength = headers.getContentLength();
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.putAll(super.getHeaders());
if (contentLength > 0) {
httpHeaders.setContentLength(contentLength);
} else {
// TODO: this causes a 'HTTP/1.1 411 Length Required' // on
// httpbin.org
httpHeaders.set(HttpHeaders.TRANSFER_ENCODING, "chunked");
// 删除自定义header
for (String customHeader : CUSTOM_HEADERS) {
headers.remove(customHeader);
return httpHeaders;
@Override
public Flux getBody() {
return outputMessage.getBody();
* 记录响应日志
* 通过 DataBufferFactory 解决响应体分段传输问题。
private ServerHttpResponseDecorator recordResponseLog(ServerWebExchange exchange, GatewayLog gatewayLog) {
ServerHttpResponse response = exchange.getResponse();
DataBufferFactory bufferFactory = response.bufferFactory();
return new ServerHttpResponseDecorator(response) {
@Override
public Mono writeWith(Publisher extends DataBuffer> body) {
if (body instanceof Flux) {
Date responseTime = new Date();
gatewayLog.setEndTime(responseTime.getTime());
// 计算执行时间
long executeTime = (responseTime.getTime() - gatewayLog.getStartTime());
gatewayLog.setExecuteTime(executeTime);
gatewayLog.setStatus(response.getStatusCode().value() == 200 ? "成功" : "失败");
// 获取响应类型,如果是 json 就打印
String originalResponseContentType = exchange.getAttribute(ServerWebExchangeUtils.ORIGINAL_RESPONSE_CONTENT_TYPE_ATTR);
if (Objects.equals(this.getStatusCode(), HttpStatus.OK)
&& StringUtils.isNotBlank(originalResponseContentType)
&& originalResponseContentType.contains(CONTENT_TYPE)) {
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);
// 释放掉内存
DataBufferUtils.release(join);
String responseResult = new String(content, StandardCharsets.UTF_8);
gatewayLog.setResponseData(responseResult);
return bufferFactory.wrap(content);
// if body is not a flux. never got there.
return super.writeWith(body);
* 将map参数转换成url参数
* @param map
* @return
private String getUrlParamsByMap(MultiValueMap map) {
if (ObjectUtils.isEmpty(map)) {
return "";
StringBuilder sb = new StringBuilder();
for (Map.Entry> entry : map.entrySet()) {
sb.append(entry.getKey()).append("=").append(entry.getValue().get(0));
sb.append("&");
String s = sb.toString();
if (s.endsWith("&")) {
s = StringUtils.substringBeforeLast(s, "&");
return s;
日志收集效果
个人博客纯净版SpringCloud Gateway网关收集请求日志 | 代码搬运工网关负责服务的转发,所有通过网关转发的服务。都可以通过网关进行收集相关请求日志,具体实现如下:日志实体类package com.mk.common.beans;import lombok.*;@Data@Builder@NoArgsConstructor@AllArgsConstructorpublic class GatewayLog { /**请求来源**/ pri
accesslog :
enabled : true # enable or disable logging, default value is true.
timeZone : Asia/Seoul # log datetime zone setting, default is UTC
自定义访问用户信息解析器
public interface AccessUserInformationResolver {
String resolveAccessUserInformation ( ServerWebExchange serverWebExchange );
Acces
日志
打印
收集
是开发中调试和定位线上问题的关键手段也是重中之重,
gateway
作为
请求
入口转发的核心模块,合理、规范的
日志
打印很重要。增加一个
请求
入参过滤器,用来打印入参信息。
ResponseLogFilter
网关
请求
的响应报文不在这里打印,post body 参数没有打印...
请求
应答
日志
时在日常开发调试问题的重要手段之一,那么如何基于
Spring
Cloud
Gateway
做呢,请看我上代码。
创建RecorderServerHttpRequestDecorator,缓存
请求
参数,解决body只能读一次问题。
public class RecorderServerHttpRequestDecorator extends ServerHttpRequestDe...
这几天有个对
SpringCloud
-
Gateway
网关
请求
参数做安全校验的需求,网上多数教程不适用于
Spring
Boot
2.1+版本。我在
SpringCloud
gateway
Github中的Issues找到了答案,放上连接:作者采纳的答案。
具体代码如下:
@Componentpublic class CacheBody
Gateway
Filter implements Ordered, Globa...
Spring
Cloud
Gateway
是一个非常流行的
微服务
网关
,在
微服务
架构设计中有着重要的作用,可以用来实现各种功能,例如路由转发、负载均衡、流量控制、限流等;除此之外,
Spring
Cloud
Gateway
还能够支持统一认证,即只需要一次认证,就能够访问所有的
微服务
,这对于复杂的
微服务
架构来说非常的有用。
实现统一认证的实现方法如下:
1. 接入认证中心:首先需要在
Spring
Cloud
Gateway
中接入一个认证中心,比如使用
Spring
Security 或 OAuth2 进行认证,实现单点登录。
2. 统一身份验证:在用户登录认证成功之后,会生成一个 token,
Spring
Cloud
Gateway
会通过这个 token 来进行用户身份认证。而在
微服务
中,只需要进行简单的 token 验证就能够获取到用户信息,并进行相应的操作。
3. 统一资源管理:在认证中心中进行配置,指定可以访问的资源,进而限制了用户访问
微服务
的范围。这样就能够确保只有获得了相应权限的用户才能够访问
微服务
。
4.
日志
跟踪:
Spring
Cloud
Gateway
还能够记录每个
请求
的详细信息和用户信息,进而进行
日志
跟踪和监控,这对于故障排除和性能优化非常重要。
总之,实现
Spring
Cloud
Gateway
的统一认证,能够提供更好的安全性,同时提高开发效率,显著降低了
微服务
的管理难度,减轻了
微服务
的管理负担。
Eden4J:
SpringCloud Gateway网关收集请求日志
chaojunma:
SpringCloud Gateway网关收集请求日志
chaojunma: