一、引子
2年前有幸使用過一次Spring Cloud (1.5.9),1.* 集成的是ZUUL做網關。終於在2年后,這次果斷使用Spring Cloud Gateway。
區別:
Zuul構建於 Servlet 2.5,兼容 3.x,使用的是阻塞式的 API,不支持長連接,比如 websockets。
Spring Cloud Gateway構建於 Spring 5+,基於 Spring Boot 2.x 響應式的、非阻塞式的 API。支持 websockets,和Spring 框架緊密集成。底層使用netty模型,性能極高。
一個簡單的創業項目架構圖如下:
二、Gateway設計思想
2.1 官網設計
自從撇開netflex zuul后,spring Cloud速度搜搜的。我開發時還是用2.1.4,目前最新已經到2.2.1,附上官網飛機票
2.1.1 特性
-
Built on Spring Framework 5, Project Reactor and Spring Boot 2.0:基於 Spring Framework 5,Project Reactor 和 Spring Boot 2.0
-
Able to match routes on any request attribute.:能匹配任意請求屬性的路由
-
Predicates and filters are specific to routes.:針對特定路由使用匹配策略和過濾器
-
Hystrix Circuit Breaker integration. :集成Hystri斷路器
-
Spring Cloud DiscoveryClient integration:集成服務發現(gateway一樣可注冊到eureka)
-
Easy to write Predicates and Filters:易於寫策略(斷言)+過濾器
-
Request Rate Limiting 請求限流
-
Path Rewriting:重寫path
簡單來說就是Route、Predicate、Filter三大核心組件。
2.1.2 流程圖
如上圖,Gateway Client客戶端發送請求在Gateway Handler Mapping中查找是否命中路由策略,命中的話請求轉發給Gateway Web Handler來處理。根據定義的多個Filter鏈,執行順序:Pre Filter->代理請求->Post Filter。
2.1.3 內置Predicates+Filter
Gateway內置了11個Predicates Factories路由策略(斷言)工廠類。
Filter分2類:
- 31個GatewayFilter Factories網關過濾器工廠類
- 10個GlobalFilter 全局過濾器接口
這里就不在過多介紹,建議有需求時可以去官網找找,沒有的話再自己開發。
2.2 我們的使用
1.使用Route結合Hystrix實現默認降級策略
2.使用GatewayFilter接口,自定義過濾器類,實現登錄態(token)校驗
三、Gateway簡單使用
3.1 實現微服務的默認降級策略
spring: cloud: gateway: discovery: locator: enabled: false #開啟小寫驗證,默認feign根據服務名查找都是用的全大寫 lowerCaseServiceId: true default-filters: - AddResponseHeader=X-Response-Default-Foo, Default-Bar routes: - id: OLOAN-FINANCIAL-PRODUCT-SERVICE # lb代表從注冊中心獲取服務 uri: lb://OLOAN-FINANCIAL-PRODUCT-SERVICE predicates: # 轉發該路徑 - Path=/gateway/financialProduct/** # 帶前綴 filters: - StripPrefix=1 - name: Hystrix args: name: fallbackcmd fallbackUri: forward:/defaultfallback - id: ADMIN-SERVICE uri: lb://ADMIN-SERVICE predicates: - Path=/gateway/auth/** filters: - StripPrefix=2 - name: Hystrix args: name: fallbackcmd fallbackUri: forward:/defaultfallback
如上圖,我們開啟了2個微服務route路由。
- 1)前端請求時path帶/gateway/,在gateway層使用StripPrefix=1,去掉gateway,最終微服務上的path不帶"/gateway/".
- 2)使用Hystrix實現默認降級策略,降級接口實現如下:
@Slf4j
@RestController
public class DefaultHystrixController {
@RequestMapping("/defaultfallback")
public ApiResult defaultfallback(){
log.info("服務降級中");
return ApiResult.failure("服務異常");
}
}
3.2 實現登錄態(token)校驗
3.2.1 自定義過濾器
自定義過濾器,實現GatewayFilter, Ordered 2個接口。
1 import com.*.auth.UserTokenTools; 2 import lombok.extern.slf4j.Slf4j; 3 import org.apache.commons.lang3.StringUtils; 4 import org.springframework.cloud.gateway.filter.GatewayFilter; 5 import org.springframework.cloud.gateway.filter.GatewayFilterChain; 6 import org.springframework.core.Ordered; 7 import org.springframework.http.HttpHeaders; 8 import org.springframework.http.HttpStatus; 9 import org.springframework.http.server.reactive.ServerHttpRequest; 10 import org.springframework.http.server.reactive.ServerHttpResponse; 11 import org.springframework.stereotype.Component; 12 import org.springframework.web.server.ServerWebExchange; 13 import reactor.core.publisher.Mono; 14 15 /** 16 * @author denny 17 * @Description token過濾器 18 * @date 2019/12/12 13:55 19 */ 20 @Slf4j 21 @Component 22 public class LoginTokenFilter implements GatewayFilter, Ordered { 23 24 private static final String AUTHORIZE_TOKEN = "Authorization"; 25 private static final String BEARER = "Bearer "; 26 27 /** 28 * token過濾 29 * 30 * @param exchange 31 * @param chain 32 * @return 33 */ 34 @Override 35 public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { 36 log.info("當前環境已開啟token校驗"); 37 ServerHttpRequest request = exchange.getRequest(); 38 HttpHeaders headers = request.getHeaders(); 39 ServerHttpResponse response = exchange.getResponse(); 40 // 取Authorization 41 String tokenHeader = headers.getFirst(AUTHORIZE_TOKEN); 42 log.info("tokenHeader=" + tokenHeader); 43 // token不存在 44 if (StringUtils.isEmpty(tokenHeader)) { 45 response.setStatusCode(HttpStatus.UNAUTHORIZED); 46 return response.setComplete(); 47 } 48 // 取token 49 String token = this.getToken(tokenHeader); 50 log.info("token=" + token); 51 52 // token不存在 53 if (StringUtils.isEmpty(token)) { 54 log.info("token不存在"); 55 response.setStatusCode(HttpStatus.UNAUTHORIZED); 56 return response.setComplete(); 57 } 58 // 校驗 token是否失效 59 if (UserTokenTools.isTokenExpired(token, null)) { 60 log.info("token失效"); 61 response.setStatusCode(HttpStatus.UNAUTHORIZED); 62 return response.setComplete(); 63 } 64 // 校驗 token是否正確 65 if (!UserTokenTools.checkToken(token, null)) { 66 response.setStatusCode(HttpStatus.UNAUTHORIZED); 67 return response.setComplete(); 68 } 69 70 // //有token 這里可根據具體情況,看是否需要在gateway直接把解析出來的用戶信息塞進請求中,我們最終沒有使用 71 // UserTokenInfo userTokenInfo = UserTokenTools.getUserTokenInfo(token); 72 // log.info("token={},userTokenInfo={}",token,userTokenInfo); 73 // request.getQueryParams().add("token",token); 74 //request.getHeaders().set("token", token); 75 return chain.filter(exchange); 76 } 77 78 79 @Override 80 public int getOrder() { 81 return -10; 82 } 83 84 /** 85 * 解析Token 86 */ 87 public String getToken(String requestHeader) { 88 //2.Cookie中沒有從header中獲取 89 if (requestHeader != null && requestHeader.startsWith(BEARER)) { 90 return requestHeader.substring(7); 91 } 92 return ""; 93 } 94 }
上圖中,UserTokenTools是我們自定義的一個JWT工具類,用來生成token,校驗token過期、正確等。
3.2.2 配置路由
大家可根據具體情況,如果只有一套登錄態,那就用一個filter即可。
1 import com.*.gateway.filter.AuthorizeGatewayFilter; 2 import com.*.gateway.filter.LoginTokenFilter; 3 import org.springframework.cloud.gateway.route.RouteLocator; 4 import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder; 5 import org.springframework.context.annotation.Bean; 6 import org.springframework.context.annotation.Configuration; 7 8 9 @Configuration 10 public class GatewayConfig { 11 12 @Bean 13 public RouteLocator getRouteLocator(RouteLocatorBuilder builder) { 14 return builder.routes() 15 // token校驗1 16 .route(predicateSpec -> predicateSpec 17 .path("/gateway/pay/card/**", "/gateway/app/**") 18 .filters(gatewayFilterSpec -> gatewayFilterSpec.stripPrefix(1).filter(new AuthorizeGatewayFilter())) 19 .uri("lb://OLOAN-PAY-SERVICE") 20 .id("OLOAN-PAY-SERVICE-token")) 21 22 // token校驗2 23 .route(predicateSpec -> predicateSpec 24 .path("/gateway/order-audit/**", "/gateway/order/**", "/gateway/order-payment/**") 25 .filters(gatewayFilterSpec -> gatewayFilterSpec.stripPrefix(1).filter(new LoginTokenFilter())) 26 .uri("lb://OLOAN-ORDER-SERVICE") 27 .id("OLOAN-ORDER-ORDER-token")) 28 .build(); 29 } 30 }
四、總結
4.1.WebFlux
Spring Cloud Gateway使用WebFlux,和spring boot web包沖突,使用時一定記得pom中排除原來老WEB那一套(servlet)相關jar,否則會報錯。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
</exclusion>
</exclusions>
</dependency>
4.2.Gateway Filter
Gateway Filter 自帶的源碼支撐錯誤碼response.setStatusCode(HttpStatus.UNAUTHORIZED);並不是那么的友好。錯誤碼枚舉使用的是spring自帶框架的枚舉類:
org.springframework.http.HttpStatus:
UNAUTHORIZED(401, "Unauthorized")
code message data)不同。當然官方也是提供了解決方案。后續再去優化吧。 的結構體和一般定義的JSON格式(
4.3 限流
gateway默認實現了幾個簡單的限流策略(依賴redis),后續可以使用一下。