SpringCloud快速学习(6)——gateway

SpringCloud快速学习(6)——gateway

个人的动力节点视频学习笔记 视频地址:https://www.bilibili.com/video/BV1f94y1U7AB

基本信息

网关

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

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

image-20220718121503913

Gateway

介绍

SpringCloud Gateway作为Spring Cloud生态的网关,目标是替代Zuul,在SpringCloud2.0 以上的版本中,没有对新版本的 zuul2.0 以上的最新高性能版本进行集成,仍然还是使用的 zuul1.x[可以看项目依赖找到]非 Reactor 模式的老版本。而为了提升网关的性能,SpringCloud Gateway 是基于 webFlux 框架实现的,而 webFlux 框架底层则使用了高性能 的 Reactor 模式通信框架的 Netty NIO(非阻塞式 io) BIO 你只需要了解网关能做什么

Spring Cloud Gateway 三大核心概念

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

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

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

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

Java 8 中的断言函数。 lambda 四大接口 供给形,消费性,函数型,断言型 Spring Cloud Gateway 中 的 断 言 函 数 输 入 类 型 是Spring 5.0 框 架 中 的 ServerWebExchange。Spring Cloud Gateway 的断言函数允许开发者去定义匹配来自于 Http Request 中的任何信息比如请求头和参数。

  • Filter(过滤) (重点)

一个标准的 Spring WebFilter。

Web 三大组件(servlet listener filter) mvc interceptor

Spring Cloud Gateway 中的 Filter 分为两种类型的 Filter,分别是 Gateway Filter 和 Global Filter。过滤器 Filter 将会对请求和响应进行修改处理。 一个是针对某一个路由(路径)的 filter 对某一个接口做限流 一个是针对全局的 filter token ip 黑名单

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

扫描二维码关注公众号,回复: 14814728 查看本文章

Nginx 和 Gateway 的区别

Nginx 在做路由,负载均衡,限流之前,都有修改 nginx.conf 的配置文件,把需要负载均衡, 路由,限流的规则加在里面。Eg:使用 nginx 做 tomcat 的负载均衡 但是 gateway 不同,gateway 自动的负载均衡和路由,gateway 和 eureka 高度集成,实现 自动的路由,和 Ribbon 结合,实现了负载均衡(lb),gateway 也能轻易的实现限流和权 限验证。 Nginx(c)比 gateway(java)的性能高一点。 本质的区别呢?

Nginx

(更大 服务器级别的)

Gateway

(项目级别的)

image-20220718122818015

nginx的地位:

在过去几年微服务架构还没有流行的日子里,nginx 已经得到了广大 开发者的认可,其性能高、扩展性强、可以灵活利用 lua 脚本构建插件的特点让人没有抵抗力。 (nginx 的请求转发 最大并发是多个次,每秒 5w-10w 左右) 3w 左右 有一个能满足我所有需求还很方便我扩展的东西,还免费,凭啥不用??

但是,如今很多微服务架构的项目中不会选择 nginx,我认为原因有以下几点:

微服务框架一般来说是配套的,集成起来更容易

如今微服务架构中,仅有很少的公司会面对无法解决的性能瓶颈,而他们也不会因此使用 nginx,而是选择开发一套适合自己的微服务框架(很多公司会对现有框架进行修改) spring boot 对于一些模板引擎如 FreeMarker、themleaf 的支持是非常好的,很多应用还没 有达到动、静态文件分离的地步,对 nginx

快速上手

02-login-service 生产者模块

  • pom
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>06-gateway</artifactId>
        <groupId>edu.bcy</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>02-login-service</artifactId>

    <properties>
        <java.version>1.8</java.version>
        <spring-cloud.version>Hoxton.SR12</spring-cloud.version>
    </properties>
    <dependencies>
        <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>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

  • yml
server:
    port: 8081
spring:
    application:
        name: login-service
eureka:
    client:
        service-url:
            defaultZone: http://localhost:8761/eureka
    instance:
        hostname: localhost
        instance-id: ${
    
    eureka.instance.hostname}:${
    
    spring.application.name}:${
    
    server.port}

  • 接口
@RestController
@CrossOrigin // 加上这个注解之后 这个controller里面的方法就可以直接被访问了
public class LoginController {
    
    


