第06章 - 网关服务详解
第06章 - 网关服务详解
6.1 网关概述
6.1.1 网关的作用
Spring Cloud Gateway是RuoYi-Cloud的统一入口,承担着微服务架构中的核心职责:
外部请求
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ Spring Cloud Gateway │
│ (ruoyi-gateway) │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 核心功能 │ │
│ ├───────────────┬───────────────┬───────────────┬───────────────────┤ │
│ │ 路由转发 │ 负载均衡 │ 协议转换 │ 服务发现 │ │
│ └───────────────┴───────────────┴───────────────┴───────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 安全功能 │ │
│ ├───────────────┬───────────────┬───────────────┬───────────────────┤ │
│ │ 身份认证 │ 权限校验 │ XSS过滤 │ 黑名单过滤 │ │
│ └───────────────┴───────────────┴───────────────┴───────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 流量控制 │ │
│ ├───────────────┬───────────────┬───────────────┬───────────────────┤ │
│ │ 限流控制 │ 熔断降级 │ 请求聚合 │ 灰度发布 │ │
│ └───────────────┴───────────────┴───────────────┴───────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
│
┌───────────────────────────────┼───────────────────────────────┐
│ │ │
▼ ▼ ▼
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
│ ruoyi-auth │ │ ruoyi-system │ │ ruoyi-gen │
└───────────────┘ └───────────────┘ └───────────────┘
6.1.2 项目结构
ruoyi-gateway/
├── src/main/java/com/ruoyi/gateway/
│ ├── RuoYiGatewayApplication.java # 启动类
│ ├── config/
│ │ ├── GatewayConfig.java # 网关配置
│ │ ├── RouterFunctionConfiguration.java
│ │ ├── SwaggerProvider.java # Swagger聚合配置
│ │ └── properties/
│ │ ├── CaptchaProperties.java # 验证码配置
│ │ ├── IgnoreWhiteProperties.java # 白名单配置
│ │ └── XssProperties.java # XSS配置
│ ├── filter/
│ │ ├── AuthFilter.java # 认证过滤器
│ │ ├── BlackListUrlFilter.java # 黑名单过滤器
│ │ ├── CacheRequestFilter.java # 请求缓存过滤器
│ │ ├── ValidateCodeFilter.java # 验证码过滤器
│ │ └── XssFilter.java # XSS过滤器
│ ├── handler/
│ │ ├── GatewayExceptionHandler.java # 异常处理器
│ │ ├── SentinelFallbackHandler.java # Sentinel降级处理
│ │ └── ValidateCodeHandler.java # 验证码处理器
│ └── service/
│ └── ValidateCodeService.java # 验证码服务
└── src/main/resources/
└── bootstrap.yml # 启动配置
6.2 路由配置
6.2.1 路由配置详解
spring:
cloud:
gateway:
discovery:
locator:
# 开启从注册中心动态创建路由的功能
enabled: true
# 服务名小写
lower-case-service-id: true
routes:
# 认证中心
- id: ruoyi-auth
uri: lb://ruoyi-auth
predicates:
- Path=/auth/**
filters:
- CacheRequestFilter
- ValidateCodeFilter
- StripPrefix=1
# 代码生成
- id: ruoyi-gen
uri: lb://ruoyi-gen
predicates:
- Path=/code/**
filters:
- StripPrefix=1
# 定时任务
- id: ruoyi-job
uri: lb://ruoyi-job
predicates:
- Path=/schedule/**
filters:
- StripPrefix=1
# 系统模块
- id: ruoyi-system
uri: lb://ruoyi-system
predicates:
- Path=/system/**
filters:
- StripPrefix=1
# 文件服务
- id: ruoyi-file
uri: lb://ruoyi-file
predicates:
- Path=/file/**
filters:
- StripPrefix=1
6.2.2 路由核心概念
Route(路由)
路由是网关的基本构建块,由ID、目标URI、谓词集合和过滤器集合组成。
Predicate(谓词)
用于匹配HTTP请求的条件,常用谓词:
| 谓词类型 | 说明 | 示例 |
|---|---|---|
| Path | 路径匹配 | Path=/api/** |
| Method | HTTP方法匹配 | Method=GET,POST |
| Header | 请求头匹配 | Header=X-Request-Id,\d+ |
| Query | 查询参数匹配 | Query=name,zhangsan |
| Cookie | Cookie匹配 | Cookie=sessionId,abc |
| Host | 主机名匹配 | Host=**.ruoyi.vip |
| Before | 时间之前 | Before=2023-12-31T23:59:59 |
| After | 时间之后 | After=2023-01-01T00:00:00 |
| Between | 时间区间 | Between=2023-01-01,2023-12-31 |
| Weight | 权重路由 | Weight=group1,8 |
Filter(过滤器)
过滤器允许以某种方式修改传入的HTTP请求或返回的HTTP响应。
filters:
# 路径前缀去除
- StripPrefix=1
# 路径添加前缀
- PrefixPath=/api
# 添加请求头
- AddRequestHeader=X-Request-red, blue
# 添加响应头
- AddResponseHeader=X-Response-Red, Blue
# 请求重试
- name: Retry
args:
retries: 3
statuses: BAD_GATEWAY
# 限流
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 10
redis-rate-limiter.burstCapacity: 20
6.3 过滤器详解
6.3.1 认证过滤器 AuthFilter
@Component
public class AuthFilter implements GlobalFilter, Ordered {
private static final Logger log = LoggerFactory.getLogger(AuthFilter.class);
// 排除过滤的 uri 地址,nacos自行配置
@Autowired
private IgnoreWhiteProperties ignoreWhite;
@Autowired
private RedisService redisService;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
ServerHttpRequest.Builder mutate = request.mutate();
String url = request.getURI().getPath();
// 跳过不需要验证的路径
if (StringUtils.matches(url, ignoreWhite.getWhites())) {
return chain.filter(exchange);
}
String token = getToken(request);
if (StringUtils.isEmpty(token)) {
return unauthorizedResponse(exchange, "令牌不能为空");
}
Claims claims = JwtUtils.parseToken(token);
if (claims == null) {
return unauthorizedResponse(exchange, "令牌已过期或验证不正确!");
}
String userkey = JwtUtils.getUserKey(claims);
boolean islogin = redisService.hasKey(getTokenKey(userkey));
if (!islogin) {
return unauthorizedResponse(exchange, "登录状态已过期");
}
String userid = JwtUtils.getUserId(claims);
String username = JwtUtils.getUserName(claims);
if (StringUtils.isEmpty(userid) || StringUtils.isEmpty(username)) {
return unauthorizedResponse(exchange, "令牌验证失败");
}
// 设置用户信息到请求
addHeader(mutate, SecurityConstants.USER_KEY, userkey);
addHeader(mutate, SecurityConstants.DETAILS_USER_ID, userid);
addHeader(mutate, SecurityConstants.DETAILS_USERNAME, username);
// 内部请求来源参数清除
removeHeader(mutate, SecurityConstants.FROM_SOURCE);
return chain.filter(exchange.mutate().request(mutate.build()).build());
}
private void addHeader(ServerHttpRequest.Builder mutate, String name, Object value) {
if (value == null) {
return;
}
String valueStr = value.toString();
String valueEncode = ServletUtils.urlEncode(valueStr);
mutate.header(name, valueEncode);
}
private void removeHeader(ServerHttpRequest.Builder mutate, String name) {
mutate.headers(httpHeaders -> httpHeaders.remove(name)).build();
}
private Mono<Void> unauthorizedResponse(ServerWebExchange exchange, String msg) {
log.error("[鉴权异常处理]请求路径:{}", exchange.getRequest().getPath());
return ServletUtils.webFluxResponseWriter(exchange.getResponse(), msg, HttpStatus.UNAUTHORIZED);
}
/**
* 获取缓存key
*/
private String getTokenKey(String token) {
return CacheConstants.LOGIN_TOKEN_KEY + token;
}
/**
* 获取请求token
*/
private String getToken(ServerHttpRequest request) {
String token = request.getHeaders().getFirst(TokenConstants.AUTHENTICATION);
// 如果前端设置了令牌前缀,则裁剪掉前缀
if (StringUtils.isNotEmpty(token) && token.startsWith(TokenConstants.PREFIX)) {
token = token.replaceFirst(TokenConstants.PREFIX, StringUtils.EMPTY);
}
return token;
}
@Override
public int getOrder() {
return -200;
}
}
6.3.2 验证码过滤器 ValidateCodeFilter
@Component
public class ValidateCodeFilter extends AbstractGatewayFilterFactory<Object> {
private final static String[] VALIDATE_URL = new String[] { "/auth/login", "/auth/register" };
@Autowired
private ValidateCodeService validateCodeService;
@Autowired
private CaptchaProperties captchaProperties;
private static final String CODE = "code";
private static final String UUID = "uuid";
@Override
public GatewayFilter apply(Object config) {
return (exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest();
// 非登录/注册请求或验证码关闭,不处理
if (!StringUtils.equalsAnyIgnoreCase(request.getURI().getPath(), VALIDATE_URL)
|| !captchaProperties.getEnabled()) {
return chain.filter(exchange);
}
try {
String rspStr = resolveBodyFromRequest(request);
JSONObject obj = JSON.parseObject(rspStr);
validateCodeService.checkCaptcha(obj.getString(CODE), obj.getString(UUID));
} catch (Exception e) {
return ServletUtils.webFluxResponseWriter(exchange.getResponse(), e.getMessage());
}
return chain.filter(exchange);
};
}
private String resolveBodyFromRequest(ServerHttpRequest request) {
// 获取请求体中的内容
Flux<DataBuffer> body = request.getBody();
AtomicReference<String> bodyRef = new AtomicReference<>();
body.subscribe(buffer -> {
CharBuffer charBuffer = StandardCharsets.UTF_8.decode(buffer.asByteBuffer());
DataBufferUtils.release(buffer);
bodyRef.set(charBuffer.toString());
});
return bodyRef.get();
}
}
6.3.3 XSS过滤器 XssFilter
@Component
@ConditionalOnProperty(value = "security.xss.enabled", havingValue = "true")
public class XssFilter implements GlobalFilter, Ordered {
@Autowired
private XssProperties xss;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
// GET DELETE 不过滤
HttpMethod method = request.getMethod();
if (method == null || method == HttpMethod.GET || method == HttpMethod.DELETE) {
return chain.filter(exchange);
}
// 非json类型,不过滤
if (!isJsonRequest(exchange)) {
return chain.filter(exchange);
}
// excludeUrls 不过滤
String url = request.getURI().getPath();
if (StringUtils.matches(url, xss.getExcludeUrls())) {
return chain.filter(exchange);
}
ServerHttpRequestDecorator httpRequestDecorator = requestDecorator(exchange);
return chain.filter(exchange.mutate().request(httpRequestDecorator).build());
}
private ServerHttpRequestDecorator requestDecorator(ServerWebExchange exchange) {
ServerHttpRequestDecorator serverHttpRequestDecorator = new ServerHttpRequestDecorator(
exchange.getRequest()) {
@Override
public Flux<DataBuffer> getBody() {
Flux<DataBuffer> body = super.getBody();
return body.buffer().map(dataBuffers -> {
DataBufferFactory dataBufferFactory = new DefaultDataBufferFactory();
DataBuffer join = dataBufferFactory.join(dataBuffers);
byte[] content = new byte[join.readableByteCount()];
join.read(content);
DataBufferUtils.release(join);
String bodyStr = new String(content, StandardCharsets.UTF_8);
// XSS过滤
bodyStr = EscapeUtil.clean(bodyStr);
// 转成字节
byte[] bytes = bodyStr.getBytes();
NettyDataBufferFactory nettyDataBufferFactory = new NettyDataBufferFactory(
ByteBufAllocator.DEFAULT);
DataBuffer buffer = nettyDataBufferFactory.allocateBuffer(bytes.length);
buffer.write(bytes);
return buffer;
});
}
@Override
public HttpHeaders getHeaders() {
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.putAll(super.getHeaders());
// 由于修改了请求体的body,导致content-length长度不确定,因此需要删除原先的content-length
httpHeaders.remove(HttpHeaders.CONTENT_LENGTH);
httpHeaders.set(HttpHeaders.TRANSFER_ENCODING, "chunked");
return httpHeaders;
}
};
return serverHttpRequestDecorator;
}
/**
* 是否是Json请求
*/
public boolean isJsonRequest(ServerWebExchange exchange) {
String contentType = exchange.getRequest().getHeaders().getFirst(HttpHeaders.CONTENT_TYPE);
if (StringUtils.isNotEmpty(contentType)) {
return StringUtils.startsWithIgnoreCase(contentType, MediaType.APPLICATION_JSON_VALUE);
}
return false;
}
@Override
public int getOrder() {
return -100;
}
}
6.3.4 黑名单过滤器 BlackListUrlFilter
@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 ServletUtils.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));
});
}
}
}
6.3.5 请求缓存过滤器 CacheRequestFilter
@Component
public class CacheRequestFilter extends AbstractGatewayFilterFactory<CacheRequestFilter.Config> {
public CacheRequestFilter() {
super(Config.class);
}
@Override
public String name() {
return "CacheRequestFilter";
}
@Override
public GatewayFilter apply(Config config) {
CacheRequestGatewayFilter cacheRequestGatewayFilter = new CacheRequestGatewayFilter();
Integer order = config.getOrder();
if (order == null) {
return cacheRequestGatewayFilter;
}
return new OrderedGatewayFilter(cacheRequestGatewayFilter, order);
}
public static class CacheRequestGatewayFilter implements GatewayFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// GET DELETE 不过滤
HttpMethod method = exchange.getRequest().getMethod();
if (method == null || method == HttpMethod.GET || method == HttpMethod.DELETE) {
return chain.filter(exchange);
}
return ServerWebExchangeUtils.cacheRequestBodyAndRequest(exchange, (serverHttpRequest) -> {
if (serverHttpRequest == exchange.getRequest()) {
return chain.filter(exchange);
}
return chain.filter(exchange.mutate().request(serverHttpRequest).build());
});
}
}
static public class Config {
private Integer order;
public Integer getOrder() {
return order;
}
public void setOrder(Integer order) {
this.order = order;
}
}
}
6.4 Sentinel限流
6.4.1 Sentinel配置
spring:
cloud:
sentinel:
# 取消控制台懒加载
eager: true
transport:
# 控制台地址
dashboard: localhost:8718
# nacos配置持久化
datasource:
ds1:
nacos:
server-addr: localhost:8848
dataId: sentinel-ruoyi-gateway
groupId: DEFAULT_GROUP
data-type: json
rule-type: gw-flow
6.4.2 Sentinel规则配置
[
{
"resource": "ruoyi-auth",
"count": 500,
"grade": 1,
"limitApp": "default",
"strategy": 0,
"controlBehavior": 0
},
{
"resource": "ruoyi-system",
"count": 1000,
"grade": 1,
"limitApp": "default",
"strategy": 0,
"controlBehavior": 0
}
]
6.4.3 Sentinel降级处理
@Component
public class SentinelFallbackHandler implements WebExceptionHandler {
private Mono<Void> writeResponse(ServerResponse response, ServerWebExchange exchange) {
return ServletUtils.webFluxResponseWriter(exchange.getResponse(), "请求超过最大数,请稍后再试");
}
@Override
public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {
if (exchange.getResponse().isCommitted()) {
return Mono.error(ex);
}
if (!BlockException.isBlockException(ex)) {
return Mono.error(ex);
}
return handleBlockedRequest(exchange, ex)
.flatMap(response -> writeResponse(response, exchange));
}
private Mono<ServerResponse> handleBlockedRequest(ServerWebExchange exchange, Throwable throwable) {
return GatewayCallbackManager.getBlockHandler().handleRequest(exchange, throwable);
}
}
6.5 跨域配置
6.5.1 全局跨域配置
@Configuration
public class GatewayConfig {
@Bean
public CorsWebFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.addAllowedOriginPattern("*");
config.addAllowedHeader("*");
config.addAllowedMethod("*");
config.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(new PathPatternParser());
source.registerCorsConfiguration("/**", config);
return new CorsWebFilter(source);
}
}
6.5.2 Nacos配置跨域
spring:
cloud:
gateway:
globalcors:
cors-configurations:
'[/**]':
allowedOriginPatterns: "*"
allowed-methods: "*"
allowed-headers: "*"
allow-credentials: true
exposedHeaders: "Content-Disposition,Content-Type,Cache-Control"
6.6 异常处理
6.6.1 全局异常处理器
@Order(-1)
@Configuration
public class GatewayExceptionHandler implements ErrorWebExceptionHandler {
private static final Logger log = LoggerFactory.getLogger(GatewayExceptionHandler.class);
@Override
public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {
ServerHttpResponse response = exchange.getResponse();
if (exchange.getResponse().isCommitted()) {
return Mono.error(ex);
}
String msg;
if (ex instanceof NotFoundException) {
msg = "服务未找到";
} else if (ex instanceof ResponseStatusException) {
ResponseStatusException responseStatusException = (ResponseStatusException) ex;
msg = responseStatusException.getMessage();
} else {
msg = "内部服务器错误";
}
log.error("[网关异常处理]请求路径:{},异常信息:{}", exchange.getRequest().getPath(), ex.getMessage());
return ServletUtils.webFluxResponseWriter(response, msg);
}
}
6.6.2 响应工具类
public class ServletUtils {
/**
* WebFlux响应结果
*/
public static Mono<Void> webFluxResponseWriter(ServerHttpResponse response, String msg) {
return webFluxResponseWriter(response, msg, HttpStatus.OK);
}
public static Mono<Void> webFluxResponseWriter(ServerHttpResponse response, String msg, HttpStatus status) {
response.setStatusCode(status);
response.getHeaders().add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
R<?> result = R.fail(msg);
DataBuffer dataBuffer = response.bufferFactory().wrap(JSON.toJSONString(result).getBytes());
return response.writeWith(Mono.just(dataBuffer));
}
/**
* URL编码
*/
public static String urlEncode(String str) {
try {
return URLEncoder.encode(str, Constants.UTF8);
} catch (UnsupportedEncodingException e) {
return StringUtils.EMPTY;
}
}
}
6.7 Swagger聚合
6.7.1 Swagger配置
@Component
@Primary
public class SwaggerProvider implements SwaggerResourcesProvider {
/**
* Swagger3默认的url后缀
*/
public static final String SWAGGER3URL = "/v3/api-docs";
@Autowired
private RouteLocator routeLocator;
@Autowired
private GatewayProperties gatewayProperties;
@Override
public List<SwaggerResource> get() {
List<SwaggerResource> resources = new ArrayList<>();
List<String> routes = new ArrayList<>();
routeLocator.getRoutes().subscribe(route -> routes.add(route.getId()));
gatewayProperties.getRoutes().stream()
.filter(routeDefinition -> routes.contains(routeDefinition.getId()))
.forEach(route -> {
route.getPredicates().stream()
.filter(predicateDefinition -> "Path".equalsIgnoreCase(predicateDefinition.getName()))
.forEach(predicateDefinition -> resources.add(
swaggerResource(route.getId(),
predicateDefinition.getArgs().get(NameUtils.GENERATED_NAME_PREFIX + "0")
.replace("/**", SWAGGER3URL))
));
});
return resources;
}
private SwaggerResource swaggerResource(String name, String url) {
SwaggerResource swaggerResource = new SwaggerResource();
swaggerResource.setName(name);
swaggerResource.setUrl(url);
swaggerResource.setSwaggerVersion("3.0");
return swaggerResource;
}
}
6.7.2 Swagger路由
@Configuration
public class RouterFunctionConfiguration {
@Autowired(required = false)
private SwaggerProvider swaggerProvider;
@Bean
public RouterFunction routerFunction() {
return RouterFunctions.route(
RequestPredicates.GET("/swagger-resources")
.and(RequestPredicates.accept(MediaType.ALL)),
request -> ServerResponse.ok()
.contentType(MediaType.APPLICATION_JSON)
.body(BodyInserters.fromValue(swaggerProvider.get()))
);
}
}
6.8 配置属性类
6.8.1 白名单配置
@Configuration
@RefreshScope
@ConfigurationProperties(prefix = "security.ignore")
public class IgnoreWhiteProperties {
/**
* 放行白名单配置,网关不校验此处的白名单
*/
private List<String> whites = new ArrayList<>();
public List<String> getWhites() {
return whites;
}
public void setWhites(List<String> whites) {
this.whites = whites;
}
}
6.8.2 验证码配置
@Configuration
@RefreshScope
@ConfigurationProperties(prefix = "security.captcha")
public class CaptchaProperties {
/**
* 验证码开关
*/
private Boolean enabled;
/**
* 验证码类型(math 数字计算 char 字符验证)
*/
private String type;
// getter/setter...
}
6.8.3 XSS配置
@Configuration
@RefreshScope
@ConfigurationProperties(prefix = "security.xss")
public class XssProperties {
/**
* Xss开关
*/
private Boolean enabled;
/**
* 排除路径
*/
private List<String> excludeUrls = new ArrayList<>();
// getter/setter...
}
6.9 网关最佳实践
6.9.1 性能优化建议
- 合理设置超时时间
spring:
cloud:
gateway:
httpclient:
connect-timeout: 10000
response-timeout: 30000
- 启用响应压缩
server:
compression:
enabled: true
mime-types: application/json,application/xml,text/html,text/xml,text/plain
min-response-size: 2048
- 连接池优化
spring:
cloud:
gateway:
httpclient:
pool:
max-connections: 500
max-idle-time: 20000
6.9.2 安全建议
- 启用XSS过滤
- 配置合理的白名单
- 使用HTTPS
- 配置请求限流
- 开启访问日志
6.9.3 监控建议
- 集成Prometheus监控
- 配置健康检查端点
- 开启请求追踪
- 配置告警规则
6.10 小结
本章详细介绍了RuoYi-Cloud的网关服务,包括:
- 网关职责:路由转发、负载均衡、安全认证
- 路由配置:谓词、过滤器的使用
- 过滤器:认证、验证码、XSS、黑名单过滤器
- Sentinel限流:流量控制和降级处理
- 跨域配置:CORS处理
- 异常处理:全局异常处理机制
- Swagger聚合:API文档聚合
网关是微服务架构的核心组件,理解其工作原理对于系统开发和运维至关重要。

浙公网安备 33010602011771号