SpringCloud Gateway 网关常用技术实现
SpringCloud Gateway 是目前非常流行的网关中间件,类似于 nginx 一样,主要提供【路由转发】和【负载均衡】功能,目的是为微服务架构提供一种简单而有效的统一的 API 路由管理方式。
我们通常也会在网关中添加【身份认证】和【鉴权】,阻止非法请求访问后端服务。
SpringCloud Gateway 由于全部采用 Java 语言,学习和使用门槛很低,自身功能强大且性能优越。本篇博客将通过代码方式,为大家演示如何通过配置的方式实现【路由转发】和【负载均衡】,如果通过全局过滤器实现【身份认证】和【鉴权】功能。在本篇博客的最后,会提供源代码的下载。
SpringCloud Gateway 的官网地址为:https://spring.io/projects/spring-cloud-gateway
一、搭建工程
采用 Maven 搭建 springcloud_gateway 父工程,下面包含 6 个子工程:
其中 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 非常简单,本篇博客搭建后的工程结果如下所示:
主要通过以下 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/6 和 http://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 和不添加的访问效果。
ok,到此为止,常用的 SpringCloud Gateway 技术已经介绍完毕,有关全面的技术细节,请参考官网。
本篇博客的源代码下载地址为:https://files.cnblogs.com/files/blogs/699532/springcloud_gateway.zip