    @Autowired
    public StringRedisTemplate redisTemplate;

    @GetMapping("doLogin")
    @CrossOrigin
    public String doLogin(String name, String pwd) {
    
    
        System.out.println(name);
        System.out.println(pwd);
        // 这里假设去做了登录
        User user = new User(1, name, pwd, 18);
        // token
        String token = UUID.randomUUID().toString();
        // 存起来
//        redisTemplate.opsForValue().set(token, user.toString(), Duration.ofSeconds(7200));
        return token;
    }

}

01-gateway-server

  • pom
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>06-gateway</artifactId>
        <groupId>edu.bcy</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>01-gateway-server</artifactId>

    <properties>
        <java.version>1.8</java.version>
        <spring-cloud.version>Hoxton.SR12</spring-cloud.version>
    </properties>
    <dependencies>
        <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-data-redis-reactive</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <finalName>aaaa</finalName>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

  • yml
server:
    port: 80 # 网关一般是80
spring:
    application:
        name: gateway-server
    cloud:
        gateway:
            enabled: true # =只要加了依赖 默认开启
            routes: # 如果一个服务里面有100个路径  如果我想做负载均衡 ??   动态路由
                -   id: login-service-route  # 这个是路由的id 保持唯一即可
                    #                    uri: http://localhost:8081   # uri统一资源定位符   url 统一资源标识符
                    uri: lb://login-service   # uri统一资源定位符   url 统一资源标识符
                    predicates: # 断言是给某一个路由来设定的一种匹配规则 默认不能作用在动态路由上
                        - Path=/doLogin  # 匹配规则  只要你Path匹配上了/doLogin 就往 uri 转发 并且将路径带上
#                        - After=2022-03-22T08:42:59.521+08:00[Asia/Shanghai]
                        - Method=GET,POST
                    #                        - Query=name,admin.   #正则表达式的值
#                    filters:
#                        -   name: RequestRateLimiter  # 这个是过滤器的名称
#                            args: # 这个过滤器的参数
#                                key-resolver: '#{@apiKeyResolver}' # 通过spel表达式取 ioc容器中bean的值
#                                redis-rate-limiter.replenishRate: 1  # 生成令牌的速度
#                                redis-rate-limiter.burstCapacity: 3  # 桶容量
            discovery:
                locator:
                    enabled: true  # 开启动态路由  开启通用应用名称 找到服务的功能
                    lower-case-service-id: true  # 开启服务名称小写
#            globalcors:
#                corsConfigurations:
#                    '[/]':
#                        allowCredentials: true  # 可以携带cookie
#                        allowedHeaders: ''
#                        allowedMethods: ''
#                        allowedOrigins: ''
eureka:
    client:
        service-url:
            defaultZone: http://localhost:8761/eureka
        registry-fetch-interval-seconds: 3 # 网关拉去服务列表的时间缩短
    instance:
        hostname: localhost
        instance-id: ${
    
    eureka.instance.hostname}:${
    
    spring.application.name}:${
    
    server.port}

  • 启动类
@SpringBootApplication
@EnableEurekaClient
public class GatewayServerApplication {
    
    

    public static void main(String[] args) {
    
    
        SpringApplication.run(GatewayServerApplication.class, args);
    }

}
  • 结果

01-gateway-server将请求转发给了02-login-service

image-20220718214605321

Predicate 断言工厂

介绍

在 gateway 启动时会去加载一些路由断言工厂(判断一句话是否正确 一个 boolean 表达式 )

断言就是路由添加一些条件(丰富路由功能的) ,通俗的说,断言就是一些布尔表达式,满足条件的返回 true,不满足的返回 false。 Spring Cloud Gateway 将路由作为 Spring WebFlux HandlerMapping 基础架构的一部分 进行匹配。Spring Cloud Gateway 包括许多内置的路由断言工厂。所有这些断言都与 HTTP 请求的不同属性匹配。您可以将多个路由断言可以组合使用 Spring Cloud Gateway 创建对象时,使用 RoutePredicateFactory 创建 Predicate 对象, Predicate 对象可以赋值给 Route。

image-20220718215139027

使用

配制文件使用

image-20220718215257620

自定义断言工厂

image-20220718215446826

Filter 过滤器工厂

介绍

  • 基本信息

