SpringCloud學習五-GateWay網關

1.GateWay是什麼?

GateWay 是SpringCloud 生態系統中的網關,目標是替代Zuul,同樣提供了限流,監控,路由轉發、權限校驗等功能。

相關名詞:

  • Route(路由):這是網關的基本構建塊。它由一個 ID,一個目標 URI,一組斷言和一組過濾器定義。如果斷言爲真,則路由匹配。
  • Predicate(斷言):這是一個 Java 8 的 Predicate。輸入類型是一個 ServerWebExchange。我們可以使用它來匹配來自 HTTP 請求的任何內容,例如 headers 或參數。
  • Filter(過濾器):這是org.springframework.cloud.gateway.filter.GatewayFilter的實例,我們可以使用它修改請求和響應。

工作流程:
GateWay.png

客戶端向 Spring Cloud Gateway 發出請求,如果HandlerMapping中找到了請求相匹配的路由,將其發送到Web Handler。Handler再通過指定過濾器鏈將請求發送到實際服務之星業務邏輯,然後返回。虛線是過濾器可能會在發送代理請求之前pre活之後post執行業務邏輯。

2.創建工程

2.1依賴

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webflux</artifactId>
</dependency>

注意:springcloud gateway使用的web框架爲webflux,和springMVC不兼容。

2.2代碼

@SpringBootApplication
public class GateWayApplication {
	public static void main(String[] args) {
		SpringApplication.run(GateWayApplication.class, args);
	}
	@Bean
	public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
		return builder.routes()
				.route("test_route", r -> r.path("/test")
						.uri("http://baidu.com"))
				.build();
	}
}

路由有兩種配置方式,一種是像上面這樣配置一個id名爲test_route的路由,當訪問項目地址/test的時候,會自動轉發到baidu.com。
另一種是在配置文件中配置,下面介紹。

2.3配置文件

server:
  port: 8080
spring:
  cloud:
    gateway:
      routes:
      - id: test_route
        uri: http://www.baidu.com
        predicates:
        - Path=/foo/**
        filters:
            - StripPrefix=1

字段含義如下:

  • id: 自定有路由id,唯一
  • uri: 跳轉的目標地址
  • predicates: 路由條件
  • filters: 過濾規則

上面配置的意思是配置了一個id爲test_route的路由規則,當訪問項目地址/foo/hello的時候自動轉發到地址baidu.com。如果上面例子中沒有加一個StripPrefix=1過濾器,則目標uri爲http://localhost:8000/foo/bar,StripPrefix過濾器是去掉一個路徑。
routes配置和代碼配置保留一處即可。

3.路由規則詳解

Predicate 來源於 Java 8,是 Java 8 中引入的一個函數,Predicate 接受一個輸入參數,返回一個布爾值結果。該接口包含多種默認方法來將 Predicate 組合成其他複雜的邏輯(比如:與,或,非)。可以用於接口請求參數校驗、判斷新老數據是否有變化需要進行更新操作。
在 Spring Cloud Gateway 中 Spring 利用 Predicate 的特性實現了各種路由匹配規則,有通過 Header、請求參數等不同的條件來進行作爲條件匹配到對應的路由。網上有一張圖總結了 Spring Cloud 內置的幾種 Predicate 的實現。

spring-cloud-gateway3.png

3.1時間匹配

spring:
  cloud:
    gateway:
      routes:
       - id: time_route
        uri: http://ityouknow.com
        predicates:
         - After=2018-01-20T06:06:06+08:00[Asia/Shanghai]

Spring 是通過 ZonedDateTime 來對時間進行的對比,ZonedDateTime 是 Java 8
中日期時間功能裏,用於表示帶時區的日期與時間信息的類,ZonedDateTime
支持通過時區來設置時間,中國的時區是:Asia/Shanghai。 After Route Predicate
是指在這個時間之後的請求都轉發到目標地址。上面的示例是指,請求時間在
2018年1月20日6點6分6秒之後的所有請求都轉發到地址http://ityouknow.com。+08:00是指時間和UTC時間相差八個小時,時間地區爲Asia/Shanghai。

3.2 Cookie匹配

Cookie Route Predicate 可以接收兩個參數,一個是 Cookie name ,一個是正則表達式,路由規則會通過獲取對應的 Cookie name 值和正則表達式去匹配,如果匹配上就會執行路由,如果沒有匹配上則不執行。

spring:
  cloud:
    gateway:
      routes:
       - id: cookie_route
         uri: http://ityouknow.com
         predicates:
         - Cookie=ityouknow, kee.e

3.3 通過請求路徑匹配(常用)

spring:
  cloud:
    gateway:
      routes:
      - id: host_route
        uri: http://baidu.com
        predicates:
        - Path=/foo/{segment}

如果請求路徑符合要求,則此路由將匹配,例如:/foo/1 或者 /foo/bar。
使用 curl 測試,命令行輸入:
curl http://localhost:8080/foo/1

3.4通過請求參數匹配

spring:
  cloud:
    gateway:
      routes:
      - id: query_route
        uri: http://ityouknow.com
        predicates:
        - Query=smile

這樣配置,只要請求中包含 smile 屬性的參數即可匹配路由。
使用 curl 測試,命令行輸入:
curl localhost:8080?smile=x&id=2

4.利用過濾器修改接口的返回報文(後置過濾)

各個子服務返回的報文各異,需要在網關對返回報文進行包裝統一返回格式。

@Component
@Slf4j
public class ResponseFilter  implements GlobalFilter, Ordered {
    //白名單
    @Value("${filter.url.white.list.rsp}")
    private String[] skipAuthUrls ;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpResponse originalResponse = exchange.getResponse();
        String url = exchange.getRequest().getURI().getPath();
        //跳過不需要驗證的路徑
        if(Arrays.asList(skipAuthUrls).contains(url)){
            return chain.filter(exchange);
        }
        DataBufferFactory bufferFactory = originalResponse.bufferFactory();
        ServerHttpResponseDecorator decoratedResponse = new ServerHttpResponseDecorator(originalResponse) {
            public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
                if (body instanceof Flux) {
                    Flux<? extends DataBuffer> fluxBody = (Flux<? extends DataBuffer>) body;
                    return super.writeWith(fluxBody.map(dataBuffer -> {
                        // probably should reuse buffers
                        byte[] content = new byte[dataBuffer.readableByteCount()];
                        dataBuffer.read(content);
                        // 釋放掉內存
                        DataBufferUtils.release(dataBuffer);
                        String rs = new String(content, Charset.forName("UTF-8"));
                        //默認失敗
                        CommonResult commonResult = CommonResult.failed();
                        try {
                            if(StringUtils.isNotBlank(rs)){
                                JsonResult jsonResult= JSONObject.parseObject(rs,JsonResult.class);
                                if(null != jsonResult ){
                                    if(0 == jsonResult.getCode()){
                                        commonResult = CommonResult.success(jsonResult.getData(),jsonResult.getMessage());
                                    }else{
                                        commonResult = CommonResult.failed(jsonResult.getMessage());
                                    }
                                }
                            }
                        }catch (Exception e){
                            log.error("轉換異常,異常報文:{}",rs);
                            log.error(e.getMessage(),e);
                        }
                        byte[] newRs = JSON.toJSONString(commonResult).getBytes(Charset.forName("UTF-8"));
                        originalResponse.getHeaders().setContentLength(newRs.length);//如果不重新設置長度則收不到消息。
                        return bufferFactory.wrap(newRs);
                    }));
                }
                return super.writeWith(body);
            }
        };
        return chain.filter(exchange.mutate().response(decoratedResponse).build());
    }

    /**
     *
     * 功能描述: 執行優先級
     * 此處order需要小於-1,需要先於NettyWriteResponseFilter過濾器執行
     * @param:
     * @return:
     * @auther: lfc
     * @date: 2019/8/25 18:56
     */
    @Override
    public int getOrder() {
        return -99;
    }

