基本信息

网关

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

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

spring gateway 集成Sentinel_gateway

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 的核心逻辑也就是 路由转发 + 执行过滤器链

Nginx 和 Gateway 的区别

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

Nginx

(更大 服务器级别的)

Gateway

(项目级别的)

spring gateway 集成Sentinel_gateway_02

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

spring gateway 集成Sentinel_spring_03

Predicate 断言工厂

介绍

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

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

spring gateway 集成Sentinel_ci_04

使用

配制文件使用

spring gateway 集成Sentinel_ci_05

自定义断言工厂

spring gateway 集成Sentinel_gateway_06

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限流实战
  • 限流原理

spring gateway 集成Sentinel_spring cloud_07

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

安全的架构

spring gateway 集成Sentinel_ci_08