大家都都知道在微服务架构中,一个系统会被拆分为很多个微服务。那么作为客户端要如何去调用这么多的微服务呢?如果没有网关的存在,我们只能在客户端记录每个微服务的地址,然后分别去调用。
这样的架构,会存在着诸多的问题:
l 客户端多次请求不同的微服务,增加客户端代码或配置编写的复杂性
l 认证复杂,每个服务都需要独立认证。
l 存在跨域请求,在一定场景下处理相对复杂。
上面的这些问题可以借助API网关来解决。
所谓的API网关,就是指系统的统一入口,它封装了应用程序的内部结构,为客户端提供统一服 务,一些与业务本身功能无关的公共逻辑可以在这里实现,诸如认证、鉴权、监控、路由转发等等。 添加上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
引入jar包
创建启动类
配置文件
启动查看nacos
服务添加成功
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:接收一个[组名,权重], 然后对于同一个组内的路由按照权重转发
内置断言的使用
搜索文件
复制,重建,修改
自定义age的断言
package com.example.config;// // Source code recreated from a .class file by IntelliJ IDEA // (powered by FernFlower decompiler) // import java.time.ZonedDateTime; import java.util.Arrays; import java.util.List; import java.util.function.Predicate; import javax.validation.constraints.NotNull; import org.springframework.cloud.gateway.handler.predicate.AbstractRoutePredicateFactory; import org.springframework.cloud.gateway.handler.predicate.GatewayPredicate; import org.springframework.http.HttpRequest; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.stereotype.Component; import org.springframework.util.Assert; import org.springframework.validation.annotation.Validated; import org.springframework.web.server.ServerWebExchange; @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; } } }
修改yml
局部过滤器是针对单个路由的过滤器。
在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 |
为所有路由添加过滤器 |
过滤器工厂名称及值 |
内置局部过滤器的使用
搜索文件
能满足需要就直接用,满足不了需求就复制重写
修改yml文件 修改测试类
访问
全局过滤器作用于所有路由, 无需配置。通过全局过滤器可以实现对权限的统一校验,安全性验证等功能。
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进行解密,判断是否有效。
如上图,对于验证用户是否已经登录鉴权的过程可以在网关统一检验。
检验的标准就是请求中是否携带token凭证以及token的正确性。
下面的我们自定义一个GlobalFilter,去校验所有请求的请求参数中是否包含“token”,如何不包含请求参数“token”则不转发路由,否则执行正常的逻辑
自定义全局过滤器 要求:必须实现GlobalFilter,Ordered接口
创建配置文件
package com.example.filter; import cn.hutool.json.JSONUtil; import com.alibaba.cloud.commons.lang.StringUtils; import com.sun.xml.internal.ws.api.ha.HaInfo; import org.apache.http.HttpResponse; 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.server.reactive.ServerHttpResponse; import org.springframework.stereotype.Component; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; import java.io.UnsupportedEncodingException; import java.util.HashMap; import java.util.Map; @Component public class MyGlobalFilter implements GlobalFilter, Ordered { @Override public Monofilter(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; } }
测试访问
token为空不能访问 返回json数据 ,token有值可以访问
修改yml文件
spring: cloud: gateway: discovery: locator: enabled: true lower-case-service-id: true
enabled 表示 根据微服务的名字进行转发