Spring-Cloud-Gateway-07

前言

1、什么是网关

网关是微服务最边缘的服务,直接暴露给用户,用来做用户和微服务的桥梁

在这里插入图片描述

  • 没有网关:客户端直接访问我们的微服务,会需要在客户端配置很多的ip:port,如果user-service并发比较大,则无法完成负载均衡
  • 有网关:客户端访问网关,网关来访问微服务,(网关可以和注册中心整合,通过服务名称找到目标的ip:prot)这样只需要使用服务名称即可访问微服务,可以实现负载均衡,可以实现token拦截、权限验证、限流等操作

2、Geteway简介

网关共有两个:Zuul和Geteway

  • Zuul:由newflix公司创建,分为1.0、2.0版本。Zuul的本质,一组过滤器,根据自定义的过滤器顺序来执行,本质就是web组件
    • Zuul1.0:使用的是BIO(Blocking IO) tomcat7.0以前都是BIO 性能一般
    • Zuul2.0:使用的NIO,性能好
  • SpringCloud:基于spring5.x,springboot2.x和ProjectReactor等技术。它的目的是让路由更加简单,灵活,还提供了一些强大的过滤器功能,如:熔断、限流、重试、自定义过滤器等token校验ip黑名单等。
  • SpringCloud Geteway作为Spring Cloud生态的网关,目标是替代Zuul,在SpringCloud2.0以上的版本中,没有对新版本的zuul2.0以上的最新高性能版本进行集成,仍然还是使用的zuul1.x。
  • SpringCloud Geteway是基于webFlux框架实现的,而webFlus框架底层则使用了高性能的Reactor模式通信框架的Netty
  • SpringCloud Geteway功能:
    • 建立在Spring Framework 5,Project Reactor和Spring Boot2.0之上
    • 能够匹配任何请求属性上的路由
    • Hystrix断路器集成
    • Spring Cloud DiscoveryClient集成
    • 易于编写的谓词和过滤器
    • 请求速率限制
    • 路径改写

3、Geteway工作流程

网关的核心:就是一组过滤器,按照先后执行顺序来执行过滤操作order 0 1 2

客户端向springCloud Gateway发出请求,然后在Gateway Handler Mapping中找到与请求相匹配的路由,将其发送到Gateway Web Handler

Handler 再通过指定的过滤器来将请求发送到我们实际的服务的业务逻辑,然后返回。过滤器之间用虚线分开是因为过滤器可能会在发送请求之间[pre]或之后[post]执行业务逻辑,对其进行加强或处理。

Filter在[pre]类型的过滤器可以做参数校验、权限校验、流量监控、日志输出、协议转换等

在[post]类型的过滤器中可以做响应的内容、响应头的修改、日志的输出,流量监控等有着非常重要的作用

总结:Gateway的核心逻辑也就是 路由转发 + 执行过滤器链

在这里插入图片描述

4、Gateway三大概念

4.1、Route(路由)(重点 和 eureka结合做动态路由)

路由信息的组成:

由一个ID、一个目的URL、一组断言工厂、一组Filter组成。

如果路由断言为真,说明请求URL和配置路由匹配

4.2、Predicate(断言)(就是一个返回bool的表达式)

Java 8 中的断言函数,lambda四大接口 供给型,消费性,函数型,断言型

Spring Cloud Gateway 中的断言函数输入类型是Spring5.0框架中的ServerWebExchange。String Cloud Gateway的断言函数允许开发者去定义匹配来自于Http Request中的任何信息。比如请求头和参数

4.3、Filter(过滤)(重点)

一个标准的Spring WebFilter。 Web三大组件(servlet listener filter) mvc interceptor

Spirng Cloud Gateway中的Filter分为两种类型的Filter,分别是Gateway Filter 和Global Filter。过滤器Filter将会对请求和相应进行修改处理。

一个针对某一个路由的filter 对某一个接口做限流

一个针对全局的filter (token ip黑名单)

5、Nginx和Gateway的区别

Nginx在做路由,负载均衡,限流之前,都有修改nginx.conf的配置文件,把需要负载均衡,路由,限流的规则加在里面。

在这里插入图片描述

快速使用

