SpringCloud實戰十一:Gateway之 Spring Cloud Gateway


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,全局過濾器默認對所有路由有效。
文檔地址:https://cloud.spring.io/spring-cloud-static/Finchley.SR2/multi/multi_spring-cloud.html

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

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> 

在主類上啟用服務發現注冊注解 @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 

上面就完成了網關代碼部分,下面新建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> 

在主類上啟用服務發現注冊注解 @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/
  •  

新建 IndexController ,添加一個 hello 方法,傳入name參數,訪問后返回 hi + name 字符串

@RestController
public class IndexController {

    @RequestMapping("/hello")
    public String hello(String name){
        return "hi " + name;
    }
} 
  • 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;
    }
} 

重新運行網關項目,並訪問 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;
    }
} 

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

routes:
- id: consumer-service
  uri: lb://consumer-service
  predicates:
  - Path=/consumer/**
  filters:
  # 進行token過濾
  - TokenAuthenticationFilter
  - StripPrefix=1 

運行項目,再次訪問 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);
		};
	} 

代碼已上傳至碼雲,源碼,項目使用的版本信息如下:

- SpringBoot 2.0.6.RELEASE
- SpringCloud Finchley.SR2

轉自:https://blog.csdn.net/dark868/article/details/106532216/


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM