侧边栏壁纸
博主头像
乌拉队长博主等级

你只管努力,其余的交给命运

  • 累计撰写 129 篇文章
  • 累计创建 34 个标签
  • 累计收到 34 条评论

目 录CONTENT

文章目录

Springcloud微服务之Gateway网关

乌拉队长
2022-12-14 / 0 评论 / 0 点赞 / 45 阅读 / 2,835 字

什么是网关

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

我们一般的微服务模块如下图所示:

gateway

当没有网关时:

  • 我们需要将所有的微服务模块都在客户端中进行配置,缺点是当微服务的ip或者端口发生改变时,我们需要手动修改客户端的代码,并重启客户端服务
  • 当某个微服务的并发量很大时,通常会进行分布式部署,进而需要实现负载均衡,如果不使用网关,则无法完成负载均衡的效果(这当然也不是绝对的,例如可以通过nginx进行负载均衡,这不在本文的讨论范围)

当使用网关时(通常需要结合注册中心完成,如Nacos,Eureka等):

  • 客户端可以直接请求网关,然后由网关来访问相关的微服务
  • 直接使用服务名称即可访问微服务,可以实现负载均衡
  • 可以对一些统一性的服务进行划分,如:Token拦截,权限验证以及限流等操作,都可以通过网关进行统一操作,实现业务解耦,避免在每个服务中进行验证,提高系统性能

SpringCloud Gateway

什么是Gateway

Gateway的核心是一组过滤器,这些过滤器按照一定的先后顺序来执行过滤操作

image-1671006967587

三大核心概念

1.Filter过滤器

SpringCloud Gateway中的Filter主要分为两种类型,分别是Gateway Filter和Global Filter。这些Filter将会对请求和响应进行相应的修改和处理等。

  • Gateway Filter:针对某一个路由(路径)的过滤器,如:对某个接口做限流操作,就可以通过该Filter实现
  • Global Filter:针对全局的过滤器,如:实现全局的Token拦截,ip黑名单的处理

这里以全局过滤器为例,实现全局的Token拦截,限流操作

实现全局Token拦截功能

通常我们开发的大部分web系统都需要登录功能,在分布式中如果每个微服务都需要验证用户身份,那么将会极大地降低系统性能,重复性代码也会很高。然而在我们使用Gateway网关后,所有的请求都会经过Gateway,那么我们就可以将登陆验证的操作放在Gateway进行,如此一来,就可以有效地避免前面提到的问题。

因为我们要对所有的请求都进行token拦截验证,所以需要使用Gateway的Global Filter进行实现。

然后我们新建TokenCheckFilter.java过滤器类,具体代码如下:

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
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.core.io.buffer.DataBuffer;
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.http.server.reactive.ServerHttpResponse;
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.Arrays;
import java.util.HashMap;
import java.util.List;

/**
 * token校验
 * 使用@Component注解将该过滤器注入到Spring容器中
 */
@Component
public class TokenCheckFilter implements GlobalFilter, Ordered {


    // 指定放行的路径列表 如果是这些路径则不检验token直接放行
    public static final List<String> ALLOW_URL = Arrays.asList("/login-service/login", "/otherUrl", "/login");

    @Autowired
    private StringRedisTemplate redisTemplate;

    /**
     * 步骤:
     * 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);
        }
        // 否则,对token进行校验
        HttpHeaders headers = request.getHeaders();
        List<String> authorization = headers.get("Authorization");
        if (!CollectionUtils.isEmpty(authorization)){
            String token = authorization.get(0);
            String real_token = token.replaceFirst("bearer ", "");
            if (StringUtils.hasText(real_token) && redisTemplate.hasKey(real_token)){
                return chain.filter(exchange);
            }
        }
        // 如果token校验未通过,则返回相应的状态码
        ServerHttpResponse response = exchange.getResponse();
        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();
        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 0;
    }
}

借助redis实现限流功能

2. route路由

Gateway提供了两种实现路由的方式:(1)通过代码方式实现路由配置 (2)通过配置文件方式实现路由配置

(1)代码方式实现路由

通过官方给的Demo可以知道,只需要自定义一个Route的配置类并将其作为Bean组件注入到Springboot应用中即可。

根据官方提供的Demo,我们可以实现自定义的路由配置,一个简单的例子如下:

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;

@Configuration
public class RouteConfig {

    /**
     * 代码方式实现路由,和yml配置的路由不冲突
     * @param builder
     * @return
     */
    @Bean
    public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
        return builder.routes()
                .route("dologin_route", r -> r.path("/login")
                        .uri("http://localhost:8080"))
		// 可以配置多个路由                        
//                .route("host_route", r -> r.host("*.myhost.org")
//                        .uri("http://httpbin.org"))
                .build();
    }
}

然后我们就可以通过访问http://localhost:82/login实现对http://localhost:8080/login的访问,从而实现路由的转发

(2)配置文件方式实现路由

单接口路由转发配置如下:

server:
  port: 81