1、配置文件写法

  1. 新建一个项目,然后依赖只需要导入Gateway,不需要导入web依赖,因为Gateway是用netty写的,Spring Web集成的是Tomcat

    在这里插入图片描述

  2. 项目创建好后,修改SpringCloud依赖版本HoxtenSr12、SpringBoot依赖版本2.3.12.RELEASE

    在这里插入图片描述

  3. 依赖修改好后,编写路由的配置文件,本次测试使用Spring-Cloud-Feign-03创建的order-server测试服务,转发调用buyCar接口,先把测试项目启动起来

  4. 然后编写GateWay的配置文件

    server:
      port: 80 # 网关一般是80
    
    spring:
      application:
        name: gateway-server
      cloud:
        gateway:
          enabled: true # 只要加了依赖,就默认开启
          routes:
            - id: order-server # 路由的id,保持唯一即可
              uri: http://localhost:8080 # uri统一资源标识符 url统一资源定位符
              predicates:
                - Path=/buyCar # 匹配规则 只要你Path匹配上了/buyCar 就往uri转发 并且将路径带上
    
  5. 配置好后,然后启动项目,先访问localhost:8080/buyCar(先测试order-a接口是否可以正常调用)

    在这里插入图片描述

  6. 可以调用后,我们再访问locahost/buyCar(端口为80的可以省略:80),调用Gateway项目的buyCar接口,查看是否可以转发到localhost:8080/buyCar接口上,可以发现,调用成功

    在这里插入图片描述

  7. 到此为止,配置文件配置路由已经完毕。

2、代码写法

配置路由可以使用配置文件进行配置,也可以使用Bean对象进行配置,官网给我们的案例也是使用Bean对象进行配置的Gateway

在这里插入图片描述

  1. 我们把配置文件配置的给注解掉,然后copy上方案例,进行修改配置接口

    package com.tcc.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;
    
    /**
     * @author :tcc
     * @date :Created on 2022/12/18 15:50
     * @description:
     * @modified By:
     * @version:
     */
    // 先注入到Bean中
    @Configuration
    public class RouteConfit {
        // 然后copy官网的这段配置,进行修改
        // 代码的路由,和yml不冲突,都可以使用
        @Bean
        public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
            return builder.routes()
                    .route("order-service", r -> r.path("/buyCar")
                            .uri("http://localhost:8080"))
                    .build();
        }
    }
    
  2. 然后再访问localhost/buyCar可以查看到依旧访问成功

    在这里插入图片描述

  3. 到此为止,代码配置路由完毕

配置动态路由(重要)