gateway 里面的过滤器和 Servlet 里面的过滤器,功能差不多,路由过滤器可以用于修改进入

Http 请求和返回 Http 响应

  • 分类

    • 按照生命周期:pre 在业务逻辑之前 ,post 在业务逻辑之后

    • 按照种类分:GatewayFilter 和 GlobalFilter

GatewayFilter: 需要配置某个路由,才能过滤。

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

自定义网关过滤器

样例

/**
 * 定义了一个过滤器
 * 十个过滤器
 */
@Component
public class MyGlobalFilter implements GlobalFilter, Ordered {
    
    

    /**
     * 这个就是过滤的方法
     * 过滤器链模式
     * 责任链模式
     * 网关里面有使用  mybatis的 二级缓存有变种责任链模式
     *
     * @param exchange
     * @param chain
     * @return
     */
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
    
    
        // 针对请求的过滤  拿到请求  header  url 参数 ....
        ServerHttpRequest request = exchange.getRequest();
        // HttpServletRequest  这个是web里面的
        // ServerHttpRequest  webFlux里面 响应式里面的
        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 ip = request.getHeaders().getHost().getHostString();
        System.out.println(ip);
        // 响应相关的数据
        ServerHttpResponse response = exchange.getResponse();
        // 用了微服务 肯定是前后端分离的 前后端分离 一般前后通过 json
        // {"code":200,"msg":"ok"}
        // 设置编码 响应头里面置
//        response.getHeaders().set("content-type","application/json;charset=utf-8");
//        // 组装业务返回值
//        HashMap<String, Object> map = new HashMap<>(4);
//        map.put("code", HttpStatus.UNAUTHORIZED.value());
//        map.put("msg","你未授权");
//        ObjectMapper objectMapper = new ObjectMapper();
//        // 把一个map转成一个字节
//        byte[] bytes = new byte[0];
//        try {
    
    
//            bytes = objectMapper.writeValueAsBytes(map);
//        } catch (JsonProcessingException e) {
    
    
//            e.printStackTrace();
//        }
//        // 通过buffer工厂将字节数组包装成 一个数据包
//        DataBuffer wrap = response.bufferFactory().wrap(bytes);
//        return response.writeWith(Mono.just(wrap));
        // 放行 到下一个过滤器了
        return chain.filter(exchange);
    }

    /**
     * 指定顺序的方法
     * 越小越先执行
     * @return
     */
    @Override
    public int getOrder() {
    
    
        return 0;
    }
}

IP 认证拦截实战

/**
 * 网关里面 过滤器
 * ip拦截
 * 请求都有一个源头
 * 电话 144  027  010
 * 请求------->gateway------->service
 * 黑名单 black_list
 * 白名单 white_list
 * 根据数量
 * 像具体的业务服务 一般黑名单
 * 一般像数据库 用白名单
 */
@Component
public class IPCheckFilter implements GlobalFilter, Ordered {
    
    

    /**
     * 网关的并发比较高 不要再网关里面直接操作mysql
     * 后台系统可以查询数据库 用户量 并发量不大
     * 如果并发量大 可以查redis 或者 在内存中写好
     */
    public static final List<String> BLACK_LIST = Arrays.asList("127.0.0.1", "144.128.232.147");

    /**
     * 1.拿到ip
     * 2.校验ip是否符合规范
     * 3.放行/拦截
     *
     * @param exchange
     * @param chain
     * @return
     */
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
    
    
        ServerHttpRequest request = exchange.getRequest();
        String ip = request.getHeaders().getHost().getHostString();
        // 查询数据库 看这个ip是否存在黑名单里面   mysql数据库的并发
        // 只要是能存储数据地方都叫数据库 redis  mysql
        if (!BLACK_LIST.contains(ip)) {
    
    
            return chain.filter(exchange);
        }
        // 拦截
        ServerHttpResponse response = exchange.getResponse();
        response.getHeaders().set("content-type","application/json;charset=utf-8");
        HashMap<String, Object> map = new HashMap<>(4);
        map.put("code", 438);
        map.put("msg","你是黑名单");
        ObjectMapper objectMapper = new ObjectMapper();
        byte[] bytes = new byte[0];
        try {
    
    
            bytes = objectMapper.writeValueAsBytes(map);
        } catch (JsonProcessingException e) {
    
    
            e.printStackTrace();
        }
        DataBuffer wrap = response.bufferFactory().wrap(bytes);
        return response.writeWith(Mono.just(wrap));
    }

    @Override
    public int getOrder() {
    
    
        return -5;
    }
}

