SpringCloud GateWay
一 API 网关
1.什么是API 网关?
API 网关的作用就是把各个服务对外提供的API汇聚起来,让外界看起来是一个统一的入口,同时也可以在网关中提供额外的功能。
总结:网关就是所有项目的一个统一入口
2.网关组成
网关=路由转发+过滤器(编写额外的功能)
2.1 路由转发
接收外界请求,通过网关的路由转发,转发到后端的服务上。
如果只有这一个功能看起来跟nginx反向代理服务器很像,外界访问nginx,由nginx做负载均衡,后把请求转发到对应的服务器上。
2.2 过滤器
网关非常重要的作用就是过滤器。
过滤其中默认提供了25种内置的功能,还可以根据自己的额外的需求,自定义过滤器。对于日常开发中比较常用的功能有网关的容器、限流以及请求及相应的额外处理。
二 Spring Cloud Gareway介绍
1.简介
Spring Cloud Gateway 是Spring Cloud的二级子项目,提供了微服务网关的功能,包含:权限安全、监控、指标等功能。例如: 熔断、限流、重试、自定义过滤器等token校验、IP黑名单。
链接:
SpringCloud Gateway
你们项目里面用的什么网关? gateway zuul
spring cloud gaeway 是用来取代zuul(netflix) 的新一代网关组件。
(zuul: 1.0,2.0**, zuul的本质,一组过滤器,根据自定义的过滤器顺序来执行,本质就是web组件 web三大组件(监听器 过滤器 servlet) 拦截 SpringMVC**)。
SpringCloud Gateway是基于webFlux框架实现的,而webFlux框架底层使用了高性能的Reactor模式通信框架Netty。
Zuul1.0使用的是BIO(Blocking IO) tomcat7.0之前都是BIO,性能一般。Zuul2.0 性能好 NIO
AIO 异步非阻塞IO。 aio=async+no blocking io
2 名词解释
2.1 Route 路由
Route中文为路由,GateWay里面的Route是主要学习内容·,一个GateWay项目包含有多个Route。
一个路由包含 ID、URI、Predicate集合、Filter集合。
在Route中ID是自定义的,URI就是一个地址。
2.2 Predicate(断言)(就是一个返回bool的表达式)
Spring Cloud Gateway中断言函数输入类型是Spring 5.0框架中的ServerWebExchange。Spring Cloud Gateway的断言函数允许开发者去定义匹配来自于Http Request中的任何信息,比如请求头和参数。
2.3 Filter 过滤器
所有生效的Filter都是GatewayFilter的实例,在GateWay运行过程中Filter负责在代理服务之前和之后做一些事情。
Sping Cloud Gateway中的Filter分为两种类型的Filter.分别是Gateway Filter和Global Filter。过滤器Filter将会对请求和响应进行修改处理。
一个是针对某一个路由的Filter 对某一个接口做限流。
一个是针对全局的filter token\ip白名单
2.4 流程
网关客户端访问Gateway网关,Gateway中的 Handler Mapping对请求URI进行处理,处理完成后交换Web Handler. Web Handler会被Filter进行过滤。Filter中前半部分代理是处理请求的代码,处理完成后调用真实的被代理的服务。被代理的服务响应结果,结果会被Filter中后半部分代码进行操作,操作完成后把结果返回给Web Handler,再返回给Handler Mapping,最终响应给客户端。
三、相关实操
1.入门案例之配置文件路由
手写一个登录服务loginService的Controller
@RestController
public class LoginController {
@GetMapping("/login")
public String login(String userName,String Password){
return UUID.randomUUID().toString();
}
}
gateway-service 配置中维护
server:
port: 80
spring:
cloud:
gateway:
enabled: true # 只要加了依赖 默认是开启的
routes:
- id: gateway-server-login # 路由的id 保持唯一即可
uri: http://localhost:8086 #uri 统一资源标识符号
predicates:
- Path=/login # 匹配规则 只要path匹配上了 /login 就往uri转发 http://localhost:8086/login
预期效果:
2. 入门案例之代码路由
1.自定义配置类,注入相关的bean
@Configuration
public class GatewayConfig {
@Bean
public RouteLocator routes(RouteLocatorBuilder builder) {
return builder.routes()
.route("demo1", r -> r.path("/v/kichiku").uri("https://www.bilibili.com/"))
.route("demo2",r->r.path("/v/douga").uri("https://www.bilibili.com/"))
.route("demo3",r->r.path("/v/knowledge").uri("https://www.bilibili.com/"))
.build();
}
}
关键代码逻辑是实现RouteLocator接口,该接口的实现可以通过RouteLocatorBuilder 类中的routes 定义若干个route,route中含有两个参数,一个是route的id,一个是函数Function,可以通过链式编程依次实现断言、URI等参数的注入。
实现效果:
3.动态路由
从之前的配置可以看出,URL的配置项是写死的,这不符合微服务的要求。微服务的要求是只要知道服务的名字,根据名字去找,而直接写死就没有负载均衡的效果了。
默认情况下,GateWay会根据注册中心的服务列表,以注册中心上微服务名为路径创建动态路由进行转发,从而实现动态路由的功能。
需要注意的是,uri的协议为lb(load balance) ,表示启动GateWay的负载均衡的功能。lb://serviceName 是Spring Cloud Gateway 在微服务中自动为我们创建的负载均衡uri
动态路由需要结合注册中心进行服务发现,以EureKa注册中心为例:
配置文件的配置
server:
port: 80
spring:
cloud:
gateway:
enabled: true # 只要加了依赖 默认是开启的
routes: #如果一个服务里面有100个路径 如果我想做负载均衡?? 动态路由
- id: gateway-server-login # 路由的id 保持唯一即可
uri: lb://login-service #uri 统一资源标识符号
predicates:
- Path=/login # 匹配规则 只要path匹配上了 /login 就往uri转发 http://localhost:8086/login
discovery:
locator:
enabled: true #开启动态路由 开启通过应用名称找到服务的功能
lower-case-service-id: true #开启服务名称小写
application:
name: gateway-service
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka
registry-fetch-interval-seconds: 3 #网关拉取服务列表的时间缩短
instance:
hostname: localhost
instance-id: ${eureka.instance.hostname}:${spring.application.name}:${server.port}
GateWay启动类添加Eureka的客户端启动类
@EnableEurekaClient
@SpringBootApplication
public class LoginApplication {
public static void main(String[] args) {
SpringApplication.run(LoginApplication.class,args);
}
}
效果:
eureka server的配置项
server:
port: 8761
spring:
application:
name: eureka-server
启动类的代码
@EnableEurekaServer
@SpringBootApplication
public class EureKaApplication {
public static void main(String[] args) {
SpringApplication.run(EureKaApplication.class,args);
}
}
服务注册效果
4. 断言工厂的使用
在gateway启动时会去加载一些路由断言工厂(判断一句话是否正确 一个boolean表达式)
After断言demo
spring:
cloud:
gateway:
enabled: true # 只要加了依赖 默认是开启的
routes: #如果一个服务里面有100个路径 如果我想做负载均衡?? 动态路由
- id: gateway-server-login # 路由的id 保持唯一即可
uri: lb://login-service #uri 统一资源标识符号
predicates:
- Path=/login # 匹配规则 只要path匹配上了 /login 就往uri转发 http://localhost:8086/login
- After=2024-02-15T21:26:41.466+08:00[Asia/Shanghai]
- Method=POST,GET
在2024-02-15 21:26之前访问:
在2024-02-15 21:26之后访问:
断言Predicate就是为了实现一组匹配规则,让请求过来找到对应的Route进行处理。
自定义断言工厂
自定义断言工厂可以继承AbstractRoutePredicateFactory 抽象工厂类,重写apply方法的逻辑。
在apply方法中可以通过exchange.getRequest()拿到ServerHttpRequest对象,从而可以获取到请求的参数、请求方式、请求头等信息。
apply方法的参数是自定义的配置类,在使用的时候配置参数,在apply方法中直接获取使用。
源码demo:
public class HeaderRoutePredicateFactory extends AbstractRoutePredicateFactory<HeaderRoutePredicateFactory.Config> {
public static final String HEADER_KEY = "header";
public static final String REGEXP_KEY = "regexp";
public HeaderRoutePredicateFactory() {
super(HeaderRoutePredicateFactory.Config.class);
}
public List<String> shortcutFieldOrder() {
return Arrays.asList("header", "regexp");
}
public Predicate<ServerWebExchange> apply(HeaderRoutePredicateFactory.Config config) {
final boolean hasRegex = !StringUtils.isEmpty(config.regexp);
return new GatewayPredicate() {
public boolean test(ServerWebExchange exchange) {
List<String> values = (List)exchange.getRequest().getHeaders().getOrDefault(config.header, Collections.emptyList());
if (values.isEmpty()) {
return false;
} else {
return hasRegex ? values.stream().anyMatch((value) -> {
return value.matches(config.regexp);
}) : true;
}
}
public String toString() {
return String.format("Header: %s regexp=%s", config.header, config.regexp);
}
};
}
@Validated
public static class Config {
@NotEmpty
private String header;
private String regexp;
public Config() {
}
public String getHeader() {
return this.header;
}
public HeaderRoutePredicateFactory.Config setHeader(String header) {
this.header = header;
return this;
}
public String getRegexp() {
return this.regexp;
}
public HeaderRoutePredicateFactory.Config setRegexp(String regexp) {
this.regexp = regexp;
return this;
}
}
}
5.Filter 过滤器工厂(重点)
gateway里面的过滤器和Servlet里面的过滤器功能差不多,路由过滤器可以用于修改进入Http请求和返回Http响应。
分类
1.按照生命周期分为两种: pre 在业务逻辑之前 post 在业务逻辑之后
2.按照种类划分也是两种:
GatewayFilter 需要配置某个路由,才能过滤。如果需要使用全局过滤,需要配置DefaultFilters.
GlobalFilter 全局过滤器,不需要配置路由,系统初始化作用到所有路由上。
全局过滤器 统计请求次数 限流 token的校验 ip黑名单的拦截 跨域本质(filter) 144开头的电话 限制一些ip的访问等
具体过滤器的详情,可以参考官方文档
过滤器官方文档
自定义过滤器
自定义过滤器主要实现GlobalFilter, Ordered两个接口,具体的IP黑名单的自定义
过滤器逻辑如下:
package com.sgm.esb.filter;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
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.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
/**
* @author: Runqiang_Jiang
* @Time: 2024/2/15 22:38
*/
/**
* IP 过滤器
*/
@Component
public class IPCheckFilter implements GlobalFilter, Ordered {
/**
* 网关的并发量比较高,不要在网关里面直接操作数据库
*/
public static final List<String> BLACK_LIST= Arrays.asList("127.0.0.1");
/**
* 1.拿到ip
* 2.检查ip是否符合规范
* 3.放行\拦截
* @param exchange
* @param chain
* @return
*/
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
String ip = request.getHeaders().getHost().getHostString();
// ip 是否存在黑名单里面
if(!BLACK_LIST.contains(ip)){
//放行
return chain.filter(exchange);
}
//拦截
//响应相关的内容
ServerHttpResponse response = exchange.getResponse();
response.getHeaders().set("content-type","application/json;charSet=UTF-8");
HashMap<String, Object> hashMap = new HashMap<>(4);
hashMap.put("code", 438);
hashMap.put("msg","你是黑名单");
ObjectMapper objectMapper = new ObjectMapper();
DataBuffer wrap=null;
try {
byte[] bytes = objectMapper.writeValueAsBytes(hashMap);
wrap = response.bufferFactory().wrap(bytes);
} catch (JsonProcessingException e) {
}
return response.writeWith(Mono.just(wrap));
}
@Override
public int getOrder() {
return -5;
}
}
结果展示:
拦截:
放行:
网关全局过滤器token的校验
演示demo
package com.sgm.esb.filter;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.stream.Collectors;
import org.apache.http.HttpStatus;
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.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 org.w3c.dom.stylesheets.LinkStyle;
import reactor.core.publisher.Mono;
/**
* @author: Runqiang_Jiang
* @Time: 2024/2/15 23:34
*/
@Component
public class TokenCheckFilter implements GlobalFilter, Ordered {
@Autowired
private StringRedisTemplate redisTemplate;
public static final List<String> ALLOW_URL= Arrays.asList("/login-service/login","/myurl","/login");
/**
* token约定放在请求头中的Authorization
* 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);
}
//检查
HttpHeaders headers = request.getHeaders();
List<String> authorization = headers.get("Authorization");
if(!CollectionUtils.isEmpty(authorization)){
String token= authorization.get(0);
if(StringUtils.hasText(token)){
//约定好有前缀 bearer token
String realToken = token.replace("bearer ", "");
if(StringUtils.hasText(realToken)&& redisTemplate.hasKey(realToken)){
return chain.filter(exchange);
}
}
}
//拦截
ServerHttpResponse response = exchange.getResponse();
response.getHeaders().set("Content-Type","application/json;charset=UTF-8");
HashMap<String, Object> hashMap = new HashMap<>(4);
hashMap.put("code", HttpStatus.SC_UNAUTHORIZED);
hashMap.put("msg","未授权");
ObjectMapper objectMapper = new ObjectMapper();
byte[] bytes = new byte[0];
try {
bytes = objectMapper.writeValueAsBytes(hashMap);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
DataBuffer wrap = response.bufferFactory().wrap(bytes);
return response.writeWith(Mono.just(wrap));
}
@Override
public int getOrder() {
return 2;
}
}
演示结果:
用户登录生成token
请求teach-service服务
成功
失败
6 gateway集成Redis限流
6.1 什么是限流?
通俗的说,限流就是限制一段时间内,用户访问资源的次数,减轻服务器的压力,限流大致分为两种:
1.IP限流(5s内同一个ip访问超过3次,则限制不让访问,过一段时间才可能继续访问)
2.请求量限流(只要在一段时间内(窗口期),请求次数达到阈值,就直接拒绝后面来得访问了,过一段时间才可以继续访问)(粒度可以细化到一个api,一个服务)
6.2本地限流模型
限流模型:漏斗算法 、令牌桶算法、滑动窗口算法、计数器算法
6.3 GateWay 结合redis实现请求量限流
Spring Cloud GateWay 已经内置了一个 RequestRateLimiterGatewayFilterFactory,可以直接使用
相关的依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
代码实例:
/**
* 自定义请求限制
*/
@Configuration
public class RequestLimitConfig {
//针对某一个接口ip来限流 /login
@Bean("ipKeyResolver")
@Primary
public KeyResolver ipKeyResolver(){
return exchange ->
Mono.just(exchange.getRequest().getHeaders().getHost().getHostString());
}
//针对某个路径进行限流 /login
// api 就是接口
@Bean("apiKeyResolver")
public KeyResolver apiKeyResolver(){
return exchange -> Mono.just(exchange.getRequest().getPath().value());
}
}
相关的配置
spring:
cloud:
gateway:
enabled: true # 只要加了依赖 默认是开启的
routes: #如果一个服务里面有100个路径 如果我想做负载均衡?? 动态路由
- id: gateway-server-login # 路由的id 保持唯一即可
uri: lb://login-service #uri 统一资源标识符号
predicates:
- Path=/login # 匹配规则 只要path匹配上了 /login 就往uri转发 http://localhost:8086/login
# - After=2024-02-15T21:26:41.466+08:00[Asia/Shanghai]
- Method=POST,GET
# - Query=name,admin. #正则表达式的值
filters:
- name: RequestRateLimiter # 过滤器的名称
args: #过滤器的参数
key-resolver: "#{@ipKeyResolver}" #通过spel表达式取ioc容器中bean的值
redis-rate-limiter.replenishRate: 1 #生成令牌的速度
redis-rate-limiter.burstCapacity: 3 #桶容量
# redis-rate-limiter.requestedTokens: 1
discovery:
locator:
enabled: true #开启动态路由 开启通过应用名称找到服务的功能
lower-case-service-id: true #开启服务名称小写
application:
name: gateway-service
链接:
https://docs.spring.io/spring-cloud-gateway/docs/4.0.9/reference/html/#the-requestratelimiter-gatewayfilter-factory
7.网关与跨域配置