Spring Cloud Gateway (一)入門篇

哆啦A夢的猜想發表於2019-11-12

1.閘道器是怎麼演化來的

  • 單體應用拆分成多個服務後,對外需要一個統一入口,解耦客戶端與內部服務
    在這裡插入圖片描述

2.閘道器的基本功能

  • 閘道器核心功能是路由轉發,因此不要有耗時操作在閘道器上處理,讓請求快速轉發到後端服務上
  • 閘道器還能做統一的熔斷、限流、認證、日誌監控等
    在這裡插入圖片描述

3.關於Spring Cloud Gateway

Spring Cloud Gateway是由spring官方基於Spring5.0、Spring Boot2.0、Project Reactor等技術開發的閘道器,使用非阻塞API,Websockets得到支援,目的是代替原先版本中的Spring Cloud Netfilx Zuul,目前Netfilx已經開源了Zuul2.0,但Spring 沒有考慮整合,而是推出了自己開發的Spring Cloud GateWay。這裡需要注意一下gateway使用的netty+webflux實現,不要加入web依賴(不要引用webmvc),否則初始化會報錯 ,需要加入webflux依賴。

gateway與zuul的簡單比較:gateway使用的是非同步請求,zuul是同步請求,gateway的資料封裝在ServerWebExchange裡,zuul封裝在RequestContext裡,同步方便調式,可以把資料封裝在ThreadLocal中傳遞。

Spring Cloud Gateway有三個核心概念:路由、斷言、過濾器
過濾器:gateway有兩種filter:GlobalFilter、GatewayFilter,全域性過濾器預設對所有路由有效。
文件地址:cloud.spring.io/spring-clou…

閘道器作為所有請求流量的入口,在實際生產環境中為了保證高可靠和高可用,儘量避免重啟,需要用到動態路由配置,在閘道器執行過程中更改路由配置

4.程式碼實踐

需要用到3個專案,eureka-server、gateway、consumer-service

  • 1.eureka-server 服務發現註冊,供gateway轉發請求時獲取服務例項 ip+port,使用前面部落格中的示例程式碼
  • 2.新建 gateway 閘道器專案,專案引用如下:
<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
123456789101112複製程式碼

在主類上啟用服務發現註冊註解 @EnableDiscoveryClient
配置檔案內容如下:

server:
  port: 9999
spring:
  profiles:
    active: dev
  application:
    name: gateway-service
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true
          # 服務名小寫
          lower-case-service-id: true
      routes:
      - id: consumer-service
        # lb代表從註冊中心獲取服務,且已負載均衡方式轉發
        uri: lb://consumer-service
        predicates:
        - Path=/consumer/**
        # 加上StripPrefix=1,否則轉發到後端服務時會帶上consumer字首
        filters:
        - StripPrefix=1

# 註冊中心
eureka:
  instance:
    prefer-ip-address: true
  client:
    service-url:
      defaultZone: http://zy:zy123@localhost:10025/eureka/


# 暴露監控端點
management:
  endpoints:
    web:
      exposure:
        include: '*'
  endpoint:
    health:
      show-details: always
123456789101112131415161718192021222324252627282930313233343536373839404142複製程式碼

上面就完成了閘道器程式碼部分,下面新建consumer-service

  • 3.consumer-service 消費者服務 ,通過閘道器路由轉發到消費者服務,並返回資訊回去,因此是個mvc的專案
    專案引用如下:
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
12345678複製程式碼

在主類上啟用服務發現註冊註解 @EnableDiscoveryClient
在配置檔案中新增配置:

server.port=9700
spring.application.name=consumer-service
eureka.instance.prefer-ip-address=true
# 配置eureka-server security的賬戶資訊
eureka.client.serviceUrl.defaultZone=http://zy:zy123@localhost:10025/eureka/
12345複製程式碼

新建 IndexController ,新增一個 hello 方法,傳入name引數,訪問後返回 hi + name 字串

@RestController
public class IndexController {

    @RequestMapping("/hello")
    public String hello(String name){
        return "hi " + name;
    }
}
12345678複製程式碼
  • 4.分別啟動3個專案,訪問 http://localhost:10025 看eureka上gateway與consumer-service例項是否註冊了,可以看到已經註冊了,分別在9700、9999埠
    在這裡插入圖片描述

通過閘道器訪問consumer-service的hello方法,http://localhost:9999/consumer/hello?name=zy ,效果如下,說明請求已經轉發到consumer-service服務上了
在這裡插入圖片描述

以上完成了閘道器的基本程式碼,下面繼續介紹一些常用的過濾器,通過過濾器實現統一認證鑑權、日誌、安全等檢驗

  • 5.在閘道器專案中新增 GlobalFilter 全域性過濾器,列印每次請求的url,程式碼如下:
/**
 * 全域性過濾器
 * 所有請求都會執行
 * 可攔截get、post等請求做邏輯處理
 */