Token校验实战

/**
 * token校验
 */
@Component
public class TokenCheckFilter implements GlobalFilter, Ordered {
    
    

    /**
     * 指定好放行的路径
     */
    public static final List<String> ALLOW_URL = Arrays.asList("/login-service/doLogin", "/myUrl","/doLogin");


    @Autowired
    private StringRedisTemplate redisTemplate;

    /**
     * 前提是? 和前端约定好 一般放在请求头里面 一般key   Authorization   value bearer token
     * 1.拿到请求url
     * 2.判断放行
     * 3.拿到请求头
     * 4.拿到token
     * 5.校验
     * 6.放行/拦截
     *
     * @param exchange
     * @param chain
     * @return
     */
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
    
    
        ServerHttpRequest request = exchange.getRequest();
        String path = request.getURI().getPath();
        if (ALLOW_URL.contains(path)) {
    
    
            return chain.filter(exchange);
        }
        // 检查
        HttpHeaders headers = request.getHeaders();
        List<String> authorization = headers.get("Authorization");
        if (!CollectionUtils.isEmpty(authorization)) {
    
    
            String token = authorization.get(0);
            if (StringUtils.hasText(token)) {
    
    
                // 约定好的有前缀的 bearer token
                String realToken = token.replaceFirst("bearer ", "");
                if (StringUtils.hasText(realToken) && redisTemplate.hasKey(realToken)) {
    
    
                    return chain.filter(exchange);
                }
            }
        }
        // 拦截
        ServerHttpResponse response = exchange.getResponse();
        response.getHeaders().set("content-type","application/json;charset=utf-8");

        HashMap<String, Object> map = new HashMap<>(4);
        // 返回401
        map.put("code", HttpStatus.UNAUTHORIZED.value());
        map.put("msg","未授权");
        ObjectMapper objectMapper = new ObjectMapper();
        byte[] bytes = new byte[0];
        try {
    
    
            bytes = objectMapper.writeValueAsBytes(map);
        } catch (JsonProcessingException e) {
    
    
            e.printStackTrace();
        }
        DataBuffer wrap = response.bufferFactory().wrap(bytes);
        return response.writeWith(Mono.just(wrap));
    }

    /**
     * 这个顺序 最好在0附近  -2 -1 0 1
     * @return
     */
    @Override
    public int getOrder() {
    
    
        return 1;
    }
}

借助redis限流实战

  • 限流原理

image-20220718230745270

  • pom
<!--限流要引入 Redis--> 
<dependency> 
    <groupId>org.springframework.boot</groupId> 
    <artifactId>spring-boot-starter-data-redis-reactive</artifactId> 
</dependency>
  • yml
key-resolver: '#{@apiKeyResolver}' # 通过spel表达式取 ioc容器中bean的值
redis-rate-limiter.replenishRate: 1  # 生成令牌的速度
redis-rate-limiter.burstCapacity: 3  # 桶容量

在上面的配置文件,配置了 redis 的信息,并配置了 RequestRateLimiter 的限流过滤器,

该过滤器需要配置三个参数:

burstCapacity:令牌桶总容量。

replenishRate:令牌桶每秒填充平均速率。

key-resolver:用于限流的键的解析器的 Bean 对象的名字。它使用 SpEL 表达式根据**#{@beanName}从 Spring 容器中获取 Bean 对象。**

  • 配置类
/**
 * 自定义请求限制的
 */
@Configuration
public class RequestLimitConfig {
    
    

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

    // 针对这个路径来限制  /doLogin
    // api 就是 接口  外面一般把gateway    api网关  新一代网关
    @Bean
    public KeyResolver apiKeyResolver() {
    
    
        return exchange -> Mono.just(exchange.getRequest().getPath().value());
    }
}

gateway解决跨域问题

常规来说,可以使用配置类和配置文件两种办法,以下为配置类:

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;

@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);
    }
}

安全的架构

image-20220718232434779

猜你喜欢

转载自blog.csdn.net/qq_50665031/article/details/125862521