持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第3天,点击查看活动详情
11. 网关日志记录
在网关定义日志全局过滤器,获取请求ID、时间、路径、客户端地址以及响应码等参数:
package com.deepinsea.common.filter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.annotation.Order;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
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.stereotype.Component;
import org.springframework.util.MultiValueMap;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Optional;
import java.util.UUID;
/**
* Created by deepinsea on 2022/6/28.
* 访问日志全局过滤器
*/
@Slf4j
@Component
@Order(value = Integer.MIN_VALUE)
public class AccessLogGlobalFilter implements GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//filter的前置处理
ServerHttpRequest request = exchange.getRequest();
String requestId = UUID.randomUUID().toString(); //请求ID
String date = new SimpleDateFormat("yyyy-MM-dd HH:mm yyyy-MM-dd HH:mm:ss.SSS").format(new Date()); //时间
HttpMethod method = request.getMethod(); //请求方法
// HttpHeaders headers = request.getHeaders(); //请求头
MediaType contentType = request.getHeaders().getContentType();//请求类型(POST:表单或JSON, GET:空)
String path = request.getPath().pathWithinApplication().value(); //请求路径
MultiValueMap<String, String> requestParams = request.getQueryParams(); //请求参数
//请求体(webflux是异步获取请求体,采用pub/sub订阅通知机制,注意处理400 BAD_REQUEST问题)
//不打印请求体了,考虑到请求体size过大的问题
Flux<DataBuffer> requestBody = request.getBody();
// AtomicReference<String> bodyRef = new AtomicReference<>(); //直接返回bodyRef.get会出现200和400 BAD_REQUEST间隔出现问题
// requestBody.subscribe(buffer -> {
// CharBuffer charBuffer = StandardCharsets.UTF_8.decode(buffer.asByteBuffer());
// DataBufferUtils.release(buffer);
// bodyRef.set(charBuffer.toString());
// });
// InetSocketAddress remoteAddress = request.getRemoteAddress(); //非真实IP
String ip = getIP(request); //客户端真实IP
//真实的url
return chain
//继续调用filter
.filter(exchange)
//filter的后置处理
.then(Mono.fromRunnable(() -> {
ServerHttpResponse response = exchange.getResponse();
HttpStatus statusCode = response.getStatusCode(); //响应码
log.info("请求ID:{}, 请求时间:{}, 请求方法:{}, 数据类型:{}, 请求路径:{}, 客户端IP地址:{}, 请求参数:{}, 响应码:{}",
requestId, date, method, contentType, path, ip, requestParams, statusCode);
}));
}
//下面为获取真实IP的全局静态变量
private static final String IP_UNKNOWN = "unknown";
private static final String IP_LOCAL = "127.0.0.1";
private static final int IP_LEN = 15;
/**
* 获取客户端真实ip
*/
public static String getIP(ServerHttpRequest request) {
HttpHeaders headers = request.getHeaders();
String ipAddress = headers.getFirst("x-forwarded-for");
if (ipAddress == null || ipAddress.length() == 0 || IP_UNKNOWN.equalsIgnoreCase(ipAddress)) {
ipAddress = headers.getFirst("Proxy-Client-IP");
}
if (ipAddress == null || ipAddress.length() == 0 || IP_UNKNOWN.equalsIgnoreCase(ipAddress)) {
ipAddress = headers.getFirst("WL-Proxy-Client-IP");
}
if (ipAddress == null || ipAddress.length() == 0 || IP_UNKNOWN.equalsIgnoreCase(ipAddress)) {
ipAddress = Optional.ofNullable(request.getRemoteAddress())
.map(address -> address.getAddress().getHostAddress())
.orElse("");
if (IP_LOCAL.equals(ipAddress)) {
// 根据网卡取本机配置的IP
try {
InetAddress inet = InetAddress.getLocalHost();
ipAddress = inet.getHostAddress();
} catch (UnknownHostException e) {
// ignore
}
}
}
// 对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割
if (ipAddress != null && ipAddress.length() > IP_LEN) {
int index = ipAddress.indexOf(",");
if (index > 0) {
ipAddress = ipAddress.substring(0, index);
}
}
return ipAddress;
}
}
注意:网关的全局过滤器的作用范围是走了网关代理的请求,对于后端和网关真实服务地址是不生效的,因为没有走网关代理。如果需要所有请求都走网关,要么每个服务配置拦截器,要么内外网隔离网关和后端服务。
启动 service-openfeign 子模块,使用curl命令请求调用者的POST接口和网关的GET接口,测试日志打印:
C:\Users\deepinsea>curl -X POST -H "Content-Type:application/json" -d '{"name":"admin"}' http://localhost:9070/service-consumer-openfeign/consumer-openfeign/hello?ahh=111
hi, this is service-provider-api!
C:\Users\deepinsea>curl -d '123' http://localhost:9070/service-consumer-openfeign/consumer-openfeign/hello?ahh=111
hi, this is service-provider-nacos!
C:\Users\deepinsea>curl http://localhost:9070/service-gateway/gateway/api/hello?ahh=111
hello, 这里是service-gateway网关, 恭喜你请求了正确的路径!
控制台日志,如下所示:
2022-06-30 17:40:01 [reactor-http-nio-4] INFO com.deepinsea.common.filter.AccessLogGlobalFilter - 请求ID:5e3ab22d-e00a-41a3-9590-2ab0e36935af, 请求时间:2022-06-30 17:40 2022-06-30 17:40:01.510, 请求方法:POST, 数据类型:application/json, 请求路径:/consumer-openfeign/hello, 客户端IP地址:192.168.174.1, 请求参数:{ahh=[111]}, 响应码:200 OK
2022-06-30 17:40:03 [reactor-http-nio-4] INFO com.deepinsea.common.filter.AccessLogGlobalFilter - 请求ID:5a5594a9-d204-49c1-b080-b671694cc9ce, 请求时间:2022-06-30 17:40 2022-06-30 17:40:03.996, 请求方法:POST, 数据类型:application/x-www-form-urlencoded, 请求路径:/consumer-openfeign/hello, 客户端IP地址:192.168.174.1, 请求参数:{ahh=[111]}, 响应码:200 OK
2022-06-30 17:47:17 [reactor-http-nio-8] INFO com.deepinsea.common.filter.AccessLogGlobalFilter - 请求ID:a3254b26-6678-46c4-bc9c-94ef6cf3dc0a, 请求时间:2022-06-30 17:47 2022-06-30 17:47:17.575, 请求方法:GET, 数据类型:null, 请求路径:/gateway/api/hello, 客户端IP地址:192.168.174.1, 请求参数:{ahh=[111]}, 响应码:200 OK
测试成功,成功记录到网关代理的请求和响应日志信息!
注意:post请求方式时Content-Type数据类型参数可为json或form表单,get请求方式时为null,注意判断方式。
完整的网关日志配置
上面的请求日志并没有记录请求体,因为gateway请求是基于webflux的原因,请求只能被消费一次。因此,日志要记录请求体比较麻烦。
下面我们来解决这个问题,并基于全局过滤器实现日志审计功能。
首先先定义一个日志参数实体类AccessLog:
package com.deepinsea.common.log.trace;
import lombok.Data;
import org.springframework.http.HttpHeaders;
import org.springframework.util.MultiValueMap;
/**
* Created by deepinsea on 2022/6/29.
*/
@Data
public class AccessLog {
/**
* 路径
*/
private String path;
/**
* 协议(scheme)
*/
private String scheme;
/**
* 请求方法
*/
private String method;
/**
* 请求头
*/
private HttpHeaders headers;
/**
* 目标地址
*/
private String targetUri;
/**
* 远程地址
*/
private String remoteAddress;
/**
* 查询参数
*/
private MultiValueMap<String, String> queryParam;
/**
* 请求体
*/
private String body;
}
然后创建全局过滤器LogFilter,对经过网关代理的请求进行记录:
package com.deepinsea.common.log.trace;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.cloud.gateway.support.ServerWebExchangeUtils;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.codec.HttpMessageReader;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpRequestDecorator;
import org.springframework.stereotype.Component;
import org.springframework.util.MultiValueMap;
import org.springframework.web.reactive.function.server.HandlerStrategies;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.net.InetSocketAddress;
import java.net.URI;
import java.util.List;
/**
* Created by deepinsea on 2022/6/29.
*/
@Component
public class LogFilter implements GlobalFilter, Ordered {
private Logger log = LoggerFactory.getLogger(LogFilter.class);
private final ObjectMapper objectMapper = new ObjectMapper();
private static final String START_TIME = "startTime";
private static final List<HttpMessageReader<?>> messageReaders = HandlerStrategies.withDefaults().messageReaders();
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
// 请求路径
String path = request.getPath().pathWithinApplication().value();
// 请求schema: http/https
String scheme = request.getURI().getScheme();
// 请求方法
HttpMethod method = request.getMethod();
// 路由服务地址
URI targetUri = exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR);
// 请求头
HttpHeaders headers = request.getHeaders();
// 设置startTime
exchange.getAttributes().put(START_TIME, System.currentTimeMillis());
// 远程请求地址
InetSocketAddress remoteAddress = request.getRemoteAddress();
// 表单数据类型的请求体(注意:queryParam参数不能与ServerHttpRequest中的queryParams参数同名,否则会出现请求404的问题)
MultiValueMap<String, String> queryParam = request.getQueryParams();
AccessLog accessLog = new AccessLog();
accessLog.setPath(path);
accessLog.setScheme(scheme);
accessLog.setMethod(method.name());
accessLog.setTargetUri(targetUri.toString());
accessLog.setRemoteAddress(remoteAddress.toString());
accessLog.setHeaders(headers);
accessLog.setQueryParam(queryParam);
//GET请求处理
if (method == HttpMethod.GET) {
writeAccessRecord(accessLog);
}
//POST请求处理
if (method == HttpMethod.POST) {
Mono<Void> voidMono = null;
//分别有三种类型数据:POST: application/x-www-form-urlencoded和application/json, GET: null
if (headers.getContentType().equals(MediaType.APPLICATION_JSON)) {
// JSON
voidMono = readBody(exchange, chain, accessLog);
}
if (headers.getContentType().equals(MediaType.APPLICATION_FORM_URLENCODED)) {
// x-www-form-urlencoded
voidMono = readFormData(exchange, chain, accessLog);
}
if (voidMono != null) {
return voidMono;
}
}
return chain.filter(exchange);
}
//表单类型的请求体数据
private Mono<Void> readFormData(ServerWebExchange exchange, GatewayFilterChain chain, AccessLog accessLog) {
Mono<Void> formDateBody = readBody(exchange, chain, accessLog);
return formDateBody;
}
private Mono<Void> readBody(ServerWebExchange exchange, GatewayFilterChain chain, AccessLog accessLog) {
return DataBufferUtils.join(exchange.getRequest().getBody()).flatMap(dataBuffer -> {
byte[] bytes = new byte[dataBuffer.readableByteCount()];
dataBuffer.read(bytes);
DataBufferUtils.release(dataBuffer);
Flux<DataBuffer> cachedFlux = Flux.defer(() -> {
DataBuffer buffer = exchange.getResponse().bufferFactory().wrap(bytes);
DataBufferUtils.retain(buffer);
return Mono.just(buffer);
});
// 重写请求体,因为请求体数据只能被消费一次
ServerHttpRequest mutatedRequest = new ServerHttpRequestDecorator(exchange.getRequest()) {
@Override
public Flux<DataBuffer> getBody() {
return cachedFlux;
}
};
ServerWebExchange mutatedExchange = exchange.mutate().request(mutatedRequest).build();
return ServerRequest.create(mutatedExchange, messageReaders)
.bodyToMono(String.class)
.doOnNext(objectValue -> {
accessLog.setBody(objectValue);
writeAccessRecord(accessLog);
}).then(chain.filter(mutatedExchange));
});
}
@Override
public int getOrder() {
return Ordered.LOWEST_PRECEDENCE;
}
/**
* TODO 异步日志
*
* @param accessLog
*/
private void writeAccessRecord(AccessLog accessLog) {
log.info("\n start------------------------------------------------- \n " +
"请求路径:{}\n " +
"scheme:{}\n " +
"请求方法:{}\n " +
"目标服务:{}\n " +
"请求头:{}\n " +
"远程IP地址:{}\n " +
"查询参数:{}\n " +
"请求体:{}\n " +
"end------------------------------------------------- ",
accessLog.getPath(), accessLog.getScheme(), accessLog.getMethod(), accessLog.getTargetUri(),
accessLog.getHeaders(), accessLog.getRemoteAddress(), accessLog.getQueryParam(), accessLog.getBody());
}
}
这里同样注意处理多种请求方式的数据类型返回请求体情况,post请求方式时Content-Type数据类型参数可为json或form表单,get请求方式时为null。
下面启动GET请求接口的两个互为负载均衡的 service-provider-nacos 服务,以及POST接口的服务消费者 service-consumer-openfeign,并启动网关服务,一共四个服务来进行请求测试:
2022-07-02 04:26:50 [boundedElastic-3] INFO com.deepinsea.common.log.trace.LogFilter -
start-------------------------------------------------
请求路径:/gateway/api/hello
scheme:http
请求方法:GET
目标服务:http://192.168.174.1:9070/gateway/api/hello?msg=test
请求头:[Host:"localhost:9070", User-Agent:"curl/7.79.1", Accept:"*/*"]
远程IP地址:/127.0.0.1:63577
查询参数:{msg=[test]}
请求体:null
end-------------------------------------------------
2022-07-02 04:27:02 [reactor-http-nio-16] INFO com.deepinsea.common.log.trace.LogFilter -
start-------------------------------------------------
请求路径:/consumer-openfeign/hello
scheme:http
请求方法:POST
目标服务:http://192.168.174.1:9020/consumer-openfeign/hello?token
请求头:[Host:"localhost:9070", User-Agent:"curl/7.79.1", Accept:"*/*", Content-Type:"application/x-www-form-urlencoded", content-length:"5"]
远程IP地址:/127.0.0.1:63579
查询参数:{token=[null]}
请求体:'123'
end-------------------------------------------------
2022-07-02 04:27:14 [reactor-http-nio-1] INFO com.deepinsea.common.log.trace.LogFilter -
start-------------------------------------------------
请求路径:/consumer-openfeign/hello
scheme:http
请求方法:POST
目标服务:http://192.168.174.1:9020/consumer-openfeign/hello?msg=test
请求头:[Host:"localhost:9070", User-Agent:"curl/7.79.1", Accept:"*/*", Content-Type:"application/json", content-length:"13"]
远程IP地址:/127.0.0.1:63583
查询参数:{msg=[test]}
请求体:'{name:test}'
end-------------------------------------------------
注意:这里由于构建动态路由比较耗时间(注册中心服务构建优先级较低的耗时最高),并且由于日志的Order较大(位于过滤器最下游)的原因,在启动项目后请求正常的接口前端会报404的错误(后台日志输出正常) 。
这时需要持久化路由到存储中,并且设置合理的请求的连接超时时间和响应时间。
另外SpringBoot默认日志的实现方式为logback(l像logback-spring.xml 放到 resources 目录即可被SpringBoot识别),我们可以除了在控制台输出日志,还可以配置日志输出到文件中:
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- 日志输出路径(./logs为父项目的根目录,/logs为当前磁盘的根目录), 建议相对路径方式 -->
<property name="LOGS" value="./service-gateway/src/main/resources/logs" />
<appender name="Console" class="ch.qos.logback.core.ConsoleAppender">
<!-- 控制台自定义字体颜色 -->
<!-- 字体颜色配置方案一 -->
<!-- <layout class="ch.qos.logback.classic.PatternLayout">-->
<!-- <Pattern>-->
<!--<!– %black(%d{ISO8601}) %highlight(%-5level) [%blue(%t)] %yellow(%C{1.}): %msg%n%throwable–>-->
<!-- %date{yyyy-MM-dd HH:mm:ss} | %highlight(%-5level) | %boldYellow(%thread) | %boldGreen(%logger) | %msg%n"-->
<!-- </Pattern>-->
<!-- </layout>-->
<!-- 字体颜色配置方案二(推荐) -->
<encoder>
<!--格式化输出:%d:表示日期,%thread:表示线程名,%-5level:级别从左显示5个字符宽度,%msg:日志消息,%n:是换行符-->
<pattern>%red(%d{yyyy-MM-dd HH:mm:ss}) %green([%thread]) %highlight(%-5level) %boldMagenta(%logger) - %cyan(%msg%n)</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<appender name="RollingFile" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOGS}/logback-gateway.log</file>
<encoder
class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<Pattern>%d %p %C{1.} [%t] %m%n</Pattern>
</encoder>
<rollingPolicy
class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- rollover daily and when the file reaches 10 MegaBytes -->
<fileNamePattern>${LOGS}/archived/logback-gateway-%d{yyyy-MM-dd}.%i.log
</fileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy
class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>50MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
</rollingPolicy>
</appender>
<!--配置异步日志-->
<appender name="Async_Appender" class="ch.qos.logback.classic.AsyncAppender">
<appender-ref ref="RollingFile"/>
</appender>
<!-- LOG everything at INFO level -->
<root level="info">
<!-- 日志打印开关 -->
<appender-ref ref="Async_Appender" />
<!-- 控制台打印开关 -->
<appender-ref ref="Console" />
</root>
<!-- LOG "cn.idea360*" at TRACE level additivity:是否向上级logger传递打印信息。默认是true-->
<logger name="cn.idea360.gateway" level="info" additivity="false">
<appender-ref ref="RollingFile" />
<appender-ref ref="Console" />
</logger>
<!-- 以下部分可适当注释 -->
<!--自定义 log -->
<!-- <logger name="org.springframework.web" level="ERROR"/>-->
<!-- <logger name="org.springboot.sample" level="ERROR"/>-->
<!-- <logger name="com.deepinsea" level="DEBUG"/>-->
<!-- 开发、测试环境 -->
<!-- <springProfile name="dev,test">-->
<!-- <root level="info">-->
<!-- <appender-ref ref="RollingFile" />-->
<!-- <appender-ref ref="Console" />-->
<!-- </root>-->
<!-- </springProfile>-->
<!-- 所有环境都要记录错误日志 -->
<!-- <root level="ERROR">-->
<!-- <appender-ref ref="ASYNC_APPENDER"/>-->
<!-- </root>-->
<!-- 分割线 -->
</configuration>
这样console和日志目录下就都有日志了。
网关日志部分到此结束。关于 Webflux 的学习刚入门,觉得可以像 Rxjava 那样在 onNext 中拿到异步数据,然而在 post 获取body中没生效。经测试可知 getBody 获得的数据输出为null,而自己通过 Flux.create 创建的数据可以在订阅者中获取到。
12. 请求超时配置
# 网关
gateway:
# 启用开关(默认开启)
enabled: true
discovery:
locator:
# 开启从注册中心动态创建路由的功能,利用微服务名进行动态路由(在真实服务请求路径上加上/服务名)
# 例如:http://localhost:9070/service-provider-nacos/provider-nacos/hello
enabled: true
# 服务名转为小写(默认yml配置就是小写,这里只是保证一下)
lower-case-service-id: true
# 路由数组[路由 就是指定当请求满足什么条件的时候转到哪个微服务] --本质就是反向代理
routes:
- id: service-provider-nacos # 当前路由的标识, 要求唯一
uri: lb://service-provider-nacos # lb指的是从 nacos 中按照名称获取微服务,并遵循负载均衡策略路由请求(动态路由)
order: 10 # 路由的优先级,数字越小代表路由的优先级越高
predicates: # 断言(就是路由转发要满足的条件)
- Path=/provider-nacos/** # 当请求路径满足Path指定的规则时,才进行路由转发
# filters:
# - AddRequestHeader=gateway, blue
## 配置过滤器(局部)
filters:
# - AddResponseHeader=X-Response-Foo, Bar
- AddResponseHeader=licence, value
## AuthorizeGatewayFilterFactory自定义过滤器配置,值为true需要验证授权,false不需要
# - Authorize=true
# 我们⾃定义的路由 ID,保持唯⼀
- id: service-gateway
# ⽬标服务地址(部署多实例,不能加子路径)
# uri: http://localhost:9070 # 指定具体的微服务地址(原真实服务地址还是可以访问,如果要限制走网关可以加token验证机制)
# uri: https://www.baidu.com # 指定具体的微服务地址
uri: lb://service-gateway
# gateway⽹关从服务注册中⼼获取实例信息然后负载后路由
# 断⾔:路由条件,Predicate 接受⼀个输⼊参数,返回⼀个布尔值结果。该接⼝包含多种默认⽅法来将 Predicate 组合成其他复杂的逻辑(⽐如:与,或,⾮)。
predicates:
# 当请求的路径为http://localhost:9070/looptest/gateway/api/hello时,转发到http://localhost:9070/gateway/api/hello
- Path=/looptest/gateway/api/hello # 本身就是基于path的反向代理
# - Path=/**
filters: # 过滤器,请求在传递过程中可以通过过滤器对其进行一定的修改
- StripPrefix=1 # 转发之前去掉1层路径(去除原始请求路径中的前1级路径,即/looptest去除)
# - SetStatus=220 # 修改原始响应的状态码
# 名称必须为过滤器工厂类名的前缀(Log),而且参数只能有两个,由于NameValueConfig里只定义了两个属性
# - Log=testName,testValue #自定义局部过滤器
# 超时配置
httpclient:
connect-timeout: 10000
response-timeout: 5s
13. 请求跨域配置
# 网关
gateway:
# 启用开关(默认开启)
enabled: true
discovery:
locator:
# 开启从注册中心动态创建路由的功能,利用微服务名进行动态路由(在真实服务请求路径上加上/服务名)
# 例如:http://localhost:9070/service-provider-nacos/provider-nacos/hello
enabled: true
# 服务名转为小写(默认yml配置就是小写,这里只是保证一下)
lower-case-service-id: true
# 路由数组[路由 就是指定当请求满足什么条件的时候转到哪个微服务] --本质就是反向代理
routes:
- id: service-provider-nacos # 当前路由的标识, 要求唯一
uri: lb://service-provider-nacos # lb指的是从 nacos 中按照名称获取微服务,并遵循负载均衡策略路由请求(动态路由)
order: 10 # 路由的优先级,数字越小代表路由的优先级越高
predicates: # 断言(就是路由转发要满足的条件)
- Path=/provider-nacos/** # 当请求路径满足Path指定的规则时,才进行路由转发
# filters:
# - AddRequestHeader=gateway, blue
## 配置过滤器(局部)
filters:
# - AddResponseHeader=X-Response-Foo, Bar
- AddResponseHeader=licence, value
## AuthorizeGatewayFilterFactory自定义过滤器配置,值为true需要验证授权,false不需要
# - Authorize=true
# 我们⾃定义的路由 ID,保持唯⼀
- id: service-gateway
# ⽬标服务地址(部署多实例,不能加子路径)
# uri: http://localhost:9070 # 指定具体的微服务地址(原真实服务地址还是可以访问,如果要限制走网关可以加token验证机制)
# uri: https://www.baidu.com # 指定具体的微服务地址
uri: lb://service-gateway
# gateway⽹关从服务注册中⼼获取实例信息然后负载后路由
# 断⾔:路由条件,Predicate 接受⼀个输⼊参数,返回⼀个布尔值结果。该接⼝包含多种默认⽅法来将 Predicate 组合成其他复杂的逻辑(⽐如:与,或,⾮)。
predicates:
# 当请求的路径为http://localhost:9070/looptest/gateway/api/hello时,转发到http://localhost:9070/gateway/api/hello
- Path=/looptest/gateway/api/hello # 本身就是基于path的反向代理
# - Path=/**
filters: # 过滤器,请求在传递过程中可以通过过滤器对其进行一定的修改
- StripPrefix=1 # 转发之前去掉1层路径(去除原始请求路径中的前1级路径,即/looptest去除)
# - SetStatus=220 # 修改原始响应的状态码
# 名称必须为过滤器工厂类名的前缀(Log),而且参数只能有两个,由于NameValueConfig里只定义了两个属性
# - Log=testName,testValue #自定义局部过滤器
# 超时配置
httpclient:
connect-timeout: 10000
response-timeout: 5s
# 全局的跨域处理
globalcors:
add-to-simple-url-handler-mapping: true # 是否将当前cors配置加入到SimpleUrlHandlerMapping中,解决options请求被拦截问题
corsConfigurations:
'[/**]':
allowedOrigins: # 允许哪些网站的跨域请求
- "http://www.deepinsea.top"
- "http://pic.deepinsea.top"
allowedMethods: # 允许的跨域ajax的请求方式
- "GET"
- "POST"
- "DELETE"
- "PUT"
- "OPTIONS"
allowedHeaders: "*" # 允许在请求中携带的头信息
allowCredentials: true # 是否允许携带cookie
maxAge: 360000 # 这次跨域检测的有效期
另外,还有SSL安全配置,我们下面直接参考官网配置(自己就先不配置了)。
14. SS证书配置
网关可以通过遵循通常的 Spring 服务器配置来监听 HTTPS 上的请求。以下示例显示了如何执行此操作:
示例 62.application.yml
server:
ssl:
enabled: true
key-alias: scg
key-store-password: scg1234
key-store: classpath:scg-keystore.p12
key-store-type: PKCS12
您可以将网关路由路由到 HTTP 和 HTTPS 后端。如果您要路由到 HTTPS 后端,则可以使用以下配置将网关配置为信任所有下游证书:
示例 63.application.yml
spring:
cloud:
gateway:
httpclient:
ssl:
useInsecureTrustManager: true
使用不安全的信任管理器不适合生产。对于生产部署,您可以使用一组已知证书配置网关,它可以使用以下配置信任这些证书:
示例 64.application.yml
spring:
cloud:
gateway:
httpclient:
ssl:
trustedX509Certificates:
- cert1.pem
- cert2.pem
如果 Spring Cloud Gateway 没有配置受信任的证书,则使用默认的信任存储(您可以通过设置javax.net.ssl.trustStore系统属性来覆盖它)。
TLS 握手
网关维护一个客户端池,用于路由到后端。通过 HTTPS 进行通信时,客户端会启动 TLS 握手。许多超时与此握手相关。您可以配置这些超时,可以按如下方式配置(显示默认值):
示例 65.application.yml
spring:
cloud:
gateway:
httpclient:
ssl:
handshake-timeout-millis: 10000
close-notify-flush-timeout-millis: 3000
close-notify-read-timeout-millis: 0
Spring Cloud Gateway网关的基本配置就到这里告一段落了,关于限速、降级以及熔断在下面熔断器集成后再回过头来学习。
下面是集成Sentinel熔断器,搭配网关来实现服务熔断功能:
欢迎点赞,谢谢大佬了ヾ(◍°∇°◍)ノ゙