This commit is contained in:
kk
2026-07-07 18:05:54 +08:00
commit d4ef46bf97
743 changed files with 159169 additions and 0 deletions

15
blade-gateway/Dockerfile Normal file
View File

@@ -0,0 +1,15 @@
FROM bladex/alpine-java:openjdk17_cn_slim
LABEL maintainer="bladejava@qq.com"
RUN mkdir -p /blade/gateway
WORKDIR /blade/gateway
EXPOSE 80
COPY ./target/blade-gateway.jar ./app.jar
ENTRYPOINT ["java", "--add-opens", "java.base/java.lang=ALL-UNNAMED", "--add-opens", "java.base/java.lang.reflect=ALL-UNNAMED", "-Djava.security.egd=file:/dev/./urandom", "-jar", "app.jar"]
CMD ["--spring.profiles.active=test"]

103
blade-gateway/pom.xml Normal file
View File

@@ -0,0 +1,103 @@
<?xml version="1.0" encoding="UTF-8"?>
<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>
<artifactId>BladeX</artifactId>
<groupId>org.springblade</groupId>
<version>${revision}</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>blade-gateway</artifactId>
<name>${project.artifactId}</name>
<packaging>jar</packaging>
<dependencies>
<!--Blade-->
<dependency>
<groupId>org.springblade</groupId>
<artifactId>blade-core-launch</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</exclusion>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-undertow</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springblade</groupId>
<artifactId>blade-common</artifactId>
<exclusions>
<exclusion>
<groupId>org.springblade</groupId>
<artifactId>blade-core-launch</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springblade</groupId>
<artifactId>blade-starter-metrics</artifactId>
</dependency>
<dependency>
<groupId>org.springblade</groupId>
<artifactId>blade-starter-jwt</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
</dependency>
<!--Spring-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway-server-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-starter-client</artifactId>
</dependency>
<!-- 开启knife4j -->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-gateway-spring-boot-starter</artifactId>
</dependency>
<!-- 开启swagger-ui -->
<!--<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webflux-ui</artifactId>
</dependency>-->
</dependencies>
<build>
<plugins>
<plugin>
<groupId>io.fabric8</groupId>
<artifactId>docker-maven-plugin</artifactId>
<configuration>
<skip>${docker.fabric.skip}</skip>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-antrun-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,48 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.gateway;
import org.springblade.core.launch.BladeApplication;
import org.springblade.core.launch.constant.AppConstant;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.scheduling.annotation.EnableScheduling;
/**
* 项目启动
*
* @author Chill
*/
@EnableScheduling
@EnableDiscoveryClient
@SpringBootApplication
public class GateWayApplication {
public static void main(String[] args) {
BladeApplication.run(AppConstant.APPLICATION_GATEWAY_NAME, GateWayApplication.class, args);
}
}

View File

@@ -0,0 +1,53 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.gateway.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springblade.gateway.handler.ErrorExceptionHandler;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.boot.autoconfigure.web.reactive.error.ErrorWebFluxAutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 异常处理配置类
*
* @author Chill
*/
@Configuration(proxyBeanMethods = false)
@AutoConfigureBefore(ErrorWebFluxAutoConfiguration.class)
@EnableConfigurationProperties({ServerProperties.class})
public class ErrorHandlerConfiguration {
@Bean
public ErrorExceptionHandler globalExceptionHandler(ObjectMapper objectMapper) {
return new ErrorExceptionHandler(objectMapper);
}
}

View File

@@ -0,0 +1,50 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.gateway.config;
import com.github.xiaoymin.knife4j.spring.gateway.Knife4jGatewayProperties;
import com.github.xiaoymin.knife4j.spring.gateway.discover.router.DiscoverClientRouteServiceConvert;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cloud.gateway.discovery.DiscoveryClientRouteDefinitionLocator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* knife4j自动聚合配置
*
* @author BladeX
*/
@Configuration(proxyBeanMethods = false)
public class ReactiveDiscoveryConfiguration {
@Bean
@ConditionalOnProperty(name = {"spring.cloud.gateway.server.webflux.discovery.locator.enabled"})
public DiscoverClientRouteServiceConvert discoverClientRouteServiceConvert(DiscoveryClientRouteDefinitionLocator discoveryClient,
Knife4jGatewayProperties knife4jGatewayProperties) {
return new DiscoverClientRouteServiceConvert(discoveryClient, knife4jGatewayProperties);
}
}

View File

@@ -0,0 +1,57 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.gateway.config;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springblade.gateway.filter.GatewayFilter;
import org.springblade.gateway.props.AuthProperties;
import org.springblade.gateway.props.RequestProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.server.WebFilter;
/**
* 路由配置信息
*
* @author Chill
*/
@Slf4j
@Configuration(proxyBeanMethods = false)
@AllArgsConstructor
@EnableConfigurationProperties({AuthProperties.class, RequestProperties.class})
public class RouterFunctionConfiguration {
/**
* 全局配置
*/
@Bean
public WebFilter gatewayFilter(RequestProperties requestProperties) {
return new GatewayFilter(requestProperties);
}
}

View File