需要注意的是order需要小於-1,需要先於NettyWriteResponseFilter過濾器執行。

5.過濾器攔截權限認證(前置過濾)

@Component
@Slf4j
public class AuthFilter implements GlobalFilter, Ordered {

    @Value("${filter.url.white.list.req}")
    private String[] skipAuthUrls;

    private String jwtBlacklistKeyFormat;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String url = exchange.getRequest().getURI().getPath();
        //跳過不需要驗證的路徑
        if(Arrays.asList(skipAuthUrls).contains(url)){
            return chain.filter(exchange);
        }
        //從請求頭中取出token
        String token = exchange.getRequest().getHeaders().getFirst("Authorization");
        //未攜帶token或token在黑名單內
        if (token == null ||
                token.isEmpty() ||
                isBlackToken(token)) {
            if(log.isDebugEnabled() && isBlackToken(token)){
                log.debug("**********此token已加入黑名單**********");
            }
            ServerHttpResponse originalResponse = exchange.getResponse();
            originalResponse.setStatusCode(HttpStatus.OK);
            originalResponse.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
            byte[] response = JSONObject.toJSONString(CommonResult.unauthorized(null)).getBytes(StandardCharsets.UTF_8);
            DataBuffer buffer = originalResponse.bufferFactory().wrap(response);
            return originalResponse.writeWith(Flux.just(buffer));
        }
        //取出token包含的身份
        String data = JWTUtil.getData(token, Constant.JWT_TYPE);
        if(data.isEmpty()){
            ServerHttpResponse originalResponse = exchange.getResponse();
            originalResponse.setStatusCode(HttpStatus.OK);
            originalResponse.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
            byte[] response = JSONObject.toJSONString(CommonResult.forbidden(null)).getBytes(StandardCharsets.UTF_8);
            DataBuffer buffer = originalResponse.bufferFactory().wrap(response);
            return originalResponse.writeWith(Flux.just(buffer));
        }
        UserDTO ud = JSONObject.parseObject(data,UserDTO.class);
        // 1.解析判斷是否被修改 2.根據token 獲取redis中的數據以此判斷是否超時或有效
        if(!JWTUtil.verify(token) || StringUtils.isBlank(JedisUtil.getJson(ud.getJti()))){
            ServerHttpResponse originalResponse = exchange.getResponse();
            originalResponse.setStatusCode(HttpStatus.OK);
            originalResponse.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
            byte[] response = JSONObject.toJSONString(CommonResult.tamperToken(null)).getBytes(StandardCharsets.UTF_8);
            DataBuffer buffer = originalResponse.bufferFactory().wrap(response);
            return originalResponse.writeWith(Flux.just(buffer));
        }
        //redis 更新過期時間
        JedisUtil.setJson(ud.getJti(),token,Constant.EXRP_HOUR);
        data = JWTUtil.sign(JSONObject.toJSONString(ud));

        //將現在的request,添加當前身份
        ServerHttpRequest mutableReq = exchange.getRequest().mutate().header("Authorization", data).build();
        ServerWebExchange mutableExchange = exchange.mutate().request(mutableReq).build();

        return chain.filter(mutableExchange);
    }
    @Override
    public int getOrder() {
        return -100;
    }
    
    /**
     *
     * 功能描述: 判斷token是否在黑名單內
     * @param token
     * @return
     * @auther: lfc
     * @date: 2019/9/9 0:34
     */
    private boolean isBlackToken(String token){
//        assert token != null;
//        return stringRedisTemplate.hasKey(String.format(jwtBlacklistKeyFormat, token));
        return false;
    }

GateWay中區分前置過濾還是後置過濾取決於動作在chain.filter方法前還是之後,之後回調的是後置,之前調用的是前置。

參考資源:純潔微笑
方誌鵬-深入理解Spring Cloud與微服務構建

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章