大家都都知道在微服务架构中,一个系统会被拆分为很多个微服务。那么作为客户端要如何去调用这么多的微服务呢?如果没有网关的存在,我们只能在客户端记录每个微服务的地址,然后分别去调用。
所谓的API网关,就是指系统的统一入口,它封装了应用程序的内部结构,为客户端提供统一服 务,一些与业务本身功能无关的公共逻辑可以在这里实现,诸如认证、鉴权、监控、路由转发等等。
l Ngnix+lua
使用nginx的反向代理和负载均衡可实现对api服务器的负载均衡及高可用
lua是一种脚本语言,可以来编写一些简单的逻辑, nginx支持lua脚本
l Kong
基于Nginx+Lua开发,性能高,稳定,有多个可用的插件(限流、鉴权等等)可以开箱即用。
问题:
只支持Http协议;二次开发,自由扩展困难;提供管理API,缺乏更易用的管控、配置方式。
l Zuul
Netflflix开源的网关,功能丰富,使用JAVA开发,易于二次开发
问题:缺乏管控,无法动态配置;依赖组件较多;处理Http请求依赖的是Web容器,性能不如Nginx
l Spring Cloud Gateway
Spring公司为了替换Zuul而开发的网关服务,将在下面具体介绍。
注意:SpringCloud alibaba技术栈中并没有提供自己的网关,我们可以采用Spring Cloud Gateway来做网关
Spring Cloud Gateway是Spring公司基于Spring 5.0,Spring Boot 2.0 和 Project Reactor 等术开发的网关,它旨在为微服务架构提供一种简单有效的统一的 API 路由管理方式。它的目标是替代 Netflflix Zuul,其不仅提供统一的路由方式,并且基于 Filter 链的方式提供了网关基本的功能,例如:安全,监控和限流。
优点:
l 性能强劲:是第一代网关Zuul的1.6倍
l 功能强大:内置了很多实用的功能,例如转发、监控、限流等
l 设计优雅,容易扩展
缺点:
l 其实现依赖Netty与WebFlux,不是传统的Servlet编程模型,学习成本高
l 不能将其部署在Tomcat、Jetty等Servlet容器里,只能打成jar包执行
l 需要Spring Boot 2.0及以上的版本,才支持
实现以下功能 网关的微服务模块所需的全部依赖
org.springframework.cloud
spring-cloud-starter-gateway
com.alibaba.cloud
spring-cloud-starter-alibaba-nacos-discovery
cn.hutool
hutool-all
5.8.16
org.springframework.cloud
spring-cloud-starter-loadbalancer
要求: 通过浏览器访问api网关,然后通过网关将请求转发到商品微服务
org.springframework.cloud
spring-cloud-starter-gateway
server:
port: 8088
spring:
application:
name: api-gateway
# 配置api
cloud:
gateway:
routes:
- id: product_route # 路由的唯一标识,只要不重复都可以,如果不写默认会通过UUID产生,一般写成被路由的服务名称
uri: http://localhost:8472/ # 被路由的地址
order: 1 #表示优先级 数字越小优先级越高
predicates: #断言: 执行路由的判断条件
- Path=/product_serv/**
filters: # 过滤器: 可以在请求前或请求后作一些手脚
- StripPrefix=1
properties文件的写法
#设置一下当前的应用的名字
spring.application.name=testgateway
#将当前的微服务注册到注册中心
spring.cloud.nacos.server-addr=localhost:8848
server.port=8088
#开始设置路由
#设置一下id
spring.cloud.gateway.routes[0].id=protest
#设置一下优先级
spring.cloud.gateway.routes[0].order=1
#断言serviceid为gate-pro的路径是以/pro为开头的
#友情提示 P是大写 而且P前面一定有空格 否则,会出错
spring.cloud.gateway.routes[0].predicates[0]= Path=/produ/**
#指向的微服务是什么
spring.cloud.gateway.routes[0].uri=http://localhost:8076
#内置过滤器 访问的真实的路径需要截取的是什么
spring.cloud.gateway.routes[0].filters[0]=StripPrefix=1
但是通常情况下我们配置的nacos实例并不止一个,对于多个实例,这样的配置显然不太方便,此时我们需要加依赖因为 cloud2021版本剔除了 ribbon
导入loadbalancer 负载均衡的依赖
org.springframework.cloud
spring-cloud-starter-loadbalancer
默认是轮询的
要改为随机需要加 config 文件
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.loadbalancer.core.RandomLoadBalancer;
import org.springframework.cloud.loadbalancer.core.ReactorLoadBalancer;
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.core.env.Environment;
public class LoadBalancerConfig {
@Bean
ReactorLoadBalancer randomLoadBalancer(Environment environment,
LoadBalancerClientFactory loadBalancerClientFactory) {
String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME); //loadbalancer.client.name
// 对应的需要进行负载均衡的名字是什么
System.out.println("======"+name);
// product
return new RandomLoadBalancer(loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class), name);
}
}
使用负载均衡的时候需要在启动类上设置信息
@SpringBootApplication
// 所有需要用到负载均衡的服务使用的策略全部都是随机
@LoadBalancerClients(defaultConfiguration= LoadBalancerConfig.class)
public class GateWayApplication {
public static void main(String[] args) {
SpringApplication.run(GateWayApplication.class);
}
}
最后修改 yml 文件 Path 即可
server:
port: 8088
spring:
application:
name: api-gateway
# 配置api
cloud:
gateway:
routes:
- id: product_route # 路由的唯一标识,只要不重复都可以,如果不写默认会通过UUID产生,一般写成被路由的服务名称
uri: lb://product # 被路由的地址 lb代表负载均衡 loadbalanced
order: 1 #表示优先级 数字越小优先级越高
predicates: #断言: 执行路由的判断条件
- Path=/product_serv/**
filters: # 过滤器: 可以在请求前或请求后作一些手脚
- StripPrefix=1
nacos:
discovery:
server-addr: localhost:8848
用于上面的 yml 文件的配置我们会感觉特别的麻烦,每加了新的微服务模块我们就需要通过-的标识加新的对象,这样极其不方便,所以可以采用以下的简写版方式进行配置
server:
port: 8088
spring:
application:
name: api-gateway
# 配置api
cloud:
nacos:
discovery:
server-addr: localhost:8848
gateway:
discovery:
locator:
enabled: true
#使用服务发现路由
spring.cloud.gateway.discovery.locator.enabled=true
#服务路由名小写
spring.cloud.gateway.discovery.locator.lower-case-service-id=true
spring.cloud.gateway.discovery.locator.enabled为true,表明gateway开启服务注册和发现的功能,并且spring cloud gateway自动根据服务发现为每一个服务创建了一个router,这个router将以服务名开头的请求路径转发到对应的服务。spring.cloud.gateway.discovery.locator.lowerCaseServiceId是将请求路径上的服务名配置为小写比如以/service-hi/*的请求路径被路由转发到服务名为service-hi的服务上。
访问的形式:
ip:
端口(网关的端口)/
服务的名字(nacos发现配置的名字spring.application.name)/
请求的路径
路由(Route) 是 gateway 中最基本的组件之一,表示一个具体的路由信息载体。主要定义了下面的几个信息:
l id,路由标识符,区别于其他 Route。唯一 不写 默认的唯一的值
l uri,路由指向的目的地 uri,即客户端请求最终被转发到的微服务。
l order,用于多个 Route 之间的排序,数值越小排序越靠前,匹配优先级越高。
l predicate,断言的作用是进行条件判断,只有断言都返回真,才会真正的执行路由。
l fifilter,过滤器用于修改请求和响应信息。
SpringCloud Gateway包括许多内置的断言工厂,所有这些断言都与HTTP请求的不同属性匹配体如下:
l 基于Datetime类型的断言工厂
此类型的断言根据时间做判断,主要有三个:
AfterRoutePredicateFactory: 接收一个日期参数,判断请求日期是否晚于指定日期
BeforeRoutePredicateFactory: 接收一个日期参数,判断请求日期是否早于指定日期
BetweenRoutePredicateFactory: 接收两个日期参数,判断请求日期是否在指定时间段内
-After=2019-12-31T23:59:59.789+08:00[Asia/Shanghai]
l 基于远程地址的断言工厂 RemoteAddrRoutePredicateFactory:
接收一个IP地址段,判断请求主机地址是否在地址段中
-RemoteAddr=192.168.1.24
l 基于Cookie的断言工厂
CookieRoutePredicateFactory:接收两个参数,cookie 名字和一个正则表达式。 判断请求
cookie是否具有给定名称且值与正则表达式匹配。
-Cookie=chocolate, ch.
l 基于Header的断言工厂
HeaderRoutePredicateFactory:接收两个参数,标题名称和正则表达式。 判断请求Header是否
具有给定名称且值与正则表达式匹配。
-Header=X-Request-Id, \d+
l 基于Host的断言工厂
HostRoutePredicateFactory:接收一个参数,主机名模式。判断请求的Host是否满足匹配规则。
-Host=**.testhost.org
l 基于Method请求方法的断言工厂
MethodRoutePredicateFactory:接收一个参数,判断请求类型是否跟指定的类型匹配。
-Method=GET
l 基于Path请求路径的断言工厂
PathRoutePredicateFactory:接收一个参数,判断请求的URI部分是否满足路径规则。
-Path=/foo/{segment}基于Query请求参数的断言工厂
QueryRoutePredicateFactory :接收两个参数,请求param和正则表达式, 判断请求参数是否具
有给定名称且值与正则表达式匹配。
-Query=baz, ba.
l 基于路由权重的断言工厂
WeightRoutePredicateFactory:接收一个[组名,权重], 然后对于同一个组内的路由按照权重转发
routes:
predicates:
- 路径
- 自定义断言的类名
实例
@Component public class AgeRoutePredicateFactory extends AbstractRoutePredicateFactory
{ public static final String MIN_AGE = "minAge"; public static final String MAX_AGE = "maxAge"; public AgeRoutePredicateFactory() { super(Config.class); } public List shortcutFieldOrder() { return Arrays.asList("minAge", "maxAge"); } public Predicate apply(final Config config) { // Assert.isTrue(config.getDatetime1().isBefore(config.getDatetime2()), config.getDatetime1() + " must be before " + config.getDatetime2()); return new GatewayPredicate() { public boolean test(ServerWebExchange serverWebExchange) { // 年龄是不是在最小值和最大值之间 // age 作为一个参数传过来 ServerHttpRequest request = serverWebExchange.getRequest(); List age = request.getQueryParams().get("age"); System.out.println("________________________"+age); String s = age.get(0); int i = Integer.parseInt(s); // 》=minAge&&<=maxAge return i>=config.getMinAge()&&i<= config.getMaxAge(); // true false } // public String toString() { // return String.format("Between: %s and %s", config.getDatetime1(), config.getDatetime2()); // } }; } @Validated public static class Config { private @NotNull Integer minAge; private @NotNull Integer maxAge; public Config() { } public Integer getMinAge() { return minAge; } public void setMinAge(Integer minAge) { this.minAge = minAge; } public Integer getMaxAge() { return maxAge; } public void setMaxAge(Integer maxAge) { this.maxAge = maxAge; } } }
三个知识点:
1 作用: 过滤器就是在请求的传递过程中,对请求和响应做一些操作
2 生命周期: Pre Post
3 分类: 局部过滤器(作用在某一个路由上) 全局过滤器(作用全部路由上)
在Gateway中, Filter的生命周期只有两个:“pre” 和 “post”。
l PRE: 这种过滤器在请求被路由之前调用。我们可利用这种过滤器实现身份验证、在集群中选择请求的微服务、记录调试信息等。
l POST:这种过滤器在路由到微服务以后执行。这种过滤器可用来为响应添加标准的HTTP Header、收集统计信息和指标、将响应从微服务发送给客户端等。
Gateway 的Filter从作用范围可分为两种: GatewayFilter与GlobalFilter。
l GatewayFilter:应用到单个路由或者一个分组的路由上。
l GlobalFilter:应用到所有的路由上。
局部过滤器是针对单个路由的过滤器。
在SpringCloud Gateway中内置了很多不同类型的网关路由过滤器。
https://www.cnblogs.com/zhaoxiangjun/p/13042189.html
过滤器工厂 |
作用 |
参数 |
AddRequestHeader |
为原始请求添加Header |
Header的名称及值 |
AddRequestParameter |
为原始请求添加请求参数 |
参数名称及值 |
AddResponseHeader |
为原始响应添加Header |
Header的名称及值 |
DedupeResponseHeader |
剔除响应头中重复的值 |
需要去重的Header名称及去重策略 |
Hystrix |
为路由引入Hystrix的断路器保护 |
HystrixCommand的名称 |
FallbackHeaders |
为fallbackUri的请求头中添加具体的异常信息 |
Header的名称 |
PrefixPath |
为原始请求路径添加前缀 |
前缀路径 |
PreserveHostHeader |
为请求添加一个preserveHostHeader=true的属性,路由过滤器会检查该属性以决定是否要发送原始的Host |
无 |
RequestRateLimiter |
用于对请求限流,限流算法为令牌桶 |
keyResolver、rateLimiter、statusCode、denyEmptyKey、emptyKeyStatus |
RedirectTo |
将原始请求重定向到指定的URL |
http状态码及重定向的url |
RemoveHopByHopHeadersFilter |
为原始请求删除IETF组织规定的一系列Header |
默认就会启用,可以通过配置指定仅删除哪些Header |
RemoveRequestHeader |
为原始请求删除某个Header |
Header名称 |
RemoveResponseHeader |
为原始响应删除某个Header |
Header名称 |
RewritePath |
重写原始的请求路径 |
原始路径正则表达式以及重写后路径的正则表达式 |
RewriteResponseHeader |
重写原始响应中的某个Header |
Header名称,值的正则表达式,重写后的值 |
SaveSession |
在转发请求之前,强制执行WebSession::save操作 |
无 |
secureHeaders |
为原始响应添加一系列起安全作用的响应头 |
无,支持修改这些安全响应头的值 |
SetPath |
修改原始的请求路径 |
修改后的路径 |
SetResponseHeader |
修改原始响应中某个Header的值 |
Header名称,修改后的值 |
SetStatus |
修改原始响应的状态码 |
HTTP 状态码,可以是数字,也可以是字符串 |
StripPrefix |
用于截断原始请求的路径 |
使用数字表示要截断的路径的数量 |
Retry |
针对不同的响应进行重试 |
retries、statuses、methods、series |
RequestSize |
设置允许接收最大请求包的大小。如果请求包大小超过设置的值,则返回 413 Payload Too Large |
请求包大小,单位为字节,默认值为5M |
ModifyRequestBody |
在转发请求之前修改原始请求体内容 |
修改后的请求体内容 |
ModifyResponseBody |
修改原始响应体的内容 |
修改后的响应体内容 |
Default |
为所有路由添加过滤器 |
过滤器工厂名称及值 |
实例
在配置文件中,添加一个Log的过滤器配置
filters: # 过滤器: 可以在请求前或请求后作一些手脚 - StripPrefix=1 - SetStatus=2000 - Log=true,false 第2步:自定义一个过滤器工厂,实现方法 package com.yyl.config; /** * @author yuyongli * @Date 2021/5/28 */ import org.springframework.cloud.gateway.filter.GatewayFilter; import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory; import org.springframework.cloud.gateway.filter.factory.SetStatusGatewayFilterFactory; import org.springframework.cloud.gateway.support.HttpStatusHolder; import org.springframework.cloud.gateway.support.ServerWebExchangeUtils; import org.springframework.stereotype.Component; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; import java.util.Arrays; import java.util.List; @Component public class LogGatewayFilterFactory extends AbstractGatewayFilterFactory
{ public LogGatewayFilterFactory() { super(LogGatewayFilterFactory.Config.class); } public List shortcutFieldOrder() { return Arrays.asList("consoleLog","cacheLog"); } @Override public GatewayFilter apply(Config config) { return new GatewayFilter() { @Override public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { if(config.cacheLog){ System.out.println("开启缓存日志"); } if(config.consoleLog){ System.out.println("开启控制台日志"); } return chain.filter(exchange); } }; } public static class Config { private Boolean consoleLog; private Boolean cacheLog; public Config() { } public Boolean getConsoleLog() { return consoleLog; } public void setConsoleLog(Boolean consoleLog) { this.consoleLog = consoleLog; } public Boolean getCacheLog() { return cacheLog; } public void setCacheLog(Boolean cacheLog) { this.cacheLog = cacheLog; } } }
全局过滤器作用于所有路由, 无需配置。通过全局过滤器可以实现对权限的统一校验,安全性验证等功能。
SpringCloud Gateway内部也是通过一系列的内置全局过滤器对整个路由转发进行处理如下:
全局过滤器 |
作用 |
Combined Global Filter and GatewayFilter Ordering |
对过滤器执行顺序进行排序 |
Forward Routing Filter |
用于本地forward,也就是将请求在Gateway服务内进行转发,而不是转发到下游服务 |
Netty Routing Filter |
使用Netty的 HttpClient 转发http、https请求 |
Netty Write Response Filter |
将代理响应写回网关的客户端侧 |
RouteToRequestUrl Filter |
将从request里获取的原始url转换成Gateway进行请求转发时所使用的url |
Websocket Routing Filter |
使用Spring Web Socket将转发 Websocket 请求 |
Gateway Metrics Filter |
整合监控相关,提供监控指标 |
Marking An Exchange As Routed |
防止重复的路由转发 |
内置的过滤器已经可以完成大部分的功能,但是对于企业开发的一些业务功能处理,还是需要我们自己编写过滤器来实现的,那么我们一起通过代码的形式自定义一个过滤器,去完成统一的认证校验。
开发中的鉴权逻辑:
l 当客户端第一次请求服务时,服务端对用户进行信息认证(登录)
l 认证通过,将用户信息进行加密形成token,返回给客户端,作为登录凭证
l 以后每次请求,客户端都携带认证的token
l 服务端对token进行解密,判断是否有效。
实例
@Component public class MyGlobalFilter implements GlobalFilter, Ordered { @Override public Mono
filter(ServerWebExchange exchange, GatewayFilterChain chain) { /** * token * 有 放行 * 没有 json数据 code 500 msg ”“ ,data:null */ String token = exchange.getRequest().getHeaders().getFirst("token"); // token不为空 if(StringUtils.isNotBlank(token)){ // 放行 return chain.filter(exchange);// 走下面的过滤器 }else{ // 返回json数据 Map map =new HashMap<>(); map.put("code",500); map.put("msg","错误!!!"); map.put("data","sdfjdvhgs"); // 转化为json数据 String s = JSONUtil.toJsonStr(map); // 返回json数据 ServerHttpResponse response = exchange.getResponse(); // // Mono voidMono = response.setComplete(); DataBuffer dataBuffer; try { byte[] bytes = s.getBytes("utf8"); dataBuffer = response.bufferFactory().wrap(bytes); } catch (UnsupportedEncodingException e) { throw new RuntimeException(e); } return response.writeWith(Mono.just(dataBuffer)); } //return null; } /** * * @return */ @Override public int getOrder() { return 0; } }
@Component
注解概述@Component
注解是 Spring Framework 中的核心注解之一,它标识一个类作为 Spring 容器中的组件,允许 Spring 在运行时自动发现、创建和管理这些组件。借助 @Component
注解,开发者可以实现松耦合的组件化开发,从而更好地管理应用程序的结构和功能。