update 删除 gateway webflux 版本 统一使用 MVC版本
This commit is contained in:
@@ -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"]
|
||||
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
此服务为 spring-cloud-gateway 的 Servlet MVC 版本
|
||||
|
||||
建议在JDK21启动虚拟线程的基础上使用 在JDK17下使用性能比原版差很多
|
||||
|
||||
+22
-4
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+1
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
package org.dromara.gateway.filter.support;
|
||||
|
||||
import jakarta.servlet.ReadListener;
|
||||
import jakarta.servlet.ServletInputStream;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletRequestWrapper;
|
||||
import org.springframework.util.StreamUtils;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
/**
|
||||
* 可重复读取请求体的包装类
|
||||
*
|
||||
* @author Lion Li
|
||||
*/
|
||||
public class CachedBodyHttpServletRequest extends HttpServletRequestWrapper {
|
||||
|
||||
private final byte[] cachedBody;
|
||||
|
||||
public CachedBodyHttpServletRequest(HttpServletRequest request) throws IOException {
|
||||
super(request);
|
||||
this.cachedBody = StreamUtils.copyToByteArray(request.getInputStream());
|
||||
}
|
||||
|
||||
@Override
|
||||
public ServletInputStream getInputStream() {
|
||||
ByteArrayInputStream inputStream = new ByteArrayInputStream(cachedBody);
|
||||
return new ServletInputStream() {
|
||||
@Override
|
||||
public int read() {
|
||||
return inputStream.read();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFinished() {
|
||||
return inputStream.available() == 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isReady() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setReadListener(ReadListener listener) {
|
||||
// noop
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public BufferedReader getReader() {
|
||||
Charset charset = getCharacterEncoding() == null
|
||||
? StandardCharsets.UTF_8
|
||||
: Charset.forName(getCharacterEncoding());
|
||||
return new BufferedReader(new InputStreamReader(getInputStream(), charset));
|
||||
}
|
||||
|
||||
public String getCachedBodyAsString() {
|
||||
Charset charset = getCharacterEncoding() == null
|
||||
? StandardCharsets.UTF_8
|
||||
: Charset.forName(getCharacterEncoding());
|
||||
return new String(cachedBody, charset);
|
||||
}
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
package org.dromara.gateway.filter.support;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletRequestWrapper;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Enumeration;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.Vector;
|
||||
|
||||
/**
|
||||
* 可追加请求头的包装类
|
||||
*
|
||||
* @author Lion Li
|
||||
*/
|
||||
public class MutableHttpServletRequest extends HttpServletRequestWrapper {
|
||||
|
||||
private final Map<String, String> customHeaders = new LinkedHashMap<>();
|
||||
|
||||
public MutableHttpServletRequest(HttpServletRequest request) {
|
||||
super(request);
|
||||
}
|
||||
|
||||
public void putHeader(String name, String value) {
|
||||
customHeaders.put(name, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHeader(String name) {
|
||||
String value = customHeaders.get(name);
|
||||
return value != null ? value : super.getHeader(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Enumeration<String> getHeaders(String name) {
|
||||
String value = customHeaders.get(name);
|
||||
if (value != null) {
|
||||
return Collections.enumeration(Collections.singletonList(value));
|
||||
}
|
||||
return super.getHeaders(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Enumeration<String> getHeaderNames() {
|
||||
Set<String> names = new LinkedHashSet<>();
|
||||
Enumeration<String> headerNames = super.getHeaderNames();
|
||||
while (headerNames.hasMoreElements()) {
|
||||
names.add(headerNames.nextElement());
|
||||
}
|
||||
names.addAll(customHeaders.keySet());
|
||||
return new Vector<>(names).elements();
|
||||
}
|
||||
}
|
||||
+36
-23
@@ -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));
|
||||
}
|
||||
}
|
||||
-115
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 / | (_| || |_| |
|
||||
|_| \__,_| \___/ \__, ||_| \__, | \__,_| \__| \___| \_/\_/ \__,_| \__, |
|
||||
__/ | __/ | __/ |
|
||||
|___/ |___/ |___/
|
||||
Reference in New Issue
Block a user