filter(ServerWebExchange exchange, GatewayFilterChain chain) {
+ String language = exchange.getRequest().getHeaders().getFirst("content-language");
+ Locale locale = Locale.getDefault();
+ if (language != null && language.length() > 0) {
+ String[] split = language.split("_");
+ locale = new Locale(split[0], split[1]);
+ }
+ LocaleContextHolder.setLocaleContext(new SimpleLocaleContext(locale), true);
+ return chain.filter(exchange);
+ }
+
+ @Override
+ public int getOrder() {
+ return Ordered.HIGHEST_PRECEDENCE;
+ }
+
+}
diff --git a/ruoyi-site/ruoyi-gateway/src/main/java/org/dromara/gateway/filter/GlobalLogFilter.java b/ruoyi-site/ruoyi-gateway/src/main/java/org/dromara/gateway/filter/GlobalLogFilter.java
new file mode 100644
index 000000000..49646793e
--- /dev/null
+++ b/ruoyi-site/ruoyi-gateway/src/main/java/org/dromara/gateway/filter/GlobalLogFilter.java
@@ -0,0 +1,84 @@
+package org.dromara.gateway.filter;
+
+import cn.hutool.core.map.MapUtil;
+import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.json.JSONUtil;
+import lombok.extern.slf4j.Slf4j;
+import org.dromara.gateway.config.properties.ApiDecryptProperties;
+import org.dromara.gateway.config.properties.CustomGatewayProperties;
+import org.dromara.gateway.utils.WebFluxUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.cloud.gateway.filter.GatewayFilterChain;
+import org.springframework.cloud.gateway.filter.GlobalFilter;
+import org.springframework.core.Ordered;
+import org.springframework.http.server.reactive.ServerHttpRequest;
+import org.springframework.stereotype.Component;
+import org.springframework.util.MultiValueMap;
+import org.springframework.web.server.ServerWebExchange;
+import reactor.core.publisher.Mono;
+
+/**
+ * 全局日志过滤器
+ *
+ * 用于打印请求执行参数与响应时间等等
+ *
+ * @author Lion Li
+ */
+@Slf4j
+@Component
+public class GlobalLogFilter implements GlobalFilter, Ordered {
+
+ @Autowired
+ private CustomGatewayProperties customGatewayProperties;
+ @Autowired
+ private ApiDecryptProperties apiDecryptProperties;
+
+ private static final String START_TIME = "startTime";
+
+ @Override
+ public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) {
+ if (!customGatewayProperties.getRequestLog()) {
+ return chain.filter(exchange);
+ }
+ ServerHttpRequest request = exchange.getRequest();
+ String path = WebFluxUtils.getOriginalRequestUrl(exchange);
+ String url = request.getMethod().name() + " " + path;
+
+ // 打印请求参数
+ if (WebFluxUtils.isJsonRequest(exchange)) {
+ if (apiDecryptProperties.getEnabled()
+ && ObjectUtil.isNotNull(request.getHeaders().getFirst(apiDecryptProperties.getHeaderFlag()))) {
+ log.info("[PLUS]开始请求 => URL[{}],参数类型[encrypt]", url);
+ } else {
+ String jsonParam = WebFluxUtils.resolveBodyFromCacheRequest(exchange);
+ log.info("[PLUS]开始请求 => URL[{}],参数类型[json],参数:[{}]", url, jsonParam);
+ }
+ } else {
+ MultiValueMap parameterMap = request.getQueryParams();
+ if (MapUtil.isNotEmpty(parameterMap)) {
+ String parameters = JSONUtil.toJsonStr(parameterMap);
+ log.info("[PLUS]开始请求 => URL[{}],参数类型[param],参数:[{}]", url, parameters);
+ } else {
+ log.info("[PLUS]开始请求 => URL[{}],无参数", url);
+ }
+ }
+
+ exchange.getAttributes().put(START_TIME, System.currentTimeMillis());
+ return chain.filter(exchange).then(Mono.fromRunnable(() -> {
+ Long startTime = exchange.getAttribute(START_TIME);
+ if (startTime != null) {
+ long executeTime = (System.currentTimeMillis() - startTime);
+ log.info("[PLUS]结束请求 => URL[{}],耗时:[{}]毫秒", url, executeTime);
+ }
+ }));
+ }
+
+ @Override
+ public int getOrder() {
+ // 日志处理器在负载均衡器之后执行 负载均衡器会导致线程切换 无法获取上下文内容
+ // 如需在日志内操作线程上下文 例如获取登录用户数据等 可以打开下方注释代码
+ // return ReactiveLoadBalancerClientFilter.LOAD_BALANCER_CLIENT_FILTER_ORDER - 1;
+ return Ordered.LOWEST_PRECEDENCE;
+ }
+
+}
diff --git a/ruoyi-site/ruoyi-gateway/src/main/java/org/dromara/gateway/filter/SseException.java b/ruoyi-site/ruoyi-gateway/src/main/java/org/dromara/gateway/filter/SseException.java
new file mode 100644
index 000000000..6c460aaa6
--- /dev/null
+++ b/ruoyi-site/ruoyi-gateway/src/main/java/org/dromara/gateway/filter/SseException.java
@@ -0,0 +1,62 @@
+package org.dromara.gateway.filter;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+
+import java.io.Serial;
+
+/**
+ * sse 特制异常
+ *
+ * @author LionLi
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@NoArgsConstructor
+@AllArgsConstructor
+public final class SseException extends RuntimeException {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 错误码
+ */
+ private Integer code;
+
+ /**
+ * 错误提示
+ */
+ private String message;
+
+ /**
+ * 错误明细,内部调试错误
+ */
+ private String detailMessage;
+
+ public SseException(String message) {
+ this.message = message;
+ }
+
+ public SseException(String message, Integer code) {
+ this.message = message;
+ this.code = code;
+ }
+
+ @Override
+ public String getMessage() {
+ return message;
+ }
+
+ public SseException setMessage(String message) {
+ this.message = message;
+ return this;
+ }
+
+ public SseException setDetailMessage(String detailMessage) {
+ this.detailMessage = detailMessage;
+ return this;
+ }
+}
diff --git a/ruoyi-site/ruoyi-gateway/src/main/java/org/dromara/gateway/handler/GatewayExceptionHandler.java b/ruoyi-site/ruoyi-gateway/src/main/java/org/dromara/gateway/handler/GatewayExceptionHandler.java
new file mode 100644
index 000000000..375542811
--- /dev/null
+++ b/ruoyi-site/ruoyi-gateway/src/main/java/org/dromara/gateway/handler/GatewayExceptionHandler.java
@@ -0,0 +1,46 @@
+package org.dromara.gateway.handler;
+
+import lombok.extern.slf4j.Slf4j;
+import org.dromara.gateway.utils.WebFluxUtils;
+import org.springframework.boot.web.reactive.error.ErrorWebExceptionHandler;
+import org.springframework.cloud.gateway.support.NotFoundException;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.core.annotation.Order;
+import org.springframework.http.server.reactive.ServerHttpResponse;
+import org.springframework.web.server.ResponseStatusException;
+import org.springframework.web.server.ServerWebExchange;
+import reactor.core.publisher.Mono;
+
+/**
+ * 网关统一异常处理
+ *
+ * @author ruoyi
+ */
+@Slf4j
+@Order(-1)
+@Configuration
+public class GatewayExceptionHandler implements ErrorWebExceptionHandler {
+
+ @Override
+ public Mono handle(ServerWebExchange exchange, Throwable ex) {
+ ServerHttpResponse response = exchange.getResponse();
+
+ if (exchange.getResponse().isCommitted()) {
+ return Mono.error(ex);
+ }
+
+ String msg;
+
+ if (ex instanceof NotFoundException) {
+ msg = "服务未找到";
+ } else if (ex instanceof ResponseStatusException responseStatusException) {
+ msg = responseStatusException.getMessage();
+ } else {
+ msg = "内部服务器错误";
+ }
+
+ log.error("[网关异常处理]请求路径:{},异常信息:{}", exchange.getRequest().getPath(), ex.getMessage());
+
+ return WebFluxUtils.webFluxResponseWriter(response, msg);
+ }
+}
diff --git a/ruoyi-site/ruoyi-gateway/src/main/java/org/dromara/gateway/utils/WebFluxUtils.java b/ruoyi-site/ruoyi-gateway/src/main/java/org/dromara/gateway/utils/WebFluxUtils.java
new file mode 100644
index 000000000..8af32bd80
--- /dev/null
+++ b/ruoyi-site/ruoyi-gateway/src/main/java/org/dromara/gateway/utils/WebFluxUtils.java
@@ -0,0 +1,149 @@
+package org.dromara.gateway.utils;
+
+import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.json.JSONUtil;
+import org.dromara.common.core.domain.R;
+import org.dromara.common.core.utils.StringUtils;
+import org.dromara.gateway.filter.GlobalCacheRequestFilter;
+import org.springframework.cloud.gateway.support.ServerWebExchangeUtils;
+import org.springframework.core.io.buffer.DataBuffer;
+import org.springframework.core.io.buffer.DataBufferUtils;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.http.server.reactive.ServerHttpRequest;
+import org.springframework.http.server.reactive.ServerHttpResponse;
+import org.springframework.web.server.ServerWebExchange;
+import org.springframework.web.util.UriComponentsBuilder;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import java.net.URI;
+import java.nio.CharBuffer;
+import java.nio.charset.StandardCharsets;
+import java.util.LinkedHashSet;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Function;
+
+import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_ORIGINAL_REQUEST_URL_ATTR;
+
+/**
+ * WebFlux 工具类
+ *
+ * @author Lion Li
+ */
+public class WebFluxUtils {
+
+ /**
+ * 获取原请求路径
+ */
+ public static String getOriginalRequestUrl(ServerWebExchange exchange) {
+ ServerHttpRequest request = exchange.getRequest();
+ LinkedHashSet uris = exchange.getAttributeOrDefault(GATEWAY_ORIGINAL_REQUEST_URL_ATTR, new LinkedHashSet<>());
+ URI requestUri = uris.stream().findFirst().orElse(request.getURI());
+ return UriComponentsBuilder.fromPath(requestUri.getRawPath()).build().toUriString();
+ }
+
+ /**
+ * 是否是Json请求
+ *
+ * @param exchange HTTP请求
+ */
+ public static boolean isJsonRequest(ServerWebExchange exchange) {
+ String header = exchange.getRequest().getHeaders().getFirst(HttpHeaders.CONTENT_TYPE);
+ return StringUtils.startsWithIgnoreCase(header, MediaType.APPLICATION_JSON_VALUE);
+ }
+
+ /**
+ * 读取request内的body
+ *
+ * 注意一个request只能读取一次 读取之后需要重新包装
+ */
+ public static String resolveBodyFromRequest(ServerHttpRequest serverHttpRequest) {
+ // 获取请求体
+ Flux body = serverHttpRequest.getBody();
+ AtomicReference bodyRef = new AtomicReference<>();
+ body.subscribe(buffer -> {
+ try (DataBuffer.ByteBufferIterator iterator = buffer.readableByteBuffers()) {
+ CharBuffer charBuffer = StandardCharsets.UTF_8.decode(iterator.next());
+ DataBufferUtils.release(buffer);
+ bodyRef.set(charBuffer.toString());
+ }
+ });
+ return bodyRef.get();
+ }
+
+ /**
+ * 从缓存中读取request内的body
+ *
+ * 注意要求经过 {@link ServerWebExchangeUtils#cacheRequestBody(ServerWebExchange, Function)} 此方法创建缓存
+ * 框架内已经使用 {@link GlobalCacheRequestFilter} 全局创建了body缓存
+ *
+ * @return body
+ */
+ public static String resolveBodyFromCacheRequest(ServerWebExchange exchange) {
+ Object obj = exchange.getAttributes().get(ServerWebExchangeUtils.CACHED_REQUEST_BODY_ATTR);
+ if (ObjectUtil.isNull(obj)) {
+ return null;
+ }
+ DataBuffer buffer = (DataBuffer) obj;
+ try (DataBuffer.ByteBufferIterator iterator = buffer.readableByteBuffers()) {
+ CharBuffer charBuffer = StandardCharsets.UTF_8.decode(iterator.next());
+ return charBuffer.toString();
+ }
+ }
+
+ /**
+ * 设置webflux模型响应
+ *
+ * @param response ServerHttpResponse
+ * @param value 响应内容
+ * @return Mono
+ */
+ public static Mono webFluxResponseWriter(ServerHttpResponse response, Object value) {
+ return webFluxResponseWriter(response, HttpStatus.OK, value, R.FAIL);
+ }
+
+ /**
+ * 设置webflux模型响应
+ *
+ * @param response ServerHttpResponse
+ * @param code 响应状态码
+ * @param value 响应内容
+ * @return Mono
+ */
+ public static Mono webFluxResponseWriter(ServerHttpResponse response, Object value, int code) {
+ return webFluxResponseWriter(response, HttpStatus.OK, value, code);
+ }
+
+ /**
+ * 设置webflux模型响应
+ *
+ * @param response ServerHttpResponse
+ * @param status http状态码
+ * @param code 响应状态码
+ * @param value 响应内容
+ * @return Mono
+ */
+ public static Mono webFluxResponseWriter(ServerHttpResponse response, HttpStatus status, Object value, int code) {
+ return webFluxResponseWriter(response, MediaType.APPLICATION_JSON_VALUE, status, value, code);
+ }
+
+ /**
+ * 设置webflux模型响应
+ *
+ * @param response ServerHttpResponse
+ * @param contentType content-type
+ * @param status http状态码
+ * @param code 响应状态码
+ * @param value 响应内容
+ * @return Mono
+ */
+ public static Mono webFluxResponseWriter(ServerHttpResponse response, String contentType, HttpStatus status, Object value, int code) {
+ response.setStatusCode(status);
+ response.getHeaders().add(HttpHeaders.CONTENT_TYPE, contentType);
+ R> result = R.fail(code, value.toString());
+ DataBuffer dataBuffer = response.bufferFactory().wrap(JSONUtil.toJsonStr(result).getBytes());
+ return response.writeWith(Mono.just(dataBuffer));
+ }
+}
diff --git a/ruoyi-site/ruoyi-gateway/src/main/resources/application.yml b/ruoyi-site/ruoyi-gateway/src/main/resources/application.yml
new file mode 100644
index 000000000..c64e50ec3
--- /dev/null
+++ b/ruoyi-site/ruoyi-gateway/src/main/resources/application.yml
@@ -0,0 +1,33 @@
+# Tomcat
+server:
+ port: 8080
+ servlet:
+ context-path: /
+
+# Spring
+spring:
+ application:
+ # 应用名称
+ name: ruoyi-gateway
+ profiles:
+ # 环境配置
+ active: @profiles.active@
+
+--- # nacos 配置
+spring:
+ cloud:
+ nacos:
+ # nacos 服务地址
+ server-addr: http://localhost:8848
+ discovery:
+ # 注册组
+ group: dev
+ namespace: c5c00044-93e6-4e66-8060-3f39aa7ab82b
+ config:
+ # 配置组
+ group: dev
+ namespace: c5c00044-93e6-4e66-8060-3f39aa7ab82b
+ config:
+ import:
+ - optional:nacos:application-common.yml
+ - optional:nacos:${spring.application.name}.yml
diff --git a/ruoyi-site/ruoyi-gateway/src/main/resources/banner.txt b/ruoyi-site/ruoyi-gateway/src/main/resources/banner.txt
new file mode 100644
index 000000000..ceced29f4
--- /dev/null
+++ b/ruoyi-site/ruoyi-gateway/src/main/resources/banner.txt
@@ -0,0 +1,10 @@
+Spring Boot Version: ${spring-boot.version}
+Spring Application Name: ${spring.application.name}
+ _ _
+ (_) | |
+ _ __ _ _ ___ _ _ _ ______ __ _ __ _ | |_ ___ __ __ __ _ _ _
+| '__|| | | | / _ \ | | | || ||______| / _` | / _` || __| / _ \\ \ /\ / / / _` || | | |
+| | | |_| || (_) || |_| || | | (_| || (_| || |_ | __/ \ V V / | (_| || |_| |
+|_| \__,_| \___/ \__, ||_| \__, | \__,_| \__| \___| \_/\_/ \__,_| \__, |
+ __/ | __/ | __/ |
+ |___/ |___/ |___/
\ No newline at end of file
diff --git a/ruoyi-site/ruoyi-gateway/src/main/resources/logback-plus.xml b/ruoyi-site/ruoyi-gateway/src/main/resources/logback-plus.xml
new file mode 100644
index 000000000..4d66014c6
--- /dev/null
+++ b/ruoyi-site/ruoyi-gateway/src/main/resources/logback-plus.xml
@@ -0,0 +1,114 @@
+
+
+
+
+
+
+
+
+
+
+
+ ${console.log.pattern}
+ utf-8
+
+
+
+
+
+ ${log.path}/console.log
+
+
+ ${log.path}/console.%d{yyyy-MM-dd}.log
+
+ 1
+
+
+ ${log.pattern}
+ utf-8
+
+
+
+ INFO
+
+
+
+
+
+ ${log.path}/info.log
+
+
+
+ ${log.path}/info.%d{yyyy-MM-dd}.log
+
+ 60
+
+
+ ${log.pattern}
+
+
+
+ INFO
+
+ ACCEPT
+
+ DENY
+
+
+
+
+ ${log.path}/error.log
+
+
+
+ ${log.path}/error.%d{yyyy-MM-dd}.log
+
+ 60
+
+
+ ${log.pattern}
+
+
+
+ ERROR
+
+ ACCEPT
+
+ DENY
+
+
+
+
+
+
+ 0
+
+ 512
+
+
+
+
+
+
+
+ 0
+
+ 512
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+