gateway:完整实现 AccessLogFilter 访问日志

This commit is contained in:
YunaiV 2022-06-08 01:13:49 +08:00
parent b2fc171609
commit 213ec8bd72
6 changed files with 107 additions and 133 deletions

View File

@ -41,6 +41,11 @@ public class JsonUtils {
JsonUtils.objectMapper = objectMapper;
}
@SneakyThrows
public static String toJsonPrettyString(Object object) {
return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(object);
}
@SneakyThrows
public static String toJsonString(Object object) {
return objectMapper.writeValueAsString(object);

View File

@ -2,6 +2,7 @@ package cn.iocoder.yudao.gateway.filter.logging;
import lombok.Data;
import org.springframework.cloud.gateway.route.Route;
import org.springframework.http.HttpStatus;
import org.springframework.util.MultiValueMap;
import java.util.Date;
@ -11,7 +12,7 @@ import java.util.Map;
* 网关的访问日志
*/
@Data
public class GatewayLog {
public class AccessLog {
/**
* 链路追踪编号
@ -71,6 +72,10 @@ public class GatewayLog {
* 响应头
*/
private MultiValueMap<String, String> responseHeaders;
/**
* 响应结果
*/
private HttpStatus httpStatus;
/**
* 开始请求时间

View File

@ -1,8 +1,10 @@
package cn.iocoder.yudao.gateway.filter.logging;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.json.JSONUtil;
import cn.iocoder.yudao.framework.common.util.date.DateUtils;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.gateway.util.SecurityFrameworkUtils;
import cn.iocoder.yudao.gateway.util.WebFrameworkUtils;
import com.alibaba.nacos.common.utils.StringUtils;
import lombok.extern.slf4j.Slf4j;
@ -19,7 +21,6 @@ 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.ReactiveHttpOutputMessage;
import org.springframework.http.codec.HttpMessageReader;
@ -38,17 +39,64 @@ import reactor.core.publisher.Mono;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import static cn.hutool.core.date.DatePattern.NORM_DATETIME_MS_FORMAT;
/**
* 网关的访问日志过滤器
*
* 从功能上它类似 yudao-spring-boot-starter-web ApiAccessLogFilter 过滤器
*
* TODO 芋艿如果网关执行异常不会记录访问日志后续研究下 https://github.com/Silvmike/webflux-demo/blob/master/tests/src/test/java/ru/hardcoders/demo/webflux/web_handler/filters/logging
*
* @author 芋道源码
*/
@Slf4j
@Component
public class AccessLogFilter implements GlobalFilter, Ordered {
private final List<HttpMessageReader<?>> messageReaders = HandlerStrategies.withDefaults().messageReaders();
/**
* 打印日志
*
* @param gatewayLog 网关日志
*/
private void writeAccessLog(AccessLog gatewayLog) {
// 方式一打印 Logger 通过 ELK 进行收集
// log.info("[writeAccessLog][日志内容:{}]", JsonUtils.toJsonString(gatewayLog));
// 方式二调用远程服务记录到数据库中
// TODO 芋艿暂未实现
// 方式三打印到控制台方便排查错误
Map<String, Object> values = new LinkedHashMap<>(); // 手工拼接保证排序
values.put("userId", gatewayLog.getUserId());
values.put("userType", gatewayLog.getUserType());
values.put("routeId", gatewayLog.getRoute() != null ? gatewayLog.getRoute().getId() : null);
values.put("schema", gatewayLog.getSchema());
values.put("requestUrl", gatewayLog.getRequestUrl());
values.put("queryParams", gatewayLog.getQueryParams().toSingleValueMap());
values.put("requestBody", JsonUtils.isJson(gatewayLog.getRequestBody()) ? // 保证 body 的展示好看
JSONUtil.parse(gatewayLog.getRequestBody()) : gatewayLog.getRequestBody());
values.put("requestHeaders", JsonUtils.toJsonString(gatewayLog.getRequestHeaders().toSingleValueMap()));
values.put("userIp", gatewayLog.getUserIp());
values.put("responseBody", JsonUtils.isJson(gatewayLog.getResponseBody()) ? // 保证 body 的展示好看
JSONUtil.parse(gatewayLog.getResponseBody()) : gatewayLog.getResponseBody());
values.put("responseHeaders", JsonUtils.toJsonString(gatewayLog.getResponseHeaders().toSingleValueMap()));
values.put("httpStatus", gatewayLog.getHttpStatus());
values.put("startTime", DateUtil.format(gatewayLog.getStartTime(), NORM_DATETIME_MS_FORMAT));
values.put("endTime", DateUtil.format(gatewayLog.getEndTime(), NORM_DATETIME_MS_FORMAT));
values.put("duration", gatewayLog.getDuration() != null ? gatewayLog.getDuration() + " ms" : null);
log.info("[writeAccessLog][网关日志:{}]", JsonUtils.toJsonPrettyString(values));
}
@Override
public int getOrder() {
return -100;
return Ordered.HIGHEST_PRECEDENCE;
}
@Override
@ -56,7 +104,7 @@ public class AccessLogFilter implements GlobalFilter, Ordered {
// Request 中可以直接获取到的参数设置到网关日志
ServerHttpRequest request = exchange.getRequest();
// TODO traceId
GatewayLog gatewayLog = new GatewayLog();
AccessLog gatewayLog = new AccessLog();
gatewayLog.setRoute(WebFrameworkUtils.getGatewayRoute(exchange));
gatewayLog.setSchema(request.getURI().getScheme());
gatewayLog.setRequestMethod(request.getMethodValue());
@ -75,7 +123,7 @@ public class AccessLogFilter implements GlobalFilter, Ordered {
return filterWithoutRequestBody(exchange, chain, gatewayLog);
}
private Mono<Void> filterWithoutRequestBody(ServerWebExchange exchange, GatewayFilterChain chain, GatewayLog accessLog) {
private Mono<Void> filterWithoutRequestBody(ServerWebExchange exchange, GatewayFilterChain chain, AccessLog accessLog) {
// 包装 Response用于记录 Response Body
ServerHttpResponseDecorator decoratedResponse = recordResponseLog(exchange, accessLog);
return chain.filter(exchange.mutate().response(decoratedResponse).build())
@ -87,7 +135,7 @@ public class AccessLogFilter implements GlobalFilter, Ordered {
*
* 差别主要在于使用 modifiedBody 来读取 Request Body 数据
*/
private Mono<Void> filterWithRequestBody(ServerWebExchange exchange, GatewayFilterChain chain, GatewayLog gatewayLog) {
private Mono<Void> filterWithRequestBody(ServerWebExchange exchange, GatewayFilterChain chain, AccessLog gatewayLog) {
// 设置 Request Body 读取时设置到网关日志
ServerRequest serverRequest = ServerRequest.create(exchange, messageReaders);
Mono<String> modifiedBody = serverRequest.bodyToMono(String.class).flatMap(body -> {
@ -113,23 +161,15 @@ public class AccessLogFilter implements GlobalFilter, Ordered {
// 记录普通的
return chain.filter(exchange.mutate().request(decoratedRequest).response(decoratedResponse).build())
.then(Mono.fromRunnable(() -> writeAccessLog(gatewayLog))); // 打印日志
}));
}
/**
* 打印日志
*
* @param gatewayLog 网关日志
*/
private void writeAccessLog(GatewayLog gatewayLog) {
log.info("[writeAccessLog][日志内容:{}]", JsonUtils.toJsonString(gatewayLog));
}));
}
/**
* 记录响应日志
* 通过 DataBufferFactory 解决响应体分段传输问题
*/
private ServerHttpResponseDecorator recordResponseLog(ServerWebExchange exchange, GatewayLog gatewayLog) {
private ServerHttpResponseDecorator recordResponseLog(ServerWebExchange exchange, AccessLog gatewayLog) {
ServerHttpResponse response = exchange.getResponse();
return new ServerHttpResponseDecorator(response) {
@ -141,16 +181,15 @@ public class AccessLogFilter implements GlobalFilter, Ordered {
gatewayLog.setEndTime(new Date());
gatewayLog.setDuration((int) DateUtils.diff(gatewayLog.getEndTime(), gatewayLog.getStartTime()));
// 设置其它字段
gatewayLog.setUserId(SecurityFrameworkUtils.getLoginUserId(exchange));
gatewayLog.setUserType(SecurityFrameworkUtils.getLoginUserType(exchange));
gatewayLog.setResponseHeaders(response.getHeaders());
gatewayLog.setHttpStatus(response.getStatusCode());
// 获取响应类型如果是 json 就打印
String originalResponseContentType = exchange.getAttribute(ServerWebExchangeUtils.ORIGINAL_RESPONSE_CONTENT_TYPE_ATTR);
if (ObjectUtil.equal(getStatusCode(), HttpStatus.OK)
&& StringUtils.isNotBlank(originalResponseContentType)
if (StringUtils.isNotBlank(originalResponseContentType)
&& originalResponseContentType.contains("application/json")) {
Flux<? extends DataBuffer> fluxBody = Flux.from(body);
return super.writeWith(fluxBody.buffer().map(dataBuffers -> {
// 设置 response body 到网关日志

View File

@ -68,6 +68,8 @@ public class TokenAuthenticationFilter implements GlobalFilter, Ordered {
return exchange;
}
// 设置登录用户
SecurityFrameworkUtils.setLoginUser(exchange, result.getData());
// 将访问令牌封装成 LoginUser并设置到 login-user 的请求头使用 json 存储值
return exchange.mutate().request(builder -> SecurityFrameworkUtils.setLoginUserHeader(builder, result.getData())).build();
}

View File

@ -1,111 +0,0 @@
package cn.iocoder.yudao.gateway.filter.web;
import cn.hutool.core.util.ReflectUtil;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.cloud.gateway.filter.factory.rewrite.CachedBodyOutputMessage;
import org.springframework.cloud.gateway.support.BodyInserterContext;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.http.HttpHeaders;
import org.springframework.http.codec.HttpMessageReader;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpRequestDecorator;
import org.springframework.stereotype.Component;
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;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.List;
import java.util.function.Function;
/**
* 缓存 Request Body 的过滤器
*
* 小知识Request Body 都是无法重复读取的所以需要实现一个缓存的
* 从功能上它类似 yudao-spring-boot-starter-web CacheRequestBodyFilter 过滤器
*
* 实现基本是拷贝 {@link org.springframework.cloud.gateway.filter.factory.rewrite.ModifyRequestBodyGatewayFilterFactory}
*
* @author 芋道源码
*/
@Component
public class CacheRequestBodyFilter implements GlobalFilter, Ordered {
private final List<HttpMessageReader<?>> messageReaders = HandlerStrategies.withDefaults().messageReaders();
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerRequest serverRequest = ServerRequest.create(exchange, messageReaders);
// TODO: flux or mono
Mono<String> modifiedBody = serverRequest.bodyToMono(String.class);
// .flatMap()
// .switchIfEmpty(Mono.just(""));
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())
// .log("modify_request", Level.INFO)
.then(Mono.defer(() -> {
ServerHttpRequest decorator = decorate(exchange, headers, outputMessage);
return chain.filter(exchange.mutate().request(decorator).build());
})).onErrorResume((Function<Throwable, Mono<Void>>) throwable -> release(exchange,
outputMessage, throwable));
}
@Override
public int getOrder() {
// 必须小于等于 -2否则无法获取响应结果. 因为 WebClientWriteResponseFilter NettyWriteResponseFilter -1优先级要高于它们
return -2;
}
protected Mono<Void> release(ServerWebExchange exchange, CachedBodyOutputMessage outputMessage,
Throwable throwable) {
// add by 芋道源码由于 CachedBodyOutputMessage isCached public 方法所以只能反射调用
if ((boolean) ReflectUtil.getFieldValue(outputMessage, "cached")) {
return outputMessage.getBody().map(DataBufferUtils::release).then(Mono.error(throwable));
}
return Mono.error(throwable);
}
ServerHttpRequestDecorator decorate(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(headers);
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");
}
return httpHeaders;
}
@Override
public Flux<DataBuffer> getBody() {
return outputMessage.getBody();
}
};
}
}

View File

@ -24,6 +24,9 @@ public class SecurityFrameworkUtils {
private static final String LOGIN_USER_HEADER = "login-user";
private static final String LOGIN_USER_ID_ATTR = "login-user-id";
private static final String LOGIN_USER_TYPE_ATTR = "login-user-type";
private SecurityFrameworkUtils() {}
/**
@ -44,6 +47,37 @@ public class SecurityFrameworkUtils {
return authorization.substring(index + 7).trim();
}
/**
* 设置登录用户
*
* @param exchange 请求
* @param token 访问令牌
*/
public static void setLoginUser(ServerWebExchange exchange, OAuth2AccessTokenCheckRespDTO token) {
exchange.getAttributes().put(LOGIN_USER_ID_ATTR, token.getUserId());
exchange.getAttributes().put(LOGIN_USER_TYPE_ATTR, token.getUserType());
}
/**
* 获得登录用户的编号
*
* @param exchange 请求
* @return 用户编号
*/
public static Long getLoginUserId(ServerWebExchange exchange) {
return MapUtil.getLong(exchange.getAttributes(), LOGIN_USER_ID_ATTR);
}
/**
* 获得登录用户的类型
*
* @param exchange 请求
* @return 用户类型
*/
public static Integer getLoginUserType(ServerWebExchange exchange) {
return MapUtil.getInt(exchange.getAttributes(), LOGIN_USER_TYPE_ATTR);
}
/**
* 将访问令牌封装成 LoginUser并设置到 login-user 的请求头使用 json 存储值
*