SpringCloud Gateway 网关常用技术实现

SpringCloud Gateway 是目前非常流行的网关中间件,类似于 nginx 一样,主要提供【路由转发】和【负载均衡】功能,目的是为微服务架构提供一种简单而有效的统一的 API 路由管理方式。

我们通常也会在网关中添加【身份认证】和【鉴权】,阻止非法请求访问后端服务。

SpringCloud Gateway 由于全部采用 Java 语言,学习和使用门槛很低,自身功能强大且性能优越。本篇博客将通过代码方式,为大家演示如何通过配置的方式实现【路由转发】和【负载均衡】,如果通过全局过滤器实现【身份认证】和【鉴权】功能。在本篇博客的最后,会提供源代码的下载。

SpringCloud Gateway 的官网地址为:https://spring.io/projects/spring-cloud-gateway


一、搭建工程

采用 Maven 搭建 springcloud_gateway 父工程,下面包含 6 个子工程:

image

其中 eureka_app 是注册中心,我们将所有微服务都需要注册到 eureka 中。

gateway_app 就是本篇博客的主角:网关。这是本篇博客重点介绍的内容。

为了演示网关的路由转发,本篇博客创建了两类服务:provider-a 和 provider-b 。

为了演示负载均衡,给每类服务都设置了两个节点,比如 provider-a 服务有 provider-a1 和 provider-a2 两个节点。

首先介绍一下 springcloud_gateway 父工程的 pom 文件,方便大家了解所使用的 springboot 版本和 springcloud 版本。

<?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">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.jobs</groupId>
    <artifactId>springcloud_gateway</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>pom</packaging>
    <modules>
        <module>eureka_app</module>
        <module>provider_a1</module>
        <module>provider_a2</module>
        <module>provider_b1</module>
        <module>provider_b2</module>
        <module>gateway_app</module>
    </modules>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
    </properties>

    <!--spring boot-->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.12.RELEASE</version>
        <relativePath/>
    </parent>

    <!--Spring Cloud-->
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Hoxton.SR12</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
</project>

二、准备工作

这里主要以 provider-a 服务为例,介绍其提供的接口细节,为后续在 gateway 网关中进行路由转发和负载均衡配置做准备工作。对于 provider-b 服务的接口细节,其与 provider-a 一样,只不过是接口地址和打印内容做了一些更改而已,因此不再赘述。

package com.jobs.provider.controller;

import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

@RequestMapping("/testa")
@RestController
public class TestAController {

    //这是 provider-a1 提供的接口
    @RequestMapping("/getdata/{id}")
    public Map GetData(@PathVariable("id") int id) {

        Map result = new HashMap();
        result.put("status", 0);
        result.put("msg", "调用了 a1 服务的接口...");
        result.put("get_id_value", id);
        result.put("version", UUID.randomUUID().toString());

        return result;
    }
}
package com.jobs.provider.controller;

import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

@RequestMapping("/testa")
@RestController
public class TestAController {

    //这是 provider-a2 提供的接口
    @RequestMapping("/getdata/{id}")
    public Map GetData(@PathVariable("id") int id) {

        Map result = new HashMap();
        result.put("status", 0);
        result.put("msg", "调用了 a2 服务的接口...");
        result.put("get_id_value", id);
        result.put("version", UUID.randomUUID().toString());

        return result;
    }
}

正式环境中,对于 provider-a 服务,其两个节点 provider-a1 和 provider-a2 的代码应该是一模一样的。为了能够让大家看到负载均衡的效果,这里将接口返回的内容进行了更改,有利于区分每次服务调用的节点。有关接口服务的搭建过程和细节,这里不再赘述,请到本篇博客的最下面,下载源代码查看。


三、通过网关实现负载均衡路由转发

搭建 SpringCloud Gateway 非常简单,本篇博客搭建后的工程结果如下所示:

image

主要通过以下 2 个步骤即可实现:(非常简单)

1 在 pom 文件中引入 spring cloud gateway 的起步依赖 jar 包

<?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>springcloud_gateway</artifactId>
        <groupId>com.jobs</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>gateway_app</artifactId>

    <dependencies>
        <!--引入 gateway 网关-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>

        <!-- eureka-client -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
    </dependencies>