我们像上面一样配置,就需要一个api配置一个路由,肯定很不合理,解决办法共有两种:

  • 一种是修改配置文件的匹配规则为Path=/car/**,这样就能转发car路由下所有子路由(/car/buyCar),但是这样也需要配置很多路由
  • 另外一种就是配置动态路由(常用)

我们这里一一演示

1、配置为主路由

  1. 为了方便测试,我们在order-service服务增加/car主路由,并配置多个子路由

    package com.tcc.controller;
    
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    /**
     * @author 宇辰
     * @date 2022/9/27-8:59
     **/
    @RestController
    @RequestMapping("car")
    public class TestController {
    
        // 买车
        @RequestMapping("buyCar")
        public String buyCar() {
            return "您购买了:劳斯莱斯-幻觉";
        }
    
        // 洗车
        @RequestMapping("washCar")
        public String washCar() {
            return "您洗了洗您的:劳斯莱斯-幻觉";
        }
    
        // 卖车
        @RequestMapping("sellCar")
        public String sellCar(){
            return "您卖出了:劳斯莱斯-幻觉";
        }
    }
    
  2. 配置好后,启动项目,进行测试

    在这里插入图片描述

  3. 然后在Gateway项目里修改配置文件,本次演示使用配置文件,配置/car下所有子路由的转发,记得把上面代码写法的配置文件解注,代码配置注掉

    server:
      port: 80 # 网关一般是80
    
    spring:
      application:
        name: gateway-server
      cloud:
        gateway:
          enabled: true # 只要加了依赖,就默认开启
          routes:
            - id: order-server # 路由的id,保持唯一即可
              uri: http://localhost:8080 # uri统一资源标识符 url统一资源定位符
              predicates:
                - Path=/car/** # 匹配规则 只要你Path匹配上了/car下的路由 就往uri转发 并且将路径带上
    
  4. 配置好后,重启项目,然后依次访问localhost/car/buyCar、localhost/car/washCar、localhost/car/sellCar

    在这里插入图片描述

  5. 到此为止,这种做法配置完毕

2、配置动态路由

配置动态路由需要在Gateway项目里添加Eureka依赖,根据拉取的服务列表,来根据地址上的服务名称,获取列表里的该服务的地址及端口号

  1. 添加Eureka的依赖以及配置文件

    server:
      port: 80 # 网关一般是80
    
    spring:
      application:
        name: gateway-server
      cloud:
        gateway:
          enabled: true # 只要加了依赖,就默认开启
          routes:
            - id: order-server # 路由的id,保持唯一即可
              uri: http://localhost:8080 # uri统一资源标识符 url统一资源定位符
              predicates:
                - Path=/car/buyCar # 匹配规则 只要你Path匹配上了/buyCar 就往uri转发 并且将路径带上
    
    eureka: # 配置Eureka
      client:
        service-url:
          defaultZone: http://82.157.246.210:8761/eureka
        registry-fetch-interval-seconds: 3 # 网关拉取服务列表的时间缩短
    
  2. 添加好后,开启Gateway的动态路由

    server:
      port: 80 # 网关一般是80
    
    spring:
      application:
        name: gateway-server
      cloud:
        gateway:
          enabled: true # 只要加了依赖,就默认开启
          routes:
            - id: order-server # 路由的id,保持唯一即可
              uri: http://localhost:8080 # uri统一资源标识符 url统一资源定位符
              predicates:
                - Path=/car/buyCar # 匹配规则 只要你Path匹配上了/buyCar 就往uri转发 并且将路径带上
          discovery: # 配置动态路由
            locator:
              enabled: true # 开启动态路由 开启通过应用名称 找到服务的功能
              lower-case-service-id: true # 开启服务名称小写
    
    eureka: # 配置Eureka
      client:
        service-url:
          defaultZone: http://82.157.246.210:8761/eureka
        registry-fetch-interval-seconds: 3 # 网关拉取服务列表的时间缩短
    
  3. 开启好后,访问localhost/服务名称/api地址即可

    1. 配置的静态路由依旧可以访问

      在这里插入图片描述

    2. 动态路由

      在这里插入图片描述

  4. 到此为止,动态路由配置完毕!

负载均衡

从之前的配置里面我们可以看到我们的URL都是写死的,这不符合我们微服务的要求,我们微服务是只要知道服务的名字,根据名字去找,而直接写死就没有负载均衡的效果了。

默认情况下Gateway会根据注册中心的服务列表,以注册中心上微服务名为路径创建动态路由进行转发,从而实现动态路由的功能

需要注意的是uri的协议为lb(load Balance),表示启用Gateway的负载均衡功能。

lb://serviceName是spring cloud gateway在微服务中自动为我们创建的负载均衡uri

协议:就是双方约定的一个街头暗号

Predicate断言

在gateway启动时会去加载一些路由断言工厂,启动的时候在控制台也能看到。使用断言可以在限时抢购的时候使用

在这里插入图片描述

使用方法

可以参考官网:Spring Cloud Gateway,我下面演示也会使用如下方法

在这里插入图片描述

Gateway接受一个由ZonedDateTime类生成的时间

在这里插入图片描述

断言可以匹配很多东西,比如指定主机(hose)、Cookie、方法参数等,下面只演示After一种,有其他需求的可以查看上面地址的官网演示,我这里也从bilibili上观看的学习视频上截取了几张图片

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

1、快速使用

  1. 修改配置文件,改为/washCar必须在2022年12月19日之后才能进行访问

  2. 先获取指定的时间格式

    @Test
    void contextLoads() {
        ZonedDateTime now = ZonedDateTime.now();
        System.out.println(now); // 2022-12-19T20:24:25.659+08:00[Asia/Shanghai]
    }
    
  3. 然后修改配置文件,在指定的时间之后才能进行访问

    server:
      port: 80 # 网关一般是80
    
    spring:
      application:
        name: gateway-server
      cloud:
        gateway:
          enabled: true # 只要加了依赖,就默认开启
          routes:
            - id: order-server # 路由的id,保持唯一即可
              uri: http://localhost:8080 # uri统一资源标识符 url统一资源定位符
    #          uri: lb://localhost:8080 # 使用lb协议 微服务名称做负载均衡
              predicates:
                - Path=/car/washCar # 匹配规则 只要你Path匹配上了/car/washCar 就往uri转发 并且将路径带上
                - After=2022-12-19T20:28:25.659+08:00[Asia/Shanghai] # 在20:28分钟后才能进行访问
          discovery: # 配置动态路由
            locator:
              enabled: true # 开启动态路由 开启通过应用名称 找到服务的功能
              lower-case-service-id: true # 开启服务名称小写
    
    eureka:
      client:
        service-url:
          defaultZone: http://82.157.246.210:8761/eureka
        registry-fetch-interval-seconds: 3 # 网关拉取服务列表的时间缩短
    
  4. 配置完毕后,然后启动服务,查看在指定时间之前是否可访问

    1. 28之前

      在这里插入图片描述

    2. 28的时候

      在这里插入图片描述

  5. 到此为止,断言演示完毕

2、查看源码

有兴趣了解源码的,可以查看RoutePredicateFactory接口类,查看它的实现类,里面写了所有断言规则,意味着你也可以自定义一个

在这里插入图片描述

在这里插入图片描述

Filter过滤器(重点)

1、概述

gateway里面的过滤器和Servlet里面的过滤器,功能差不多,路由过滤器可以用于修改进入Http请求和返回Http响应

2、过滤器分类

  • 按生命周期分两种

    • pre 在业务逻辑之前**(本编主要讲的是在逻辑之前的一个过滤)**

    • post 在业务逻辑之后

  • 按种类分也是两种

    • GatewayFilter 需要配置某个路由,才能过滤。如果需要使用全局路由,需要配置Default Filters

    • GlobalFilter 全局过滤器,不需要配置路由,系统初始化作用到所有的路由上

全局过滤器用处:统计请求次数、限流、token的校验、ip黑名单拦截、跨域、144开头的电话和一些ip的访问

过滤器实例也可以查看官网:Spring Cloud Gateway示例,官网提供了很多种过滤器,不过我们一般自己根据自己项目的需求写属于自己的过滤器,下面演示自定义过滤器的使用

3、局部过滤器

局部过滤器就不演示了,可以针对某个/些路由进行过滤操作,需要自写过滤器的话,可以实现GatewayFilter接口,参考官方提供的org.springframework.cloud.gateway.filter.factory.AddRequestHeaderGatewayFilterFactory过滤器

4、全局过滤器

主要演示的就是全局过滤器,对所有路由都适用的过滤器的实现操作

自定义过滤器需要实现一个GlobalFilter的接口,然后在实现的方法里面编写自己的需求

4.1、全局过滤器的放行

package com.tcc.filter;

import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

/**
 * @author :tcc
 * @date :Created on 2022/12/21 19:38
 * @description:
 * @modified By:
 * @version:
 */