@Component
public class RequestGlobalFilter implements GlobalFilter,Ordered {

    //執行邏輯
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest serverHttpRequest= exchange.getRequest();
        String uri = serverHttpRequest.getURI().toString();
        System.out.println(" uri : " + uri);//列印每次請求的url
        String method = serverHttpRequest.getMethodValue();
        if ("POST".equals(method)) {
            //從請求裡獲取Post請求體
            String bodyStr = resolveBodyFromRequest(serverHttpRequest);
            //TODO 得到Post請求的請求引數後,做你想做的事
 
            //下面的將請求體再次封裝寫回到request裡,傳到下一級,否則,由於請求體已被消費,後續的服務將取不到值
            URI uri = serverHttpRequest.getURI();
            ServerHttpRequest request = serverHttpRequest.mutate().uri(uri).build();
            DataBuffer bodyDataBuffer = stringBuffer(bodyStr);
            Flux<DataBuffer> bodyFlux = Flux.just(bodyDataBuffer);
 
            request = new ServerHttpRequestDecorator(request) {
                @Override
                public Flux<DataBuffer> getBody() {
                    return bodyFlux;
                }
            };
            //封裝request,傳給下一級
            return chain.filter(exchange.mutate().request(request).build());
        } else if ("GET".equals(method)) {
            Map requestQueryParams = serverHttpRequest.getQueryParams();
            //TODO 得到Get請求的請求引數後,做你想做的事
 
            return chain.filter(exchange);
        }
        return chain.filter(exchange);
    }
	/**
     * 從Flux<DataBuffer>中獲取字串的方法
     * @return 請求體
     */
    private String resolveBodyFromRequest(ServerHttpRequest serverHttpRequest) {
        //獲取請求體
        Flux<DataBuffer> body = serverHttpRequest.getBody();
 
        AtomicReference<String> bodyRef = new AtomicReference<>();
        body.subscribe(buffer -> {
            CharBuffer charBuffer = StandardCharsets.UTF_8.decode(buffer.asByteBuffer());
            DataBufferUtils.release(buffer);
            bodyRef.set(charBuffer.toString());
        });
        //獲取request body
        return bodyRef.get();
    }
 
    private DataBuffer stringBuffer(String value) {
        byte[] bytes = value.getBytes(StandardCharsets.UTF_8);
 
        NettyDataBufferFactory nettyDataBufferFactory = new NettyDataBufferFactory(ByteBufAllocator.DEFAULT);
        DataBuffer buffer = nettyDataBufferFactory.allocateBuffer(bytes.length);
        buffer.write(bytes);
        return buffer;
    }

    //執行順序
    @Override
    public int getOrder() {
        return 1;
    }
}
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475複製程式碼

重新執行閘道器專案,並訪問 http://localhost:9999/consumer/hello?name=zy ,檢視控制檯,可看到 uri 日誌被列印出來了
在這裡插入圖片描述

  • 6.在閘道器專案中新增 GatewayFilter 過濾器 ,我們給consumer-service 新增 token 認證過濾器 ,和全域性過濾器u同的是,GatewayFilter需要在配置檔案中指定那個服務使用此過濾器,程式碼如下:
/**
 * 可對客戶端header 中的 Authorization 資訊進行認證
 */
@Component
public class TokenAuthenticationFilter extends AbstractGatewayFilterFactory {

    private static final String Bearer_ = "Bearer ";