</project>

2 编写 application.yml 配置文件,配置请求转发的目标地址

server:
  port: 80

eureka:
  instance:
    # 配置主机名
    hostname: gateway-app
    # 显示 ip 地址,代替显示主机名
    prefer-ip-address: true
    # 所注册服务实例名称的显示形式
    instance-id: ${eureka.instance.hostname}:${server.port}
    # 每隔 3 秒发一次心跳包
    lease-renewal-interval-in-seconds: 3
    # 如果 15 秒没有发送心跳包,就让 eureka 把自己从服务列表中移除
    lease-expiration-duration-in-seconds: 15
  client:
    service-url:
      # 将当前 springboot 服务注册到 eureka 中
      defaultZone: http://localhost:8761/eureka
    # 是否将自己的路径注册到 eureka 上
    register-with-eureka: true
    # 是否需要从 eureka 中抓取路径
    fetch-registry: true

# provider 集群需要使用相同的 application 名称
spring:
  application:
    name: gateway-app
  cloud:
    # 网关配置
    gateway:
      # 允许跨域请求(仅配置这里可能不行,还得代码中进行配置)
      globalcors:
        add-to-simple-url-handler-mapping: true
        corsConfigurations:
          '[/**]':
            allowedHeaders: "*"
            allowedOrigins: "*"
            allowedMethods:
              - GET
              - POST
              - DELETE
              - PUT
              - OPTION
      # 路由配置:转发规则
      routes:
        - id: aaa
          # 静态路由
          # uri: http://localhost:8100/
          # 动态路由
          uri: lb://PROVIDER-A
          predicates:
            - Path=/testa/**
        - id: gateway-consumer
          uri: lb://PROVIDER-B
          predicates:
            - Path=/testb/**
          # 微服务名称配置
      discovery:
        locator:
          # 设置为true 请求路径前可以添加微服务名称
          enabled: true
          # 允许为小写
          lower-case-service-id: true

如果在服务中,你只有一个网关的话,绝大多数情况下,网关的端口都是 80 端口。这就类似你平常使用 nginx 一样,毕竟用户访问你的网站时,不可能记住你的特殊端口,所以网关大部分情况下都是配置为 80 端口。

另外有可能前端 js 直接请求网关,因此绝大多数情况下,网关还需要支持跨域请求,除了在 yml 文件中配置跨域支持外,我们还需要在代码中进行配置,这里创建了一个 CorsConfig 配置类,具体内容如下:

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

//跨域支持
@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);
    }
}

有关 routes 转发路由的配置:

  • id 这个可以随便填写,只要不重复即可。

  • uri 要转发的目标地址,可以配置为静态地址,如:https://www.baidu.com 。绝大多数情况下,都是配置为动态地址,如:lb://微服务的名称。其中 lb 是指 loadbalance 负载均衡的意思,微服务的名称可以直接复制 eureka 中注册的微服务名称。相同的微服务名称,可以注册有不同 ip 和端口的多个为服务器,springcloud gateway 会自动从 eureka 中获取所配置的微服务名称所对应的所有微服务的 ip 和端口号,通过负载均衡算法,进行路由转发。默认采用轮询的负载均衡算法。

  • predicates 表示判断条件,当满足条件时,springcloud gateway 就会将请求转发到对应的 uri 上。


到此为止,springcloud gateway 就已经快速配置完毕,可以启动服务进行测试验证了。
先启动 eureka ,然后启动 gateway ,两个 provider-a 服务,两个 provider-b 服务,进行验证。

当你访问 http://localhost/testa/getdata/2 时,就会访问 provider-a 服务的节点,每次刷新会轮询调用两个节点的接口。
当你访问 http://localhost/testb/getdata/3 时,就会访问 provider-b 服务的节点,每次刷新会轮询调用两个节点的接口。


在上面的 application.yml 配置中,有这样一段配置:

discovery:
  locator:
    # 设置为true 请求路径前可以添加微服务名称
    enabled: true
    # 允许为小写
    lower-case-service-id: true

这段配置表示:可以在地址上面加上微服务的名称,方便识别所调用的是哪个微服务。有关微服务的名称,可以在 eureka 中进行复制过来,yml 中已经配置了允许微服务的名称是小写字母。以调用 provider-a 服务的接口为例:

http://localhost/provider-a/testa/getdata/6http://localhost/testa/getdata/2 调用效果是一样的。


四、通过网关实现身份认证和鉴权

网关提供了全局过滤器,在全局过滤器中可以编写相关的 java 代码实现身份认证和鉴权功能。

这里通过模拟身份认证进行举例,快速介绍 SpringCloud Gateway 的全局过滤器的使用方法。

新建一个 AuthenFilter 类实现 GlobalFilter 和 Ordered 接口,即可实现一个全局过滤器,自动拦截所有访问网关的请求。

package com.jobs.gateway.globalfilter;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.lang.ObjectUtils;
import org.apache.commons.lang.StringUtils;
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.HttpHeaders;
import org.springframework.http.HttpMethod;
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.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;

//全局过滤器
@Component
public class AuthenFilter implements GlobalFilter, Ordered {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {

        System.out.println("执行了全局过滤器...");

        //模拟身份认证(假设 headers 中包含 token 属性,并且不为空,就认为已经登录了)
        HttpHeaders headers = exchange.getRequest().getHeaders();
        if (headers.containsKey("token")) {
            String token = headers.get("token").toString();
            if (StringUtils.isNotBlank(token)) {
                System.out.println("获取到的token为:" + token);
                return chain.filter(exchange);
            }
        }

        //未授权,返回给前端信息
        ServerHttpResponse response = exchange.getResponse();

        //获取请求方式
        HttpMethod method = exchange.getRequest().getMethod();
        System.out.println("当前请求的方式为:" + method);

        if (method.matches("GET")) {
            //如果是 Get 请求,则跳转到百度页面
            String url = "https://www.baidu.com";
            //设置重定向状态
            response.setStatusCode(HttpStatus.SEE_OTHER);
            response.getHeaders().set(HttpHeaders.LOCATION, url);
            return response.setComplete();
        } else if (method.matches("POST")) {
            //如果是 post 请求,则返回 json 数据
            //指定响应的字符集编码
            response.getHeaders().add("Content-Type", "text/plain;charset=UTF-8");
            //设置未授权状态
            response.setStatusCode(HttpStatus.UNAUTHORIZED);
            Map map = new HashMap<>();
            map.put("status", 1);
            map.put("msg", "未登录...");

            String json = "";
            ObjectMapper om = new ObjectMapper();
            try {
                json = om.writeValueAsString(map);
            } catch (JsonProcessingException e) {
                json = e.getMessage();
            }

            DataBuffer dbf = response.bufferFactory().wrap(json.getBytes(StandardCharsets.UTF_8));
            return response.writeWith(Mono.just(dbf));
        } else {
            //其它情况下终止请求
            return response.setComplete();
        }
    }

    //过滤器序号,序号越小,越先执行
    @Override
    public int getOrder() {
        return 0;
    }
}

该全局过滤器,拦截所有访问网关的请求。你可以在全局过滤器中增加任何你想要的业务逻辑,放行或拒绝请求。

注意:当前在实际工作中,你可以通过 exchange.getRequest() 对象获取更多内容,比如请求的路径、ip 地址等,忽略一些请求的验证,比如访问登录页面就需要忽略请求的验证,要不然就永远无法登录进行身份认证,后续就没法玩了。

本 Demo 中简单模拟身份认证:判断 http 请求头中是否包含 token ,如果包含了就认为已经登录,否则就认为没有登录。
在没有登录的情况下,如果是 get 请求,就自动跳转到百度网站,如果是 post 请求,就返回 json 字符串。

此时你可以使用浏览器访问网关,测试在 http 头不包含 token 的情况下,是否会跳转到百度。可以使用 postman 工具请求接口,分别测试验证在请求 header 中添加 token 和不添加的访问效果。

image


ok,到此为止,常用的 SpringCloud Gateway 技术已经介绍完毕,有关全面的技术细节,请参考官网。

本篇博客的源代码下载地址为:https://files.cnblogs.com/files/blogs/699532/springcloud_gateway.zip

posted @ 2022-11-21 22:51  乔京飞  阅读(10895)  评论(0编辑  收藏  举报