gateway:完整实现 AccessLogFilter 访问日志
parent
b2fc171609
commit
213ec8bd72
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
||||
/**
|
||||
* 开始请求时间
|
|
@ -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 到网关日志
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
|
@ -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 存储值
|
||||
*
|
||||
|
|
Loading…
Reference in New Issue