    @Override
    public GatewayFilter apply(Object config) {
        return (exchange, chain) -> {
            ServerHttpRequest request = exchange.getRequest();
            ServerHttpRequest.Builder mutate = request.mutate();
            ServerHttpResponse response = exchange.getResponse();
            try {
                //String token = exchange.getRequest().getQueryParams().getFirst("authToken");
                //1.獲取header中的Authorization
                String header = request.getHeaders().getFirst("Authorization");
                if (header == null || !header.startsWith(Bearer_)) {
                    throw new RuntimeException("請求頭中Authorization資訊為空");
                }
                //2.擷取Authorization Bearer
                String token = header.substring(7);
                //可把token存到redis中,此時直接在redis中判斷是否有此key,有則校驗通過,否則校驗失敗
                if(!StringUtils.isEmpty(token)){
                    System.out.println("驗證通過");
                    //3.有token,把token設定到header中,傳遞給後端服務
                    mutate.header("userDetails",token).build();
                }else{
                    //4.token無效
                    System.out.println("token無效");
                    DataBuffer bodyDataBuffer = responseErrorInfo(response , HttpStatus.UNAUTHORIZED.toString() ,"無效的請求");
                    return response.writeWith(Mono.just(bodyDataBuffer));
                }
            }catch (Exception e){
                //沒有token
                DataBuffer bodyDataBuffer = responseErrorInfo(response , HttpStatus.UNAUTHORIZED.toString() ,e.getMessage());
                return response.writeWith(Mono.just(bodyDataBuffer));
            }
            ServerHttpRequest build = mutate.build();
            return chain.filter(exchange.mutate().request(build).build());
        };
    }

    /**
     * 自定義返回錯誤資訊
     * @param response
     * @param status
     * @param message
     * @return
     */
    public DataBuffer responseErrorInfo(ServerHttpResponse response , String status ,String message){
        HttpHeaders httpHeaders = response.getHeaders();
        httpHeaders.add("Content-Type", "application/json; charset=UTF-8");
        httpHeaders.add("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0");

        response.setStatusCode(HttpStatus.UNAUTHORIZED);
        Map<String,String> map = new HashMap<>();
        map.put("status",status);
        map.put("message",message);
        DataBuffer bodyDataBuffer = response.bufferFactory().wrap(map.toString().getBytes());
        return bodyDataBuffer;
    }
}
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364複製程式碼

在配置檔案中指定consumer-service服務使用 TokenAuthenticationFilter ,配置如下:

routes:
- id: consumer-service
  uri: lb://consumer-service
  predicates:
  - Path=/consumer/**
  filters:
  # 進行token過濾
  - TokenAuthenticationFilter
  - StripPrefix=1
123456789複製程式碼

執行專案,再次訪問 http://localhost:9999/consumer/hello?name=zy
在這裡插入圖片描述

  • 7.前後端分離專案解決閘道器跨域問題,在閘道器主類中新增以下程式碼:
@Bean
	public WebFilter corsFilter() {
		return (ServerWebExchange ctx, WebFilterChain chain) -> {
			ServerHttpRequest request = ctx.getRequest();
			if (!CorsUtils.isCorsRequest(request)) {
				return chain.filter(ctx);
			}

			HttpHeaders requestHeaders = request.getHeaders();
			ServerHttpResponse response = ctx.getResponse();
			HttpMethod requestMethod = requestHeaders.getAccessControlRequestMethod();
			HttpHeaders headers = response.getHeaders();
			headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, requestHeaders.getOrigin());
			headers.addAll(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS, requestHeaders.getAccessControlRequestHeaders());
			if (requestMethod != null) {
				headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS, requestMethod.name());
			}
			headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true");
			headers.add(HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS, "all");
			headers.add(HttpHeaders.ACCESS_CONTROL_MAX_AGE, "3600");
			if (request.getMethod() == HttpMethod.OPTIONS) {
				response.setStatusCode(HttpStatus.OK);
				return Mono.empty();
			}
			return chain.filter(ctx);
		};
	}
123456789101112131415161718192021222324252627複製程式碼

程式碼已上傳至碼雲,原始碼,專案使用的版本資訊如下:

- SpringBoot 2.0.6.RELEASE
- SpringCloud Finchley.SR2複製程式碼

連結:blog.csdn.net/zhuyu199110…


相關文章