public class MyGlobalFilter implements GatewayFilter {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        
        // 放行
        return chain.filter(exchange);
    }
}

4.2、过滤器内能获取的信息

通过前端发出请求,我们能获取到请求内的所有信息,以及ip、方法名称、请求路径等信息,用作过滤逻辑使用

注意:编写完过滤器后,我们要对过滤器进行排序操作,以保证代码以正确的过滤顺序依次执行

package com.tcc.filter;

import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpHeaders;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

/**
 * @author :tcc
 * @date :Created on 2022/12/21 19:38
 * @description:Ordered是对过滤器的排序
 * @modified By:
 * @version:
 */
@Component
public class MyGlobalFilter implements GlobalFilter, Ordered {

    /**
    	实现过滤方法
    **/
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        // 访问地址
        String path = request.getURI().getPath();
        System.out.println(path);

        // 请求头
        HttpHeaders headers = request.getHeaders();
        System.out.println(headers);

        // 请求方式
        String methodName = request.getMethod().name();
        System.out.println(methodName);

        // 主机名称
        String hostName = headers.getHost().getHostString();
        System.out.println(hostName);

        // 放行
        return chain.filter(exchange);
    }

    /**
    	实现排序方法
    **/
    @Override
    public int getOrder() {
        // 过滤的顺序,数值越小,越先执行过滤
        return 0;
    }
}