@@ -0,0 +1,117 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.gateway.dynamic;
import org.springframework.cloud.gateway.event.RefreshRoutesEvent;
import org.springframework.cloud.gateway.route.RouteDefinition;
import org.springframework.cloud.gateway.route.RouteDefinitionWriter;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;
import java.util.List;
/**
* 动态路由业务类
*
* @author Chill
*/
@Service
public class DynamicRouteService implements ApplicationEventPublisherAware {
private final RouteDefinitionWriter routeDefinitionWriter;
private ApplicationEventPublisher publisher;
public DynamicRouteService(RouteDefinitionWriter routeDefinitionWriter) {
this.routeDefinitionWriter = routeDefinitionWriter;
}
@Override
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
this.publisher = applicationEventPublisher;
}
/**
* 增加路由
*/
public String save(RouteDefinition definition) {
try {
routeDefinitionWriter.save(Mono.just(definition)).subscribe();
this.publisher.publishEvent(new RefreshRoutesEvent(this));
return "save success";
} catch (Exception e) {
e.printStackTrace();
return "save failure";
}
}
/**
* 更新路由
*/
public String update(RouteDefinition definition) {
try {
this.routeDefinitionWriter.delete(Mono.just(definition.getId()));
this.routeDefinitionWriter.save(Mono.just(definition)).subscribe();
this.publisher.publishEvent(new RefreshRoutesEvent(this));
return "update success";
} catch (Exception e) {
e.printStackTrace();
return "update failure";
}
}
/**
* 更新路由
*/
public String updateList(List<RouteDefinition> routeDefinitions) {
try {
if (!routeDefinitions.isEmpty()) {
routeDefinitions.forEach(this::update);
}
return "update done";
} catch (Exception e) {
e.printStackTrace();
return "update failure";
}
}
/**
* 删除路由
*/
public String delete(String id) {
try {
this.routeDefinitionWriter.delete(Mono.just(id));
return "delete success";
} catch (Exception e) {
e.printStackTrace();
return "delete failure";
}
}
}

View File

@@ -0,0 +1,109 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.gateway.dynamic;
import com.alibaba.cloud.nacos.NacosConfigProperties;
import com.alibaba.cloud.nacos.NacosDiscoveryProperties;
import com.alibaba.fastjson.JSON;
import com.alibaba.nacos.api.NacosFactory;
import com.alibaba.nacos.api.PropertyKeyConst;
import com.alibaba.nacos.api.config.ConfigService;
import com.alibaba.nacos.api.config.listener.Listener;
import com.alibaba.nacos.api.exception.NacosException;
import lombok.extern.slf4j.Slf4j;
import org.springblade.core.launch.constant.NacosConstant;
import org.springblade.core.launch.props.BladeProperties;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.cloud.gateway.route.RouteDefinition;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Properties;
import java.util.concurrent.Executor;
/**
* 动态路由监听器
*
* @author Chill
*/
@Order
@Slf4j
@Component
@RefreshScope
public class DynamicRouteServiceListener {
private final DynamicRouteService dynamicRouteService;
private final NacosDiscoveryProperties nacosDiscoveryProperties;
private final NacosConfigProperties nacosConfigProperties;
private final BladeProperties bladeProperties;
public DynamicRouteServiceListener(DynamicRouteService dynamicRouteService, NacosDiscoveryProperties nacosDiscoveryProperties, NacosConfigProperties nacosConfigProperties, BladeProperties bladeProperties) {
this.dynamicRouteService = dynamicRouteService;
this.nacosDiscoveryProperties = nacosDiscoveryProperties;
this.nacosConfigProperties = nacosConfigProperties;
this.bladeProperties = bladeProperties;
dynamicRouteServiceListener();
}
/**
* 监听Nacos下发的动态路由配置
*/
private void dynamicRouteServiceListener() {
try {
String dataId = bladeProperties.getName() + "-" + bladeProperties.getEnv() + "." + NacosConstant.NACOS_CONFIG_JSON_FORMAT;
String group = nacosConfigProperties.getGroup();
Properties properties = new Properties();
properties.setProperty(PropertyKeyConst.SERVER_ADDR, nacosDiscoveryProperties.getServerAddr());
properties.setProperty(PropertyKeyConst.NAMESPACE, nacosDiscoveryProperties.getNamespace());
properties.setProperty(PropertyKeyConst.USERNAME, nacosDiscoveryProperties.getUsername());
properties.setProperty(PropertyKeyConst.PASSWORD, nacosDiscoveryProperties.getPassword());
ConfigService configService = NacosFactory.createConfigService(properties);
configService.addListener(dataId, group, new Listener() {
@Override
public void receiveConfigInfo(String configInfo) {
List<RouteDefinition> routeDefinitions = JSON.parseArray(configInfo, RouteDefinition.class);
if (!routeDefinitions.isEmpty()) {
dynamicRouteService.updateList(routeDefinitions);
}
}
@Override
public Executor getExecutor() {
return null;
}
});
String configInfo = configService.getConfig(dataId, group, 5000);
if (configInfo != null) {
List<RouteDefinition> routeDefinitions = JSON.parseArray(configInfo, RouteDefinition.class);
dynamicRouteService.updateList(routeDefinitions);
}
} catch (NacosException ignored) {
ignored.printStackTrace();
}
}
}

View File

@@ -0,0 +1,50 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.gateway.dynamic;
import lombok.Data;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* 过滤器定义模型
*
* @author Chill
*/
@Data
public class GatewayFilter {
/**
* 过滤器对应的Name
*/
private String name;
/**
* 对应的路由规则
*/
private Map<String, String> args = new LinkedHashMap<>();
}

View File

@@ -0,0 +1,50 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.gateway.dynamic;
import lombok.Data;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* 路由断言定义模型
*
* @author Chill
*/
@Data
public class GatewayPredicate {
/**
* 断言对应的Name
*/
private String name;
/**
* 配置的断言规则
*/
private Map<String, String> args = new LinkedHashMap<>();
}

View File