# 应用名称
spring:
  application:
    name: gateway-server
  cloud:
    gateway:
      routes:
        - id: login-service-route # 路由id保持唯一即可
          uri: http://localhost:8080
          predicates: # 断言
            - Path=/login # 匹配规则 只要路径中包含/login 就往http://localhost:8080转发 并且将路径带上
        - id: login-service-route2 # 路由id保持唯一即可
          uri: http://localhost:8082
          predicates: # 断言
            - Path=/service2 # 匹配规则 只要路径中包含/service2 就往http://localhost:8080转发 并且将路径带上

然而当某个uri中有多个服务时,我们不可能给每个路径都配置一个route规则,那么此时就可以使用如下配置进行多个路径接口的转发

server:
  port: 81
# 应用名称
spring:
  application:
    name: gateway-server
  cloud:
    gateway:
      routes:
        # 动态路由  当一个uri下有多个服务时使用
        - id: login-service-route # 路由id保持唯一即可
          uri: http://localhost:8080
          predicates: # 断言
            - Path=/login-service/** # 匹配规则 只要路径中包含/login-service/ 就往http://localhost:8080转发 并且将路径带上

此外,我们更多的一种情况是:某个微服务有多台服务器,那么我们就需要实现负载均衡的效果,这时我们就需要用到动态路由

(3)动态路由

如果要实现动态路由以及负载均衡,就需要使用注册发现中心,如:Nacos,Eureka等。

server:
  port: 81
spring:
  application:
    name: gateway-nacos
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
        username: nacos
        password: nacos
    gateway:
      routes:
          - id: login-service-route # 路由id保持唯一即可
            uri: lb://login-service # 负载均衡 login-service是nacos中注册的服务名称
            predicates: # 断言
              - Path=/login # 匹配规则 只要路径匹配导航了 就往uri转发 并且将路径带上
      discovery:
        locator:
          enabled: true # 开启动态路由

3.Predicates断言

断言的意思就是一个Boolean表达式,如满足条件则执行相应的操作

SpringCloud Gateway提供了12个断言工厂,其中10个常用的断言如下:

  • 基于Datetime类型的断言工厂,包括:
    • AfterRoutePredicateFactory:接收一个日期参数,判断请求日期是否晚于指定日期
    • BeforeRoutePredicateFactory:接收一个日期参数,判断请求日期是否早于指定日期
    • BetweenRoutePredicateFactory:接收两个日期参数,判断请求日期是否在指定时间段内
  • 基于Cookie的断言工厂:CookieRoutePredicateFactory:接收两个参数,cookie 名字和一个正则表达式,判断请求cookie是否具有给定名称且值与正则表达式匹配
  • 基于Header的断言工厂:HeaderRoutePredicateFactory:接收两个参数,标题名称和正则表达式,判断请求Header是否具有给定名称且值与正则表达式匹配
  • 基于Host的断言工厂:HostRoutePredicateFactory:接收一个参数(一个host name数组),主机名模式,判断请求的Host是否满足匹配规则
  • 基于Method请求方法的断言工厂:MethodRoutePredicateFactory:接收一个参数,判断请求类型是否跟指定的类型匹配
  • 基于Path请求路径的断言工厂:PathRoutePredicateFactory:接收一个参数,判断请求的URI部分是否满足路径规则
  • 基于Query请求参数的断言工厂:QueryRoutePredicateFactory :接收两个参数,请求param和正则表达式, 判断请求参数是否具有给定名称且值与正则表达式匹配
  • 基于Remote的断言工厂:RemoteAddrRoutePredicateFactory:接收一组参数(至少一个参数),判断请求中的主机地址是否在给定的地址段中
  • 基于路由权重的断言工厂:WeightRoutePredicateFactory:接收一个[组名,权重], 然后对于同一个组内的路由按照权重转发

举个栗子:

spring:
  application:
    name: gateway-server
  cloud:
    gateway:
      routes:
        # 路由id保持唯一即可
        - id: login-service-route
          uri: http://localhost:8080
          predicates:
            # 请求路径  只要路径匹配导航了 就往uri转发 并且将路径带上
            - Path=/loginService/**
            # 请求方式匹配上了才允许访问接口
            - Method=GET,POST
            # 在给定时间之后允许访问接口
            - After=2022-12-15T20:47:07.470+08:00[Asia/Shanghai]
            # 在给定时间之前允许访问接口
            - Before=2022-12-15T21:47:07.470+08:00[Asia/Shanghai]
            # 在给定的时间段内允许访问接口
            - Between=2022-12-15T21:47:07.470+08:00[Asia/Shanghai], 2022-12-15T22:47:07.470+08:00[Asia/Shanghai]
            # 判断请求携带的cookie中是否具有给定名称的键,且值与正则表达式匹配
            - Cookie=user_info, zhangsan.
            # 判断请求的header中是否具有给定名称的键,且值与正则表达式匹配
            - Header=X-Request-Id, \d+
            # 判断请求的host是否满足匹配规则
            - Host=**.somehost.org,**.anotherhost.org
            # 判断请求中的主机地址是否在给定的地址段中
            - RemoteAddr=192.168.1.1/24
            # 判断请求携带的参数中是否具有给定名称的键,且值与正则表达式匹配
            - Query=name, admin.
            # 如果和其他路由属于同一个组,则按照响应的权重进行转发
            - Weight=group1, 2
        - id: login-service-route2
          uri: http://localhost:8080
          predicates:
            - Path=/loginService/**
            - Weight=group1, 8
0

评论区