访问接口后,结果

在这里插入图片描述

4.3、不放行返回内容

如果该请求满足条件,被过滤掉了,我们通常会返回一个状态码和信息,告知前端因为什么原因请求没有被放行,通常数据的格式为json格式。下面就模拟过滤失败返回的内容

package com.tcc.filter;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
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.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.util.HashMap;

/**
 * @author :tcc
 * @date :Created on 2022/12/21 19:38
 * @description:Ordered是对过滤器的排序
 * @modified By:
 * @version:
 */
@Component
public class MyGlobalFilter implements GlobalFilter, Ordered {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // 放行
//        return chain.filter(exchange);

        ServerHttpResponse response = exchange.getResponse();
        // 防止乱码
        response.getHeaders().set("content-type","application/json;charset=utf-8");
        // {"code":"200","msg","ok"}

        // 组装返回值
        HashMap<String, Object> map = new HashMap<>();
        map.put("code", HttpStatus.UNAUTHORIZED.value());
        map.put("msg","你未授权");

        ObjectMapper objectMapper = new ObjectMapper();
        // 把map转换成一个字节
        byte[] bytes = null;
        try {
            bytes = objectMapper.writeValueAsBytes(map);
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }
        // 通过buffer工厂将字节数组包装成一个数据包
        DataBuffer wrap = response.bufferFactory().wrap(bytes);
        return response.writeWith(Mono.just(wrap));
    }

    @Override
    public int getOrder() {
        // 过滤的顺序,数值越小,越先执行过滤
        return 0;
    }
}

再次访问接口,结果

在这里插入图片描述

4.4、校验ip

通过过滤器,我们可以做一些黑名单/白名单的操作。

  • 黑名单:不让指定的人进入,比如打电话,有一些骚扰电话会进行自动拦截
  • 白名单:只允许指定的人进入,比如数据库权限

有时候,我们需要过滤一些国外的ip,或恶意攻击的ip,相当于是一个黑名单的操作

后面我们会进行多种情节使用举例,我们可以先把过滤失败返回的操作给封装城一个工具类使用

工具类

package com.tcc.util;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.util.HashMap;

/**
 * @author :tcc
 * @date :Created on 2022/12/21 20:18
 * @description:
 * @modified By:
 * @version:
 */
public class MyUtil {
    public static Mono<Void> returnData(ServerWebExchange exchange, Integer code, String msg){
        ServerHttpResponse response = exchange.getResponse();
        // 防止乱码
        response.getHeaders().set("content-type","application/json;charset=utf-8");
        // {"code":"200","msg","ok"}

        // 组装返回值
        HashMap<String, Object> map = new HashMap<>();
        map.put("code", code);
        map.put("msg",msg);
        ObjectMapper objectMapper = new ObjectMapper();
        // 把map转换成一个字节
        byte[] bytes = null;
        try {
            bytes = objectMapper.writeValueAsBytes(map);
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }
        // 通过buffer工厂将字节数组包装成一个数据包
        DataBuffer wrap = response.bufferFactory().wrap(bytes);
        return response.writeWith(Mono.just(wrap));
    }
}

代码

package com.tcc.filter;

import com.tcc.util.MyUtil;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpHeaders;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.util.Arrays;
import java.util.List;

/**
 * @author :tcc
 * @date :Created on 2022/12/21 19:38
 * @description:Ordered是对过滤器的排序
 * @modified By:
 * @version:
 */
@Component
public class IPGlobalFilter implements GlobalFilter, Ordered {

    // 每次查数据库会很消耗性能,所以一般会写在内存里,或者使用正则进行匹配过滤
    private List<String> blackList = Arrays.asList("127.0.0.1");

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        HttpHeaders headers = request.getHeaders();
        String hostString = headers.getHost().getHostString();
        // 判断是否满足条件
        boolean b = blackList.stream().anyMatch(element -> element.equals(hostString));
        // 如果为黑名单内容,则返回失败
        if (b){
            return MyUtil.returnData(exchange, 520,"你是黑名单");
        }
        // 放行
        return chain.filter(exchange);
    }

    @Override
    public int getOrder() {
        // 过滤的顺序,数值越小,越先执行过滤
        return 0;
    }
}

测试

