Spring Cloud Gateway 的介绍与快速上手

1.什么是网关

网关是什么?网关(Gateway)本质上不是一个产品而是一个网络层的概念,网关(Gateway)就是一个网络连接到另一个网络的“关口”。简单来说可以理解成火车站的检票口,统一去检票。

2.网关的作用

统一去进行一些操作、处理一些问题。
比如:

  • 路由
  • 负载均衡
  • 统一鉴权
  • 跨域
  • 统一业务处理(缓存)
  • 访问控制
  • 发布控制
  • 流量染色
  • 接口保护
    • 限制请求信息
    • 脱敏
    • 降级(熔断)
    • 限流:(比如令牌桶算法、学习漏桶算法)
  • 超时时间
  • 统一日志
  • 统一文档
    在这里插入图片描述

3.主流网关的对比与选型

网关的分类:

  • 全局网关(接入层网关):作用是负载均衡、请求日志等,不和业务逻辑绑定
  • 业务网关(微服务网关):会有一些业务逻辑,作用是将请求转发到不同的业务 / 项目 / 接口 / 服务

网关实现方式:

  • Nginx(全局网关)、Kong 网关(API 网关,Kong:https://github.com/Kong/kong),编程成本相对高一点
  • Spring Cloud Gateway(取代了 Zuul)性能高、可以用 Java 代码来写逻辑,适于学习

主流网关的对比:
在这里插入图片描述
我们这里主要介绍springcloudgateway

4.Spring Cloud Gateway 使用方法

Spring Cloud Gateway 官网地址:https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/

核心概念

  • 路由(根据什么条件,转发请求到哪里)
  • 断言:一组规则、条件,用来确定如何转发路由
  • 过滤器:对请求进行一系列的处理,比如添加请求头、添加请求参数

请求流程:

  1. 客户端发起请求
  2. Handler Mapping:根据断言,去将请求转发到对应的路由
  3. Web Handler:处理请求(一层层经过过滤器)
  4. 实际调用服务

在这里插入图片描述

两种配置方式

  1. 配置式(方便、规范)简化版全称版 √
  2. 编程式(灵活、相对麻烦)

引入方式:

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>

开启日志方式(推荐):

在yml配置文件中配置

logging:
  level:
    org:
      springframework:
        cloud:
          gateway: trace

5.简单示例快速上手具体实现

1.请求转发配置
  1. 使用前缀匹配断言:https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/#the-path-route-predicate-factory

  2. 假设对所有路径为:/api/** 的请求进行转发,转发到 http://localhost:8080/api/**
    比如请求网关:http://localhost:8090/api/name/get?name=111转发到:
    http://localhost:8123/api/name/get?name=111

server:
  port: 8090
spring:
  cloud:
    gateway:
      default-filters:
        - AddResponseHeader=source, genius
      routes:
        - id: api_route
          uri: http://localhost:8080
          predicates:
            - Path=/api/**
2.编写业务逻辑

使用了 GlobalFilter(编程式),全局请求拦截处理(类似 AOP)
因为网关项目没引入 第三方类库比如MyBatis 等操作数据库的类库,如果该操作较为复杂,可以由增删改查项目提供接口,我们直接调用,不用再重复写逻辑了。

  • HTTP 请求(用 HTTPClient、用 RestTemplate、Feign)
  • RPC(Dubbo)

因为我们没有引入数据库类库,所以在直接启动application的时候可能会报以下错误:
在这里插入图片描述
解决方法:
在启动类加上以下注解

@SpringBootApplication(exclude = {
        DataSourceAutoConfiguration.class,
        DataSourceTransactionManagerAutoConfiguration.class,
        HibernateJpaAutoConfiguration.class})
@Service
public class GeniusApiGatewayApplication {

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

}

编写过滤器代码:

@Slf4j
@Component
public class CustomGlobalFilter implements GlobalFilter, Ordered {

    private static final String INTERFACE_HOST = "http://localhost:8080";

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // 1. 请求日志
        ServerHttpRequest request = exchange.getRequest();
        // 打印请求日志
        String path = request.getPath().value();
        String method = request.getMethod().toString();
        log.info("请求唯一标识:" + request.getId());
        log.info("请求路径:" + path);
        log.info("请求方法:" + method);
        log.info("请求参数:" + request.getQueryParams());
        String sourceAddress = request.getLocalAddress().getHostString();
        log.info("请求来源地址:" + sourceAddress);
        log.info("请求来源地址:" + request.getRemoteAddress());
        ServerHttpResponse response = exchange.getResponse();
        // 2. 访问控制 - 黑白名单
        if (!IP_WHITE_LIST.contains(sourceAddress)) {
            response.setStatusCode(HttpStatus.FORBIDDEN);
            return response.setComplete();
        }
        // 3. 简单的用户鉴权
        HttpHeaders headers = request.getHeaders();
        String accessKey = headers.getFirst("accessKey");
        String nonce = headers.getFirst("nonce");
        String timestamp = headers.getFirst("timestamp");
        String sign = headers.getFirst("sign");
        String body = headers.getFirst("body");
        if (Long.parseLong(nonce) >10000L){
            return handleNoAuth(response);
        }
        User invokeUser = null;
        try {
            invokeUser = innerUserService.getInvokeUser(accessKey);
        } catch (Exception e) {
            log.error("getInvokeUser error", e);
        }
        if (invokeUser == null) {
            return handleNoAuth(response);
        }
        // 时间和当前时间不能超过 5 分钟
        Long currentTime = System.currentTimeMillis() / 1000;
        final Long FIVE_MINUTES = 60 * 5L;
        if ((currentTime - Long.parseLong(timestamp)) >= FIVE_MINUTES) {
            return handleNoAuth(response);
        }
      return handleResponse(exchange,chain);
    }

/**
     * 处理响应
     *
     * @param exchange
     * @param chain
     * @return
     */
    public Mono<Void> handleResponse(ServerWebExchange exchange, GatewayFilterChain chain) {
        try {
            ServerHttpResponse originalResponse = exchange.getResponse();
            // 缓存数据的工厂
            DataBufferFactory bufferFactory = originalResponse.bufferFactory();
            // 拿到响应码
            HttpStatus statusCode = originalResponse.getStatusCode();
            if (statusCode == HttpStatus.OK) {
                // 装饰,增强能力
                ServerHttpResponseDecorator decoratedResponse = new ServerHttpResponseDecorator(originalResponse) {
                    // 等调用完转发的接口后才会执行
                    @Override
                    public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
                        log.info("body instanceof Flux: {}", (body instanceof Flux));
                        if (body instanceof Flux) {
                            Flux<? extends DataBuffer> fluxBody = Flux.from(body);
                            // 往返回值里写数据
                            // 拼接字符串
                            return super.writeWith(
                                    fluxBody.map(dataBuffer -> {
                                        // 7. 调用成功,接口调用次数 + 1 invokeCount
                                        byte[] content = new byte[dataBuffer.readableByteCount()];
                                        dataBuffer.read(content);
                                        DataBufferUtils.release(dataBuffer);//释放掉内存
                                        // 构建日志
                                        StringBuilder sb2 = new StringBuilder(200);
                                        List<Object> rspArgs = new ArrayList<>();
                                        rspArgs.add(originalResponse.getStatusCode());
                                        String data = new String(content, StandardCharsets.UTF_8); //data
                                        sb2.append(data);
                                        // 打印日志
                                        log.info("响应结果:" + data);
                                        return bufferFactory.wrap(content);
                                    }));
                        } else {
                            // 8. 调用失败,返回一个规范的错误码
                            log.error("<--- {} 响应code异常", getStatusCode());
                        }
                        return super.writeWith(body);
                    }
                };
                // 设置 response 对象为装饰过的
                return chain.filter(exchange.mutate().response(decoratedResponse).build());
            }
            return chain.filter(exchange); // 降级处理返回数据
        } catch (Exception e) {
            log.error("网关处理响应异常" + e);
            return chain.filter(exchange);
        }
    }

然后在启动我们8080端口的后端服务
后面访问到网关的请求都会访问到8080端口的服务并且会打印日志信息了√

在这里插入图片描述
可以看到我们的日志信息就被打印出来了

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值