持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第8天,点击查看活动详情
源码解析-Spring Cloud Gateway,包括:能做什么、如何工作、过滤器、代码和配置使用说明
能做什么
性能:API高可用,负载均衡,容错机制。 安全:权限身份认证、脱敏,流量清洗,后端签名(保证全链路可信调用),黑名单(非法调用的限制)。 日志:日志记录(spainid,traceid)一旦涉及分布式,全链路跟踪必不可少。 缓存:数据缓存。 监控:记录请求响应数据,api耗时分析,性能监控。 限流:流量控制,错峰流控,可以定义多种限流规则。 灰度:线上灰度部署,可以减小风险。 路由:动态路由规则。
术语
- Route(路由): 它由一个ID、一个目标URI、一组谓词和一组过滤器定义。 如果聚合谓词为true,则匹配路由。
- Predicate(谓语): 可以匹配来自HTTP请求的任何内容,比如头部或参数。
- Filter(过滤器): 可以在发送下游请求之前或之后修改请求和响应。
如何工作
- Gateway Handler Mapping接收请求,匹配路由;
- Gateway Web Handler通过过滤器链来处理请求;
- Filter允许修改HTTP请求和响应,作用在链上;
- Proxied Service代理具体业务。
- org.springframework.cloud.gateway.config.GatewayAutoConfiguration 网关自动配置类。
- org.springframework.web.server.ServerWebExchange HTTP请求-响应交互数据的约定,提供访问HTTP请求和响应属性的方法。
- org.springframework.web.server.WebHandler 处理web请求。
- org.springframework.cloud.gateway.filter.GlobalFilter
全局过滤器,不需要配置,作用在所有路由上,最终通过GatewayFilterAdapter包装成GatewayFilterChain可识别的过滤器。为请求业务的路由URI转换为真是业务服务地址的核心过滤器。
- org.springframework.cloud.gateway.filter.GatewayFilter 网关路由过滤器,需要通过配置[spring.cloud.routes.filters]作用在具体路由上,可通过配置[spring.cloud.default-filters]作用在所有路由上。允许以某种方式修改传入的HTTP请求或传出的HTTP响应。
- org.springframework.cloud.gateway.filter.factory.GatewayFilterFactory
网关过滤器工厂。实现类有AddRequestHeaderGatewayFilterFactory、AddRequestParameterGatewayFilterFactory等
- org.springframework.web.server.WebFilter Web相关过滤器接口。
- org.springframework.cloud.gateway.filter.GatewayFilterChain 网关过滤链表。默认实现DefaultGatewayFilterChain。
- org.springframework.cloud.gateway.handler.FilteringWebHandler
- org.springframework.cloud.gateway.handler.predicate.RoutePredicateFactory 路由谓语工厂
过滤器
| Filter | Priority | Desc |
|---|---|---|
| RemoveCachedBodyFilter | Integer.MIN_VALUE | |
| AdaptCachedBodyGlobalFilter | Integer.MIN_VALUE+1000 | |
| NettyWriteResponseFilter | -1 | |
| ForwardPathFilter | 0 | |
| GatewayMetricsFilter | 0 | |
| RouteToRequestUrlFilter | 10000 | |
| LoadBalancerClientFilter | 10100 | |
| WebsocketRoutingFilter | Integer.MAX_VALUE-1 | |
| NettyRoutingFilter | Integer.MAX_VALUE | |
| ForwardRoutingFilter | Integer.MAX_VALUE |
spring:
cloud:
gateway:
routes:
# 路由ID
- id: after_route
# 路由规则转发目标uri
uri: lb://demo-service
# 谓语信息
predicates:
# 以下配置等于(Cookie=mycookie,mycookievalue)
- name: Cookie
args:
name: mycookie
regexp: mycookievalue
# 路由过滤器
filters:
AddRequestHeader=X-Request-red, blue
# 路由顺序,值越小优先级越高
order: 0
默认的GatewayFilterChain信息:
代码
- AccessLogRequestFilter.java
@Component
@Slf4j
public class AccessLogRequestFilter implements GlobalFilter, Ordered {
@Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE;
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
Consumer<HttpHeaders> httpHeaders = httpHeader -> {
httpHeader.set(Constants.LIGHT_GATEWAY_START, String.valueOf(System.currentTimeMillis()));
};
ServerHttpRequest serverHttpRequest = exchange.getRequest().mutate().headers(httpHeaders).build();
return chain.filter(exchange.mutate().request(serverHttpRequest).build());
}
}
- AccessLogResponseFilter.java
接收到响应后记录日志
@Component
@Slf4j
public class AccessLogResponseFilter implements GlobalFilter, Ordered {
@Override
public int getOrder() {
return -2;
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpResponse response = exchange.getResponse();
DataBufferFactory bufferFactory = response.bufferFactory();
ServerHttpResponseDecorator decoratedResponse = new ServerHttpResponseDecorator(response) {
@Override
public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
if (body instanceof Flux) {
Flux<? extends DataBuffer> fluxBody = Flux.from(body);
return super.writeWith(fluxBody.buffer().map(dataBuffers -> {
DataBuffer join = bufferFactory.join(dataBuffers);
byte[] content = new byte[join.readableByteCount()];
join.read(content);
DataBufferUtils.release(join);
if (log.isInfoEnabled()) {
ServerHttpRequest serverHttpRequest = exchange.getRequest();
String startTime = serverHttpRequest.getHeaders().getFirst(Constants.LIGHT_GATEWAY_START);
String username = serverHttpRequest.getHeaders().getFirst(ServletUtils.HEADER_KEY_LIGHT_USERNAME);
long elapsedTime = -1;
if (!StringUtils.isEmpty(startTime)) {
elapsedTime = System.currentTimeMillis() - Long.valueOf(startTime).longValue();
}
log.info("{} {} \"{} {}\" {} {} {} ms {}", serverHttpRequest.getId(), IpUtils.getClientIp(serverHttpRequest), serverHttpRequest.getMethodValue(), serverHttpRequest.getURI().getPath(), exchange.getResponse().getStatusCode(), content.length, elapsedTime, username);
}
return bufferFactory.wrap(content);
}));
}
return super.writeWith(body); // if body is not a flux. never got there.
}
};
return chain.filter(exchange.mutate().response(decoratedResponse).build());
}
}
- AuthenticationFilter.java
@Component
@Slf4j
public class AuthenticationFilter implements GlobalFilter, Ordered {
@Autowired
private BasicClient basicClient;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String token = exchange.getRequest().getHeaders().getFirst(Constants.AUTHORIZATION);
if (StringUtils.isEmpty(token)) {
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
try {
String url = exchange.getRequest().getURI().getPath();
String subPath = url.substring(url.indexOf("/", 1) + 1);
String permission = subPath.replaceAll("/", ":");
AuthenticateRsp authorizationRsp = basicClient.authenticate(permission, token);
if (authorizationRsp == null || authorizationRsp.getCode() != 200 || authorizationRsp.getUser() == null) {
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
Consumer<HttpHeaders> httpHeaders = httpHeader -> {
httpHeader.set(ServletUtils.HEADER_KEY_LIGHT_USERNAME, authorizationRsp.getUser().getUserName());
httpHeader.set(ServletUtils.HEADER_KEY_LIGHT_USERID, String.valueOf(authorizationRsp.getUser().getUserId()));
httpHeader.set(ServletUtils.HEADER_KEY_LIGHT_DEPTID, String.valueOf(authorizationRsp.getUser().getDeptId()));
};
ServerHttpRequest serverHttpRequest = exchange.getRequest().mutate().headers(httpHeaders).build();
return chain.filter(exchange.mutate().request(serverHttpRequest).build());
} catch (Exception e) {
log.error("Exception: ", e);
return exchange.getResponse().setComplete();
}
}
// modify Response Body
private Mono<Void> authError(ServerHttpResponse resp, String mess) {
resp.setStatusCode(HttpStatus.OK);
resp.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
BaseRsp baseRsp = new BaseRsp();
baseRsp.setStatus(RspStatus.UNAUTHORIZED.value());
baseRsp.setMessage(mess);
String returnStr = JSON.toJSONString(baseRsp);
DataBuffer buffer = resp.bufferFactory().wrap(returnStr.getBytes(StandardCharsets.UTF_8));
return resp.writeWith(Flux.just(buffer));
}
// modify Response Body
private Mono<Void> operationExchange(ServerWebExchange exchange, GatewayFilterChain chain, AuthenticateRsp authorizationRsp) {
// mediaType
MediaType mediaType = exchange.getRequest().getHeaders().getContentType();
// read & modify body
ServerRequest serverRequest = new DefaultServerRequest(exchange);
Mono<String> modifiedBody = serverRequest.bodyToMono(String.class)
.flatMap(body -> {
if (MediaType.APPLICATION_JSON.isCompatibleWith(mediaType)) {
// 对原先的body进行修改操作
JSONObject jsonObject = JSON.parseObject(body);
jsonObject.put("user", authorizationRsp.getUser());
// jsonObject.put("username", authorizationRsp.getUser().getUserName());
// jsonObject.put("deptId", authorizationRsp.getUser().getDeptId());
return Mono.just(jsonObject.toJSONString());
}
return Mono.empty();
});
BodyInserter bodyInserter = BodyInserters.fromPublisher(modifiedBody, String.class);
HttpHeaders headers = new HttpHeaders();
headers.putAll(exchange.getRequest().getHeaders());
headers.remove(HttpHeaders.CONTENT_LENGTH);
CachedBodyOutputMessage outputMessage = new CachedBodyOutputMessage(exchange, headers);
return bodyInserter.insert(outputMessage, new BodyInserterContext())
.then(Mono.defer(() -> {
ServerHttpRequestDecorator decorator = new ServerHttpRequestDecorator(
exchange.getRequest()) {
@Override
public HttpHeaders getHeaders() {
long contentLength = headers.getContentLength();
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.putAll(super.getHeaders());
if (contentLength > 0) {
httpHeaders.setContentLength(contentLength);
} else {
httpHeaders.set(HttpHeaders.TRANSFER_ENCODING, "chunked");
}
return httpHeaders;
}
@Override
public Flux<DataBuffer> getBody() {
return outputMessage.getBody();
}
};
return chain.filter(exchange.mutate().request(decorator).build());
}));
}
@Override
public int getOrder() {
return -100;
}
}
配置使用说明
spring:
cloud:
gateway:
enabled: true
# netty httpclient线程池配置
httpclient:
pool:
# Type of pool for HttpClient to use, defaults to ELASTIC.
type = ELASTIC
# Only for type FIXED, the maximum number of connections before starting pending acquisition on existing ones. - 连接池最大数
max-connections: 16
# Only for type FIXED, the maximum time in millis to wait for aquiring. - 从池中获取连接的的最大等待时长
acquire-timeout: 45000
# Time in millis after which the channel will be closed. If NULL, there is no max idle time. - 连接的最大空闲时长,超时则关闭通道
max-idle-time:
# Duration after which the channel will be closed. If NULL, there is no max life time. - 连接的最大生命时长,超时则关闭通道
max-life-time:
# The connect timeout in millis, the default is 45s.
connect-timeout: 45000
# The response timeout.
response-timeout: 60s
# The max response header size.
max-header-size: Integer.MAX_VALUE
# The max initial line length.
max-initial-line-length: Integer.MAX_VALUE
actuator:
verbose:
# /actuator/gateway/routes监控信息中添加更多细节
enabled: false
discovery:
locator:
# 动态路由配置。 是否与服务发现组件进行结合,通过 serviceId(必须设置成大写) 转发到具体的服务实例。默认为false,设为true便开启通过服务中心的自动根据 serviceId 创建路由的功能。
enabled: false
# 请求路径上的服务名改为小写
lower-case-service-id: true
# 代理配置
proxy:
# SSL配置
ssl:
# Forwarded头部转发配置
x-forwarded:
# Redis限流配置
redis-rate-limiter:
# 默认限流
default-filters:
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 100
redis-rate-limiter.burstCapacity: 1000
redis-rate-limiter.requestedTokens: 1
key-resolver: "#{@userKeyResolver}"
deny-empty-key: false
# 路由配置
routes:
- id: light-demo1-service-route
uri: lb://light-demo1-service/
predicates:
- Path=/demo1/**
filters:
- StripPrefix=1
- id: light-demo2-service-route
uri: lb://light-demo2-service/
predicates:
- Path=/demo2/**
filters:
- StripPrefix=1
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 1
redis-rate-limiter.burstCapacity: 60
redis-rate-limiter.requestedTokens: 10
key-resolver: "#{@userKeyResolver}"
metadata:
connect-timeout: 3000
# The per-route response timeout.
response-timeout: 5200
参考
Spring Cloud Gateway 之 请求应答日志打印 blog.csdn.net/weixin_3408…
从零搭建Spring Cloud Gateway网关(二)—— 打印请求响应日志 www.lifengdi.com/archives/ar… github.com/lifengdi/sp…
Spring-Cloud-Gateway之请求响应日志打印过滤器 www.jianshu.com/p/9b781fb1a…
spring cloud gateway 统一响应结果 blog.csdn.net/zhouwangzon…
Spring Cloud Gateway 之 限流 zhuanlan.zhihu.com/p/62621868
spring-cloud-gateway限流 juejin.im/post/684490…
gateway nacos sentinel 三剑客强强组合 cloud.tencent.com/developer/a…
spring cloud gateway中Path Predicate规则类似/A/B/** /A/B/C/**时,无法定位到/A/B/C/**的问题 blog.csdn.net/Horizon_Zy/…
Spring Cloud Gateway中多规则限流(RequestRateLimiter覆盖问题) blog.csdn.net/lizz861109/…
完美解决spring cloud gateway 获取body内容并修改 blog.csdn.net/seantdj/art…
Spring Cloud Gateway 之获取请求体(Request Body)的几种方式 www.cnblogs.com/hyf-huangyo…