diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/json/JsonUtils.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/json/JsonUtils.java index 2a46edf6a..f41234736 100644 --- a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/json/JsonUtils.java +++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/json/JsonUtils.java @@ -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); diff --git a/yudao-gateway/src/main/java/cn/iocoder/yudao/gateway/filter/logging/GatewayLog.java b/yudao-gateway/src/main/java/cn/iocoder/yudao/gateway/filter/logging/AccessLog.java similarity index 91% rename from yudao-gateway/src/main/java/cn/iocoder/yudao/gateway/filter/logging/GatewayLog.java rename to yudao-gateway/src/main/java/cn/iocoder/yudao/gateway/filter/logging/AccessLog.java index 090d6c2aa..dd2bc24b0 100644 --- a/yudao-gateway/src/main/java/cn/iocoder/yudao/gateway/filter/logging/GatewayLog.java +++ b/yudao-gateway/src/main/java/cn/iocoder/yudao/gateway/filter/logging/AccessLog.java @@ -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 responseHeaders; + /** + * 响应结果 + */ + private HttpStatus httpStatus; /** * 开始请求时间 diff --git a/yudao-gateway/src/main/java/cn/iocoder/yudao/gateway/filter/logging/AccessLogFilter.java b/yudao-gateway/src/main/java/cn/iocoder/yudao/gateway/filter/logging/AccessLogFilter.java index a9f7f7e04..d55d2149a 100644 --- a/yudao-gateway/src/main/java/cn/iocoder/yudao/gateway/filter/logging/AccessLogFilter.java +++ b/yudao-gateway/src/main/java/cn/iocoder/yudao/gateway/filter/logging/AccessLogFilter.java @@ -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> messageReaders = HandlerStrategies.withDefaults().messageReaders(); + /** + * 打印日志 + * + * @param gatewayLog 网关日志 + */ + private void writeAccessLog(AccessLog gatewayLog) { + // 方式一:打印 Logger 后,通过 ELK 进行收集 + // log.info("[writeAccessLog][日志内容:{}]", JsonUtils.toJsonString(gatewayLog)); + + // 方式二:调用远程服务,记录到数据库中 + // TODO 芋艿:暂未实现 + + // 方式三:打印到控制台,方便排查错误 + Map 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 filterWithoutRequestBody(ServerWebExchange exchange, GatewayFilterChain chain, GatewayLog accessLog) { + private Mono 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 filterWithRequestBody(ServerWebExchange exchange, GatewayFilterChain chain, GatewayLog gatewayLog) { + private Mono filterWithRequestBody(ServerWebExchange exchange, GatewayFilterChain chain, AccessLog gatewayLog) { // 设置 Request Body 读取时,设置到网关日志 ServerRequest serverRequest = ServerRequest.create(exchange, messageReaders); Mono 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 fluxBody = Flux.from(body); return super.writeWith(fluxBody.buffer().map(dataBuffers -> { // 设置 response body 到网关日志 diff --git a/yudao-gateway/src/main/java/cn/iocoder/yudao/gateway/filter/security/TokenAuthenticationFilter.java b/yudao-gateway/src/main/java/cn/iocoder/yudao/gateway/filter/security/TokenAuthenticationFilter.java index 0785d26f1..fd072e48e 100644 --- a/yudao-gateway/src/main/java/cn/iocoder/yudao/gateway/filter/security/TokenAuthenticationFilter.java +++ b/yudao-gateway/src/main/java/cn/iocoder/yudao/gateway/filter/security/TokenAuthenticationFilter.java @@ -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(); } diff --git a/yudao-gateway/src/main/java/cn/iocoder/yudao/gateway/filter/web/CacheRequestBodyFilter.java b/yudao-gateway/src/main/java/cn/iocoder/yudao/gateway/filter/web/CacheRequestBodyFilter.java deleted file mode 100644 index 2b617818b..000000000 --- a/yudao-gateway/src/main/java/cn/iocoder/yudao/gateway/filter/web/CacheRequestBodyFilter.java +++ /dev/null @@ -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> messageReaders = HandlerStrategies.withDefaults().messageReaders(); - - @Override - public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { - ServerRequest serverRequest = ServerRequest.create(exchange, messageReaders); - - // TODO: flux or mono - Mono 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 -> release(exchange, - outputMessage, throwable)); - } - - @Override - public int getOrder() { - // 必须小于等于 -2,否则无法获取响应结果. 因为 WebClientWriteResponseFilter 和 NettyWriteResponseFilter 是 -1,优先级要高于它们 - return -2; - } - - protected Mono 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 getBody() { - return outputMessage.getBody(); - } - }; - } - -} diff --git a/yudao-gateway/src/main/java/cn/iocoder/yudao/gateway/util/SecurityFrameworkUtils.java b/yudao-gateway/src/main/java/cn/iocoder/yudao/gateway/util/SecurityFrameworkUtils.java index ea107fd32..a8edad267 100644 --- a/yudao-gateway/src/main/java/cn/iocoder/yudao/gateway/util/SecurityFrameworkUtils.java +++ b/yudao-gateway/src/main/java/cn/iocoder/yudao/gateway/util/SecurityFrameworkUtils.java @@ -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 存储值 *