@@ -0,0 +1,66 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.gateway.dynamic;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
/**
* Gateway的路由定义模型
*
* @author Chill
*/
@Data
public class GatewayRoute {
/**
* 路由的id
*/
private String id;
/**
* 路由断言集合配置
*/
private List<GatewayPredicate> predicates = new ArrayList<>();
/**
* 路由过滤器集合配置
*/
private List<GatewayFilter> filters = new ArrayList<>();
/**
* 路由规则转发的目标uri
*/
private String uri;
/**
* 路由执行的顺序
*/
private int order = 0;
}

View File

@@ -0,0 +1,156 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.gateway.filter;
import com.alibaba.nacos.common.utils.StringUtils;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.jsonwebtoken.Claims;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springblade.core.jwt.JwtCrypto;
import org.springblade.core.jwt.JwtUtil;
import org.springblade.core.jwt.props.JwtProperties;
import org.springblade.core.launch.constant.TokenConstant;
import org.springblade.core.launch.props.BladeProperties;
import org.springblade.gateway.props.AuthProperties;
import org.springblade.gateway.provider.AuthProvider;
import org.springblade.gateway.provider.KeyProvider;
import org.springblade.gateway.provider.RequestProvider;
import org.springblade.gateway.provider.ResponseProvider;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.nio.charset.StandardCharsets;
import static org.springblade.core.jwt.JwtCrypto.BLADE_TOKEN_CRYPTO_KEY;
/**
* 鉴权认证
*
* @author Chill
*/
@Slf4j
@Component
@AllArgsConstructor
public class AuthFilter implements GlobalFilter, Ordered {
private static final String MSG_TOKEN_MISSING = "缺失令牌,鉴权失败";
private static final String MSG_REQUEST_UNAUTHORIZED = "请求未授权";
private static final String MSG_TOKEN_EXPIRED = "令牌已失效";
private final AuthProperties authProperties;
private final ObjectMapper objectMapper;
private final JwtProperties jwtProperties;
private final BladeProperties bladeProperties;
private final AntPathMatcher antPathMatcher = new AntPathMatcher();
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//校验 Token 放行
String originalRequestUrl = RequestProvider.getOriginalRequestUrl(exchange);
String path = exchange.getRequest().getURI().getPath();
if (isSkip(path) || isSkip(originalRequestUrl)) {
return chain.filter(exchange);
}
//校验 Token 合法性
ServerHttpResponse resp = exchange.getResponse();
String headerToken = exchange.getRequest().getHeaders().getFirst(AuthProvider.AUTH_KEY);
String paramToken = exchange.getRequest().getQueryParams().getFirst(AuthProvider.AUTH_KEY);
if (StringUtils.isBlank(headerToken) && StringUtils.isBlank(paramToken)) {
return unAuth(resp, MSG_TOKEN_MISSING);
}
String auth = StringUtils.isBlank(headerToken) ? paramToken : headerToken;
// 判断 auth 是否为空
if (StringUtils.isBlank(auth)) {
return unAuth(resp, MSG_REQUEST_UNAUTHORIZED);
}
// 校验 超级密钥 合法性,剩余属性交由微服务内部自行鉴权
if (KeyProvider.isApiKey(auth, bladeProperties)) {
return chain.filter(exchange);
}
String token = JwtUtil.getToken(auth);
// 判断 token 是否为空
if (StringUtils.isBlank(token)) {
return unAuth(resp, MSG_REQUEST_UNAUTHORIZED);
}
// 校验 加密Token 合法性
if (JwtUtil.isCrypto(auth)) {
token = JwtCrypto.decryptToString(token, bladeProperties.getEnvironment().getProperty(BLADE_TOKEN_CRYPTO_KEY));
}
Claims claims = JwtUtil.parseJWT(token);
if (token == null || claims == null) {
return unAuth(resp, MSG_REQUEST_UNAUTHORIZED);
}
// 判断 Token 状态
if (jwtProperties.getState()) {
String tenantId = String.valueOf(claims.get(TokenConstant.TENANT_ID));
String clientId = String.valueOf(claims.get(TokenConstant.CLIENT_ID));
String userId = String.valueOf(claims.get(TokenConstant.USER_ID));
String accessToken = JwtUtil.getAccessToken(tenantId, clientId, userId, token);
if (!token.equalsIgnoreCase(accessToken)) {
return unAuth(resp, MSG_TOKEN_EXPIRED);
}
}
return chain.filter(exchange);
}
private boolean isSkip(String path) {
return AuthProvider.getDefaultSkipUrl().stream().anyMatch(pattern -> antPathMatcher.match(pattern, path))
|| authProperties.getSkipUrl().stream().anyMatch(pattern -> antPathMatcher.match(pattern, path))
|| authProperties.getAuth().stream().anyMatch(auth -> antPathMatcher.match(auth.getPattern(), path))
|| authProperties.getBasic().stream().anyMatch(basic -> antPathMatcher.match(basic.getPattern(), path))
|| authProperties.getSign().stream().anyMatch(sign -> antPathMatcher.match(sign.getPattern(), path));
}
private Mono<Void> unAuth(ServerHttpResponse resp, String msg) {
resp.setStatusCode(HttpStatus.UNAUTHORIZED);
resp.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
String result = "";
try {
result = objectMapper.writeValueAsString(ResponseProvider.unAuth(msg));
} catch (JsonProcessingException e) {
log.error(e.getMessage(), e);
}
DataBuffer buffer = resp.bufferFactory().wrap(result.getBytes(StandardCharsets.UTF_8));
return resp.writeWith(Flux.just(buffer));
}
@Override
public int getOrder() {
return -100;
}
}

View File

