一. 基础
- 什么是网关: 分布式环境下客户端请求统一通过网关服务进行接收,通过网关服务将请求转发到指定的服务器上有点像nginx,拦截到请求后可以实现权限控制,负载均衡,日志管理,接口调用监控等功能
- nginx 适用于服务端接收请求负载,将请求发送到网关服务,通过网关服务对接收到的请求进行一些拦截处理,例如黑名单,白名单等
- 现在主流 网关框架 Zuul 和 SpringCloud 推出的 GateWay
Zuul 与 Gateway 区别
- Gateway
- Gateway 底层通过 Netty 实现
- Gateway 基于异步非阻塞模型性能较高
- 动态路由,能够匹配任何请求属性
- 可以对路由指定 Predicate 断言,和 Filter 过滤器
- 集成Hystrix断路器功能
- 集成 Spring Cloud 服务发现功能
- 支持请求限流功能,支持路径重写功能
- Zuul
- Zuul1 底层通过 Ribbo 实现,基于 Servlet2.5使用阻塞式的,不支持唱两句,
- Zuul2 向基于 Netty实现非阻塞,并支持长连接
- 在 SpringCloud Finchley 版推荐使用 Zuul
二. Zuul1 网关
- Zuul 网关底层通过 Ribbon 和传统的 Servlet IO处理模型与 SpringCloud 进行整合,该Servlet是由servlet container进行管理,当container启动时会通过init()方法初始化Servlet,在接收请求时在线程池中获取一个空闲线程,调用service()执行请求,当container关闭是调用 destory() 销毁servlet
- 由于是通过线程绑定的方式,在并发较高的情况下线程数量也要增加切换等,有性能问题
三. Zuul 核心过滤器
- Zuul 内部集成了各种过滤器,在接收请求时按照顺序执行过滤器,对请求进行拦截,实现各种判断,看大神的
- 过滤器分为:
“pre”: 请求路由之前执行
“route”: 请求路由之后执行
“post”: 在"routing" 和 error 过滤器之后执行
pre过滤器
- ServletDetectionFilter:pre类型,执行顺序为 -3,最先被执行的过滤器。该过滤器总是会被执行,主要用来检测当前请求是通过Spring的DispatcherServlet处理运行的,还是通过ZuulServlet来处理运行的。它的检测结果会以布尔类型保存在当前请求上下文的isDispatcherServletRequest参数中,这样后续的过滤器中,我们就可以通过RequestUtils.isDispatcherServletRequest()和RequestUtils.isZuulServletRequest()方法来判断请求处理的源头,以实现后续不同的处理机制.一般情况下被 Zuul 网关服务接收到的请求由 ZuulServlet 处理,主要用来应对大文件上传的情况,另外对于ZuulServlet的访问路径/zuul/*,我们可以通过zuul.servletPath参数进行修改。
- Servlet30WrapperFilter:pre类型,执行顺序为 -2,第二个执行的过滤器,将原始的HttpServletRequest包装成Servlet30RequestWrapper对象
- FormBodyWrapperFilter:pre类型,执行顺序为-1,第三个执行的过滤器。该过滤器仅对两类请求生效,第一类是Context-Type为application/x-www-form-urlencoded的请求,第二类是Context-Type为multipart/form-data并且是由String的DispatcherServlet处理的请求(用到了ServletDetectionFilter的处理结果)。而该过滤器的主要目的是将符合要求的请求体包装成FormBodyRequestWrapper对象
- DebugFilter:pre类型,执行顺序为1,第四个执行的过滤器,该过滤器会根据配置参数zuul.debug.request和请求中的debug参数来决定是否执行过滤器中的操作。而它的具体操作内容是将当前请求上下文中的debugRouting和debugRequest参数设置为true。由于在同一个请求的不同生命周期都可以访问到这二个值,所以我们在后续的各个过滤器中可以利用这二个值来定义一些debug信息,这样当线上环境出现问题的时候,可以通过参数的方式来激活这些debug信息以帮助分析问题,另外,对于请求参数中的debug参数,我们可以通过zuul.debug.parameter来进行自定义
- PreDecorationFilter:是pre阶段最后被执行的过滤器执行顺序是5,该过滤器会判断当前请求上下文中是否存在forward.do和serviceId参数,如果都不存在,那么它就会执行具体过滤器的操作(如果有一个存在的话,说明当前请求已经被处理过了,因为这二个信息就是根据当前请求的路由信息加载进来的)。而当它的具体操作内容就是为当前请求做一些预处理,比如说,进行路由规则的匹配,在请求上下文中设置该请求的基本信息以及将路由匹配结果等一些设置信息等,这些信息将是后续过滤器进行处理的重要依据,我们可以通过RequestContext.getCurrentContext()来访问这些信息。另外,我们还可以在该实现中找到对HTTP头请求进行处理的逻辑,其中包含了一些耳熟能详的头域,比如X-Forwarded-Host,X-Forwarded-Port。另外,对于这些头域是通过zuul.addProxyHeaders参数进行控制的,而这个参数默认值是true,所以zuul在请求跳转时默认会为请求增加X-Forwarded-*头域,包括X-Forwarded-Host,X-Forwarded-Port,X-Forwarded-For,X-Forwarded-Prefix,X-Forwarded-Proto。也可以通过设置zuul.addProxyHeaders=false关闭对这些头域的添加动作。
route过滤器
- RibbonRoutingFilter: route类型,是route阶段的第一个执行的过滤器,执行顺序为10。该过滤器只对请求上下文中存在serviceId参数的请求进行处理,即只对通过serviceId配置路由规则的请求生效。而该过滤器的执行逻辑就是面向服务路由的核心,它通过使用ribbon和hystrix来向服务实例发起请求,获取服务,并将服务实例的请求结果返回
- SimpleHostRoutingFilter:它的执行顺序为100,是route阶段的第二个执行的过滤器。该过滤器只对请求上下文存在routeHost参数的请求进行处理,即只对通过url配置路由规则的请求生效。而该过滤器的执行逻辑就是直接向routeHost参数的物理地址发起请求,从源码中我们可以知道该请求是直接通过httpclient包实现的,而没有使用Hystrix命令进行包装,所以这类请求并没有线程隔离和断路器的保护
- SendForwardFilter:它的执行顺序是500,是route阶段第三个执行的过滤器。该过滤器只对请求上下文中存在的forward.do参数进行处理请求,即用来处理路由规则中的forward本地跳转装配。
post过滤器
- SendErrorFilter:它的执行顺序是0,是post阶段的第一个执行的过滤器。该过滤器仅在请求上下文中包含error.status_code参数(由之前执行的过滤器设置的错误编码)并且还没有被该过滤器处理过的时候执行。而该过滤器的具体逻辑就是利用上下文中的错误信息来组成一个forward到api网关/error错误端点的请求来产生错误响应
- SendResponseFilter:它的执行顺序为1000,是post阶段最后执行的过滤器,该过滤器会检查请求上下文中是否包含请求响应相关的头信息,响应数据流或是响应体,只有在包含它们其中一个的时候执行处理逻辑。而该过滤器的处理逻辑就是利用上下文的响应信息来组织需要发送回客户端的响应内容。
四. 搭建 Zuul 网关服务,通过 Zuul 间接访问微服务接口
1. 步骤
- 需求解释: 现有 微服务接口,需要通过 Zuul 网关转发访问
- 创建微服务项目提供服务接口,以指定名称注册到服务注册中心
- 创建 Zuul 网关服务,并将该服务注册到注册中心
- Zuul 网关服务中配置代理转发的访问路径,配置转发到的目标服务地址(使用注册中心配置目标服务名称即可,如果不使用则需要配置指定的目标服务调用地址)
2. 创建目标服务(真实的提供服务接口的目标服务)
- pom 中添加 Eureka-client 依赖
<!--eureka client-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
- yml 配置文件,指定当前服务端口号,服务名称,以指定名称将服务注册到 Eureka 注册中心
server:
port: 8001 #当前服务端口号
spring:
application:
name: cloud-payment-service #当前服务名称
datasource:
type: com.alibaba.druid.pool.DruidDataSource #当前数据源操作类型
driver-class-name: com.mysql.cj.jdbc.Driver #mysql驱动包
url: jdbc:mysql://localhost:3306/test01?serverTimezone=GMT%2B8 #连接的库
username: root
password:
#=================eureka相关配置======================
eureka:
client:
register-with-eureka: true #true 表示将当前服务注册到eureka注册中心
#true 表示是否在注册中心抓取已有的注册信息,集群环境时必须为true,配合ribbon进行负载
fetchRegistry: true
service-url:
#eureka 注册中心访问连接,集群环境多个注册地址
defaultZone: http://127.0.0.1:7001/eureka,http://127.0.0.1:7002/eureka
instance:
instance-id: payment8001 #配置当前服务向eureka注册中心注册时显示的服务器主机名称
prefer-ip-address: true #配置在开发人员直接访问eureka服务器时显示当前eureka上注册的服务的ip
lease-renewal-interval-in-seconds: 1 #指定定时向eureka注册中心发送代表当前服务心跳包的时间默认30秒
lease-expiration-duration-in-seconds: 2 # Eureka 接收到当前服务最后一次发送代表正常心跳包的等待时间,默认90秒超过表示不存活
#=================eureka相关配置end======================
mybatis:
mapperLocations: classpath:mapper/*.xml #扫描mappper.xml */
type-aliases-package: com.test.dao #扫描对应mapper.xml 接口
- 提供服务接口
@RestController
public class PaymentController {
@Autowired
private PaymentServerApi paymentServerApi;
@GetMapping(value = "/getRun")
public JsonResult getRun() {
System.out.println("执行查询所有数据8001");
return JsonResult.success();
}
}
- 目标服务启动类
@SpringBootApplication
@EnableEurekaClient //表示当前 PaymentMain8001 服务注册到Eureka中
public class PaymentMain8001 {
public static void main(String[] args) {
SpringApplication.run(PaymentMain8001.class,args);
}
}
3. 创建 Zuul 网关服务
- pom 文件中添加 Zuul 依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--Zuul 依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
<!--eureka client-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
- yml 文件配置当前网关服务名称,服务端口号,配置服务拦截请求路径,配置转发的真实服务地址
server:
port: 9527
spring:
application:
name: cloud-zuul
zuul:
host:
# 连接时间semaphores
connect-timeout-millis: 3000
# 每个router最大连接数
max-per-route-connections: 100
# 最大连接数
max-total-connections: 1000
# socket超时时间
socket-timeout-millis: 3000
#忽略路径或指定服务,*表示忽略所有,只通过下面配置的路由进行访问,多个用逗号隔开
ignored-services: *
routes:
api-a:
#假设请求当前网关服务"http://网关服务ip:网关服务端口号/此处配置拦截的路径/目标服务接口路径"
path: /myZuul-payment/** #当前网关服务拦截路径 */
#会自动转发到目标服务,通过该名称在注册中心获取调用地址列表,本地服务选择指定的地址进行调用
#实际会转发"http://cloud-payment-service/目标服务接口路径"
serviceId: cloud-payment-service #目标服务在注册中心注册的名称
#第二个
#api-b:
#path: /api-order/** #*/
#serviceId: cloud-order-service
#将当前 Gateway 服务注册到 Eureka注册中心
eureka:
client:
register-with-eureka: true #true 表示将当前服务注册到eureka注册中心
#true 表示是否在注册中心抓取已有的注册信息,集群环境时必须为true,配合ribbon进行负载
fetchRegistry: true
service-url:
#eureka 注册中心访问连接,集群环境多个注册地址
defaultZone: http://127.0.0.1:7001/eureka,http://127.0.0.1:7002/eureka
instance:
instance-id: zuul9527 #配置当前服务向eureka注册时显示id为指定的服务器主机名称
prefer-ip-address: true #配置在开发人员直接访问eureka服务器时显示当前eureka上注册的服务的ip
lease-renewal-interval-in-seconds: 1 #指定定时向eureka注册中心发送代表当前服务心跳包的时间默认30秒
lease-expiration-duration-in-seconds: 2 # Eureka 接收到当前服务最后一次发送代表正常心跳包的等待时间,默认90秒超过表示不存活
hostname: cloud-zuul-service ##配置当前服务向eureka注册时显示端口号
#=================eureka相关配置end======================
- Zuul 网关服务启动类添加 @EnableZuulProxy 开启 Zuul 网关服务代理转发
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
@SpringBootApplication
@EnableEurekaClient
@EnableZuulProxy
public class ZuulMain9527 {
public static void main(String[] args) {
SpringApplication.run(ZuulMain9527.class,args);;
}
}
4. 最终实现功能
- 以前通过 “http://目标服务ip:目标服务端口号/目标服务接口路径” 直接访问目标服务
- 现在 “http://网关服务ip:网关服务端口号/网关服务拦截的路径/目标服务接口路径” 网关服务间接访问目标服务
- 可以在网关服务中创建过滤器,对请求进行拦截,作出指定的操作,例如统一验证用户是否登录,黑白名单,设置允许跨域等
5. 在 Zuul 网关服务中创建自定义过滤器
- 在 Zuul 网关服务中创建自定义过滤器,当访问网关时拦截用户请求,做出指定判断,成功后放行,否则拦截拒绝访问等
- 自定义过滤器要继承 ZuulFilter 父类,重写父类中的抽象方法
filterType() : 设置当前过滤器类型
shouldFilter() : 设置当前过滤器是否生效
filterOrder() : 设置当前过滤器执行优先级
run() : 当前过滤器实际拦截或放行的逻辑方法
- 将自定义过滤器注入到 Spring 容器中
- 代码示例
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Component
public class MyZuulFilter extends ZuulFilter {
//1.设置当前过滤器的过滤类型
@Override
public String filterType() {
//"pre": 请求路由之前执行
//"route": 请求路由之后执行
//"post": 在"routing" 和 error 过滤器之后执行
return "pre";
}
//2.根据返回值设置当前过滤器执行的优先级,参数越小优先级越高
@Override
public int filterOrder() {
return 0;
}
//3.判断过滤是否生效
@Override
public boolean shouldFilter() {
//可以在该方法中通过 RequestContext 获取请求的上下文
//通过获取到的数据判断设置当前过滤器是否生效
HttpServletRequest request = RequestContext.getCurrentContext().getRequest();
HttpServletResponse response = RequestContext.getCurrentContext().getResponse();
return true;
}
//4.过滤方法,当向 RequestContext 中存储一个 "sendZuulResponse" 变量,值为false
//时代表当前请求被拦截,解决访问,当没有存储该变量为false时return null 拦截放行
@Override
public Object run() throws ZuulException {
//1.通过 RequestContext 获取上下文
RequestContext currentContext = RequestContext.getCurrentContext();
//2.通过上下文获取 Request
HttpServletRequest request = currentContext.getRequest();
//3.在 Request 中获取请求数据
String userToken = request.getParameter("userToken");
if (null == userToken) {
//4.注意点,当上下文 RequestContext 调用 setSendZuulResponse() 方法,
//设置为 false 时, 后续 return null,当前请求则不会继续向下执行,代表拒绝访问
//查看该方法内部是向 RequestContext 中存储了一个"sendZuulResponse"变量值为false
currentContext.setSendZuulResponse(false);
currentContext.setResponseStatusCode(401);
currentContext.setResponseBody("userToken is null");
return null;
}
//5.RequestContext 中的 SendZuulResponse 不为 false
//return null,代表当前过滤器放行,继续执行下一个过滤器
return null;
}
}
- 最终效果,当用户访问 Zuul 网关服务时,会自动执行该自定义过滤器中的 run() 方法,获取请求中是否携带了名为 “userToken” 的参数,如果有则拦截放行,执行下一个过滤器… 如果没有则拒绝访问想要前台"401",状态码与 “userToken is null” 提示信息