一.网关简介
Spring Cloud Gateway是Spring Cloud Finchley版推出来的新组件,用来代替服务网关:Zuul。
那Spring Cloud Gateway 和 Zuul 都有哪些区别呢,咱们来比较一下:
1.开源组织
Spring Cloud Gateway是Spring Cloud微服务平台的一个子项目,属于Spring开源社区,依赖名叫:spring-cloud-starter-gateway。
https://spring.io/projects/spring-cloud-gateway
Zuul是Netflix公司的开源项目,Spring Cloud在Netflix项目中也已经集成了Zuul,依赖名叫:spring-cloud-starter-netflix-zuul。
https://github.com/Netflix/zuul
2.底层实现
https://stackoverflow.com/questions/47092048/how-is-spring-cloud-gateway-different-from-zuul
据Spring Cloud Gateway原作者的解释:
Zuul构建于Servlet 2.5,兼容3.x,使用的是阻塞式的API,不支持长连接,比如websockets。另外Spring Cloud Gateway构建于Spring 5+,基于 Spring Boot 2.x 响应式的、非阻塞式的 API。同时,它支持 websockets,和 Spring 框架紧密集成,开发体验相对来说十分不错。
3.性能表现
这个没什么好比的,要比就和Zuul 2.x比,Zuul 2.x在底层上有了很大的改变,使用了异步无阻塞式的API,性能改善明显,不过现在 Spring Cloud也没集成 Zuul 2.x,所以就没什么好比的。
如何选择?
上面说的 Zuul 指Zuul 1.x,Netflix早就发布了最新的 Zuul 2.x,但Spring Cloud貌似没有整合计划,栈长看了下目前最新的包,整合的还是 Zuul 1.x。
据了解,正是因为 Zuul 2.x 的不断跳票,Spring Cloud才釜底抽薪推出了自己的服务网关:Spring Cloud Gateway,使用起来比Zuul更简单,配置更方便,所以说选 Spring Cloud Gateway没错,毕竟是 Spring Cloud 亲儿子,不会始乱终弃。
二.用途
API网关出现的原因是微服务架构的出现,不同的微服务一般会有不同的网络地址,而外部客户端可能需要调用多个服务的接口才能完成一个业务需求,如果让客户端直接与各个微服务通信,会有以下的问题:
- 客户端会多次请求不同的微服务,增加了客户端的复杂性。
- 存在跨域请求,在一定场景下处理相对复杂。
- 认证复杂,每个服务都需要独立认证。
- 难以重构,随着项目的迭代,可能需要重新划分微服务。例如,可能将多个服务合并成一个或者将一个服务拆分成多个。如果客户端直接与微服务通信,那么重构将会很难实施。
- 某些微服务可能使用了防火墙/ 浏览器不友好的协议,直接访问会有一定的困难。
以上这些问题可以借助API网关解决。API 网关是介于客户端和服务器端之间的中间层,所有的外部请求都会先经过API网关这一层。也就是说,API 的实现方面更多的考虑业务逻辑,而安全、性能、监控可以交由 API 网关来做,这样既提高业务灵活性又不缺安全性,典型的架构图如图所示
使用 API 网关后的优点如下:
- 易于监控。可以在网关收集监控数据并将其推送到外部系统进行分析。
- 易于认证。可以在网关上进行认证,然后再将请求转发到后端的微服务,而无须在每个微服务中进行认证。
- 减少了客户端与各个微服务之间的交互次数。
三.工程搭建
1.工程结构
2.新建cloud-zero-gateway
pom.xml
<?xml version="1.0" encoding="UTF-8"?> xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> cloud-zero com.wind 1.0.0 4.0.0 cloud-zero-gateway org.springframework.cloud spring-cloud-starter-gateway org.springframework.cloud spring-cloud-starter-netflix-eureka-client
3.application.yml
server: port: 9527spring: redis: host: 192.168.0.185 port: 6379 database: 5 application: name: cloud-zero-gateway devtools: restart: enabled: true #热部署生效 cloud: gateway: discovery: locator: enabled: true routes: #system 模块 - id: cloud-zero-user uri: lb://cloud-zero-user predicates: - Path=/serve-user/** filters: - StripPrefix=1 # 限流配置 - name: RequestRateLimiter args: key-resolver: '#{@remoteAddrKeyResolver}' redis-rate-limiter.replenishRate: 1 redis-rate-limiter.burstCapacity: 2 # 降级配置 - name: Hystrix args: name: fallbackcmd fallbackUri: 'forward:/fallback' - id: zmrit_route uri: http://www.zmrit.com:80/ predicates: - Path=/zmrit/** filters: - StripPrefix=1eureka: client: #客户端注册进eureka服务列表内 service-url: defaultZone: http://eureka7001.com:7001/eureka #defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/,http://eureka7003.com:7003/eureka/ instance: instance-id: ${spring.cloud.client.ip-address}:${spring.application.name}:${server.port} prefer-ip-address: true #访问路径可以显示IP地址logging: level: com.wind: debug #com.wind包下的日志打印级别是debug
4.启动类GatewayApp
package com.wind.cloud.zero.gateway;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.cloud.client.discovery.EnableDiscoveryClient;@EnableDiscoveryClient@SpringBootApplicationpublic class GatewayApp{ public static void main(String[] args){ SpringApplication.run(GatewayApp.class, args); }}
四.自定义路由
gateway是网关,同时也是一个webflux服务,和servlet一样可以提供接口、视图等web内容。
路由断言工厂
路由断言工厂有多种类型,根据请求的时间、host、路径、方法等等。如下定义的是一个基于路径的路由断言匹配。
@Beanpublic RouterFunction> routerFunction(){ return RouterFunctions.route(RequestPredicates.path("/test"), request -> ServerResponse.ok().body(BodyInserters.fromObject("hello")));}
当请求的路径为/test时,直接返回ok的状态码,且响应体为hello字符串。
过滤器工厂
网关经常需要对路由请求进行过滤,进行一些操作,如鉴权之后构造头部之类的,过滤的种类很多,如增加请求头、增加请求参数、增加响应头和断路器等等功能。
@Beanpublic RouteLocator customRouteLocator(RouteLocatorBuilder builder){ return builder.routes().route(r -> r.path("/baidu/header").filters(f -> f.addResponseHeader("token", "mytoken")) .uri("http://baidu.com:80/")).build();}
http://localhost:9527/baidu/header 转发到www.baidu.com 并在header里添加了参数token,值为mytoken。
package com.wind.cloud.zero.gateway.config;import org.springframework.cloud.gateway.route.RouteLocator;import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.http.MediaType;import org.springframework.web.reactive.function.BodyInserters;import org.springframework.web.reactive.function.server.RequestPredicates;import org.springframework.web.reactive.function.server.RouterFunction;import org.springframework.web.reactive.function.server.RouterFunctions;import org.springframework.web.reactive.function.server.ServerResponse;import com.wind.cloud.zero.gateway.handler.HystrixFallbackHandler;import lombok.AllArgsConstructor;@Configuration@AllArgsConstructorpublic class RouterFunctionConfiguration{ private final HystrixFallbackHandler hystrixFallbackHandler; /** * 路由断言工厂 * @return */ @Bean public RouterFunction> routerFunction() { return RouterFunctions .route(RequestPredicates.path("/test"), request -> ServerResponse.ok().body(BodyInserters.fromObject("hello"))) .andRoute(RequestPredicates.path("/fallback").and(RequestPredicates.accept(MediaType.TEXT_PLAIN)), hystrixFallbackHandler); } /** * 过滤器工厂 * @param builder * @return */ @Bean public RouteLocator customRouteLocator(RouteLocatorBuilder builder) { return builder.routes().route(r -> r.path("/baidu/header").filters(f -> f.addResponseHeader("token", "mytoken")) .uri("http://baidu.com:80/")).build(); }}
yml定义
- id: zmrit_route uri: http://www.zmrit.com:80/ predicates: - Path=/zmrit
http://localhost:9527/zmrit 跳转到了www.zmrit.com
五.全局过滤器
实现GlobalFilter,通用用来处理全局过滤的内容,比如鉴权
package com.wind.cloud.zero.gateway.fiflt;import java.util.Arrays;import org.apache.commons.lang.StringUtils;import org.springframework.cloud.gateway.filter.GatewayFilterChain;import org.springframework.cloud.gateway.filter.GlobalFilter;import org.springframework.core.Ordered;import org.springframework.core.io.buffer.DataBuffer;import org.springframework.http.HttpStatus;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 lombok.extern.slf4j.Slf4j;import reactor.core.publisher.Flux;import reactor.core.publisher.Mono;/** * 全局过滤器 */@Slf4j@Componentpublic class AuthFilter implements GlobalFilter, Ordered{ // 排除过滤的 uri 地址 // swagger排除自行添加 private static final String[] whiteList = {"/auth/login"}; @Override public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { String url = exchange.getRequest().getURI().getPath(); log.info("url:{}", url); // 跳过不需要验证的路径 if (Arrays.asList(whiteList).contains(url)) { return chain.filter(exchange); } String token = exchange.getRequest().getHeaders().getFirst("token"); // token为空 if (StringUtils.isBlank(token)) { return setUnauthorizedResponse(exchange, "token can't null or empty string"); } if (!token.equals("mytoken")) { return setUnauthorizedResponse(exchange, "token error"); } ServerHttpRequest mutableReq = exchange.getRequest().mutate().header("token", token.split(",")).build(); ServerWebExchange mutableExchange = exchange.mutate().request(mutableReq).build(); return chain.filter(mutableExchange); } private Mono setUnauthorizedResponse(ServerWebExchange exchange, String msg) { ServerHttpResponse originalResponse = exchange.getResponse(); originalResponse.setStatusCode(HttpStatus.UNAUTHORIZED); originalResponse.getHeaders().add("Content-Type", "application/json;charset=UTF-8"); byte[] response = msg.getBytes(); DataBuffer buffer = originalResponse.bufferFactory().wrap(response); return originalResponse.writeWith(Flux.just(buffer)); } @Override public int getOrder() { return -200; }}
六.注册过滤器
注册给微服务用的,没有注册不生效,如某个模块需要限流和降级
pom.xml
org.springframework.cloud spring-cloud-starter-netflix-hystrix org.springframework.boot spring-boot-starter-data-redis-reactive
RateLimiterConfiguration
package com.wind.cloud.zero.gateway.config;import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import reactor.core.publisher.Mono;/** * 路由限流配置 */@Configurationpublic class RateLimiterConfiguration{ @Bean(value = "remoteAddrKeyResolver") public KeyResolver remoteAddrKeyResolver() { return exchange -> Mono.just(exchange.getRequest().getRemoteAddress().getAddress().getHostAddress()); }}
application.yml
server: port: 9527spring: redis: host: 192.168.0.185 port: 6379 database: 5 application: name: cloud-zero-gateway devtools: restart: enabled: true cloud: gateway: discovery: locator: enabled: true routes: #system 模块 - id: cloud-zero-user uri: lb://cloud-zero-user predicates: - Path=/serve-user/** filters: - StripPrefix=1 # 限流配置 - name: RequestRateLimiter args: key-resolver: '#{@remoteAddrKeyResolver}' redis-rate-limiter.replenishRate: 10 redis-rate-limiter.burstCapacity: 20 # 降级配置 - name: Hystrix args: name: fallbackcmd fallbackUri: 'forward:/fallback' - id: zmrit_route uri: http://www.zmrit.com:80/ predicates: - Path=/zmrit/** filters: - StripPrefix=1eureka: client: #客户端注册进eureka服务列表内 service-url: defaultZone: http://eureka7001.com:7001/eureka #defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/,http://eureka7003.com:7003/eureka/ instance: instance-id: ${spring.cloud.client.ip-address}:${spring.application.name}:${server.port} prefer-ip-address: true #访问路径可以显示IP地址
- filter名称必须是RequestRateLimiter
- redis-rate-limiter.replenishRate:允许用户每秒处理多少个请求
- redis-rate-limiter.burstCapacity:令牌桶的容量,允许在一秒钟内完成的最大请求数
- key-resolver:使用SpEL按名称引用bean
HystrixFallbackHandler
降级由hystrix提供
package com.wind.cloud.zero.gateway.handler;import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_ORIGINAL_REQUEST_URL_ATTR;import java.util.Optional;import org.springframework.http.HttpStatus;import org.springframework.http.MediaType;import org.springframework.stereotype.Component;import org.springframework.web.reactive.function.BodyInserters;import org.springframework.web.reactive.function.server.HandlerFunction;import org.springframework.web.reactive.function.server.ServerRequest;import org.springframework.web.reactive.function.server.ServerResponse;import lombok.extern.slf4j.Slf4j;import reactor.core.publisher.Mono;@Slf4j@Componentpublic class HystrixFallbackHandler implements HandlerFunction<ServerResponse>{ @Override public Mono<ServerResponse> handle(ServerRequest serverRequest) { Optional<Object> originalUris = serverRequest.attribute(GATEWAY_ORIGINAL_REQUEST_URL_ATTR); originalUris.ifPresent(originalUri -> log.error("网关执行请求:{}失败,hystrix服务降级处理", originalUri)); return ServerResponse.status(HttpStatus.INTERNAL_SERVER_ERROR.value()).contentType(MediaType.TEXT_PLAIN) .body(BodyInserters.fromObject("服务异常")); }}