Gateway网关是我们服务的守门神,所有前端访问微服务的统一入口。Spring Cloud Gateway 是 Spring Cloud 的一个全新项目,该项目是基于 Spring 5.0,Spring Boot 2.0 和 Project Reactor 等响应式编程和事件流技术开发的网关,它旨在为微服务架构提供一种简单有效的统一的 API 路由管理方式。
在SpringCloudGateway之前,SpringCloud并不自己开发网关,而是使用Netflix公司的Zuul框架,不过zuul2.0更新迭代缓慢,难以满足Spring的更新需求。于是就有了SpringCloudGateway。其不仅提供统一的路由方式,并且基于 Filter 链的方式提供了网关基本的功能,例如:安全,监控/指标,和限流。
核心功能特性
路由:gateway加入后,一切请求都必须先经过gateway,因此gateway就必须根据某种规则,把请求转发到某个微服务,这个过程叫做路由。
权限控制:请求经过路由时,我们可以判断请求者是否有请求资格,如果没有则进行拦截。
限流:当请求流量过高时,在网关中按照下流的微服务能够接受的速度来放行请求,避免服务压力过大。
**网关的实质:**网关中的每一个功能都是由过滤器链完成的。
优点如下:
首先,我们来研究下Gateway的路由功能,基本步骤如下:
添加gateway依赖:
<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>cloud-demoartifactId>
<groupId>com.itheima.sh.demogroupId>
<version>1.0-SNAPSHOTversion>
parent>
<modelVersion>4.0.0modelVersion>
<artifactId>gateway-serverartifactId>
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-gatewayartifactId>
dependency>
dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
plugin>
plugins>
build>
project>
package com.itheima.sh;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
}
server:
port: 10010 #服务端口
spring:
application:
name: gateway-server #指定服务名
我们需要用网关来代理user-service服务,先看一下控制面板中的服务状态:
映射规则:
server:
port: 10010 #服务端口
spring:
application:
name: gateway-server #指定服务名
cloud:
gateway:
routes:
- id: user-service # 当前路由的唯一标识
uri: http://127.0.0.1:8081 # 路由的目标微服务地址
predicates: # 断言 判断 前端浏览器发送url和当前path是否匹配
- Path=/user/** # 按照路径匹配的规则
# - id: order-service # 当前路由的唯一标识
# uri: http://127.0.0.1:9081 # 路由的目标微服务地址
# predicates: # 断言 判断 前端浏览器发送url和当前path是否匹配
# - Path=/order/** # 按照路径匹配的规则
我们将符合Path
规则的一切请求,都代理到 uri
参数指定的地址
本例中,我们将 /user/**
开头的请求,代理到http://127.0.0.1:8081
我们访问:http://localhost:8081/user/itcast,可以正常请求直达用户微服务
当我们访问:http://localhost:10010/user/itcast,符合/user/**
的规则,因此请求被代理到http://localhost:8081/user/itcast
在刚才的路由规则中,我们把路径对应的服务地址写死了!如果同一服务有多个实例的话,这样做显然就不合理了。
我们应该根据服务的名称,去Eureka注册中心查找 服务对应的所有实例列表,并且对服务列表进行负载均衡才对!
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka
因为已经有了Eureka客户端,我们可以从Eureka获取服务的地址信息,因此映射时无需指定IP地址,而是通过服务名称来访问,而且Zuul已经集成了Ribbon的负载均衡功能。
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka
spring:
cloud:
gateway:
routes:
- id: user-service # 当前路由的唯一标识
uri: lb://user-service # 路由的目标微服务,lb:代表负载均衡,user-service:代表服务id
predicates: # 断言
- Path=/user/** # 按照路径匹配的规则
这里修改了uri的路由方式:
再次启动,这次gateway进行代理时,会利用Ribbon进行负载均衡访问,日志中可以看到使用了负载均衡器:
Gateway中支持各种路由方式,看官方的目录:
主要包括:
有兴趣的可以自己查看。
GatewayFilter Factories是Gateway中的局部过滤器工厂,作用于某个特定路由,允许以某种方式修改传入的HTTP请求或返回的HTTP响应。
下面我们以几个过滤器的配置为示例:
示例:AddRequestHeader GatewayFilter Factory,可以在请求中添加请求头,配置如下:
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka
spring:
cloud:
gateway:
routes:
- id: user-service # 当前路由的唯一标识
uri: lb://user-service # 路由的目标微服务,lb:代表负载均衡,user-service:代表服务id
predicates: # 断言
- Path=/user/** # 按照路径匹配的规则
filters: # 过滤项
- AddRequestHeader=name,itheima
其中:
filters
:就是当前路由规则的所有过滤器配置AddRequestHeader
是添加一个头信息重启后,再user-service的内部断点,查看请求头:
网关做请求路由转发,如果被调用的请求阻塞,需要通过Hystrix来做线程隔离和熔断,防止出现故障。
1)引入Hystrix的依赖
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-hystrixartifactId>
dependency>
2)定义降级的处理函数
定义一个controller,用来编写失败的处理逻辑:
package com.itheima.sh.gateway.web;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
@RestController
public class FallbackController {
@RequestMapping(value = "/fallbackTest")
public Map<String, String> fallBackController() {
Map<String, String> response = new HashMap<>();
response.put("code", "502");
response.put("msg", "服务超时");
return response;
}
}
3)定义降级处理规则
可以通过default-filter来配置,会作用于所有的路由规则。
server:
port: 10010 #服务端口
spring:
application:
name: gateway-server #指定服务名
cloud:
gateway:
routes:
- id: user-service # 当前路由的唯一标识
uri: lb://user-service # 路由的目标微服务,lb:代表负载均衡,user-service:代表服务id
# uri: http://127.0.0.1:8081 # 路由的目标微服务地址
predicates: # 断言
- Path=/user/** # 按照路径匹配的规则
filters: # 过滤项
- AddRequestHeader=name,itheima
default-filters: # 默认过滤项
- name: Hystrix # 指定过滤工厂名称
args: # 指定过滤的参数
name: fallbackcmd # hystrix的指令名
fallbackUri: forward:/fallbackTest # 失败后的跳转路径
hystrix:
command:
default:
execution.isolation.thread.timeoutInMilliseconds: 1000 # 失败超时时长
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka
4)启动测试
再UserController的业务中打断点,让请求超时,查看页面的效果
我们之前用/user/**
这样的映射路径代表user-service
这个服务。因此请求user-service
服务的一切路径要以/user/**
开头
比如,访问:localhost:10010/user/2
会被代理到:localhost:8081/user/2
现在,我们在user-service
中的com.itheima.sh.user.web
中定义一个新的接口:
package com.itheima.sh.user.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("address")
public class AddressController {
@GetMapping("me")
public String myAddress() {
return "上海市浦东新区航头镇航头路18号传智播客";
}
}
这个接口的路径是/address/me
,并不是以/user/
开头。当访问:localhost:10010/address/me
时,并不符合映射路径,因此会得到404.
无论是 /user/**
还是/address/**
都是user-service
中的一个controller路径,都不能作为网关到user-service的映射路径。
因此我们需要定义一个额外的映射路径,例如:/user-service
,配置如下:
spring:
cloud:
gateway:
routes:
- id: user-service # 当前路由的唯一标识
uri: lb://user-service # 路由的目标微服务,user-service:代表服务id
predicates: # 断言
- Path=/user-api/** # 按照路径匹配的规则
那么问题来了:
当我们访问:http://localhost:10010/user-api/user/itcast时,映射路径/user-service
指向用户服务,会被代理到:http://localhost:8081/user-api/user/itcast.
当我们访问:http://localhost:10010/user-api/address/me时,映射路径/user-service
指向用户服务,会被代理到:http://localhost:8081/user-api/address/me
这个/user-api
是gateway中的映射路径,不应该被代理到微服务,怎办吧?
解决思路很简单,当我们访问http://localhost:10010/user-api/user/1时,网关利用/user-api
这个映射路径匹配到了用户微服务,请求代理时,只要把/user-api
这个映射路径去除不就可以了吗。
恰好有一个过滤器:StripPrefixFilterFactory可以满足我们的需求。
https://cloud.spring.io/spring-cloud-static/spring-cloud-gateway/2.2.3.RELEASE/reference/html/#the-stripprefix-gatewayfilter-factory
我们修改刚才的路由配置:
filters: # 过滤项
- AddRequestHeader=name,itheima
- StripPrefix=1
此时,网关做路由的代理时,就不会把/user-service作为目标请求路径的一部分了。
也就是说,我们访问:http://localhost:10010/user-api/user/itcast,会代理到:http://localhost:8081/user/itcast
我们访问:http://localhost:10010/user-service/address/me,会代理到:http://localhost:8081/address/me
全局过滤器Global Filter 与局部的GatewayFilter会在运行时合并到一个过滤器链中,并且根据org.springframework.core.Ordered
来排序后执行,顺序可以通过getOrder()
方法或者@Order
注解来指定。
来看看全局过滤器的顶级接口:
public interface GlobalFilter {
/**
* Process the Web request and (optionally) delegate to the next {@code WebFilter}
* through the given {@link GatewayFilterChain}.
* @param exchange the current server exchange
* @param chain provides a way to delegate to the next filter
* @return {@code Mono} to indicate when request processing is complete
*/
Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain);
}
实现接口,就要实现其中的filter方法,在方法内部完成过滤逻辑,其中的参数包括:
ServerWebExchange:一个类似于Context的域对象,封装了Request、Response等服务相关的属性
GatewayFilterChain:过滤器链,用于放行请求到下一个过滤器
通过添加@Order注解,可以控制过滤器的优先级,从而决定了过滤器的执行顺序。
另外,一个过滤器的执行包括"pre"
和"post"
两个过程。在GlobalFilter.filter()方法中编写的逻辑属于pre阶段,在使用GatewayFilterChain.filter().then()的阶段,属于Post阶段。
优先级最高的过滤器,会在pre过程的第一个执行,在post过程的最后一个执行,如图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kaHukSDY-1607004323827)(assets/1568470116218.png)]
我们可以在pre阶段做很多事情,诸如:
定义过滤器只需要实现GlobalFilter即可,不过我们有多种方式来完成:
方式一:定义过滤器类,实现接口
方式二:通过@Configuration类结合lambda表达式
现在,我们通过自定义过滤器,模拟一个登录校验功能,逻辑非常简单:
代码如下
package com.itheima.sh.gateway.filters;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
/**
* @Description:
* @Version: V1.0
*/
@Component
@Order(-1) //越小越优先
public class LoginFilter implements GlobalFilter { //, Ordered
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 1 得到 request 对象
ServerHttpRequest request = exchange.getRequest();
// 2 从request获取请求参数 access-token
String token = request.getQueryParams().getFirst("access-token");
// 3 判断access-token值是否为admin
if (!"admin".equals(token)) { // 拦截
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
// 放行
return chain.filter(exchange);
}
// 越小越优先
// @Override
// public int getOrder() {
// return -1;
// }
}
下面我们通过lambda表达式来定义过滤器:
package com.itheima.sh.gateway.filters;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import reactor.core.publisher.Mono;
@Slf4j
@Configuration
public class FilterConfiguration {
@Bean
@Order(-2)
public GlobalFilter globalFilter1(){
return ((exchange, chain) -> {
log.info("过滤器1的pre阶段!");
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
log.info("过滤器1的post阶段!");
}));
});
}
@Bean
@Order(-1)
public GlobalFilter globalFilter2(){
return ((exchange, chain) -> {
log.info("过滤器2的pre阶段!");
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
log.info("过滤器2的post阶段!");
}));
});
}
@Bean
@Order(0)
public GlobalFilter globalFilter3(){
return ((exchange, chain) -> {
log.info("过滤器3的pre阶段!");
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
log.info("过滤器3的post阶段!");
}));
});
}
}
网关除了请求路由、身份验证,还有一个非常重要的作用:请求限流。当系统面对高并发请求时,为了减少对业务处理服务的压力,需要在网关中对请求限流,按照一定的速率放行请求。
常见的限流算法包括:
算法介绍:
https://blog.csdn.net/u012441595/article/details/102483501
SpringGateway中采用的是令牌桶算法,令牌桶算法原理:
SpringCloudGateway是采用令牌桶算法,其令牌相关信息记录在redis中,因此我们需要安装redis,并引入Redis相关依赖。
1) 引入redis有关依赖:
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redis-reactiveartifactId>
dependency>
注意:这里不是普通的redis依赖,而是响应式的Redis依赖,因为SpringGateway是基于WebFlux的响应式项目。
在application.yml中配置Redis地址:
spring:
redis:
host: 192.168.200.129
2) 配置过滤条件key:
Gateway会在Redis中记录令牌相关信息,我们可以自己定义令牌桶的规则,例如:
Redis中的一个Key和Value对就是一个令牌桶。因此Key的生成规则就是桶的定义规则。SpringCloudGateway中key的生成规则定义在KeyResolver
接口中:
public interface KeyResolver {
Mono<String> resolve(ServerWebExchange exchange);
}
这个接口中的方法返回值就是给令牌桶生成的key。API说明:
比如上面的三种令牌桶规则,生成key的方式如下:
给不同的请求URI路径设置不同令牌桶,示例代码:
return Mono.just(exchange.getRequest().getURI().getPath());// 获取请求URI
给不同的登录用户设置不同令牌桶
return exchange.getPrincipal().map(Principal::getName);// 获取用户
给不同的请求IP地址设置不同令牌桶
return Mono.just(exchange.getRequest().getRemoteAddress().getHostName());// 获取请求者IP
这里我们选择最后一种,使用IP地址的令牌桶key。
我们在com.itheima.sh.ratelimit
中定义一个类,配置一个KeyResolver的Bean实例:
package com.itheima.sh.ratelimit;
import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@Component
public class IpKeyResolver implements KeyResolver {
@Override
public Mono<String> resolve(ServerWebExchange exchange) {
return Mono.just(exchange.getRequest().getRemoteAddress().getHostName());
}
}
3) 配置桶参数:
另外,令牌桶的参数需要通过yaml文件来配置,参数有2个:
replenishRate
:每秒钟生成令牌的速率,基本上就是每秒钟允许的最大请求数量
burstCapacity
:令牌桶的容量,就是令牌桶中存放的最大的令牌的数量
完整配置如下:
server:
port: 10010 #服务端口
spring:
application:
name: gateway-server #指定服务名
cloud:
gateway:
routes:
- id: user-service # 当前路由的唯一标识
uri: lb://user-service # 路由的目标微服务,lb:代表负载均衡,user-service:代表服务id
# uri: http://127.0.0.1:8081 # 路由的目标微服务地址
predicates: # 断言
- Path=/user-api/** # 按照路径匹配的规则
filters: # 过滤项
- AddRequestHeader=name,itheima
- StripPrefix=1
default-filters: # 默认过滤项
- name: Hystrix # 指定过滤工厂名称(可以是任意过滤工厂类型)
args: # 指定过滤的参数
name: fallbackcmd # hystrix的指令名
fallbackUri: forward:/fallbackTest # 失败后的跳转路径
- name: RequestRateLimiter #请求数限流 名字不能随便写
args:
key-resolver: "#{@ipKeyResolver}" # 指定一个key生成器
redis-rate-limiter.replenishRate: 2 # 生成令牌的速率
redis-rate-limiter.burstCapacity: 2 # 桶的容量
redis:
host: 192.168.200.129
hystrix:
command:
default:
execution.isolation.thread.timeoutInMilliseconds: 1000 # 失败超时时长
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka
这里配置了一个过滤器:RequestRateLimiter,并设置了三个参数:
key-resolver
:"#{@ipKeyResolver}"
是SpEL表达式,写法是#{@bean的名称}
,ipKeyResolver就是我们定义的Bean名称
redis-rate-limiter.replenishRate
:每秒钟生成令牌的速率
redis-rate-limiter.burstCapacity
:令牌桶的容量
这样的限流配置可以达成的效果:
4)测试:
我们快速在浏览器多次访问http://localhost:10010/user-api/user/itcast,就会得到一个错误:
429:代表请求次数过多,触发限流了。
跨域:浏览器的同源策略。网络通信的三要素:IP、PORT、协议,只要是三要素中的一个不相同,则会触发浏览器同源策略,出现跨域现象。
例如:
前端:http://192.168.1.2:8080
后端:http://192.168.1.2:8080/user
不会出现跨域
前端:http://192.168.1.2:8081
后端:http://192.168.1.2:8080/user
会出现跨域,端口不同
前端:http://192.168.100.2:8080
后端:http://192.168.1.2:8080/user
会出现跨域,IP地址不同
前端:https://192.168.1.2:8080 源
后端:http://192.168.1.2:8080/user 目标
会出现跨域,协议不同
有时候,我们需要对所有微服务跨域请求进行处理,则可以在gateway中进行跨域支持。修改application.yml,添加如下代码:
spring:
cloud:
gateway:
globalcors:
cors-configurations:
'[/**]': # 匹配所有请求
allowedOrigins: "*" #跨域处理 允许所有的域
allowedMethods: # 支持的方法
- GET
- POST
- PUT
- DELETE