会拦截127.0.0.1的ip

  • 访问http://127.0.0.1/order-a/car/washCar

    在这里插入图片描述

  • 访问http://localhost/order-a/car/washCar

    在这里插入图片描述

4.5、校验token

我们token一般会存在redis中,当用户登录成功的时候,就会返回前端一个tokne,然后后面每次请求带着token即可访问接口。

token一般是放在redis中,我们下面演示会用到redis,进行存取token的操作。

前端在请求头中带token的时候,一般会有个前缀bearer (尾部有个空格),不了解的可以查看:https://www.jianshu.com/p/61d592ae33ee。下面进行实操

Redis Windows版本下载和使用可以参考:http://c.biancheng.net/redis/windows-installer.html

开发
  1. 先在order-a项目里新建一个login接口,用来开车锁。需要导入redis的依赖

    1. redis依赖

      <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-data-redis</artifactId>
      </dependency>
      
    2. testController

      package com.tcc.controller;
      
      import org.springframework.beans.factory.annotation.Autowired;
      import org.springframework.data.redis.core.StringRedisTemplate;
      import org.springframework.web.bind.annotation.RequestMapping;
      import org.springframework.web.bind.annotation.RestController;
      
      import java.util.UUID;
      
      /**
       * @author 宇辰
       * @date 2022/9/27-8:59
       **/
      @RestController
      @RequestMapping("car")
      public class TestController {
      
          @Autowired
          private StringRedisTemplate redisTemplate;
      
      
          // 买车
          @RequestMapping("buyCar")
          public String buyCar() {
              return "您购买了:劳斯莱斯-幻觉";
          }
      
          // 开锁,上车
          @RequestMapping("login")
          public String login(String username,String password){
              // 假定登录操作
              User user = new User(1L, username, password);
              String token = UUID.randomUUID().toString();
      
              // 把token存到redis中
              redisTemplate.opsForValue().set(token,user.toString(),7200L);
              return "您已开锁,请上车,您的token是:" + token;
          }
      
          // 洗车
          @RequestMapping("washCar")
          public String washCar() {
              return "您洗了洗您的:劳斯莱斯-幻觉";
          }
      
          // 卖车
          @RequestMapping("sellCar")
          public String sellCar(){
              return "您卖出了:劳斯莱斯-幻觉";
          }
      }
      
      class User{
          private Long id;
          private String username;
          private String password;
      
          public User() {
          }
      
          public User(Long id, String username, String password) {
              this.id = id;
              this.username = username;
              this.password = password;
          }
      
          public Long getId() {
              return id;
          }
      
          public void setId(Long id) {
              this.id = id;
          }
      
          public String getUsername() {
              return username;
          }
      
          @Override
          public String toString() {
              return "User{" +
                      "id=" + id +
                      ", username='" + username + '\'' +
                      ", password='" + password + '\'' +
                      '}';
          }
      
          public void setUsername(String username) {
              this.username = username;
          }
      
          public String getPassword() {
              return password;
          }
      
          public void setPassword(String password) {
              this.password = password;
          }
      }
      
  2. 编写完毕后,启动order-a项目。然后在gateway项目里也导入redis依赖,用来判断token是否正确

  3. 然后编写拦截器

    package com.tcc.filter;
    
    import com.tcc.util.MyUtil;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.cloud.gateway.filter.GatewayFilterChain;
    import org.springframework.cloud.gateway.filter.GlobalFilter;
    import org.springframework.core.Ordered;
    import org.springframework.data.redis.core.StringRedisTemplate;
    import org.springframework.http.HttpHeaders;
    import org.springframework.http.HttpStatus;
    import org.springframework.http.server.reactive.ServerHttpRequest;
    import org.springframework.stereotype.Component;
    import org.springframework.util.CollectionUtils;
    import org.springframework.util.StringUtils;
    import org.springframework.web.server.ServerWebExchange;
    import reactor.core.publisher.Mono;
    
    import java.util.List;
    
    /**
     * @author :tcc
     * @date :Created on 2022/12/21 19:38
     * @description:Ordered是对过滤器的排序
     * @modified By:
     * @version:
     */
    @Component
    public class IPGlobalFilter implements GlobalFilter, Ordered {
    
        @Autowired
        private StringRedisTemplate redisTemplate;
    
        @Override
        public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
            // 1.先获取请求头
            ServerHttpRequest request = exchange.getRequest();
            HttpHeaders headers = request.getHeaders();
    
            // 2.判断是否为登录操作,如果是,则直接放行
            String path = request.getURI().getPath();
            if("/order-a/car/login".equals(path)){
                return chain.filter(exchange);
            }
    
            // 3.不是登录操作,先从请求头中获取token,可能以为会有多个相同属性,所以是个list集合
            List<String> authorization = headers.get("authorization");
            // 4、判断前端是否带token了
            if(!CollectionUtils.isEmpty(authorization)){
                String token = authorization.get(0);
                if (StringUtils.hasText(token)){
                    // 5、带了的话,就获取token内容
                    String realToken = token.replaceFirst("bearer ", "");
                    // 6、判断是否能获取token,并且在redis中可以查到,就放行
                    if(StringUtils.hasText(realToken) && redisTemplate.hasKey(realToken)){
                        return chain.filter(exchange);
                    }
                }
            }
            // 7、没带token/没获取到token 拦截
            return MyUtil.returnData(exchange, HttpStatus.UNAUTHORIZED.value(),"未授权");
        }
    
        @Override
        public int getOrder() {
            // 过滤的顺序,数值越小,越先执行过滤
            return 0;
        }
    }
    
  4. 编写完毕后,启动gateway项目,记得在本地把Redis的服务端运行起来,否则会找不到localhost:6379的服务,存不到redis里,报错

