1.gateway概念
- 网关就是当前微服务项目的"统一入口"
- 程序中的网关就是当前微服务项目对外界开放的统一入口
- 所有外界的请求都需要先经过网关才能访问到我们的程序
- 提供了统一入口之后,方便对所有请求进行统一的检查和管理
2. 网关的主要功能
- 将所有请求统一经过网关
- 网关可以对这些请求进行检查
- 网关方便记录所有请求的日志
- 网关可以统一将所有请求路由到正确的模块\服务上
“路由"的近义词就是"分配”
3. 工作原理
Spring Gateway的工作原理基于路由、断言(Predicates)和过滤器(Filters)三大核心概念:
- 路由(Route):定义了请求的转发规则,包括目标URL和匹配条件。
- 断言(Predicates):用于匹配HTTP请求的各种条件,如路径、头信息、参数等。只有匹配成功的请求才会被路由处理。
- 过滤器(Filters):在请求处理前后执行特定的逻辑,例如权限校验、日志记录
4. 配置和使用示例
spring:
cloud:
gateway:
routes:
- id: myroute
uri: http://example.com
predicates:
- Path=/api/**
filters:
- AddRequestHeader=X-Request-ID, \${requestId}
这个配置定义了一个路由,所有路径以/api/
开头的请求都会被转发到http://example.com
,并且在请求头中添加一个X-Request-ID
字段
springcloud gateway中配置uri有3种方式:
- ws(websocket)方式: uri: ws://localhost:9000
- http方式: uri: http://localhost:8130/
- lb(注册中心中服务名字)方式: uri: lb://brilliance-consumer
springcloud gatetway命名规范
能被gateway的lb方式识别到的命名规则为:
"[a-zA-Z]([a-zA-Z]|\\d|\\+|\\.|-)*:.*"
如果名字中有非*“a-zA-Z:.”*规则字符或者使用“_”,则会报错
5.网关gateway routes的组成
1. id:必须唯一
2. predicates(断言)
关键类源码分析:
package org.springframework.cloud.gateway.handler.predicate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.style.ToStringCreator;
import org.springframework.http.server.PathContainer;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.util.pattern.PathPattern;
import org.springframework.web.util.pattern.PathPattern.PathMatchInfo;
import org.springframework.web.util.pattern.PathPatternParser;
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_PREDICATE_MATCHED_PATH_ATTR;
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_PREDICATE_MATCHED_PATH_ROUTE_ID_ATTR;
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_PREDICATE_PATH_CONTAINER_ATTR;
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_PREDICATE_ROUTE_ATTR;
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.putUriTemplateVariables;
import static org.springframework.http.server.PathContainer.parsePath;
/**
* @author Spencer Gibb
* @author Dhawal Kapil
*/
public class PathRoutePredicateFactory extends AbstractRoutePredicateFactory<PathRoutePredicateFactory.Config> {
private static final Log log = LogFactory.getLog(PathRoutePredicateFactory.class);
private static final String MATCH_TRAILING_SLASH = "matchTrailingSlash";
private PathPatternParser pathPatternParser = new PathPatternParser();
public PathRoutePredicateFactory() {
super(Config.class);
}
private static void traceMatch(String prefix, Object desired, Object actual, boolean match) {
if (log.isTraceEnabled()) {
String message = String.format("%s \"%s\" %s against value \"%s\"", prefix, desired,
match ? "matches" : "does not match", actual);
log.trace(message);
}
}
public void setPathPatternParser(PathPatternParser pathPatternParser) {
this.pathPatternParser = pathPatternParser;
}
@Override
public List<String> shortcutFieldOrder() {
return Arrays.asList("patterns", MATCH_TRAILING_SLASH);
}
@Override
public ShortcutType shortcutType() {
return ShortcutType.GATHER_LIST_TAIL_FLAG;
}
@Override
public Predicate<ServerWebExchange> apply(Config config) {
final ArrayList<PathPattern> pathPatterns = new ArrayList<>();
synchronized (this.pathPatternParser) {
pathPatternParser.setMatchOptionalTrailingSeparator(config.isMatchTrailingSlash());
config.getPatterns().forEach(pattern -> {
PathPattern pathPattern = this.pathPatternParser.parse(pattern);
pathPatterns.add(pathPattern);
});
}
return new GatewayPredicate() {
@Override
public boolean test(ServerWebExchange exchange) {
PathContainer path = (PathContainer) exchange.getAttributes().computeIfAbsent(
GATEWAY_PREDICATE_PATH_CONTAINER_ATTR,
s -> parsePath(exchange.getRequest().getURI().getRawPath()));
PathPattern match = null;
for (int i = 0; i < pathPatterns.size(); i++) {
PathPattern pathPattern = pathPatterns.get(i);
if (pathPattern.matches(path)) {
match = pathPattern;
break;
}
}
if (match != null) {
traceMatch("Pattern", match.getPatternString(), path, true);
PathMatchInfo pathMatchInfo = match.matchAndExtract(path);
putUriTemplateVariables(exchange, pathMatchInfo.getUriVariables());
exchange.getAttributes().put(GATEWAY_PREDICATE_MATCHED_PATH_ATTR, match.getPatternString());
String routeId = (String) exchange.getAttributes().get(GATEWAY_PREDICATE_ROUTE_ATTR);
if (routeId != null) {
// populated in RoutePredicateHandlerMapping
exchange.getAttributes().put(GATEWAY_PREDICATE_MATCHED_PATH_ROUTE_ID_ATTR, routeId);
}
return true;
}
else {
traceMatch("Pattern", config.getPatterns(), path, false);
return false;
}
}
@Override
public Object getConfig() {
return config;
}
@Override
public String toString() {
return String.format("Paths: %s, match trailing slash: %b", config.getPatterns(),
config.isMatchTrailingSlash());
}
};
}
@Validated
public static class Config {
private List<String> patterns = new ArrayList<>();
private boolean matchTrailingSlash = true;
public List<String> getPatterns() {
return patterns;
}
public Config setPatterns(List<String> patterns) {
this.patterns = patterns;
return this;
}
/**
* @deprecated use {@link #isMatchTrailingSlash()}
*/
@Deprecated
public boolean isMatchOptionalTrailingSeparator() {
return isMatchTrailingSlash();
}
/**
* @deprecated use {@link #setMatchTrailingSlash(boolean)}
*/
@Deprecated
public Config setMatchOptionalTrailingSeparator(boolean matchOptionalTrailingSeparator) {
setMatchTrailingSlash(matchOptionalTrailingSeparator);
return this;
}
public boolean isMatchTrailingSlash() {
return matchTrailingSlash;
}
public Config setMatchTrailingSlash(boolean matchTrailingSlash) {
this.matchTrailingSlash = matchTrailingSlash;
return this;
}
@Override
public String toString() {
return new ToStringCreator(this).append("patterns", patterns)
.append(MATCH_TRAILING_SLASH, matchTrailingSlash).toString();
}
}
}
package org.springframework.cloud.gateway.handler.predicate;
import java.util.function.Consumer;
import java.util.function.Predicate;
import org.springframework.cloud.gateway.handler.AsyncPredicate;
import org.springframework.cloud.gateway.support.Configurable;
import org.springframework.cloud.gateway.support.NameUtils;
import org.springframework.cloud.gateway.support.ShortcutConfigurable;
import org.springframework.web.server.ServerWebExchange;
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.toAsyncPredicate;
/**
* @author Spencer Gibb
*/
@FunctionalInterface
public interface RoutePredicateFactory<C> extends ShortcutConfigurable, Configurable<C> {
/**
* Pattern key.
*/
String PATTERN_KEY = "pattern";
// useful for javadsl
default Predicate<ServerWebExchange> apply(Consumer<C> consumer) {
C config = newConfig();
consumer.accept(config);
beforeApply(config);
return apply(config);
}
default AsyncPredicate<ServerWebExchange> applyAsync(Consumer<C> consumer) {
C config = newConfig();
consumer.accept(config);
beforeApply(config);
return applyAsync(config);
}
default Class<C> getConfigClass() {
throw new UnsupportedOperationException("getConfigClass() not implemented");
}
@Override
default C newConfig() {
throw new UnsupportedOperationException("newConfig() not implemented");
}
default void beforeApply(C config) {
}
Predicate<ServerWebExchange> apply(C config);
default AsyncPredicate<ServerWebExchange> applyAsync(C config) {
return toAsyncPredicate(apply(config));
}
default String name() {
return NameUtils.normalizeRoutePredicateName(getClass());
}
}
自定义Vip路由断言工厂实现
package com.wemedia.gateway.config;
import jakarta.validation.constraints.NotEmpty;
import org.springframework.cloud.gateway.handler.predicate.AbstractRoutePredicateFactory;
import org.springframework.cloud.gateway.handler.predicate.CookieRoutePredicateFactory;
import org.springframework.cloud.gateway.handler.predicate.GatewayPredicate;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.server.ServerWebExchange;
import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;
/**
* 自定义Vip路由断言工厂
*/
@Component
public class VipRoutePredicateFactory extends AbstractRoutePredicateFactory<VipRoutePredicateFactory.Config> {
public VipRoutePredicateFactory(){
super(Config.class);
}
@Override
public List<String> shortcutFieldOrder() {
return Arrays.asList("param","value");
}
@Override
public Predicate<ServerWebExchange> apply(Config config) {
return new GatewayPredicate() {
@Override
public boolean test(ServerWebExchange serverWebExchange) {
//localhost/search?q=hhh&user=jackma
ServerHttpRequest request = serverWebExchange.getRequest();
String first = request.getQueryParams().getFirst(config.param);
return StringUtils.hasText(first)&&first.equals(config.value);
}
};
}
@Validated
public static class Config {
@NotEmpty
private String value;
@NotEmpty
private String param;
public String getParam() {
return param;
}
public void setParam(String param) {
this.param = param;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
}
}
在这里插入代码片
配置Vip断言
3. Filter(过滤器)
3.1 rewritePath(路径重写)
- 添加RewritePath过滤器,重写原先路径/readDb,在访问路径前面追加/api/order/readDb,不然网关无法直接访问/readDb;
- 添加AddReponseHeader过滤器,给响应头增加参数X-Response-ABC,值为123
3.2 默认过滤器filter:
增加默认过滤器default-filters, 参数Add-ReponseHeader=X-Reponse-Abc ,值为123 给所有服务的相应头中
3.3 全局过滤器GlobalFilter
package com.wemedia.gateway.filter;
import lombok.extern.slf4j.Slf4j;
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.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
/**
* 实现响应时间全局过滤器
*/
@Component
@Slf4j
public class RTGlobalFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
String uri = request.getURI().toString();
long start=System.currentTimeMillis();
log.info("请求【{}】开始时间:{}",uri,start );
//================以上是前置逻辑==============
Mono<Void> filter = chain.filter(exchange).doFinally((result) -> {
//================后置逻辑
long end = System.currentTimeMillis();
log.info("请求【{}】结束时间:{},耗时:{}ms", uri, end, end - start);
});
return filter;
}
@Override
public int getOrder() {
return 0;
}
}
过滤器filter 列表:
3.4 自定义过滤器工厂
关键源码分析
package org.springframework.cloud.gateway.filter.factory;
import reactor.core.publisher.Mono;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.support.ServerWebExchangeUtils;
import org.springframework.http.HttpHeaders;
import org.springframework.web.server.ServerWebExchange;
import static org.springframework.cloud.gateway.support.GatewayToStringStyler.filterToStringCreator;
/**
* @author Spencer Gibb
*/
public class AddResponseHeaderGatewayFilterFactory extends AbstractNameValueGatewayFilterFactory {
@Override
public GatewayFilter apply(NameValueConfig config) {
return new GatewayFilter() {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
return chain.filter(exchange).then(Mono.fromRunnable(() -> addHeader(exchange, config)));
}
@Override
public String toString() {
return filterToStringCreator(AddResponseHeaderGatewayFilterFactory.this)
.append(config.getName(), config.getValue()).toString();
}
};
}
void addHeader(ServerWebExchange exchange, NameValueConfig config) {
final String value = ServerWebExchangeUtils.expand(exchange, config.getValue());
HttpHeaders headers = exchange.getResponse().getHeaders();
// if response has been commited, no more response headers will bee added.
if (!exchange.getResponse().isCommitted()) {
headers.add(config.getName(), value);
}
}
}
1.实现一次性token自定义过滤器工厂
package com.wemedia.gateway.filter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.factory.AbstractNameValueGatewayFilterFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.util.UUID;
/**
* 实现一次性令牌的自定义过滤器工场
*/
@Component
@Slf4j
public class OnceTokenGatewayFilterFactory extends AbstractNameValueGatewayFilterFactory {
@Override
public GatewayFilter apply(NameValueConfig config) {
return new GatewayFilter() {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//每次响应之前,添加一个一次性令牌,支持uuid,jwt等格式
return chain.filter(exchange).then(Mono.fromRunnable(()->{
ServerHttpResponse response = exchange.getResponse();
HttpHeaders headers = response.getHeaders();
String value = config.getValue();
if("uuid".equalsIgnoreCase(value)){
value = UUID.randomUUID().toString();
}
if("jwt".equalsIgnoreCase(value)){
value="";
}
headers.add(config.getName(),value);
}));
}
};
}
}
2.配置一次性token过滤器
3.访问api响应结果如下:
3.5 实现全局跨域
- 解决单机跨域方法:直接在每个controller上增加注解@CrossOrigin
- 在分布式系统上解决跨域问题,在gateway上统一处理跨域问题
配置全局跨域:
运行结果如下,增加了跨域处理: