Spring Cloud Gateway
Spring Cloud Gateway 可以用来快速开发一个网关。
通过 Spring Initializr 创建一个 gateway 项目,添加依赖:Gateway
和 Consul Discovery
。网关项目有 Spring Cloud Gateway 依赖就够了,Consul Discovery 依赖后面要用到,加上这个依赖以后就需要在项目启动前启动一个 Consul Agent,详情查看上篇文章,另外我们不需要把网关注册到 Consul,所以要给 Application 类加一行注解 @EnableDiscoveryClient(autoRegister = false)
。
增加配置文件 application.yml:
1 2 3 4 5 6 7 8 9 10 11 12
| server: port: 8222 spring: application: name: '@project.artifactId@' logging: level: root: info 'org.springframework.web.server': debug spring: profiles: active: consul
|
以及配置文件 application-consul.yml:
1 2 3 4 5
| spring: cloud: consul: host: localhost port: 8500
|
现在启动项目,访问 localhost:8222/
可以观察到日志输出,但网关还没有任何路由配置,请求结果都是 404。
路由配置
Spring Cloud Gateway 预置了一些机制用于通过配置 spring.cloud.gateway.routes
直接定义路由,下面是一个简单的示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| spring: cloud: gateway: routes: - id: example uri: https://example.org predicates: - Path=/example/** - id: redirect_example uri: https://example.org predicates: - Path=/redirect/example/** filters: - RedirectTo=302, https://example.org - id: my_get uri: https://httpbin.org predicates: - Path=/my_get/** filters: - RewritePath=/my_get/(?<path>.*), /get?path=$\{path}
|
对于每个路由定义 id
和 uri
是必须,uri
是转发目标。predicates
是路由匹配表达式,Spring Cloud Gateway 提供了很多内置的 Predicate,可以通过路径、Header、Query 参数、Cookie 等待方式匹配请求。filters
是路由中间件,所有 filter 构成 filter 链,每一层处理向下传递,Spring Cloud Gateway 也提供了很多内置的 filter,比较重要的例如熔断器、限流器等等。predicates
和 filters
列表有两种配置格式:
- 快捷格式
- 完整格式
1 2 3
| - name: <工厂> args: <参数列表>
|
具体的表达式和参数列表字段规范参考文档。
Filter
Filter 就是请求中间件,通过 Filter 构成的 GatewayFilterChain
,可以对请求做预处理、后处理或者提前中断请求,引用官方的流程图:
除了预置的 Filter,开发者可以通过 GlobalFilter
或者 GatewayFilterFactory
实现自定义 Filter。
GlobalFilter
实现 GlobalFilter
的类能自动被应用于全局请求,不过要注意一点,根据上面流程图,一个请求被 Handle 以后才会进入到 Filter 流程,也就是说如果请求没有匹配成功,就算是 GlobalFilter
也不会生效。
下面是一个记录所有请求来源 IP 的例子。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| @Component public class IpFilter implements GlobalFilter, Ordered {
static final Logger log = LoggerFactory.getLogger(IpFilter.class);
private String getClientIp(ServerHttpRequest request) { var forwardFor = request.getHeaders().getFirst("X-Forwarded-For"); if (forwardFor != null && !forwardFor.isEmpty()) { var slash = forwardFor.indexOf('/'); if (slash >= 0) { forwardFor = forwardFor.substring(0, slash); } return forwardFor; } var readIp = request.getHeaders().getFirst("X-Real-IP"); if (readIp != null && !readIp.isEmpty()) { return readIp; } var address = request.getRemoteAddress(); if (address != null) { return address.getAddress().getHostAddress(); } return ""; }
@Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { var clientIp = getClientIp(exchange.getRequest()); log.info("access {} {} from {}", exchange.getRequest().getMethod(), exchange.getRequest().getPath(), clientIp); return chain.filter(exchange); }
@Override public int getOrder() { return -1; } }
|
示例代码里还实现了 Ordered
接口,只是用来给多个 Filter 排序用的,越小越优先,最高优先级是 Ordered.HIGHEST_PRECEDENCE
。
GatewayFilterFactory
所有官方预置的 GatewayFilter 都是通过这个方式实现的,通过继承 AbstractGatewayFilterFactory
实现相应方法就能自定义 GatewayFilter,还能提供相应的配置类来维护配置。
下面是一个校验身份的简单示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| @Component public class AuthGatewayFilterFactory extends AbstractGatewayFilterFactory<AuthGatewayFilterFactory.Config> {
static final Logger log = LoggerFactory.getLogger(AuthGatewayFilterFactory.class);
public AuthGatewayFilterFactory() { super(Config.class); }
@Override public GatewayFilter apply(Config config) { return (exchange, chain) -> { var request = exchange.getRequest(); var path = request.getPath().value(); if (Arrays.stream(config.ignorePaths).anyMatch(path::startsWith)) { return chain.filter(exchange); } var authorization = request.getHeaders().getFirst("Authorization"); log.info("auth {} with {}", path, authorization); if (authorization == null || authorization.isEmpty()) { exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); return exchange.getResponse().setComplete(); } return chain.filter(exchange.mutate().request(builder -> builder.header(config.authHeader, authorization)).build()); }; }
public static class Config { String authHeader; String[] ignorePaths;
public String getAuthHeader() { return this.authHeader; }
public void setAuthHeader(String authHeader) { this.authHeader = authHeader; }
public String[] getIgnorePaths() { return this.ignorePaths; }
public void setIgnorePaths(String[] ignorePaths) { this.ignorePaths = ignorePaths; } } }
|
强调几个点:
- 如果在配置文件中的 Filter 名称默认情况下是从类名中提取,比如上面示例对应名称
Auth
,你可以重载 String name()
方法自定义名称。
- 构造中的
super(Config.class)
是必须的,否则没办法使用 Config 对象接收配置。
- 修改一个 exchange 需要
mutate()
和 build()
方式重建一个新的 exchange。
- Config 对象必须有 getter 和 setter。
这里顺便提一嘴,Spring 有专门用于鉴权的组件 Spring Security
,Filter 鉴权不算是标准做法。
通过增加 spring.cloud.gateway.default-filters
配置把 Auth
应用于所有路由。
1 2 3 4 5 6 7 8 9 10
| spring: cloud: gateway: default-filters: - name: Auth args: authHeader: 'X-User' ignorePaths: - '/login' - '/example'
|
执行命令:
1
| curl -H 'Authorization: 1' -v 'localhost:8222/my_get/hello'
|
可以观察到 headers 里有我们注入到字段。
路由
在前面我已经展示了通过配置文件配置路由的方法,Spring Cloud Gateway 还有两种常见的路由配置方法。
RouteLocator
首先是通过代码配置路由,下面是一个示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| @SpringBootApplication public class GatewayApplication {
@Bean RouteLocator buildRouteLocator(RouteLocatorBuilder builder) { return builder.routes() .route(p -> p.path("/get") .filters(f -> f.addRequestHeader("Hello", "World")) .uri("https://httpbin.org/get")) .build(); }
... }
|
代码不展开介绍,都被封装成了链式调用,根据自己的需求对照文档就能用上。代码定义的路由和配置中的路由共存,如果匹配谓词相同,代码定义的路由优先。有一个区别比较重要,就是 GlobalFilter
也会作用在代码定义的路由上,但是 spring.cloud.gateway.default-filters
不会被应用在代码定义的路由上。
discovery
这里终于要用上之前加的 Consul Discovery 插件了。
在 application.yml 中添加配置,启用网关自动发现 locator:
1 2 3 4 5 6 7 8 9
| spring: profiles: active: consul cloud: gateway: discovery: locator: enabled: true lower-case-service-id: true
|
只要这么简单的配置,网关会自动拉取服务列表,并且按照一定规则转发请求到对应服务。
这里启动之前的项目 spring-cloud-demo 后执行下面命令
1
| curl -H 'Authorization: 1' -v 'localhost:8222/demo/home/hello'
|
就可以看到网关成功把请求转发给 demo 服务了,如果启动多个 demo 实例,网关也自带了负载均衡。
Consul 服务列表可以通过加 tag 过滤,和后端服务配合,具体配置参考文档。
完整项目代码参考 spring-cloud-gateway
参考