测试
  1. 先访问http://localhost/order-a/car/login?username=张三&password=123456,进行一个登录的操作,账号密码内容随意。

    在这里插入图片描述

  2. 然后复制后台返回来的token,使用apiPost或postman访问http://localhost/order-a/car/washCar并且带上token(记得在token前面添加**bearer **),查看是否可以成功访问

    在这里插入图片描述

  3. 也可以把redis中的token删除,然后再次访问,这时候带的就是一个错误的token,就会返回未授权

    在这里插入图片描述

    在这里插入图片描述

限流

什么是限流

  • 限流就是限制一段时间内,用户访问资源的次数,减轻服务器压力,限流大致分为两种
    • IP限流(5s内同一个ip访问超过3次,则限制不让访问,过一段时间才可以继续访问)
    • 请求量限流(只要在一段时间内(窗口期),请求次数达到阈值,就直接决绝后面来的访问了,过一段时间才可以继续访问)(粒度可以细化到一个api(url),一个服务)

限流模型:漏斗算法、令牌桶算法、窗口滑动算法、计数器算法

本次演示的为令牌桶算法

1、令牌桶算法

在这里插入图片描述

用一个次来概括就是:入不敷出

  1. 所有的请求在处理之前都需要拿到一个可用的令牌才会被放行处理
  2. 根据限流大小,设置按照一定的速率往桶里添加令牌
  3. 桶设置最大的防止令牌限制,当桶满时,新添加的令牌就被丢弃或者拒绝
  4. 请求到达后,先获取令牌桶中的令牌,拿着令牌才可以进行其他的业务逻辑,处理完业务逻辑后,将令牌直接删除
  5. 令牌桶有最低限额,当令牌桶中的令牌达到最低限额的时候,请求处理完之后将不会删除令牌,以保证足够的限流

2、Gateway结合Redis实现请求量限流

Spring Cloud Gateway 已经内置了一个RequestRateLimiterGatewayFilterFactory,我们可以直接使用

目前RequestRateLimiterGatewayFilterFactory的实现依赖于Redis,所以我们还要导入spring-boot-starter-data-redis-reactive

导入依赖

<!-- 里面包含了redis,所以直接在原来的依赖上添加 -reactive 即可 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>

3、针对某一个接口ip来限流

3.1、编写配置代码

package com.tcc.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;

/**
 * @author :tcc
 * @date :Created on 2022/12/24 13:59
 * @description:
 * @modified By:
 * @version:
 */
@Configuration
public class RequestLimitConfig {
    
    // 针对某一个接口 ip来限流 /doLogin 每一个ip 根据配置文件的生成令牌规则,每秒生成1个令牌,最多装3个令牌
    @Bean
    public KeyResolver ipKeyResolver(){
        return exchange -> Mono.just(exchange.getRequest().getHeaders().getHost().getHostString());
    }
    
}