@@ -0,0 +1,171 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.gateway.filter;
import lombok.RequiredArgsConstructor;
import org.springblade.gateway.props.RequestProperties;
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.lang.NonNull;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.PatternMatchUtils;
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;
import java.util.List;
import java.util.Objects;
/**
* 全局拦截器
*
* @author Chill
*/
@RequiredArgsConstructor
public class GatewayFilter implements WebFilter, Ordered {
/**
* 请求配置
*/
private final RequestProperties requestProperties;
/**
* 路径匹配
*/
private final AntPathMatcher antPathMatcher = new AntPathMatcher();
/**
* 默认拦截地址
*/
private final List<String> defaultBlockUrl = List.of("/**/actuator/**", "/health/**");
/**
* 默认白名单
*/
private final List<String> defaultWhiteList = List.of("127.0.0.1", "172.30.*.*", "192.168.*.*", "10.*.*.*", "0:0:0:0:0:0:0:1");
/**
* 默认提示信息
*/
private final static String DEFAULT_MESSAGE = "当前请求被拒绝,请联系管理员!";
/**
* 这里为支持的请求头如果有自定义的header字段请自己添加
*/
private static final String ALLOWED_HEADERS = "X-Requested-With, Tenant-Id, Blade-Auth, Content-Type, Authorization, credential, X-XSRF-TOKEN, token, username, client, knfie4j-gateway-request, knife4j-gateway-code, request-origion";
private static final String ALLOWED_METHODS = "GET,POST,PUT,DELETE,OPTIONS,HEAD";
private static final String ALLOWED_ORIGIN = "*";
private static final String ALLOWED_EXPOSE = "*";
private static final String MAX_AGE = "18000L";
@NonNull
@Override
public Mono<Void> filter(@NonNull ServerWebExchange exchange, @NonNull 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");
if (request.getMethod() == HttpMethod.OPTIONS) {
response.setStatusCode(HttpStatus.OK);
return Mono.empty();
}
}
// 处理黑白名单与拦截请求
if (requestProperties.getEnabled()) {
String path = request.getPath().value();
String ip = Objects.requireNonNull(request.getRemoteAddress()).getHostString();
if (isRequestBlock(path, ip)) {
throw new RuntimeException(DEFAULT_MESSAGE);
}
}
return chain.filter(exchange);
}
/**
* 是否白名单
*
* @param ip ip地址
* @return boolean
*/
private boolean isWhiteList(String ip) {
List<String> whiteList = requestProperties.getWhiteList();
String[] defaultWhiteIps = defaultWhiteList.toArray(new String[0]);
String[] whiteIps = whiteList.toArray(new String[0]);
return PatternMatchUtils.simpleMatch(defaultWhiteIps, ip) || PatternMatchUtils.simpleMatch(whiteIps, ip);
}
/**
* 是否黑名单
*
* @param ip ip地址
* @return boolean
*/
private boolean isBlackList(String ip) {
List<String> blackList = requestProperties.getBlackList();
String[] blackIps = blackList.toArray(new String[0]);
return PatternMatchUtils.simpleMatch(blackIps, ip);
}
/**
* 是否禁用请求访问
*
* @param path 请求路径
* @return boolean
*/
private boolean isRequestBlock(String path) {
List<String> blockUrl = requestProperties.getBlockUrl();
return defaultBlockUrl.stream().anyMatch(pattern -> antPathMatcher.match(pattern, path)) ||
blockUrl.stream().anyMatch(pattern -> antPathMatcher.match(pattern, path));
}
/**
* 是否拦截请求
*
* @param path 请求路径
* @param ip ip地址
* @return boolean
*/
private boolean isRequestBlock(String path, String ip) {
return (isRequestBlock(path) && !isWhiteList(ip)) || isBlackList(ip);
}
@Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE;
}
}

View File

@@ -0,0 +1,63 @@
package org.springblade.gateway.filter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.util.Arrays;
import java.util.stream.Collectors;
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR;
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.addOriginalRequestUrl;
/**
* <p>
* 全局拦截器,作用所有的微服务
* <p>
* 1. 对请求头中参数进行处理 from 参数进行清洗
* 2. 重写StripPrefix = 1,支持全局
*
* @author lengleng
*/
@Component
public class RequestFilter implements GlobalFilter, Ordered {
/**
* Process the Web request and (optionally) delegate to the next
* {@code WebFilter} through the given {@link GatewayFilterChain}.
*
* @param exchange the current server exchange
* @param chain provides a way to delegate to the next filter
* @return {@code Mono<Void>} to indicate when request processing is complete
*/
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 1. 清洗请求头中from 参数
ServerHttpRequest request = exchange.getRequest().mutate()
.headers(httpHeaders -> httpHeaders.remove("X"))
.build();
// 2. 重写StripPrefix
addOriginalRequestUrl(exchange, request.getURI());
String rawPath = request.getURI().getRawPath();
String newPath = "/" + Arrays.stream(StringUtils.tokenizeToStringArray(rawPath, "/"))
.skip(1L).collect(Collectors.joining("/"));
ServerHttpRequest newRequest = request.mutate()
.path(newPath)
.build();
exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, newRequest.getURI());
return chain.filter(exchange.mutate().request(newRequest.mutate().build()).build());
}
@Override
public int getOrder() {
return -1000;
}
}

View File

@@ -0,0 +1,121 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: DreamLu (596392912@qq.com)
*/
package org.springblade.gateway.filter;
import com.alibaba.nacos.common.utils.StringUtils;
import io.jsonwebtoken.Claims;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springblade.core.jwt.JwtUtil;
import org.springblade.gateway.provider.AuthProvider;
import org.springblade.gateway.provider.RequestProvider;
import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.http.HttpHeaders;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.util.ArrayList;
import java.util.List;
/**
* webflux 日志请求记录,方便开发调试。请求日志过滤器排序尽量低。
*
* <p>
* 注意:暂时不支持结构体打印,想实现,请看下面的链接。
* https://stackoverflow.com/questions/45240005/how-to-log-request-and-response-bodies-in-spring-webflux
* https://github.com/Silvmike/webflux-demo/blob/master/tests/src/test/java/ru/hardcoders/demo/webflux/web_handler/filters/logging
* </p>
*
* @author dream.lu
*/
@Slf4j
@Configuration(proxyBeanMethods = false)
@RequiredArgsConstructor
@ConditionalOnProperty(value = "blade.log.request.enabled", havingValue = "true", matchIfMissing = true)
public class RequestLogFilter implements GlobalFilter, Ordered {
private final WebEndpointProperties endpointProperties;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
// 打印请求路径
String path = request.getPath().pathWithinApplication().value();
// 忽略 endpoint 请求
String endpointBasePath = endpointProperties.getBasePath();
if (StringUtils.isNotBlank(endpointBasePath) && path.startsWith(endpointBasePath)) {
return chain.filter(exchange);
}
String requestUrl = RequestProvider.getOriginalRequestUrl(exchange);
// 构建成一条长 日志,避免并发下日志错乱
StringBuilder beforeReqLog = new StringBuilder(300);
// 日志参数
List<Object> beforeReqArgs = new ArrayList<>();
beforeReqLog.append("\n\n================ Gateway Request Start ================\n");
// 打印路由
beforeReqLog.append("===> {}: {}\n");
// 参数
String requestMethod = request.getMethod().name();
beforeReqArgs.add(requestMethod);
beforeReqArgs.add(requestUrl);
// 打印请求头
HttpHeaders headers = request.getHeaders();
headers.forEach((headerName, headerValue) -> {
beforeReqLog.append("===Headers=== {}: {}\n");
beforeReqArgs.add(headerName);
if (AuthProvider.AUTH_KEY.toLowerCase().equals(headerName)) {
String value = headerValue.get(0);
String token = JwtUtil.getToken(value);
Claims claims = JwtUtil.parseJWT(token);
beforeReqArgs.add((claims == null) ? "" : claims.toString());
beforeReqLog.append("===Headers=== {}: {}\n");
beforeReqArgs.add(headerName.concat("-original"));
beforeReqArgs.add(headerValue.toArray());
} else {
beforeReqArgs.add(headerValue.toArray());
}
});
beforeReqLog.append("================ Gateway Request End =================\n");
// 打印执行时间
log.info(beforeReqLog.toString(), beforeReqArgs.toArray());
return chain.filter(exchange);
}
@Override
public int getOrder() {
return Ordered.LOWEST_PRECEDENCE;
}
}

View File

@@ -0,0 +1,108 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: DreamLu (596392912@qq.com)
*/
package org.springblade.gateway.filter;
import com.alibaba.nacos.common.utils.StringUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.http.HttpHeaders;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.util.MultiValueMap;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.util.UriComponentsBuilder;
import reactor.core.publisher.Mono;
import java.util.ArrayList;
import java.util.List;
/**
* webflux 相应日志,方便开发调试,注意排序要优先。
*
* @author dream.lu
*/
@Slf4j
@Configuration(proxyBeanMethods = false)
@RequiredArgsConstructor
@ConditionalOnProperty(value = "blade.log.request.enabled", havingValue = "true", matchIfMissing = true)
public class ResponseLogFilter implements GlobalFilter, Ordered {
private final WebEndpointProperties endpointProperties;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
// 打印请求路径
String path = request.getPath().pathWithinApplication().value();
// 忽略 endpoint 请求
String endpointBasePath = endpointProperties.getBasePath();
if (StringUtils.isNotBlank(endpointBasePath) && path.startsWith(endpointBasePath)) {
return chain.filter(exchange);
}
return chain.filter(exchange).then(
Mono.fromRunnable(() -> {
MultiValueMap<String, String> queryParams = request.getQueryParams();
String requestUrl = UriComponentsBuilder.fromPath(path).queryParams(queryParams).build().toUriString();
// 构建成一条长 日志,避免并发下日志错乱
StringBuilder responseLog = new StringBuilder(300);
// 日志参数
List<Object> responseArgs = new ArrayList<>();
responseLog.append("\n\n================ Gateway Response Start ================\n");
ServerHttpResponse response = exchange.getResponse();
// 打印路由 200 get: /api/xxx/xxx
responseLog.append("<=== {} {}: {}\n");
// 参数
String requestMethod = request.getMethod().name();
responseArgs.add(response.getStatusCode().value());
responseArgs.add(requestMethod);
responseArgs.add(requestUrl);
// 打印请求头
HttpHeaders headers = response.getHeaders();
headers.forEach((headerName, headerValue) -> {
responseLog.append("===Headers=== {}: {}\n");
responseArgs.add(headerName);
responseArgs.add(headerValue.toArray());
});
responseLog.append("================ Gateway Response End =================\n");
// 打印执行时间
log.info(responseLog.toString(), responseArgs.toArray());
})
);
}
@Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE;
}
}

View File

