服务网关
微服务架构的诸多问题
- 客户端多次请求不同的微服务,增加客户端代码或配置的复杂性
- 认证复杂,每个服务都需要独立认证
- 存在跨域请求,在一定场景下处理相对复杂
这些问题可以借助api网关来解决
所谓的api网关,就是指系统的统一入口,它封装了应用程序的内部结构,为客户端提供统一服务,一些与业务本身功能无关的公共逻辑可以在这里实现,诸如认证、鉴权、监控、路由转发等等
gateway
spring cloud gateway是spring公司基于spring5,springboot2和project reactor等技术开发的网关,它旨在为微服务架构提供一种简单有效的统一api路由管理方式。它的目标是替代Netflix zuul,其不仅提供统一的路由方式,并且基于filter链的方法提供了网关基本的功能,例如:安全、监控和限流
优点
- 性能强劲:是第一代网关zuul的1.6倍
- 功能强大:内置了很多实用的功能,例如转发、监控、限流等
- 设计优雅:容易扩展
缺点
- 其实现依赖netty与webflux,不是传统的servlet模型,学习成本高
- 不能将其部署在tomcat、jetty等servlet容器里,只能打成jar包运行
- 需要springboot2.0及以上的版本,才支持
使用
-
导入依赖
<!--注意:此模块不能引入 starter-web--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> </dependency>
-
在配置文件中添加路由规则
spring: cloud: gateway: routes: #路由数组,(路由:就是指当请求满足什么样的条件时,转发到指定微服务) - id: #当前路由的标识,要求唯一,默认值是uuid uri: #请求最终被转发的地址 order: #路由的优先级,数字越小代表优先级越高 predicates: #断言,条件判断,返回是boolean 转发请求要满足的条件 - Path=/xxx/** #当请求路径满足path指定的规则时,此路由信息才会正常转发 filters: #过滤器,在请求请求传递过程中,对请求做一些处理 - StripPrefix=1 #在请求转发之前去掉一层路径
整合nacos
-
加入nacos依赖
<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency>
-
启动类上添加注解:@EnableDiscoveryClient
-
添加nacos配置文件
spring: nacos: discovery: server-addr: IP地址:端口号
-
修改getaway配置
spring: cloud: gateway: discovery: locator: enabled: true #让gateway可以发现nacos中的微服务 routes: - id: uri: lb://nacos中的服务名称 #lb指的是从nacos中按照名称获取微服务,并遵循负载均衡策略 order: predicates: filters:
也可以不编写routes,gateway具有默认路由
执行流程
基本概念
路由(route)是gateway中最基本的组件之一,表示一个具体的路由信息载体。主要定义了下面几个信息
- id:路由标识符,区别于其它route
- uri:路由指向的目的地uri,即客户端请求最终被转发到的微服务
- order:用于多个route之间的排序,数值越小排序越靠前,匹配优先级越高
- predicate:断言的作用是进行条件判断,只有断言都返回true,才会真正执行路由
- filter:过滤器用于修改请求和响应信息
执行流程
- 客户端向gateway发送请求
- 请求首先会被HttpWebHandlerAdapter进行提取组装成网关上下文
- 然后网关的上下文会传递到DispatcherHandler,它负责将请求分发给RoutePredicateHandlerMapping
- RoutePredicateHandlerMapping负责匹配路由信息,并根据路由断言判断路由是否可用
- 如果断言返回true,由FilteringWebHandler创建过滤器链并调用
- 请求会依次经过PreFilter->微服务->PostFilter的方法,最终返回响应
断言
predicate(断言)用于进行条件判断,只有断言都返回true,才会真正的执行路由
内置路由断言工厂
gateway包括许多内置的断言工厂,所有这些断言都与http请求的不同属性匹配
-
基于datetime类型的断言工厂
- AfterRoutePredicateFactory:接收一个日期参数,单独请求日期是否晚于指定日期
- BeforeRoutePredicateFactory:接收一个日期参数,单独请求日期是否早于指定日期
- BetweenRoutePredicateFactory:接收两个日期参数,判断请求日期是否在指定时间段内
-After=2019-12-31T23:59:59.789+08:00[Asia/Shanghai]
-
基于远程地址的断言工厂:
- RemoteAddrRoutePredicateFactory:接收一个IP地址段,判断请求主机地址是否在地址段中
-RemoteAddr=192.168.1.1/24
-
基于cookie的断言工厂:
- CookieRoutePredicateFactory:接收两个参数,cookie 名字和一个正则表达式。 判断请求 cookie是否具有给定名称且值与正则表达式匹配
-Cookie=chocolate, ch.
-
基于header的断言工厂
-
HeaderRoutePredicateFactory:接收两个参数,标题名称和正则表达式。 判断请求Header是否
具有给定名称且值与正则表达式匹配
-Header=X-Request-Id, \d+
-
-
基于host的断言工厂
- HostRoutePredicateFactory:接收一个参数,主机名模式。判断请求的Host是否满足匹配规则
-Host=**.testhost.org
-
基于Method请求方法的断言工厂
- MethodRoutePredicateFactory:接收一个参数,判断请求类型是否跟指定的类型匹配
-Method=GET
-
基于Path请求路径的断言工厂
- PathRoutePredicateFactory:接收一个参数,判断请求的URI部分是否满足路径规则
-Path=/foo/
-
基于Query请求参数的断言工厂
-
QueryRoutePredicateFactory :接收两个参数,请求param和正则表达式, 判断请求参数是否具
有给定名称且值与正则表达式匹配
-Query=baz, ba.
-
-
基于路由权重的断言工厂
- WeightRoutePredicateFactory:接收一个[组名,权重], 然后对于同一个组内的路由按照权重转发
-Weight=group3, 1
自定义路由断言
案例:指定年龄在指定范围内的人,可用访问
/**
* 自定义路由断言工厂类
* 要求:1.名字必须是 配置+RoutePredicateFactory
* 2.必须继承AbstractRoutePredicateFactory<配置类>
*/
@Component
public class AgeRoutePredicateFactory
extends AbstractRoutePredicateFactory<AgeRoutePredicateFactory.Config> {
/**
* 构造函数
*/
public AgeRoutePredicateFactory() {
super(AgeRoutePredicateFactory.Config.class);
}
/**
* 读取配置文件中参数值,给他赋值到配置类中的属性上
* @return
*/
@Override
public List<String> shortcutFieldOrder() {
//这个位置的顺序必须跟配置文件中的值顺序对应
return Arrays.asList("minAge","maxAge");
}
/**
* 断言逻辑
* @param config
* @return
*/
@Override
public Predicate<ServerWebExchange> apply(AgeRoutePredicateFactory.Config config) {
return new Predicate<ServerWebExchange>() {
@Override
public boolean test(ServerWebExchange serverWebExchange) {
//接收前台传入age参数
String ageStr = serverWebExchange.getRequest().getQueryParams().getFirst("age");
//判断是否为空
if (ageStr ==null || "".equals(ageStr)){
return false;
}
//逻辑判断
int age = Integer.parseInt(ageStr);
if (age > config.getMaxAge() || age < config.getMinAge()){
return false;
}
return true;
}
};
}
/**
* 配置类,用于接收配置文件中的对应参数
*/
@Data
public static class Config{
private int minAge;
private int maxAge;
}
}
使用
predicates:
- Age=18,60 #自定义断言,年龄18-60可用访问
过滤器
过滤器就是在请求的传递过程中,对请求和响应做一些操作
在gateway中,filter的生命周期有两个
- pre:请求由网关发往微服务时调用
- post:请求由微服务响应回网关时调用
过滤器分为:
- GatewayFilter(局部过滤器):应用到单个路由或者一个分组的路由上
- GlobalFilter(全局过滤器):应用到所有的路由上
自定义局部过滤器
案例:控制各种日志的开关
编写配置类
@Component
public class LogGatewayFilterFactory
extends AbstractGatewayFilterFactory<LogGatewayFilterFactory.Config> {
/**
* 构造
*/
public LogGatewayFilterFactory(){
super(LogGatewayFilterFactory.Config.class);
}
/**
* 读取配置文件的参数到配置类中
* @return
*/
@Override
public List<String> shortcutFieldOrder() {
return Arrays.asList("consoleLog","cacheLog");
}
/**
* 过滤器逻辑
* @param config
* @return
*/
@Override
public GatewayFilter apply(Config config) {
return new GatewayFilter() {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
if (config.isConsoleLog()){
System.out.println("consoleLog已开启");
}
if (config.isCacheLog()){
System.out.println("cacheLog已开启");
}
return chain.filter(exchange);
}
};
}
/**
* 配置类
*/
@Data
public static class Config{
private boolean consoleLog;
private boolean cacheLog;
}
}
使用
filters:
- Log=true,false
自定义全局过滤器
内置的过滤器已经可用完成大部分的功能,但是对于企业开发的一些业务功能处理,还是需要我们自己编写过滤器来实现
案例:权限校验
编写过滤器:
@Component
@Slf4j
public class AuthGlobalFilter implements GlobalFilter, Ordered {
/**
* 编写过滤器逻辑
* @param exchange
* @param chain
* @return
*/
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//获取token
String token = exchange.getRequest().getQueryParams().getFirst("token");
//认证失败
if (!"admin".equals(token)){
log.info("认证失败");
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
//放行
return chain.filter(exchange);
}
/**
* 用来标识当前过滤器的优先级
* 返回值越小,优先级越高
* @return
*/
@Override
public int getOrder() {
return 0;
}
}
全局过滤器不需要再配置文件中手动设置,即可生效
限流
从1.6.0版本开始,sentinel提供了gateway的适配模块,可用提供两种资源维度的限流
- route维度:即在spring配置文件中配置的路由条目,资源名对应的routeId
- 自定义api维度:用户可用利用sentinel提供的api来自定义一些分组
route维度
- 导入依赖
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-spring-cloud-gateway-adapter</artifactId>
</dependency>
- 编写配置类
基于Sentinel 的Gateway限流是通过其提供的Filter来完成的,使用时只需注入对应的
SentinelGatewayFilter实例以及 SentinelGatewayBlockExceptionHandler 实例即可
@Configuration
public class GatewayConfig {
private final List<ViewResolver> viewResolvers;
private final ServerCodecConfigurer serverCodecConfigurer;
public GatewayConfig(ObjectProvider<List<ViewResolver>> viewResolversProvider,
ServerCodecConfigurer serverCodecConfigurer) {
this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList);
this.serverCodecConfigurer = serverCodecConfigurer;
}
// 初始化一个限流的过滤器
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public GlobalFilter sentinelGatewayFilter() {
return new SentinelGatewayFilter();
}
// 配置初始化的限流参数
@PostConstruct
public void initGatewayRules() {
Set<GatewayFlowRule> rules = new HashSet<>();
rules.add(
new GatewayFlowRule("product_route") //资源名称,对应路由id
.setCount(1) // 限流阈值
.setIntervalSec(1)); // 统计时间窗口,单位是秒,默认是 1 秒
GatewayRuleManager.loadRules(rules);
}
// 配置限流的异常处理器
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SentinelGatewayBlockExceptionHandler sentinelGatewayBlockExceptionHandler() {
return new SentinelGatewayBlockExceptionHandler(viewResolvers, serverCodecConfigurer);
}
// 自定义限流异常页面
@PostConstruct
public void initBlockHandlers() {
BlockRequestHandler blockRequestHandler = new BlockRequestHandler() {
public Mono<ServerResponse> handleRequest(ServerWebExchange serverWebExchange, Throwable throwable) {
Map map = new HashMap<>();
map.put("code", 0);
map.put("message", "接口被限流了");
return ServerResponse.status(HttpStatus.OK).
contentType(MediaType.APPLICATION_JSON_UTF8).
body(BodyInserters.fromObject(map));
}
};
GatewayCallbackManager.setBlockHandler(blockRequestHandler);
}
}
自定义api维度
配置类
@Configuration
public class GatewayConfig {
private final List<ViewResolver> viewResolvers;
private final ServerCodecConfigurer serverCodecConfigurer;
public GatewayConfig(ObjectProvider<List<ViewResolver>> viewResolversProvider,
ServerCodecConfigurer serverCodecConfigurer) {
this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList);
this.serverCodecConfigurer = serverCodecConfigurer;
}
// 初始化一个限流的过滤器
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public GlobalFilter sentinelGatewayFilter() {
return new SentinelGatewayFilter();
}
// 配置初始化的限流参数
@PostConstruct
public void initGatewayRules() {
Set<GatewayFlowRule> rules = new HashSet<>();
rules.add(new GatewayFlowRule("product_api1") //分组名
.setCount(1) // 限流阈值
.setIntervalSec(1)); // 统计时间窗口,单位是秒,默认是 1 秒
rules.add(new GatewayFlowRule("product_api2").setCount(1).setIntervalSec(1));
GatewayRuleManager.loadRules(rules);
}
// 配置限流的异常处理器
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SentinelGatewayBlockExceptionHandler sentinelGatewayBlockExceptionHandler() {
return new SentinelGatewayBlockExceptionHandler(viewResolvers, serverCodecConfigurer);
}
// 自定义限流异常页面
@PostConstruct
public void initBlockHandlers() {
BlockRequestHandler blockRequestHandler = new BlockRequestHandler() {
public Mono<ServerResponse> handleRequest(ServerWebExchange serverWebExchange, Throwable throwable) {
Map map = new HashMap<>();
map.put("code", 0);
map.put("message", "接口被限流了");
return ServerResponse.status(HttpStatus.OK).
contentType(MediaType.APPLICATION_JSON_UTF8).
body(BodyInserters.fromObject(map));
}
};
GatewayCallbackManager.setBlockHandler(blockRequestHandler);
}
//自定义API分组
@PostConstruct
private void initCustomizedApis() {
Set<ApiDefinition> definitions = new HashSet<>();
//定义分组1
ApiDefinition api1 = new ApiDefinition("product_api1")//分组名
//设置规则,可用设置多个规则
.setPredicateItems(new HashSet<ApiPredicateItem>() {{
//设置一个规则 以/product-serv/product/api1 开头的请求
add(new ApiPathPredicateItem().setPattern("/product-serv/product/api1/**").
setMatchStrategy(SentinelGatewayConstants.URL_MATCH_STRATEGY_PREFIX));
}});
//定义分组2
ApiDefinition api2 = new ApiDefinition("product_api2")
.setPredicateItems(new HashSet<ApiPredicateItem>() {{
// 以/product-serv/product/api2/demo1 完成的url路径匹配
add(new ApiPathPredicateItem().setPattern("/product-serv/product/api2/demo1"));
}});
definitions.add(api1);
definitions.add(api2);
GatewayApiDefinitionManager.loadApiDefinitions(definitions);
}
}