3.2、修改配置文件

server:
  port: 80 # 网关一般是80

spring:
  application:
    name: gateway-server
  cloud:
    gateway:
      enabled: true # 只要加了依赖,就默认开启
      routes:
        - id: order-server # 路由的id,保持唯一即可
          uri: http://localhost:8080 # uri统一资源标识符 url统一资源定位符
#          uri: lb://localhost:8080 # 使用lb协议 微服务名称做负载均衡
          predicates:
            - Path=/car/washCar # 匹配规则 只要你Path匹配上了/car/washCar 就往uri转发 并且将路径带上
            - After=2022-12-19T20:28:25.659+08:00[Asia/Shanghai] # 在20:28分钟后才能进行访问
          filters:
            - name: RequestRateLimiter # 过滤器的名称
              args: # 过滤器的参数
                   key-resolver: '#{@ipKeyResolver}' # 通过spel表达式取 ioc容器中的bean的值
                   redis-rate-limiter.replenishRate: 1 # 每秒生成1个令牌,方便测试
                   redis-rate-limiter.burstCapacity: 3 # 桶容量为三个令牌

      discovery: # 配置动态路由
        locator:
          enabled: true # 开启动态路由 开启通过应用名称 找到服务的功能
          lower-case-service-id: true # 开启服务名称小写

eureka:
  client:
    service-url:
      defaultZone: http://82.157.246.210:8761/eureka
    registry-fetch-interval-seconds: 3 # 网关拉取服务列表的时间缩短

3.3、编写好代码后,把上面做的所有过滤先注释掉,方便本次测试

在这里插入图片描述

3.4、启动项目,访问http://localhost/car/washCar,然后多次刷新浏览器,查看

在这里插入图片描述

3.5、打开Redis服务端,查看新增的key(有过期时间,尽快查看)

在这里插入图片描述

4、针对某个路径限流

3.1、编写配置代码

package com.tcc.config;

import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import reactor.core.publisher.Mono;

/**
 * @author :tcc
 * @date :Created on 2022/12/24 13:59
 * @description:
 * @modified By:
 * @version:
 */
@Configuration
public class RequestLimitConfig {

    // 针对某一个接口 ip来限流 /doLogin 每一个ip 10秒内只能访问3次
    @Bean
    public KeyResolver ipKeyResolver(){
        return exchange -> Mono.just(exchange.getRequest().getHeaders().getHost().getHostString());
    }

    // 针对某一个接口 ip来限流 /doLogin 每一个ip 10秒内只能访问3次
    @Bean
    @Primary // 主候选的,不然会匹配到两个相同的Bean报错
    public KeyResolver apiKeyResolver(){
        return exchange -> Mono.just(exchange.getRequest().getPath().value());
    }
}

3.2、修改配置文件

filters:
            - name: RequestRateLimiter # 过滤器的名称
              args: # 过滤器的参数
                   key-resolver: '#{@apiKeyResolver}' # 通过spel表达式取 ioc容器中的bean的值 只需要修改匹配的Bean对象即可
                   redis-rate-limiter.replenishRate: 1
                   redis-rate-limiter.burstCapacity: 3

3.3、和上面测试方法一样,这里就不再演示了

访问其他接口无限制,访问/order-a/car/washCar会被限制

跨域配置

因为网关是微服务的边缘,所有的请求都要走网关,跨域的配置只需要写在网关即可

可以使用配置文件配置、代码配置两种方式

1、yarm配置

spring:
  cloud:
    gateway:
      globalcors:
        corsConfigurations:
          '[/**]':
            allowCredentials: true
            allowedHeaders: '*'
            allowedMethods: '*'
            allowedOrigins: '*'

2、代码配置

package net.youqu.micro.service.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.reactive.CorsWebFilter;
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;
import org.springframework.web.util.pattern.PathPatternParser;

/**
 * description
 * from www.fhadmin.org
 */
@Configuration
public class CorsConfig {
    @Bean
    public CorsWebFilter corsFilter() {
        CorsConfiguration config = new CorsConfiguration();
        config.addAllowedMethod("*");
        config.addAllowedOrigin("*");
        config.addAllowedHeader("*");

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(new PathPatternParser());
        source.registerCorsConfiguration("/**", config);

        return new CorsWebFilter(source);
    }
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值