在现实中,我们每个小区为了安全,防止非法入侵,都会设置保安,安保人员的职责是负责对进出人员进行检查,监控小区环境,平时一些快递,邮件也通过安保室进行发放,那么在微服务中,是否也有这样的角色呢?答案是有的,这边是服务网关-Zuul。
一、为什么要使用Zuul?
试想一下如果我们有很多的微服务,他们都需要登录之后才能访问,那么我需要在每个微服务都去做一套登录检查逻辑,这样是不是会存在大量重复的代码和工作量,我们希望的是把登录检查这种公共的逻辑进行统一的抽取,只需要做一套检查逻辑即可,而zuul就可以用来干这类事情,我们可以把zuul看做是微服务的大门,所有的请求都需要通过zuul将请求分发到其他微服务,根据这一特性我们就可以在zuul做统一的登录检查,下游的微服务不再处理登录检查逻辑。
二、什么是Zuul?
简而言之,API网关,为微服务架构中的服务提供了统一的访问入口,客户端通过API网关访问相关服务。所有客户端的访问都通过它来进行路由及过滤。它实现了请求路由、负载均衡、校验过滤、服务容错、服务聚合等功能。
三、Zuul的配置
1.导入依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
2.yml配置
server:
port: 10050 # zuul端口号
eureka:
client:
serviceUrl: # Eureka客户端配置,指向注册中心地址
defaultZone: http://localhost:10010/eureka/
instance: # 打开IP注册
instance-id: ${spring.application.name}:${server.port} # 设置实例名称
prefer-ip-address: true # 开启IP注册
# 指定服务名称,此服务下的集群所有服务都叫此服务名
spring:
application:
name: zuul-server
zuul:
ignored-services: "*" #服务名不可访问
prefix: "/hc" #统一访问前缀,主要前面要加/
routes: #使用路由
user-server: "/user/**"
pay-server: "/pay/**"
order-server: "/order/**"
3.启动类的编写
@SpringBootApplication
@EnableEurekaClient
@EnableZuulProxy
//@EnableZuulServer 这个标签也可以开启zuul,但功能没有上一个完整
public class ZuulServerApplication
{
public static void main( String[] args )
{
SpringApplication.run(ZuulServerApplication.class,args);
}
}
4.测试调用
四、zuul的工作原理
zuul的底层是通过各种Filter来实现的,zuul中的filter按照执行顺序分为了“pre”前置(”custom”自定义一般是前置),“routing”路由,“post”后置,以及“error”异常Filter组成,当各种Filter出现了异常,请求会跳转到“error filter”,然后再经过“post filter” 最后返回结果,下面是Filter的执行流程图:
这张经典的官方流程图有些问题,其中 post Filter 抛错之后进入 error Filter,然后再进入 post Filter 是有失偏颇的。实际上 post Filter 抛错分两种情况:
在 post Filter 抛错之前,pre、route Filter 没有抛错,此时会进入 ZuulException 的逻辑,打印堆栈信息,然后再返回 status = 500 的 ERROR 信息。
在 post Filter 抛错之前,pre、route Filter 已有抛错,此时不会打印堆栈信息,直接返回status = 500 的 ERROR 信息。
也就是说,整个责任链流程终点不只是 post Filter,还可能是 error Filter,这里重新整理了一下,如图:
这样就比较直观地描述了 Zuul 关于 Filter 的请求生命周期。Zuul 中一共有四种不同生命周期的 Filter,分别是:
pre:在 Zuul 按照规则路由到下级服务之前执行。如果需要对请求进行预处理,比如鉴权、限流等,都应考虑在此类 Filter 实现。
route:这类 Filter 是 Zuul 路由动作的执行者,是 Apache Http Client 或 Netflix Ribbon 构建和发送原始 HTTP 请求的地方,目前已支持 Okhttp。
post:这类 Filter 是在源服务返回结果或者异常信息发生后执行的,如果需要对返回信息做一些处理,则在此类 Filter 进行处理。
error:在整个生命周期内如果发生异常,则会进入 error Filter,可做全局异常处理。
五、ZuulFilter
Zuul中提供了过滤器定义,可以用来过滤代理请求,提供额外功能逻辑。如:权限验证,日志记录等。
Zuul提供的过滤器是一个父类。父类是ZuulFilter。通过父类中定义的抽象方法filterType,来决定当前的Filter种类是什么。有前置过滤、路由后过滤、后置过滤、异常过滤。
-
前置过滤:是请求进入Zuul之后,立刻执行的过滤逻辑。
-
路由后过滤:是请求进入Zuul之后,并Zuul实现了请求路由后执行的过滤逻辑,路由后过滤,是在远程服务调用之前过滤的逻辑。
-
后置过滤:远程服务调用结束后执行的过滤逻辑。
-
异常过滤:是任意一个过滤器发生异常或远程服务调用无结果反馈的时候执行的过滤逻辑。无结果反馈,就是远程服务调用超时。
1.过滤器实现方式
继承父类ZuulFilter。在父类中提供了4个抽象方法,分别是:filterType, filterOrder, shouldFilter, run。其功能分别是:
-
filterType:方法返回字符串数据,代表当前过滤器的类型。可选值有-pre, route, post, error。
pre - 前置过滤器,在请求被路由前执行,通常用于处理身份认证,日志记录等;
route - 在路由执行后,服务调用前被调用;
error - 任意一个filter发生异常的时候执行或远程服务调用没有反馈的时候执行(超时),通常用于处理异常;
post - 在route或error执行后被调用,一般用于收集服务信息,统计服务性能指标等,也可以对response结果做特殊处理。
-
filterOrder:返回int数据,用于为同filterType的多个过滤器定制执行顺序,返回值越小,执行顺序越优先。
-
shouldFilter:返回boolean数据,代表当前filter是否生效。
-
run:具体的过滤执行逻辑。如pre类型的过滤器,可以通过对请求的验证来决定是否将请求路由到服务上;如post类型的过滤器,可以对服务响应结果做加工处理(如为每个响应增加footer数据)。
2.代码示例
下面我们以登录拦截做一下基本代码范例
@Component
public class LoginZuulFilter extends ZuulFilter {
@Override
public String filterType() {
return "pre";
}
@Override
public int filterOrder() {
return 0;
}
@Override
public boolean shouldFilter() {
RequestContext currentContext = RequestContext.getCurrentContext();
HttpServletRequest request = currentContext.getRequest();
String requestURI = request.getRequestURI();
if (requestURI.contains("/login")){
return false;
}
return true;
}
@Override
public Object run() throws ZuulException {
//获取请求对象
RequestContext currentContext = RequestContext.getCurrentContext();
HttpServletRequest request = currentContext.getRequest();
//获取请求对象中的token
String token = request.getHeader("token");
if (!StringUtils.hasLength(token)) {//token没有长度,意味没有token
//获取响应对象
HttpServletResponse response = RequestContext.getCurrentContext().getResponse();
//未登陆,阻止路由
currentContext.setSendZuulResponse(false);
//设置响应对象格式防止乱码
response.setContentType("application/json;Charset=UTF-8");
//返回响应的书写数据
try {
PrintWriter writer = response.getWriter();
writer.write("{\"success\":false, \"message\":\"NoLogin\"}");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
//登录有token,就放行
return null;
}
}