@@ -0,0 +1,101 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.gateway.handler;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import org.springblade.gateway.provider.ResponseProvider;
import org.springframework.boot.web.reactive.error.ErrorWebExceptionHandler;
import org.springframework.core.annotation.Order;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.web.server.ResponseStatusException;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.util.Map;
/**
* 异常处理
*
* @author Chill
*/
@Order(-1)
@RequiredArgsConstructor
public class ErrorExceptionHandler implements ErrorWebExceptionHandler {
private final ObjectMapper objectMapper;
@Override
public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
if (response.isCommitted()) {
return Mono.error(ex);
}
response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
if (ex instanceof ResponseStatusException) {
response.setStatusCode(((ResponseStatusException) ex).getStatusCode());
}
return response.writeWith(Mono.fromSupplier(() -> {
DataBufferFactory bufferFactory = response.bufferFactory();
try {
int status = 500;
if (response.getStatusCode() != null) {
status = response.getStatusCode().value();
}
Map<String, Object> result = ResponseProvider.response(status, this.buildMessage(request, ex));
return bufferFactory.wrap(objectMapper.writeValueAsBytes(result));
} catch (JsonProcessingException e) {
return bufferFactory.wrap(new byte[0]);
}
}));
}
/**
* 构建异常信息
*/
private String buildMessage(ServerHttpRequest request, Throwable ex) {
StringBuilder message = new StringBuilder("Failed to handle request [");
message.append(request.getMethod().name());
message.append(" ");
message.append(request.getURI());
message.append("]");
if (ex != null) {
message.append(": ");
message.append(ex.getMessage());
}
return message.toString();
}
}

View File

@@ -0,0 +1,68 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.gateway.props;
import lombok.Data;
import org.springblade.gateway.provider.AuthSecure;
import org.springblade.gateway.provider.BasicSecure;
import org.springblade.gateway.provider.SignSecure;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import java.util.ArrayList;
import java.util.List;
/**
* 权限过滤
*
* @author Chill
*/
@Data
@RefreshScope
@ConfigurationProperties("blade.secure")
public class AuthProperties {
/**
* 放行API集合
*/
private final List<String> skipUrl = new ArrayList<>();
/**
* 自定义授权配置
*/
private final List<AuthSecure> auth = new ArrayList<>();
/**
* 基础认证配置
*/
private final List<BasicSecure> basic = new ArrayList<>();
/**
* 签名认证配置
*/
private final List<SignSecure> sign = new ArrayList<>();
}

View File

@@ -0,0 +1,68 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.gateway.props;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import java.util.ArrayList;
import java.util.List;
/**
* Request配置类
*
* @author Chill
*/
@Data
@ConfigurationProperties("blade.request")
public class RequestProperties {
/**
* 开启自定义request
*/
private Boolean enabled = true;
/**
* 放行url
*/
private List<String> skipUrl = new ArrayList<>();
/**
* 禁用url
*/
private List<String> blockUrl = new ArrayList<>();
/**
* 白名单支持通配符例如10.20.0.8*、10.20.0.*
*/
private List<String> whiteList = new ArrayList<>();
/**
* 黑名单支持通配符例如10.20.0.8*、10.20.0.*
*/
private List<String> blackList = new ArrayList<>();
}

View File

@@ -0,0 +1,72 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.gateway.provider;
import org.springblade.core.launch.constant.TokenConstant;
import java.util.ArrayList;
import java.util.List;
/**
* 鉴权配置
*
* @author Chill
*/
public class AuthProvider {
public static final String AUTH_KEY = TokenConstant.AUTH_HEADER;
private static final List<String> DEFAULT_SKIP_URL = new ArrayList<>();
static {
DEFAULT_SKIP_URL.add("/example");
DEFAULT_SKIP_URL.add("/oauth/token/**");
DEFAULT_SKIP_URL.add("/oauth/behavior/**");
DEFAULT_SKIP_URL.add("/oauth/captcha/**");
DEFAULT_SKIP_URL.add("/oauth/sms/**");
DEFAULT_SKIP_URL.add("/oauth/clear-cache/**");
DEFAULT_SKIP_URL.add("/oauth/user-info");
DEFAULT_SKIP_URL.add("/oauth/render/**");
DEFAULT_SKIP_URL.add("/oauth/callback/**");
DEFAULT_SKIP_URL.add("/oauth/revoke/**");
DEFAULT_SKIP_URL.add("/oauth/refresh/**");
DEFAULT_SKIP_URL.add("/token/**");
DEFAULT_SKIP_URL.add("/actuator/**");
DEFAULT_SKIP_URL.add("/v3/api-docs/**");
DEFAULT_SKIP_URL.add("/tenant/info");
DEFAULT_SKIP_URL.add("/process/resource-view");
DEFAULT_SKIP_URL.add("/process/diagram-view");
DEFAULT_SKIP_URL.add("/manager/check-upload");
DEFAULT_SKIP_URL.add("/assets/**");
}
/**
* 默认无需鉴权的API
*/
public static List<String> getDefaultSkipUrl() {
return DEFAULT_SKIP_URL;
}
}

View File

@@ -0,0 +1,46 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.gateway.provider;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 自定义授权规则
*
* @author Chill
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class AuthSecure {
/**
* 请求路径
*/
private String pattern;
}

View File

@@ -0,0 +1,46 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.gateway.provider;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 基础授权规则
*
* @author Chill
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class BasicSecure {
/**
* 请求路径
*/
private String pattern;
}

View File

