前面介绍的微服务方式,不适用于微服务多和复杂的场景。前面的架构存在诸多问题:
- 客户端多次请求不同微服务,增加客户端代码和配置编写复杂性
- 认证复杂,每个服务都需要独立认证
- 微服务做集群的情况下,客户端并没有负责负责均衡的功能
以上问题可以借助API网关来解决。
网关概念
所谓的API网关,就是系统的统一入口,封装了应用程序内部结构,为客户端提供统一服务,一些与业务本身无关的公共逻辑可以在这里实现,如认证授权、监控、路由转发等。
网关的作用:
- 请求分发
- 负载均衡
- 过滤拦截
- 网络隔离
常见的网关介绍
- Nginx + Lua :nginx支持lua脚本。使用nginx负载均衡可以实现对api服务器的负载均衡高可用。
- Kong : 基于Nginx+Lua开发,高性能、稳定,插件,开箱即用。仅支持http协议。二开产品,扩展困难。缺乏易用的管控、配置方式。
- Zuul :Netflix开源网关,功能丰富,易于二开。但缺乏管控,无法动态配置,依赖组件较多。处理http请求依赖web容器,性能不及nginx和Spring Cloud Gateway。底层是Servlet。
- Gateway :Spring为替换Zuul开发的网关服务。
Gateway简介
SpringCloud Gateway是一个旨在为微服务架构提供一种简单有效的统一api路由管理方式,目标替代zuul网关。还提供了统一的对方式,且基于filter链的方式提供了网关基本的功能。eg:安全、监控、限流。
- 优点:
- 性能强大,是第一代网关zuul的1.6倍
- 功能强大:内置实用功能,eg转发、监控、限流等
- 设计优雅、容易扩展
- 缺点:
- 其实现依赖netty和webflux,而非传统的servlet模型,学习成本高
- 不能部署在tomcat、jetty等servlet容器,只可以jar包方式运行
- 需要SpringBoot2.0以上支持
微服务集成Gateway
流程:
- 请求进入网关,判断URL是否符合路由predicates设置的规则。如果符合就使用该路由进行处理。
- 根据路由filters配置,执行响应逻辑处理,对url进行改造。
- 将处理完成的URL拼接到uri,得到完整的地址,然后进行跳转。
案例代码
创建cloud2-gateway项目,并导入依赖
<?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>cloud2_parent</artifactId>
<groupId>com.hx</groupId>
<version>1.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>cloud2-gateway</artifactId>
<name>cloud2-gateway</name>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.7</maven.compiler.source>
<maven.compiler.target>1.7</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<version>${spring-cloud-alibaba.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
</project>
编写配置文件application.yml
server:
port: 8871
spring:
application:
name: cloud2-gateway
cloud:
nacos:
discovery:
server-addr: localhost:8848
gateway:
discovery:
locator:
enabled: true # 让gateway可以发现nacos中的服务
编写启动类,将该网关配置为nacos客户端
@SpringBootApplication
@EnableDiscoveryClient
public class Cloud2GatewayApp {
public static void main(String[] args) {
SpringApplication.run(Cloud2GatewayApp.class,args);
}
}
测试:访问带上服务名称即可 http://localhost:8871/cloud2-order-server/
上面这种方式,由于直接暴露服务名称给外部客户端,我们希望隐藏伪装,可以通过以下配置
spring:
cloud:
gateway:
routes:
- id: cloud2-order # 路由id,唯一
uri: http://localhost:8882 # 跳转地址,固定地址
predicates:
- Path=/od/** # 断言,路径匹配处理条件
filters:
- StripPrefix=1 # 网关局部过滤器:对通过网关请求进行拦截与加工。不同拦截器有不同逻辑。
- id: cloud2-product
uri: lb://cloud2-product-server # 跳转地址:负载均衡
predicates:
- Path=/prod/** # 拦截规则,拦截以/prod开头的路径
filters:
- StripPrefix=1 # StripPrefix表示过滤url第一节
测试:访问带上服务名称即可 http://localhost:8871/od/
过滤器Filter
过滤器就是在请求传递过程中,对请求与响应做一些手脚。
在Gateway中,filter的生命周期只有俩个:pre和post
- pre:这种过滤器在请求被路由之前调用,我们可以利用这个过滤器来实现身份验证、在集群中选择请求的微服务,记录调试信息等。
- post:这种过滤器在路由到微服务以后执行。这种过滤器可以用来为响应添加标准的http header、收集统计信息、将响应从微服务发送给客户端等。
在gateway中,filter的作用范围:
- GatewayFilter : 应用到单个路由或者一个分组的路由上,
- Global Filter :应用到所有路由上。
局部过滤器
不带参数的案例代码
编写过滤器类,过滤器工厂类将过滤器注册到网关
@Component
public class TimeGatewayFilterFactory extends AbstractGatewayFilterFactory<Object> {
@Order(0)
class TimeGatewayFilter implements GatewayFilter{
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
long begin = System.currentTimeMillis();
System.out.println("开始时间 = " + begin);
return chain.filter(exchange).then(Mono.fromRunnable(()->{
long end = System.currentTimeMillis();
System.out.println("结束时间 = " + end);
System.out.println("消耗时间 = " + (end - begin) + " 毫秒");
}));
}
}
/**
* 将自定义的局部filter注册到网关
*/
@Override
public GatewayFilter apply(Object config) {
return new TimeGatewayFilter();
}
}
编写配置
spring:
cloud:
gateway:
routes:
- id: cloud2-order # 路由id,唯一
uri: http://localhost:8882 # 跳转地址,固定地址
predicates:
- Path=/od/** # 断言,路径匹配处理条件
filters:
- Time
带参数的案例代码
修改配置
spring:
cloud:
gateway:
routes:
- id: cloud2-order # 路由id,唯一
uri: http://localhost:8882 # 跳转地址,固定地址
predicates:
- Path=/od/** # 断言,路径匹配处理条件
filters:
- Time=111,2222
修改TimeGatewayFilterFactory类,创建内部Config类
@Component
public class TimeGatewayFilterFactory extends AbstractGatewayFilterFactory<TimeGatewayFilterFactory.Config> {
// 指定获取配置参数之后,要封装成什么对象
public TimeGatewayFilterFactory() {
super(Config.class);
}
// 将数据添加到哪个属性上面
@Override
public List<String> shortcutFieldOrder() {
return Arrays.asList("val1","val2");
}
@Data
public static class Config {
private int val1;
private String val2;
}
/**
* 将自定义的局部filter注册到网关
*/
@Override
public GatewayFilter apply(TimeGatewayFilterFactory.Config config) {
return new TimeGatewayFilter(config);
}
@Order(0)
@NoArgsConstructor
class TimeGatewayFilter implements GatewayFilter {
private TimeGatewayFilterFactory.Config timeGatewayParam;
public TimeGatewayFilter(TimeGatewayFilterFactory.Config config) {
this.timeGatewayParam = config;
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
long begin = System.currentTimeMillis();
System.out.println("开始时间 = " + begin);
System.out.println("timeGatewayParam.getVal1() = " + timeGatewayParam.getVal1());
System.out.println("timeGatewayParam.val2 = " + timeGatewayParam.getVal2());
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
long end = System.currentTimeMillis();
System.out.println("结束时间 = " + end);
System.out.println("消耗时间 = " + (end - begin) + " 毫秒");
}));
}
}
}
全局过滤器
案例代码 - 鉴权
package com.hx.gateway;
import org.apache.commons.lang.StringUtils;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
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;
/**
* @author Huathy
* @date 2023-04-05 16:51
* @description
*/
@Component
public class AuthGlobalFilter implements GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String token = exchange.getRequest().getQueryParams().getFirst("token");
ServerHttpResponse response = exchange.getResponse();
response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
if(StringUtils.isBlank(token)){
System.out.println("该请求没有登录");
response.setStatusCode(HttpStatus.UNAUTHORIZED);
// return response.setComplete(); // 仅返回状态码
String res = "{\"code\":401,\"msg\":\"未登录\"}";
DataBuffer wrap = response.bufferFactory().wrap(res.getBytes(StandardCharsets.UTF_8));
return response.writeWith(Mono.just(wrap));
}
return chain.filter(exchange);
}
}
网关集成Sentinel实现限流
网关是所有请求的公共入口,所以可以在网关进行限流,而限流的方式也很多,我们本次采用Sentinel组件来实现网关限流。Sentinel支持对SpringCloud Gateway、Zuul等主流网关进行限流。
Sentinel提供了SpringCloudGateway的适配模块,可以提供俩种资源维度的限流:
- roure维度:即在spring配置文件中配置的路由条目,资源名为对应的routeId
- 自定义API维度:用户可以利用Sentinel提供的api来自定义一些api分组
API分组
Sentinel中支持按照API分组进行限流,就是我们可以按照特定规则进行限流。
在控制台页面中提供了三种方式的api分组管理
- 精准匹配
- 前缀匹配
- 正则匹配
集成案例
引入依赖
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-spring-cloud-gateway-adapter</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-sentinel-gateway</artifactId>
</dependency>
编写配置
spring:
cloud:
sentinel:
transport:
port: 18871
dashboard: localhost:8080
精确匹配
测试地址:http://localhost:8871/od/gs/t1?token=1
前缀匹配
/*
表示一级路径,/**
表示多级路径
正则匹配
网关集成Sentinel返回值修改
添加以下配置即可
@Component
public class GatewayConfig {
@PostConstruct
public void initBlockHandlers() {
BlockRequestHandler blockRequestHandler = new BlockRequestHandler() {
@Override
public Mono<ServerResponse> handleRequest(ServerWebExchange serverWebExchange, Throwable throwable) {
Map<String, Object> map = new HashMap<>();
map.put("code", 500101);
map.put("msg", "接口限流了");
return ServerResponse.status(HttpStatus.OK)
.contentType(MediaType.APPLICATION_JSON)
.body(BodyInserters.fromValue(map));
}
};
GatewayCallbackManager.setBlockHandler(blockRequestHandler);
}
}
总结
Gateway执行流程
- GatewayClient向SpringCloudGateway发送请求
- 请求会被HttpWebHandlerAdapter进行组装为网关上下文(GatewayContext)
- 然后GatewayContext转递到DispatcherHandler,负责分发请求给RoutePredicateHandlerMapping
- RoutePredicateHandlerMapping 负责路由查找,并根据路由断言,判断路由是否可用。
- 如果断言成功,由FilteringWebHandler创建过滤器链并调用
- 通过特定于请求的Filter链运行请求,Filter被虚线分割的原因是Filter可以在发送代理请求之前(pre)和之后(post)运行逻辑。
- 执行所有pre过滤器逻辑。进行代理请求,发出请求后。将运行post过滤器逻辑。
- 处理完毕,将Response返回给Gateway客户端。
Gateway过滤器
- pre过滤器:做参数校验、权限校验、流量监控、日志输出、协议转换等
- post过滤器:响应内容、响应头修改、日志输出、流量监控
核心思想
路由转发 + 执行过滤器链