update 删除 gateway webflux 版本 统一使用 MVC版本

This commit is contained in:
疯狂的狮子Li
2026-03-23 13:49:12 +08:00
parent eba382558a
commit 3870788c77
38 changed files with 315 additions and 1665 deletions
-1
View File
@@ -403,7 +403,6 @@
<modules>
<module>ruoyi-auth</module>
<module>ruoyi-gateway</module>
<module>ruoyi-gateway-mvc</module>
<module>ruoyi-visual</module>
<module>ruoyi-modules</module>
<module>ruoyi-api</module>
-26
View File
@@ -1,26 +0,0 @@
# 贝尔实验室 Spring 官方推荐镜像 JDK下载地址 https://bell-sw.com/pages/downloads/
FROM bellsoft/liberica-openjdk-rocky:17.0.16-cds
#FROM bellsoft/liberica-openjdk-rocky:21.0.8-cds
#FROM findepi/graalvm:java17-native
LABEL maintainer="Lion Li"
RUN mkdir -p /ruoyi/gateway/logs \
/ruoyi/gateway/temp \
/ruoyi/skywalking/agent
WORKDIR /ruoyi/gateway
ENV SERVER_PORT=8080 LANG=C.UTF-8 LC_ALL=C.UTF-8 JAVA_OPTS=""
EXPOSE ${SERVER_PORT}
ADD ./target/ruoyi-gateway-mvc.jar ./app.jar
SHELL ["/bin/bash", "-c"]
ENTRYPOINT java -Djava.security.egd=file:/dev/./urandom -Dserver.port=${SERVER_PORT} \
#-Dskywalking.agent.service_name=ruoyi-gateway \
#-javaagent:/ruoyi/skywalking/agent/skywalking-agent.jar \
-XX:+HeapDumpOnOutOfMemoryError -XX:+UseZGC ${JAVA_OPTS} \
-jar app.jar
-7
View File
@@ -1,7 +0,0 @@
此服务为 spring-cloud-gateway 的 Servlet MVC 版本
建议在JDK21启动虚拟线程的基础上使用 在JDK17下使用性能比原版差很多
使用教程 将 ruoyi-gateway-mvc.yml 配置文件上传到 nacos 正常启动服务 与原先版本一样使用
注意 原版与此版只能选择一个使用 不允许两个同时使用
-136
View File
@@ -1,136 +0,0 @@
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-cloud-plus</artifactId>
<version>${revision}</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>ruoyi-gateway-mvc</artifactId>
<description>
ruoyi-gateway-mvc网关模块
</description>
<dependencies>
<!-- SpringCloud Gateway -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway-server-webmvc</artifactId>
</dependency>
<!-- SpringBoot Web容器 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<artifactId>spring-boot-starter-tomcat</artifactId>
<groupId>org.springframework.boot</groupId>
</exclusion>
</exclusions>
</dependency>
<!-- Boot 4 下使用 jetty 替代 undertow -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jetty</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-nacos</artifactId>
</dependency>
<!-- SpringBoot Actuator -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-http</artifactId>
</dependency>
<!-- Sa-Token 权限认证 -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot4-starter</artifactId>
</dependency>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-satoken</artifactId>
</dependency>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-json</artifactId>
</dependency>
<!-- RuoYi Common Redis-->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-redis</artifactId>
</dependency>
<!-- 自定义负载均衡(多团队开发使用) -->
<!-- <dependency>-->
<!-- <groupId>org.dromara</groupId>-->
<!-- <artifactId>ruoyi-common-loadbalancer</artifactId>-->
<!-- </dependency>-->
<!-- ELK 日志收集 -->
<!-- <dependency>-->
<!-- <groupId>org.dromara</groupId>-->
<!-- <artifactId>ruoyi-common-logstash</artifactId>-->
<!-- </dependency>-->
<!-- skywalking 日志收集 -->
<!-- <dependency>-->
<!-- <groupId>org.dromara</groupId>-->
<!-- <artifactId>ruoyi-common-skylog</artifactId>-->
<!-- </dependency>-->
<!-- prometheus 监控 -->
<!-- <dependency>-->
<!-- <groupId>org.dromara</groupId>-->
<!-- <artifactId>ruoyi-common-prometheus</artifactId>-->
<!-- </dependency>-->
</dependencies>
<build>
<finalName>${project.artifactId}</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring-boot.version}</version>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
@@ -1,22 +0,0 @@
package org.dromara.gateway;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.metrics.buffering.BufferingApplicationStartup;
/**
* 网关启动程序
*
* @author Lion Li
*/
@SpringBootApplication
public class RuoYiGatewayMvcApplication {
public static void main(String[] args) {
SpringApplication application = new SpringApplication(RuoYiGatewayMvcApplication.class);
application.setApplicationStartup(new BufferingApplicationStartup(2048));
application.run(args);
System.out.println("(♥◠‿◠)ノ゙ MVC网关启动成功 ლ(´ڡ`ლ)゙ ");
}
}
@@ -1,71 +0,0 @@
package org.dromara.gateway.config;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import org.springframework.web.servlet.LocaleResolver;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.util.List;
import java.util.Locale;
import org.springframework.core.Ordered;
/**
* 网关配置
*
* @author Lion Li
*/
@Configuration
public class GatewayConfig {
@Bean
public LocaleResolver localeResolver() {
return new LocaleResolver() {
@Override
public Locale resolveLocale(HttpServletRequest request) {
String language = request.getHeader("content-language");
Locale locale = Locale.getDefault();
if (language == null || language.isBlank()) {
return locale;
}
String[] split = language.split("_");
if (split.length == 2) {
return new Locale(split[0], split[1]);
}
return Locale.forLanguageTag(language.replace('_', '-'));
}
@Override
public void setLocale(HttpServletRequest request, HttpServletResponse response, Locale locale) {
// noop
}
};
}
@Bean
public FilterRegistrationBean<CorsFilter> corsFilterRegistrationBean() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOriginPatterns(List.of("*"));
config.setAllowCredentials(true);
config.setAllowedHeaders(List.of(
"X-Requested-With", "Content-Language", "Content-Type",
"Authorization", "clientid", "credential", "X-XSRF-TOKEN",
"isToken", "token", "Admin-Token", "App-Token", "Encrypt-Key", "isEncrypt"
));
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS", "HEAD"));
config.setExposedHeaders(List.of("*"));
config.setMaxAge(18000L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
FilterRegistrationBean<CorsFilter> registration = new FilterRegistrationBean<>(new CorsFilter(source));
registration.setOrder(Ordered.HIGHEST_PRECEDENCE);
return registration;
}
}
@@ -1,43 +0,0 @@
//package org.dromara.gateway.config;
//
//import io.undertow.server.DefaultByteBufferPool;
//import io.undertow.server.handlers.DisallowedMethodsHandler;
//import io.undertow.util.HttpString;
//import io.undertow.websockets.jsr.WebSocketDeploymentInfo;
//import org.dromara.common.core.utils.SpringUtils;
//import org.springframework.boot.web.embedded.undertow.UndertowServletWebServerFactory;
//import org.springframework.boot.web.server.WebServerFactoryCustomizer;
//import org.springframework.context.annotation.Configuration;
//import org.springframework.core.task.VirtualThreadTaskExecutor;
//
///**
// * Boot 4 迁移后默认改用 Jetty,这里暂时保留 Undertow 配置实现以便后续回切时参考。
// */
//@Configuration
//public class UndertowConfig implements WebServerFactoryCustomizer<UndertowServletWebServerFactory> {
//
// @Override
// public void customize(UndertowServletWebServerFactory factory) {
// factory.addDeploymentInfoCustomizers(deploymentInfo -> {
// WebSocketDeploymentInfo webSocketDeploymentInfo = new WebSocketDeploymentInfo();
// webSocketDeploymentInfo.setBuffers(new DefaultByteBufferPool(true, 1024));
// deploymentInfo.addServletContextAttribute("io.undertow.websockets.jsr.WebSocketDeploymentInfo", webSocketDeploymentInfo);
//
// if (SpringUtils.isVirtual()) {
// VirtualThreadTaskExecutor executor = new VirtualThreadTaskExecutor("undertow-");
// deploymentInfo.setExecutor(executor);
// deploymentInfo.setAsyncExecutor(executor);
// }
//
// deploymentInfo.addInitialHandlerChainWrapper(handler -> {
// HttpString[] disallowedHttpMethods = {
// HttpString.tryFromString("CONNECT"),
// HttpString.tryFromString("TRACE"),
// HttpString.tryFromString("TRACK")
// };
// return new DisallowedMethodsHandler(handler, disallowedHttpMethods);
// });
// });
// }
//
//}
@@ -1,29 +0,0 @@
package org.dromara.gateway.config.properties;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.stereotype.Component;
/**
* api解密属性配置类
*
* @author wdhcr
*/
@Data
@Component
@RefreshScope
@ConfigurationProperties(prefix = "api-decrypt")
public class ApiDecryptProperties {
/**
* 加密开关
*/
private Boolean enabled;
/**
* 头部标识
*/
private String headerFlag;
}
@@ -1,24 +0,0 @@
package org.dromara.gateway.config.properties;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.context.annotation.Configuration;
/**
* 自定义gateway参数配置
*
* @author Lion Li
*/
@Data
@Configuration
@RefreshScope
@ConfigurationProperties(prefix = "spring.cloud.gateway")
public class CustomGatewayProperties {
/**
* 请求日志
*/
private Boolean requestLog;
}
@@ -1,30 +0,0 @@
package org.dromara.gateway.config.properties;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.context.annotation.Configuration;
import java.util.ArrayList;
import java.util.List;
/**
* 放行白名单配置
*
* @author ruoyi
*/
@Data
@NoArgsConstructor
@Configuration
@RefreshScope
@ConfigurationProperties(prefix = "security.ignore")
public class IgnoreWhiteProperties {
/**
* 放行白名单配置,网关不校验此处的白名单
*/
private List<String> whites = new ArrayList<>();
}
@@ -1,89 +0,0 @@
package org.dromara.gateway.filter;
import cn.dev33.satoken.exception.NotLoginException;
import cn.dev33.satoken.filter.SaServletFilter;
import cn.dev33.satoken.httpauth.basic.SaHttpBasicUtil;
import cn.dev33.satoken.interceptor.SaInterceptor;
import cn.dev33.satoken.router.SaRouter;
import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.util.SaResult;
import cn.dev33.satoken.util.SaTokenConsts;
import org.dromara.common.core.constant.HttpStatus;
import org.dromara.common.core.utils.ServletUtils;
import org.dromara.common.core.utils.SpringUtils;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.gateway.config.properties.IgnoreWhiteProperties;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
/**
* [Sa-Token 权限认证] 拦截器配置
*
* @author Lion Li
*/
@Configuration
public class AuthFilter implements WebMvcConfigurer {
private final IgnoreWhiteProperties ignoreWhite;
public AuthFilter(IgnoreWhiteProperties ignoreWhite) {
this.ignoreWhite = ignoreWhite;
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new SaInterceptor(handler -> SaRouter.match("/**")
.notMatch(ignoreWhite.getWhites())
.check(() -> {
HttpServletRequest request = ServletUtils.getRequest();
HttpServletResponse response = ServletUtils.getResponse();
if (response != null) {
response.setContentType(SaTokenConsts.CONTENT_TYPE_APPLICATION_JSON);
}
StpUtil.checkLogin();
String headerCid = request.getHeader(LoginHelper.CLIENT_KEY);
String paramCid = ServletUtils.getParameter(LoginHelper.CLIENT_KEY);
Object extra = StpUtil.getExtra(LoginHelper.CLIENT_KEY);
String clientId = extra == null ? null : extra.toString();
if (!StringUtils.equalsAny(clientId, headerCid, paramCid)) {
throw NotLoginException.newInstance(StpUtil.getLoginType(),
"-100", "客户端ID与Token不匹配",
StpUtil.getTokenValue());
}
})))
.addPathPatterns("/**")
.excludePathPatterns("/favicon.ico", "/actuator", "/actuator/**", "/resource/sse" , "/error");
}
/**
* 为 actuator 健康检查接口配置 Basic Auth 鉴权过滤器。
*
* @return Sa-Token Servlet 过滤器
*/
@Bean
public SaServletFilter getSaServletFilter() {
String username = SpringUtils.getProperty("spring.cloud.nacos.discovery.metadata.username");
String password = SpringUtils.getProperty("spring.cloud.nacos.discovery.metadata.userpassword");
return new SaServletFilter()
.addInclude("/actuator", "/actuator/**")
.setAuth(obj -> {
SaHttpBasicUtil.check(username + ":" + password);
})
.setError(e -> {
HttpServletResponse response = ServletUtils.getResponse();
response.setContentType(SaTokenConsts.CONTENT_TYPE_APPLICATION_JSON);
return SaResult.error(e.getMessage()).setCode(HttpStatus.UNAUTHORIZED);
});
}
}
@@ -1,65 +0,0 @@
package org.dromara.gateway.filter;
import cn.dev33.satoken.SaManager;
import cn.dev33.satoken.same.SaSameUtil;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.dromara.gateway.filter.support.MutableHttpServletRequest;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
/**
* 转发请求头过滤器:
* 1. 动态透传 X-Forwarded-Prefix
* 2. 转发内部 same-token
*
* @author Lion Li
*/
@Component
@Order(Ordered.HIGHEST_PRECEDENCE + 15)
public class ForwardAuthFilter extends OncePerRequestFilter {
@Override
protected boolean shouldNotFilter(HttpServletRequest request) {
return request.getRequestURI().startsWith("/actuator");
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
MutableHttpServletRequest newRequest = null;
String forwardedPrefix = resolveForwardedPrefix(request);
if (forwardedPrefix != null) {
newRequest = getOrCreateMutableRequest(request, newRequest);
newRequest.putHeader("X-Forwarded-Prefix", forwardedPrefix);
}
if (SaManager.getConfig().getCheckSameToken()) {
newRequest = getOrCreateMutableRequest(request, newRequest);
newRequest.putHeader(SaSameUtil.SAME_TOKEN, SaSameUtil.getToken());
}
filterChain.doFilter(newRequest == null ? request : newRequest, response);
}
private String resolveForwardedPrefix(HttpServletRequest request) {
String[] pathSegments = StringUtils.tokenizeToStringArray(request.getRequestURI(), "/");
if (pathSegments.length == 0) {
return null;
}
return "/" + pathSegments[0];
}
private MutableHttpServletRequest getOrCreateMutableRequest(HttpServletRequest request,
MutableHttpServletRequest currentRequest) {
return currentRequest != null ? currentRequest : new MutableHttpServletRequest(request);
}
}
@@ -1,150 +0,0 @@
package org.dromara.gateway.filter;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ObjectUtil;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.constant.SystemConstants;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.json.utils.JsonUtils;
import org.dromara.gateway.config.properties.ApiDecryptProperties;
import org.dromara.gateway.config.properties.CustomGatewayProperties;
import org.dromara.gateway.filter.support.CachedBodyHttpServletRequest;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.UriComponentsBuilder;
import tools.jackson.databind.JsonNode;
import tools.jackson.databind.json.JsonMapper;
import tools.jackson.databind.node.ArrayNode;
import tools.jackson.databind.node.ObjectNode;
import java.io.IOException;
import java.util.HashSet;
import java.util.Set;
/**
* 全局日志过滤器
* <p>
* 用于打印请求执行参数与响应时间等等
*
* @author Lion Li
*/
@Slf4j
@Component
@Order(Ordered.LOWEST_PRECEDENCE - 10)
public class GlobalLogFilter extends OncePerRequestFilter {
private final CustomGatewayProperties customGatewayProperties;
private final ApiDecryptProperties apiDecryptProperties;
public GlobalLogFilter(CustomGatewayProperties customGatewayProperties, ApiDecryptProperties apiDecryptProperties) {
this.customGatewayProperties = customGatewayProperties;
this.apiDecryptProperties = apiDecryptProperties;
}
@Override
protected boolean shouldNotFilter(HttpServletRequest request) {
return !Boolean.TRUE.equals(customGatewayProperties.getRequestLog());
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
HttpServletRequest requestToUse = request;
if (isJsonRequest(request) && !(request instanceof CachedBodyHttpServletRequest)) {
requestToUse = new CachedBodyHttpServletRequest(request);
}
String url = requestToUse.getMethod() + " " + requestToUse.getRequestURI();
logRequest(requestToUse, url);
long startTime = System.currentTimeMillis();
try {
filterChain.doFilter(requestToUse, response);
} finally {
log.info("[PLUS]结束请求 => URL[{}],耗时:[{}]毫秒", url, System.currentTimeMillis() - startTime);
}
}
private void logRequest(HttpServletRequest request, String url) {
if (isJsonRequest(request)) {
if (Boolean.TRUE.equals(apiDecryptProperties.getEnabled())
&& ObjectUtil.isNotNull(request.getHeader(apiDecryptProperties.getHeaderFlag()))) {
log.info("[PLUS]开始请求 => URL[{}],参数类型[encrypt]", url);
return;
}
String jsonParam = resolveBody(request);
if (StringUtils.isNotBlank(jsonParam)) {
jsonParam = removeSensitiveFields(jsonParam);
}
log.info("[PLUS]开始请求 => URL[{}],参数类型[json],参数:[{}]", url, jsonParam);
return;
}
MultiValueMap<String, String> parameterMap = UriComponentsBuilder.newInstance()
.query(request.getQueryString())
.build()
.getQueryParams();
if (MapUtil.isNotEmpty(parameterMap)) {
LinkedMultiValueMap<String, String> map = new LinkedMultiValueMap<>(parameterMap);
MapUtil.removeAny(map, SystemConstants.EXCLUDE_PROPERTIES);
log.info("[PLUS]开始请求 => URL[{}],参数类型[param],参数:[{}]", url, JsonUtils.toJsonString(map));
} else {
log.info("[PLUS]开始请求 => URL[{}],无参数", url);
}
}
private boolean isJsonRequest(HttpServletRequest request) {
return StringUtils.startsWithIgnoreCase(request.getContentType(), MediaType.APPLICATION_JSON_VALUE);
}
private String resolveBody(HttpServletRequest request) {
if (request instanceof CachedBodyHttpServletRequest cachedRequest) {
return cachedRequest.getCachedBodyAsString();
}
return null;
}
private String removeSensitiveFields(String jsonParam) {
try {
JsonMapper jsonMapper = JsonUtils.getJsonMapper();
JsonNode rootNode = jsonMapper.readTree(jsonParam);
removeSensitiveFields(rootNode, SystemConstants.EXCLUDE_PROPERTIES);
return rootNode.toString();
} catch (Exception ex) {
return jsonParam;
}
}
private void removeSensitiveFields(JsonNode node, String[] excludeProperties) {
if (node == null) {
return;
}
if (node.isObject()) {
ObjectNode objectNode = (ObjectNode) node;
Set<String> fieldsToRemove = new HashSet<>();
objectNode.propertyNames().forEach(fieldName -> {
if (ArrayUtil.contains(excludeProperties, fieldName)) {
fieldsToRemove.add(fieldName);
}
});
fieldsToRemove.forEach(objectNode::remove);
objectNode.values().forEach(child -> removeSensitiveFields(child, excludeProperties));
} else if (node.isArray()) {
ArrayNode arrayNode = (ArrayNode) node;
for (JsonNode child : arrayNode) {
removeSensitiveFields(child, excludeProperties);
}
}
}
}
@@ -1,59 +0,0 @@
package org.dromara.gateway.handler;
import cn.dev33.satoken.exception.NotLoginException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.constant.HttpStatus;
import org.dromara.common.core.domain.R;
import org.dromara.common.json.utils.JsonUtils;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.server.ResponseStatusException;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
/**
* 网关统一异常处理
*
* @author Lion Li
*/
@Slf4j
@RestControllerAdvice
public class GatewayExceptionHandler {
@ExceptionHandler(NotLoginException.class)
public void handleNotLogin(HttpServletRequest request, HttpServletResponse response, NotLoginException ex) throws IOException {
log.warn("[网关认证失败]请求路径:{},异常信息:{}", request.getRequestURI(), ex.getMessage());
writeJson(response, HttpStatus.UNAUTHORIZED, ex.getMessage());
}
@ExceptionHandler(Throwable.class)
public void handle(HttpServletRequest request, HttpServletResponse response, Throwable ex) throws IOException {
int code;
String msg;
if ("NotFoundException".equals(ex.getClass().getSimpleName())
|| ex.getMessage().contains("Unable to find instance")) {
code = HttpStatus.NOT_FOUND;
msg = "服务未找到";
} else if (ex instanceof ResponseStatusException responseStatusException) {
code = responseStatusException.getStatusCode().value();
msg = responseStatusException.getMessage();
} else {
code = HttpStatus.ERROR;
msg = "内部服务器错误";
}
log.error("[网关异常处理]请求路径:{},异常信息:{}", request.getRequestURI(), ex.getMessage(), ex);
writeJson(response, code, msg);
}
private void writeJson(HttpServletResponse response, int code, String msg) throws IOException {
response.setStatus(code);
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.getWriter().write(JsonUtils.toJsonString(R.fail(code, msg)));
}
}
@@ -1,35 +0,0 @@
# Tomcat
server:
port: 8080
servlet:
context-path: /
# Spring
spring:
application:
# 应用名称
name: ruoyi-gateway-mvc
profiles:
# 环境配置
active: @profiles.active@
--- # nacos 配置
spring:
cloud:
nacos:
# nacos 服务地址
server-addr: @nacos.server@
username: @nacos.username@
password: @nacos.password@
discovery:
# 注册组
group: DEFAULT_GROUP
namespace: public
config:
# 配置组
group: DEFAULT_GROUP
namespace: public
config:
import:
- optional:nacos:application-common.yml
- optional:nacos:${spring.application.name}.yml
@@ -1,10 +0,0 @@
Spring Boot Version: ${spring-boot.version}
Spring Application Name: ${spring.application.name}
_ _
(_) | |
_ __ _ _ ___ _ _ _ ______ __ _ __ _ | |_ ___ __ __ __ _ _ _
| '__|| | | | / _ \ | | | || ||______| / _` | / _` || __| / _ \\ \ /\ / / / _` || | | |
| | | |_| || (_) || |_| || | | (_| || (_| || |_ | __/ \ V V / | (_| || |_| |
|_| \__,_| \___/ \__, ||_| \__, | \__,_| \__| \___| \_/\_/ \__,_| \__, |
__/ | __/ | __/ |
|___/ |___/ |___/
@@ -1,114 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="60 seconds" debug="false">
<!-- 日志存放路径 -->
<property name="log.path" value="logs/${project.artifactId}"/>
<!-- 日志输出格式 -->
<property name="console.log.pattern"
value="%cyan(%d{yyyy-MM-dd HH:mm:ss}) %green([%thread]) %highlight(%-5level) %boldMagenta(%logger{36}%n) - %msg%n"/>
<property name="log.pattern" value="%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"/>
<!-- 控制台输出 -->
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${console.log.pattern}</pattern>
<charset>utf-8</charset>
</encoder>
</appender>
<!-- 控制台输出 -->
<appender name="file_console" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log.path}/console.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 日志文件名格式 -->
<fileNamePattern>${log.path}/console.%d{yyyy-MM-dd}.log</fileNamePattern>
<!-- 日志最大 1天 -->
<maxHistory>1</maxHistory>
</rollingPolicy>
<encoder>
<pattern>${log.pattern}</pattern>
<charset>utf-8</charset>
</encoder>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<!-- 过滤的级别 -->
<level>INFO</level>
</filter>
</appender>
<!-- 系统日志输出 -->
<appender name="file_info" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log.path}/info.log</file>
<!-- 循环政策:基于时间创建日志文件 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 日志文件名格式 -->
<fileNamePattern>${log.path}/info.%d{yyyy-MM-dd}.log</fileNamePattern>
<!-- 日志最大的历史 60天 -->
<maxHistory>60</maxHistory>
</rollingPolicy>
<encoder>
<pattern>${log.pattern}</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<!-- 过滤的级别 -->
<level>INFO</level>
<!-- 匹配时的操作:接收(记录) -->
<onMatch>ACCEPT</onMatch>
<!-- 不匹配时的操作:拒绝(不记录) -->
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<appender name="file_error" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log.path}/error.log</file>
<!-- 循环政策:基于时间创建日志文件 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 日志文件名格式 -->
<fileNamePattern>${log.path}/error.%d{yyyy-MM-dd}.log</fileNamePattern>
<!-- 日志最大的历史 60天 -->
<maxHistory>60</maxHistory>
</rollingPolicy>
<encoder>
<pattern>${log.pattern}</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<!-- 过滤的级别 -->
<level>ERROR</level>
<!-- 匹配时的操作:接收(记录) -->
<onMatch>ACCEPT</onMatch>
<!-- 不匹配时的操作:拒绝(不记录) -->
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<!-- info异步输出 -->
<appender name="async_info" class="ch.qos.logback.classic.AsyncAppender">
<!-- 不丢失日志.默认的,如果队列的80%已满,则会丢弃TRACT、DEBUG、INFO级别的日志 -->
<discardingThreshold>0</discardingThreshold>
<!-- 更改默认的队列的深度,该值会影响性能.默认值为256 -->
<queueSize>512</queueSize>
<!-- 添加附加的appender,最多只能添加一个 -->
<appender-ref ref="file_info"/>
</appender>
<!-- error异步输出 -->
<appender name="async_error" class="ch.qos.logback.classic.AsyncAppender">
<!-- 不丢失日志.默认的,如果队列的80%已满,则会丢弃TRACT、DEBUG、INFO级别的日志 -->
<discardingThreshold>0</discardingThreshold>
<!-- 更改默认的队列的深度,该值会影响性能.默认值为256 -->
<queueSize>512</queueSize>
<!-- 添加附加的appender,最多只能添加一个 -->
<appender-ref ref="file_error"/>
</appender>
<include resource="logback-logstash.xml" />
<!-- 开启 skywalking 日志收集 -->
<include resource="logback-skylog.xml" />
<!--系统操作日志-->
<root level="info">
<appender-ref ref="console"/>
<appender-ref ref="async_info"/>
<appender-ref ref="async_error"/>
<appender-ref ref="file_console"/>
</root>
</configuration>
+1 -1
View File
@@ -15,7 +15,7 @@ ENV SERVER_PORT=8080 LANG=C.UTF-8 LC_ALL=C.UTF-8 JAVA_OPTS=""
EXPOSE ${SERVER_PORT}
ADD ./target/ruoyi-gateway.jar ./app.jar
ADD ./target/ruoyi-gateway-mvc.jar ./app.jar
SHELL ["/bin/bash", "-c"]
+4
View File
@@ -0,0 +1,4 @@
此服务为 spring-cloud-gateway 的 Servlet MVC 版本
建议在JDK21启动虚拟线程的基础上使用 在JDK17下使用性能比原版差很多
+22 -4
View File
@@ -11,7 +11,7 @@
<artifactId>ruoyi-gateway</artifactId>
<description>
ruoyi-gateway网关模块
ruoyi-gateway 网关模块
</description>
<dependencies>
@@ -19,7 +19,25 @@
<!-- SpringCloud Gateway -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway-server-webflux</artifactId>
<artifactId>spring-cloud-starter-gateway-server-webmvc</artifactId>
</dependency>
<!-- SpringBoot Web容器 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<artifactId>spring-boot-starter-tomcat</artifactId>
<groupId>org.springframework.boot</groupId>
</exclusion>
</exclusions>
</dependency>
<!-- Boot 4 下使用 jetty 替代 undertow -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jetty</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<dependency>
@@ -48,10 +66,10 @@
<artifactId>hutool-http</artifactId>
</dependency>
<!-- Sa-Token 权限认证(Reactor响应式集成), 在线文档:http://sa-token.dev33.cn/ -->
<!-- Sa-Token 权限认证 -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-reactor-spring-boot3-starter</artifactId>
<artifactId>sa-token-spring-boot4-starter</artifactId>
</dependency>
<dependency>
@@ -7,14 +7,16 @@ import org.springframework.boot.context.metrics.buffering.BufferingApplicationSt
/**
* 网关启动程序
*
* @author ruoyi
* @author Lion Li
*/
@SpringBootApplication
public class RuoYiGatewayApplication {
public static void main(String[] args) {
SpringApplication application = new SpringApplication(RuoYiGatewayApplication.class);
application.setApplicationStartup(new BufferingApplicationStartup(2048));
application.run(args);
System.out.println("(♥◠‿◠)ノ゙ 网关启动成功 ლ(´ڡ`ლ)゙ ");
System.out.println("(♥◠‿◠)ノ゙ MVC网关启动成功 ლ(´ڡ`ლ)゙ ");
}
}
@@ -1,13 +1,71 @@
package org.dromara.gateway.config;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import org.springframework.web.servlet.LocaleResolver;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.util.List;
import java.util.Locale;
import org.springframework.core.Ordered;
/**
* 网关限流配置
* 网关配置
*
* @author ruoyi
* @author Lion Li
*/
@Configuration
public class GatewayConfig {
@Bean
public LocaleResolver localeResolver() {
return new LocaleResolver() {
@Override
public Locale resolveLocale(HttpServletRequest request) {
String language = request.getHeader("content-language");
Locale locale = Locale.getDefault();
if (language == null || language.isBlank()) {
return locale;
}
String[] split = language.split("_");
if (split.length == 2) {
return new Locale(split[0], split[1]);
}
return Locale.forLanguageTag(language.replace('_', '-'));
}
@Override
public void setLocale(HttpServletRequest request, HttpServletResponse response, Locale locale) {
// noop
}
};
}
@Bean
public FilterRegistrationBean<CorsFilter> corsFilterRegistrationBean() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOriginPatterns(List.of("*"));
config.setAllowCredentials(true);
config.setAllowedHeaders(List.of(
"X-Requested-With", "Content-Language", "Content-Type",
"Authorization", "clientid", "credential", "X-XSRF-TOKEN",
"isToken", "token", "Admin-Token", "App-Token", "Encrypt-Key", "isEncrypt"
));
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS", "HEAD"));
config.setExposedHeaders(List.of("*"));
config.setMaxAge(18000L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
FilterRegistrationBean<CorsFilter> registration = new FilterRegistrationBean<>(new CorsFilter(source));
registration.setOrder(Ordered.HIGHEST_PRECEDENCE);
return registration;
}
}
@@ -7,6 +7,7 @@ import org.springframework.stereotype.Component;
/**
* api解密属性配置类
*
* @author wdhcr
*/
@Data
@@ -1,92 +1,87 @@
package org.dromara.gateway.filter;
import cn.dev33.satoken.exception.NotLoginException;
import cn.dev33.satoken.filter.SaServletFilter;
import cn.dev33.satoken.httpauth.basic.SaHttpBasicUtil;
import cn.dev33.satoken.reactor.context.SaReactorSyncHolder;
import cn.dev33.satoken.reactor.filter.SaReactorFilter;
import cn.dev33.satoken.interceptor.SaInterceptor;
import cn.dev33.satoken.router.SaRouter;
import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.util.SaResult;
import cn.dev33.satoken.util.SaTokenConsts;
import org.dromara.common.core.constant.HttpStatus;
import org.dromara.common.core.utils.ServletUtils;
import org.dromara.common.core.utils.SpringUtils;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.gateway.config.properties.IgnoreWhiteProperties;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.core.Ordered;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
/**
* [Sa-Token 权限认证] 拦截器
* [Sa-Token 权限认证] 拦截器配置
*
* @author Lion Li
*/
@Configuration
public class AuthFilter {
public class AuthFilter implements WebMvcConfigurer {
/**
* 注册 Sa-Token 全局过滤器
*/
@Bean
public SaReactorFilter getSaReactorFilter(IgnoreWhiteProperties ignoreWhite) {
return new SaReactorFilter()
// 拦截地址
.addInclude("/**")
.addExclude("/favicon.ico", "/actuator", "/actuator/**", "/resource/sse")
// 鉴权方法:每次访问进入
.setAuth(obj -> {
// 登录校验 -- 拦截所有路由
SaRouter.match("/**")
.notMatch(ignoreWhite.getWhites())
.check(r -> {
ServerHttpRequest request = SaReactorSyncHolder.getExchange().getRequest();
// 检查是否登录 是否有token
StpUtil.checkLogin();
private final IgnoreWhiteProperties ignoreWhite;
// 检查 header 与 param 里的 clientid 与 token 里的是否一致
String headerCid = request.getHeaders().getFirst(LoginHelper.CLIENT_KEY);
String paramCid = request.getQueryParams().getFirst(LoginHelper.CLIENT_KEY);
String clientId = StpUtil.getExtra(LoginHelper.CLIENT_KEY).toString();
if (!StringUtils.equalsAny(clientId, headerCid, paramCid)) {
// token 无效
throw NotLoginException.newInstance(StpUtil.getLoginType(),
"-100", "客户端ID与Token不匹配",
StpUtil.getTokenValue());
}
public AuthFilter(IgnoreWhiteProperties ignoreWhite) {
this.ignoreWhite = ignoreWhite;
}
// 有效率影响 用于临时测试
// if (log.isDebugEnabled()) {
// log.debug("剩余有效时间: {}", StpUtil.getTokenTimeout());
// log.debug("临时有效时间: {}", StpUtil.getTokenActivityTimeout());
// }
});
}).setError(e -> {
ServerHttpResponse response = SaReactorSyncHolder.getExchange().getResponse();
response.getHeaders().set(SaTokenConsts.CONTENT_TYPE_KEY, SaTokenConsts.CONTENT_TYPE_APPLICATION_JSON);
if (e instanceof NotLoginException) {
return SaResult.error(e.getMessage()).setCode(HttpStatus.UNAUTHORIZED);
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new SaInterceptor(handler -> SaRouter.match("/**")
.notMatch(ignoreWhite.getWhites())
.check(() -> {
HttpServletRequest request = ServletUtils.getRequest();
HttpServletResponse response = ServletUtils.getResponse();
if (response != null) {
response.setContentType(SaTokenConsts.CONTENT_TYPE_APPLICATION_JSON);
}
return SaResult.error("认证失败,无法访问系统资源").setCode(HttpStatus.UNAUTHORIZED);
});
StpUtil.checkLogin();
String headerCid = request.getHeader(LoginHelper.CLIENT_KEY);
String paramCid = ServletUtils.getParameter(LoginHelper.CLIENT_KEY);
Object extra = StpUtil.getExtra(LoginHelper.CLIENT_KEY);
String clientId = extra == null ? null : extra.toString();
if (!StringUtils.equalsAny(clientId, headerCid, paramCid)) {
throw NotLoginException.newInstance(StpUtil.getLoginType(),
"-100", "客户端ID与Token不匹配",
StpUtil.getTokenValue());
}
})))
.addPathPatterns("/**")
.excludePathPatterns("/favicon.ico", "/actuator", "/actuator/**", "/resource/sse" , "/error");
}
/**
* 对 actuator 健康检查接口 做账号密码鉴权
* 为 actuator 健康检查接口配置 Basic Auth 鉴权过滤器。
*
* @return Sa-Token Servlet 过滤器
*/
@Bean
public SaReactorFilter actuatorFilter() {
public SaServletFilter getSaServletFilter() {
String username = SpringUtils.getProperty("spring.cloud.nacos.discovery.metadata.username");
String password = SpringUtils.getProperty("spring.cloud.nacos.discovery.metadata.userpassword");
return new SaReactorFilter()
return new SaServletFilter()
.addInclude("/actuator", "/actuator/**")
.setAuth(obj -> {
SaHttpBasicUtil.check(username + ":" + password);
})
.setError(e -> {
ServerHttpResponse response = SaReactorSyncHolder.getExchange().getResponse();
response.getHeaders().set(SaTokenConsts.CONTENT_TYPE_KEY, SaTokenConsts.CONTENT_TYPE_APPLICATION_JSON);
HttpServletResponse response = ServletUtils.getResponse();
response.setContentType(SaTokenConsts.CONTENT_TYPE_APPLICATION_JSON);
return SaResult.error(e.getMessage()).setCode(HttpStatus.UNAUTHORIZED);
});
}
@@ -1,58 +0,0 @@
package org.dromara.gateway.filter;
import org.dromara.gateway.utils.WebFluxUtils;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;
/**
* 黑名单过滤器
*
* @author ruoyi
*/
@Component
public class BlackListUrlFilter extends AbstractGatewayFilterFactory<BlackListUrlFilter.Config> {
@Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
String url = exchange.getRequest().getURI().getPath();
if (config.matchBlacklist(url)) {
return WebFluxUtils.webFluxResponseWriter(exchange.getResponse(), "请求地址不允许访问");
}
return chain.filter(exchange);
};
}
public BlackListUrlFilter() {
super(Config.class);
}
public static class Config {
private List<String> blacklistUrl;
private List<Pattern> blacklistUrlPattern = new ArrayList<>();
public boolean matchBlacklist(String url) {
return !blacklistUrlPattern.isEmpty() && blacklistUrlPattern.stream().anyMatch(p -> p.matcher(url).find());
}
public List<String> getBlacklistUrl() {
return blacklistUrl;
}
public void setBlacklistUrl(List<String> blacklistUrl) {
this.blacklistUrl = blacklistUrl;
this.blacklistUrlPattern.clear();
this.blacklistUrl.forEach(url -> {
this.blacklistUrlPattern.add(Pattern.compile(url.replaceAll("\\*\\*", "(.*?)"), Pattern.CASE_INSENSITIVE));
});
}
}
}
@@ -2,40 +2,64 @@ package org.dromara.gateway.filter;
import cn.dev33.satoken.SaManager;
import cn.dev33.satoken.same.SaSameUtil;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.dromara.gateway.filter.support.MutableHttpServletRequest;
import org.springframework.core.Ordered;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
/**
* 转发认证过滤器(内部服务外网隔离)
* 转发请求头过滤器:
* 1. 动态透传 X-Forwarded-Prefix
* 2. 转发内部 same-token
*
* @author Lion Li
*/
@Component
public class ForwardAuthFilter implements GlobalFilter, Ordered {
@Order(Ordered.HIGHEST_PRECEDENCE + 15)
public class ForwardAuthFilter extends OncePerRequestFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 未开启配置则直接跳过
if (!SaManager.getConfig().getCheckSameToken()) {
return chain.filter(exchange);
}
ServerHttpRequest newRequest = exchange
.getRequest()
.mutate()
// 为请求追加 Same-Token 参数
.header(SaSameUtil.SAME_TOKEN, SaSameUtil.getToken())
.build();
ServerWebExchange newExchange = exchange.mutate().request(newRequest).build();
return chain.filter(newExchange);
protected boolean shouldNotFilter(HttpServletRequest request) {
return request.getRequestURI().startsWith("/actuator");
}
@Override
public int getOrder() {
return -100;
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
MutableHttpServletRequest newRequest = null;
String forwardedPrefix = resolveForwardedPrefix(request);
if (forwardedPrefix != null) {
newRequest = getOrCreateMutableRequest(request, newRequest);
newRequest.putHeader("X-Forwarded-Prefix", forwardedPrefix);
}
if (SaManager.getConfig().getCheckSameToken()) {
newRequest = getOrCreateMutableRequest(request, newRequest);
newRequest.putHeader(SaSameUtil.SAME_TOKEN, SaSameUtil.getToken());
}
filterChain.doFilter(newRequest == null ? request : newRequest, response);
}
private String resolveForwardedPrefix(HttpServletRequest request) {
String[] pathSegments = StringUtils.tokenizeToStringArray(request.getRequestURI(), "/");
if (pathSegments.length == 0) {
return null;
}
return "/" + pathSegments[0];
}
private MutableHttpServletRequest getOrCreateMutableRequest(HttpServletRequest request,
MutableHttpServletRequest currentRequest) {
return currentRequest != null ? currentRequest : new MutableHttpServletRequest(request);
}
}
@@ -3,29 +3,31 @@ package org.dromara.gateway.filter;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ObjectUtil;
import lombok.SneakyThrows;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.constant.SystemConstants;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.json.utils.JsonUtils;
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.dromara.gateway.filter.support.CachedBodyHttpServletRequest;
import org.springframework.core.Ordered;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.core.annotation.Order;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.UriComponentsBuilder;
import tools.jackson.databind.JsonNode;
import tools.jackson.databind.json.JsonMapper;
import tools.jackson.databind.node.ArrayNode;
import tools.jackson.databind.node.ObjectNode;
import java.io.IOException;
import java.util.HashSet;
import java.util.Set;
@@ -38,60 +40,89 @@ import java.util.Set;
*/
@Slf4j
@Component
public class GlobalLogFilter implements GlobalFilter, Ordered {
@Order(Ordered.LOWEST_PRECEDENCE - 10)
public class GlobalLogFilter extends OncePerRequestFilter {
@Autowired
private CustomGatewayProperties customGatewayProperties;
@Autowired
private ApiDecryptProperties apiDecryptProperties;
private final CustomGatewayProperties customGatewayProperties;
private final ApiDecryptProperties apiDecryptProperties;
private static final String START_TIME = "startTime";
public GlobalLogFilter(CustomGatewayProperties customGatewayProperties, ApiDecryptProperties apiDecryptProperties) {
this.customGatewayProperties = customGatewayProperties;
this.apiDecryptProperties = apiDecryptProperties;
}
@SneakyThrows
@Override
public Mono<Void> 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;
protected boolean shouldNotFilter(HttpServletRequest request) {
return !Boolean.TRUE.equals(customGatewayProperties.getRequestLog());
}
// 打印请求参数
if (WebFluxUtils.isJsonRequest(exchange)) {
if (apiDecryptProperties.getEnabled()
&& ObjectUtil.isNotNull(request.getHeaders().getFirst(apiDecryptProperties.getHeaderFlag()))) {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
HttpServletRequest requestToUse = request;
if (isJsonRequest(request) && !(request instanceof CachedBodyHttpServletRequest)) {
requestToUse = new CachedBodyHttpServletRequest(request);
}
String url = requestToUse.getMethod() + " " + requestToUse.getRequestURI();
logRequest(requestToUse, url);
long startTime = System.currentTimeMillis();
try {
filterChain.doFilter(requestToUse, response);
} finally {
log.info("[PLUS]结束请求 => URL[{}],耗时:[{}]毫秒", url, System.currentTimeMillis() - startTime);
}
}
private void logRequest(HttpServletRequest request, String url) {
if (isJsonRequest(request)) {
if (Boolean.TRUE.equals(apiDecryptProperties.getEnabled())
&& ObjectUtil.isNotNull(request.getHeader(apiDecryptProperties.getHeaderFlag()))) {
log.info("[PLUS]开始请求 => URL[{}],参数类型[encrypt]", url);
} else {
String jsonParam = WebFluxUtils.resolveBodyFromCacheRequest(exchange);
if (StringUtils.isNotBlank(jsonParam)) {
JsonMapper jsonMapper = JsonUtils.getJsonMapper();
JsonNode rootNode = jsonMapper.readTree(jsonParam);
removeSensitiveFields(rootNode, SystemConstants.EXCLUDE_PROPERTIES);
jsonParam = rootNode.toString();
}
log.info("[PLUS]开始请求 => URL[{}],参数类型[json],参数:[{}]", url, jsonParam);
return;
}
} else {
MultiValueMap<String, String> parameterMap = request.getQueryParams();
if (MapUtil.isNotEmpty(parameterMap)) {
LinkedMultiValueMap<String, String> map = new LinkedMultiValueMap<>(parameterMap);
MapUtil.removeAny(map, SystemConstants.EXCLUDE_PROPERTIES);
String parameters = JsonUtils.toJsonString(map);
log.info("[PLUS]开始请求 => URL[{}],参数类型[param],参数:[{}]", url, parameters);
} else {
log.info("[PLUS]开始请求 => URL[{}],无参数", url);
String jsonParam = resolveBody(request);
if (StringUtils.isNotBlank(jsonParam)) {
jsonParam = removeSensitiveFields(jsonParam);
}
log.info("[PLUS]开始请求 => URL[{}],参数类型[json],参数:[{}]", url, jsonParam);
return;
}
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);
}
}));
MultiValueMap<String, String> parameterMap = UriComponentsBuilder.newInstance()
.query(request.getQueryString())
.build()
.getQueryParams();
if (MapUtil.isNotEmpty(parameterMap)) {
LinkedMultiValueMap<String, String> map = new LinkedMultiValueMap<>(parameterMap);
MapUtil.removeAny(map, SystemConstants.EXCLUDE_PROPERTIES);
log.info("[PLUS]开始请求 => URL[{}],参数类型[param],参数:[{}]", url, JsonUtils.toJsonString(map));
} else {
log.info("[PLUS]开始请求 => URL[{}],无参数", url);
}
}
private boolean isJsonRequest(HttpServletRequest request) {
return StringUtils.startsWithIgnoreCase(request.getContentType(), MediaType.APPLICATION_JSON_VALUE);
}
private String resolveBody(HttpServletRequest request) {
if (request instanceof CachedBodyHttpServletRequest cachedRequest) {
return cachedRequest.getCachedBodyAsString();
}
return null;
}
private String removeSensitiveFields(String jsonParam) {
try {
JsonMapper jsonMapper = JsonUtils.getJsonMapper();
JsonNode rootNode = jsonMapper.readTree(jsonParam);
removeSensitiveFields(rootNode, SystemConstants.EXCLUDE_PROPERTIES);
return rootNode.toString();
} catch (Exception ex) {
return jsonParam;
}
}
private void removeSensitiveFields(JsonNode node, String[] excludeProperties) {
@@ -100,7 +131,6 @@ public class GlobalLogFilter implements GlobalFilter, Ordered {
}
if (node.isObject()) {
ObjectNode objectNode = (ObjectNode) node;
// 收集要删除的字段名(避免 ConcurrentModification)
Set<String> fieldsToRemove = new HashSet<>();
objectNode.propertyNames().forEach(fieldName -> {
if (ArrayUtil.contains(excludeProperties, fieldName)) {
@@ -108,7 +138,6 @@ public class GlobalLogFilter implements GlobalFilter, Ordered {
}
});
fieldsToRemove.forEach(objectNode::remove);
// 递归处理子节点
objectNode.values().forEach(child -> removeSensitiveFields(child, excludeProperties));
} else if (node.isArray()) {
ArrayNode arrayNode = (ArrayNode) node;
@@ -118,12 +147,4 @@ public class GlobalLogFilter implements GlobalFilter, Ordered {
}
}
@Override
public int getOrder() {
// 日志处理器在负载均衡器之后执行 负载均衡器会导致线程切换 无法获取上下文内容
// 如需在日志内操作线程上下文 例如获取登录用户数据等 可以打开下方注释代码
// return ReactiveLoadBalancerClientFilter.LOAD_BALANCER_CLIENT_FILTER_ORDER - 1;
return Ordered.LOWEST_PRECEDENCE;
}
}
@@ -1,38 +0,0 @@
package org.dromara.gateway.filter;
import org.dromara.gateway.utils.WebFluxUtils;
import org.springframework.cloud.gateway.support.ServerWebExchangeUtils;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;
/**
* 缓存获取body请求数据(解决流不能重复读取问题)
*
* @author Lion Li
*/
@Component
public class WebCacheRequestFilter implements WebFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
// 只缓存json类型请求
if (!WebFluxUtils.isJsonRequest(exchange)) {
return chain.filter(exchange);
}
return ServerWebExchangeUtils.cacheRequestBody(exchange, (serverHttpRequest) -> {
if (serverHttpRequest == exchange.getRequest()) {
return chain.filter(exchange);
}
return chain.filter(exchange.mutate().request(serverHttpRequest).build());
});
}
@Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE + 1;
}
}
@@ -1,86 +0,0 @@
package org.dromara.gateway.filter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.cors.reactive.CorsUtils;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;
/**
* 跨域配置
*
* @author Lion Li
*/
@Component
public class WebCorsFilter implements WebFilter, Ordered {
/**
* 这里为支持的请求头,如果有自定义的header字段请自己添加
*/
private static final String ALLOWED_HEADERS =
"X-Requested-With, Content-Language, Content-Type, " +
"Authorization, clientid, credential, X-XSRF-TOKEN, " +
"isToken, token, Admin-Token, App-Token, Encrypt-Key, isEncrypt";
/**
* 允许的请求方法
*/
private static final String ALLOWED_METHODS = "GET,POST,PUT,DELETE,OPTIONS,HEAD";
/**
* 允许的请求来源,使用 * 表示允许任何来源
*/
private static final String ALLOWED_ORIGIN = "*";
/**
* 允许前端访问的响应头,使用 * 表示允许任何响应头
*/
private static final String ALLOWED_EXPOSE = "*";
/**
* 预检请求的缓存时间,单位为秒(此处设置为 5 小时)
*/
private static final String MAX_AGE = "18000L";
/**
* 实现跨域配置的 Web 过滤器
*
* @param exchange ServerWebExchange 对象,表示一次 Web 交换
* @param chain WebFilterChain 对象,表示一组 Web 过滤器链
* @return Mono<Void> 表示异步的过滤器链处理结果
*/
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
// 判断请求是否为跨域请求
if (CorsUtils.isCorsRequest(request)) {
ServerHttpResponse response = exchange.getResponse();
HttpHeaders headers = response.getHeaders();
headers.add("Access-Control-Allow-Headers", ALLOWED_HEADERS);
headers.add("Access-Control-Allow-Methods", ALLOWED_METHODS);
headers.add("Access-Control-Allow-Origin", ALLOWED_ORIGIN);
headers.add("Access-Control-Expose-Headers", ALLOWED_EXPOSE);
headers.add("Access-Control-Max-Age", MAX_AGE);
headers.add("Access-Control-Allow-Credentials", "true");
// 处理预检请求的 OPTIONS 方法,直接返回成功状态码
if (request.getMethod() == HttpMethod.OPTIONS) {
response.setStatusCode(HttpStatus.OK);
return Mono.empty();
}
}
return chain.filter(exchange);
}
@Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE;
}
}
@@ -1,41 +0,0 @@
package org.dromara.gateway.filter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.context.i18n.SimpleLocaleContext;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;
import java.util.Locale;
/**
* 全局国际化处理
*
* @author Lion Li
*/
@Slf4j
@Component
public class WebI18nFilter implements WebFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain 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;
}
}
@@ -1,46 +1,59 @@
package org.dromara.gateway.handler;
import org.dromara.gateway.utils.WebFluxUtils;
import cn.dev33.satoken.exception.NotLoginException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.webflux.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.dromara.common.core.constant.HttpStatus;
import org.dromara.common.core.domain.R;
import org.dromara.common.json.utils.JsonUtils;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.server.ResponseStatusException;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
/**
* 网关统一异常处理
*
* @author ruoyi
* @author Lion Li
*/
@Slf4j
@Order(-1)
@Configuration
public class GatewayExceptionHandler implements ErrorWebExceptionHandler {
@RestControllerAdvice
public class GatewayExceptionHandler {
@Override
public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {
ServerHttpResponse response = exchange.getResponse();
if (exchange.getResponse().isCommitted()) {
return Mono.error(ex);
}
@ExceptionHandler(NotLoginException.class)
public void handleNotLogin(HttpServletRequest request, HttpServletResponse response, NotLoginException ex) throws IOException {
log.warn("[网关认证失败]请求路径:{},异常信息:{}", request.getRequestURI(), ex.getMessage());
writeJson(response, HttpStatus.UNAUTHORIZED, ex.getMessage());
}
@ExceptionHandler(Throwable.class)
public void handle(HttpServletRequest request, HttpServletResponse response, Throwable ex) throws IOException {
int code;
String msg;
if (ex instanceof NotFoundException) {
if ("NotFoundException".equals(ex.getClass().getSimpleName())
|| ex.getMessage().contains("Unable to find instance")) {
code = HttpStatus.NOT_FOUND;
msg = "服务未找到";
} else if (ex instanceof ResponseStatusException responseStatusException) {
code = responseStatusException.getStatusCode().value();
msg = responseStatusException.getMessage();
} else {
code = HttpStatus.ERROR;
msg = "内部服务器错误";
}
log.error("[网关异常处理]请求路径:{},异常信息:{}", exchange.getRequest().getPath(), ex.getMessage());
log.error("[网关异常处理]请求路径:{},异常信息:{}", request.getRequestURI(), ex.getMessage(), ex);
writeJson(response, code, msg);
}
return WebFluxUtils.webFluxResponseWriter(response, msg);
private void writeJson(HttpServletResponse response, int code, String msg) throws IOException {
response.setStatus(code);
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.getWriter().write(JsonUtils.toJsonString(R.fail(code, msg)));
}
}
@@ -1,152 +0,0 @@
package org.dromara.gateway.utils;
import cn.hutool.core.util.ObjectUtil;
import org.dromara.common.core.domain.R;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.json.utils.JsonUtils;
import org.dromara.gateway.filter.WebCacheRequestFilter;
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<URI> 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<DataBuffer> body = serverHttpRequest.getBody();
AtomicReference<String> 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 WebCacheRequestFilter} 全局创建了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()) {
StringBuilder sb = new StringBuilder();
iterator.forEachRemaining(e -> {
sb.append(StandardCharsets.UTF_8.decode(e));
});
return sb.toString();
}
}
/**
* 设置webflux模型响应
*
* @param response ServerHttpResponse
* @param value 响应内容
* @return Mono<Void>
*/
public static Mono<Void> webFluxResponseWriter(ServerHttpResponse response, Object value) {
return webFluxResponseWriter(response, HttpStatus.OK, value, R.FAIL);
}
/**
* 设置webflux模型响应
*
* @param response ServerHttpResponse
* @param code 响应状态码
* @param value 响应内容
* @return Mono<Void>
*/
public static Mono<Void> 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<Void>
*/
public static Mono<Void> 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<Void>
*/
public static Mono<Void> 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(JsonUtils.toJsonString(result).getBytes());
return response.writeWith(Mono.just(dataBuffer));
}
}
@@ -1,115 +0,0 @@
/*
* Copyright 2013-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.gateway.filter.factory;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.util.Arrays;
import java.util.List;
import static org.springframework.cloud.gateway.support.GatewayToStringStyler.filterToStringCreator;
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR;
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.addOriginalRequestUrl;
/**
* This filter removes the first part of the path, known as the prefix, from the request
* before sending it downstream.
*
* @author Ryan Baxter
*/
public class StripPrefixGatewayFilterFactory
extends AbstractGatewayFilterFactory<StripPrefixGatewayFilterFactory.Config> {
/**
* Parts key.
*/
public static final String PARTS_KEY = "parts";
public StripPrefixGatewayFilterFactory() {
super(Config.class);
}
@Override
public List<String> shortcutFieldOrder() {
return Arrays.asList(PARTS_KEY);
}
@Override
public GatewayFilter apply(Config config) {
return new GatewayFilter() {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
addOriginalRequestUrl(exchange, request.getURI());
String path = request.getURI().getRawPath();
String[] originalParts = StringUtils.tokenizeToStringArray(path, "/");
// all new paths start with /
StringBuilder newPath = new StringBuilder("/");
for (int i = 0; i < originalParts.length; i++) {
if (i >= config.getParts()) {
// only append slash if this is the second part or greater
if (newPath.length() > 1) {
newPath.append('/');
}
newPath.append(originalParts[i]);
}
}
if (newPath.length() > 1 && path.endsWith("/")) {
newPath.append('/');
}
// 增加doc前缀传递
String prefix = "/" + originalParts[config.getParts() - 1];
ServerHttpRequest newRequest = request.mutate()
.header("X-Forwarded-Prefix", prefix)
.path(newPath.toString())
.build();
exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, newRequest.getURI());
return chain.filter(exchange.mutate().request(newRequest).build());
}
@Override
public String toString() {
return filterToStringCreator(StripPrefixGatewayFilterFactory.this).append("parts", config.getParts())
.toString();
}
};
}
public static class Config {
private int parts = 1;
public int getParts() {
return parts;
}
public void setParts(int parts) {
this.parts = parts;
}
}
}
+9 -9
View File
@@ -1,10 +1,10 @@
Spring Boot Version: ${spring-boot.version}
Spring Application Name: ${spring.application.name}
_ _
(_) | |
_ __ _ _ ___ _ _ _ ______ __ _ __ _ | |_ ___ __ __ __ _ _ _
| '__|| | | | / _ \ | | | || ||______| / _` | / _` || __| / _ \\ \ /\ / / / _` || | | |
| | | |_| || (_) || |_| || | | (_| || (_| || |_ | __/ \ V V / | (_| || |_| |
|_| \__,_| \___/ \__, ||_| \__, | \__,_| \__| \___| \_/\_/ \__,_| \__, |
__/ | __/ | __/ |
Spring Boot Version: ${spring-boot.version}
Spring Application Name: ${spring.application.name}
_ _
(_) | |
_ __ _ _ ___ _ _ _ ______ __ _ __ _ | |_ ___ __ __ __ _ _ _
| '__|| | | | / _ \ | | | || ||______| / _` | / _` || __| / _ \\ \ /\ / / / _` || | | |
| | | |_| || (_) || |_| || | | (_| || (_| || |_ | __/ \ V V / | (_| || |_| |
|_| \__,_| \___/ \__, ||_| \__, | \__,_| \__| \___| \_/\_/ \__,_| \__, |
__/ | __/ | __/ |
|___/ |___/ |___/
-84
View File
@@ -1,84 +0,0 @@
# 安全配置
security:
# 不校验白名单
ignore:
whites:
- /auth/code
- /auth/logout
- /auth/login
- /auth/binding/*
- /auth/register
- /auth/tenant/list
- /resource/sms/code
- /resource/sse/close
- /*/v3/api-docs
- /*/error
- /csrf
- /warm-flow-ui/**
spring:
cloud:
gateway:
# 打印请求日志(自定义)
requestLog: true
server:
webmvc:
discovery:
locator:
lowerCaseServiceId: true
enabled: true
routes:
# 认证中心
- id: ruoyi-auth
uri: lb://ruoyi-auth
predicates:
- Path=/auth/**
filters:
- StripPrefix=1
# 代码生成
- id: ruoyi-gen
uri: lb://ruoyi-gen
predicates:
- Path=/tool/**
filters:
- StripPrefix=1
# 系统模块
- id: ruoyi-system
uri: lb://ruoyi-system
predicates:
- Path=/system/**,/monitor/**
filters:
- StripPrefix=1
# 资源服务
- id: ruoyi-resource
uri: lb://ruoyi-resource
predicates:
- Path=/resource/**
filters:
- StripPrefix=1
# workflow服务
- id: ruoyi-workflow
uri: lb://ruoyi-workflow
predicates:
- Path=/workflow/**
filters:
- StripPrefix=1
# warm-flow服务
- id: warm-flow
uri: lb://ruoyi-workflow
predicates:
- Path=/warm-flow-ui/**,/warm-flow/**
# 演示服务
- id: ruoyi-demo
uri: lb://ruoyi-demo
predicates:
- Path=/demo/**
filters:
- StripPrefix=1
# MQ演示服务
- id: ruoyi-test-mq
uri: lb://ruoyi-test-mq
predicates:
- Path=/test-mq/**
filters:
- StripPrefix=1
+1 -2
View File
@@ -18,12 +18,11 @@ security:
spring:
cloud:
# 网关配置
gateway:
# 打印请求日志(自定义)
requestLog: true
server:
webflux:
webmvc:
discovery:
locator:
lowerCaseServiceId: true