@@ -0,0 +1,303 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.gateway.provider;
import org.springblade.core.launch.props.BladeProperties;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.ThreadLocalRandom;
/**
* 超级密钥加解密工具类
*
* @author BladeX
*/
public class KeyProvider {
public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
/**
* API Key 前缀
*/
private static final String API_KEY_PREFIX = "ak-";
/**
* API Key 配置项
*/
private static final String BLADE_KEY_ENABLED = "blade.key.enabled";
/**
* API Key 配置项
*/
private static final String BLADE_KEY_CRYPTO_KEY = "blade.key.crypto-key";
/**
* 随机字符串因子
*/
private static final String RANDOM_FACTOR = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
/**
* 安全随机数生成器
*/
private static final SecureRandom SECURE_RANDOM = new SecureRandom();
/**
* PKCS7 块大小
*/
private static final int BLOCK_SIZE = 16;
/**
* Hex 编码字符
*/
private static final byte[] HEX_DIGITS = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
/**
* 判断是否为合法 API Key
*
* @param auth 认证字符串
* @return 是否为 API Key
*/
public static boolean isApiKey(@Nullable String auth, BladeProperties bladeProperties) {
// 检查功能是否启用
String enabled = bladeProperties.getEnvironment().getProperty(BLADE_KEY_ENABLED);
if (!Boolean.parseBoolean(enabled)) {
return false;
}
// 检查前缀并解析
if (auth != null && auth.startsWith(API_KEY_PREFIX)) {
String cryptoKey = bladeProperties.getEnvironment().getProperty(BLADE_KEY_CRYPTO_KEY);
String parsed = parseKey(auth, cryptoKey);
return parsed != null;
}
return false;
}
/**
* 生成 API Key
*
* @param cryptoKey 加密密钥
* @return 带前缀的加密 Key
*/
public static String generateKey(String cryptoKey) {
return API_KEY_PREFIX + encryptToHex(randomUUID(), cryptoKey);
}
/**
* 解析 API Key
*
* @param key 带前缀的加密 Key
* @param cryptoKey 加密密钥
* @return 解密后的原始值
*/
@Nullable
public static String parseKey(@Nullable String key, String cryptoKey) {
if (isBlank(key) || !key.startsWith(API_KEY_PREFIX)) {
return null;
}
try {
String encryptedPart = key.substring(API_KEY_PREFIX.length());
return decryptFormHexToString(encryptedPart, cryptoKey);
} catch (Exception e) {
return null;
}
}
/**
* 加密并转换为十六进制字符串
*
* @param content 明文内容
* @param aesTextKey AES 密钥字符串
* @return 十六进制加密字符串
*/
public static String encryptToHex(String content, String aesTextKey) {
return hexEncode(encrypt(content.getBytes(DEFAULT_CHARSET), aesTextKey));
}
/**
* 从十六进制字符串解密并返回明文字符串
*
* @param content 十六进制加密字符串
* @param aesTextKey AES 密钥字符串
* @return 解密后的明文字符串
*/
@Nullable
public static String decryptFormHexToString(@Nullable String content, String aesTextKey) {
byte[] hexBytes = decryptFormHex(content, aesTextKey);
if (hexBytes == null) {
return null;
}
return new String(hexBytes, DEFAULT_CHARSET);
}
/**
* 从十六进制字符串解密并返回字节数组
*
* @param content 十六进制加密字符串
* @param aesTextKey AES 密钥字符串
* @return 解密后的字节数组
*/
@Nullable
public static byte[] decryptFormHex(@Nullable String content, String aesTextKey) {
if (isBlank(content)) {
return null;
}
return decrypt(hexDecode(content.getBytes(DEFAULT_CHARSET)), aesTextKey);
}
/**
* 加密字节数组
*
* @param content 明文字节数组
* @param aesTextKey AES 密钥字符串
* @return 加密后的字节数组
*/
public static byte[] encrypt(byte[] content, String aesTextKey) {
return aes(pkcs7Encode(content), Objects.requireNonNull(aesTextKey).getBytes(DEFAULT_CHARSET), Cipher.ENCRYPT_MODE);
}
/**
* 解密字节数组
*
* @param content 加密的字节数组
* @param aesTextKey AES 密钥字符串
* @return 解密后的字节数组
*/
public static byte[] decrypt(byte[] content, String aesTextKey) {
return pkcs7Decode(aes(content, Objects.requireNonNull(aesTextKey).getBytes(DEFAULT_CHARSET), Cipher.DECRYPT_MODE));
}
/**
* AES 加解密核心方法
*/
private static byte[] aes(byte[] data, byte[] aesKey, int mode) {
Assert.isTrue(aesKey.length == 32, "IllegalAesKey, aesKey's length must be 32");
try {
Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
SecretKeySpec keySpec = new SecretKeySpec(aesKey, "AES");
IvParameterSpec iv = new IvParameterSpec(Arrays.copyOfRange(aesKey, 0, 16));
cipher.init(mode, keySpec, iv);
return cipher.doFinal(data);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
// ==================== 内联工具方法 ====================
/**
* 生成指定长度的随机字符串
*/
public static String randomKey(int count) {
char[] buffer = new char[count];
for (int i = 0; i < count; i++) {
buffer[i] = RANDOM_FACTOR.charAt(SECURE_RANDOM.nextInt(RANDOM_FACTOR.length()));
}
return new String(buffer);
}
/**
* 生成无横线的 UUID
*/
private static String randomUUID() {
ThreadLocalRandom random = ThreadLocalRandom.current();
return new UUID(random.nextLong(), random.nextLong()).toString().replace("-", "");
}
/**
* 检查字符串是否为空或空白
*/
private static boolean isBlank(@Nullable String str) {
if (str == null || str.isEmpty()) {
return true;
}
for (int i = 0; i < str.length(); i++) {
if (!Character.isWhitespace(str.charAt(i))) {
return false;
}
}
return true;
}
/**
* 字节数组转十六进制字符串
*/
private static String hexEncode(byte[] data) {
int len = data.length;
byte[] out = new byte[len << 1];
for (int i = 0, j = 0; i < len; i++) {
out[j++] = HEX_DIGITS[(0xF0 & data[i]) >>> 4];
out[j++] = HEX_DIGITS[0x0F & data[i]];
}
return new String(out, DEFAULT_CHARSET);
}
/**
* 十六进制字节数组解码为原始字节数组
*/
private static byte[] hexDecode(byte[] data) {
int len = data.length;
if ((len & 0x01) != 0) {
throw new IllegalArgumentException("hexBinary needs to be even-length: " + len);
}
byte[] out = new byte[len >> 1];
for (int i = 0, j = 0; j < len; i++) {
int f = Character.digit(data[j++], 16) << 4;
f |= Character.digit(data[j++], 16);
out[i] = (byte) (f & 0xFF);
}
return out;
}
/**
* PKCS7 编码(填充)
*/
private static byte[] pkcs7Encode(byte[] src) {
int count = src.length;
int amountToPad = BLOCK_SIZE - (count % BLOCK_SIZE);
byte pad = (byte) (amountToPad & 0xFF);
byte[] dest = new byte[count + amountToPad];
System.arraycopy(src, 0, dest, 0, count);
Arrays.fill(dest, count, dest.length, pad);
return dest;
}
/**
* PKCS7 解码(去填充)
*/
private static byte[] pkcs7Decode(byte[] decrypted) {
int pad = decrypted[decrypted.length - 1];
if (pad < 1 || pad > BLOCK_SIZE) {
pad = 0;
}
if (pad > 0) {
return Arrays.copyOfRange(decrypted, 0, decrypted.length - pad);
}
return decrypted;
}
}

View File

@@ -0,0 +1,58 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.gateway.provider;
import org.springframework.cloud.gateway.support.ServerWebExchangeUtils;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.util.MultiValueMap;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.util.UriComponentsBuilder;
import java.net.URI;
import java.util.LinkedHashSet;
/**
* RequestProvider
*
* @author Chill
*/
public class RequestProvider {
/**
* 获取原始url
*
* @param exchange
* @return
*/
public static String getOriginalRequestUrl(ServerWebExchange exchange) {
ServerHttpRequest request = exchange.getRequest();
LinkedHashSet<URI> uris = exchange.getRequiredAttribute(ServerWebExchangeUtils.GATEWAY_ORIGINAL_REQUEST_URL_ATTR);
URI requestUri = uris.stream().findFirst().orElse(request.getURI());
MultiValueMap<String, String> queryParams = request.getQueryParams();
return UriComponentsBuilder.fromPath(requestUri.getRawPath()).queryParams(queryParams).build().toUriString();
}
}

View File

@@ -0,0 +1,93 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.gateway.provider;
import java.util.HashMap;
import java.util.Map;
/**
* 请求响应返回
*
* @author Chill
*/
public class ResponseProvider {
/**
* 成功
*
* @param message 信息
* @return
*/
public static Map<String, Object> success(String message) {
return response(200, message);
}
/**
* 失败
*
* @param message 信息
* @return
*/
public static Map<String, Object> fail(String message) {
return response(400, message);
}
/**
* 未授权
*
* @param message 信息
* @return
*/
public static Map<String, Object> unAuth(String message) {
return response(401, message);
}
/**
* 服务器异常
*
* @param message 信息
* @return
*/
public static Map<String, Object> error(String message) {
return response(500, message);
}
/**
* 构建返回的JSON数据格式
*
* @param status 状态码
* @param message 信息
* @return
*/
public static Map<String, Object> response(int status, String message) {
Map<String, Object> map = new HashMap<>(16);
map.put("code", status);
map.put("msg", message);
map.put("data", null);
return map;
}
}

View File

@@ -0,0 +1,46 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.gateway.provider;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 签名授权规则
*
* @author Chill
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class SignSecure {
/**
* 请求路径
*/
private String pattern;
}

View File

@@ -0,0 +1,11 @@
blade:
#多团队协作服务配置
loadbalancer:
#开启配置
enabled: true
#灰度版本
#version: 3.0.0
#负载均衡优先调用的ip段
prior-ip-pattern:
- 192.168.0.*
- 127.0.0.1

View File

@@ -0,0 +1,11 @@
blade:
#多团队协作服务配置
loadbalancer:
#开启配置
enabled: true
#灰度版本
#version: 3.0.0
#负载均衡优先调用的ip段
prior-ip-pattern:
- 192.168.0.*
- 127.0.0.1

View File

@@ -0,0 +1,11 @@
blade:
#多团队协作服务配置
loadbalancer:
#开启配置
enabled: true
#灰度版本
#version: 3.0.0
#负载均衡优先调用的ip段
prior-ip-pattern:
- 192.168.0.*
- 127.0.0.1

View File

@@ -0,0 +1,16 @@
knife4j:
gateway:
enabled: true
tags-sorter: order
operations-sorter: order
# 指定服务发现的模式聚合微服务文档,并且是默认`default`分组
strategy: discover
discover:
enabled: true
# 指定版本号(Swagger2|OpenAPI3)
version : openapi3
# 需要排除的微服务(eg:网关服务)
excluded-services:
- blade-admin
- blade-gateway
- blade-log

View File

@@ -0,0 +1,23 @@
server:
port: 80
spring:
cloud:
gateway:
server:
webflux:
discovery:
locator:
enabled: true
loadbalancer:
retry:
enabled: true
# 兼容性说明新版gateway配置需要最后启动才能发现服务否则会出现404的问题
# 此问题可等spring官方修复或临时处理方案采用下方旧版配置即可自动发现服务
#spring:
# cloud:
# gateway:
# discovery:
# locator:
# enabled: true