Spring Boot + Spring Cloud 实现权限管理系统 后端篇(二十一):服务网关(Zuul)
在线演示
演示地址:http://139.196.87.48:9002/kitty
用户名:admin 密码:admin
技术背景
前面我们通过 Ribbon 或 Feign 实现了微服务之间的调用和负载均衡,那我们的各种微服务又要如何提供给外部应用调用呢。
当然,因为是 REST API 接口,外部客户端直接调用各个微服务是没有问题的,但出于种种原因,这并不是一个好的选择。
让客户端直接与各个微服务通讯,会有以下几个问题:
- 客户端会多次请求不同的微服务,增加了客户端的复杂性。
- 存在跨域请求,在一定场景下处理会变得相对比较复杂。
- 实现认证复杂,每个微服务都需要独立认证。
- 难以重构,项目迭代可能导致微服务重新划分。如果客户端直接与微服务通讯,那么重构将会很难实施。
- 如果某些微服务使用了防火墙 / 浏览器不友好的协议,直接访问会有一定困难。
面对类似上面的问题,我们要如何解决呢?答案就是:服务网关!
使用服务网关具有以下几个优点:
- 易于监控。可在微服务网关收集监控数据并将其推送到外部系统进行分析。
- 易于认证。可在服务网关上进行认证,然后再转发请求到微服务,无须在每个微服务中进行认证。
- 客户端只跟服务网关打交道,减少了客户端与各个微服务之间的交互次数。
- 多渠道支持,可以根据不同客户端(WEB 端、移动端、桌面端...)提供不同的 API 服务网关。
Spring Cloud Zuul
服务网关是微服务架构中一个不可或缺的部分。通过服务网关统一向外系统提供 REST API 的过程中,除了具备服务路由、均衡负载功能之外,它还具备了权限控制等功能。
Spring Cloud Netflix 中的 Zuul 就担任了这样的一个角色,为微服务架构提供了前门保护的作用,同时将权限控制这些较重的非业务逻辑内容迁移到服务路由层面,使得服务集群主体能够具备更高的可复用性和可测试性。
在 Spring Cloud 体系中, Spring Cloud Zuul 封装了 Zuul 组件,作为一个 API 网关,负责提供负载均衡、反向代理和权限认证。
Zuul 工作机制
过滤器机制
Zuul 的核心是一系列的 filters, 其作用类似 Servlet 框架的 Filter,Zuul 把客户端请求路由到业务处理逻辑的过程中,这些 filter 在路由的特定时期参与了一些过滤处理,比如实现鉴权、流量转发、请求统计等功能。Zuul 的整个运行机制,可以用下图来描述。
过滤器的生命周期
Filter 的生命周期有 4 个,分别是“PRE”、“ROUTING”、“POST”、“ERROR”,整个生命周期可以用下图来表示。
基于 Zuul 的这些过滤器,可以实现各种丰富的功能,而这些过滤器类型则对应于请求的典型生命周期。
PRE: 这种过滤器在请求被路由之前调用。我们可利用这种过滤器实现身份验证、在集群中选择请求的微服务、记录调试信息等。
ROUTING:这种过滤器将请求路由到微服务。这种过滤器用于构建发送给微服务的请求,并使用 Apache HttpClient 或 Netfilx Ribbon 请求微服务。
POST:这种过滤器在路由到微服务以后执行。这种过滤器可用来为响应添加标准的 HTTP Header、收集统计信息和指标、将响应从微服务发送给客户端等。
ERROR:在其他阶段发生错误时执行该过滤器。
除了默认的过滤器类型,Zuul 还允许我们创建自定义的过滤器类型。例如,我们可以定制一种 STATIC 类型的过滤器,直接在 Zuul 中生成响应,而不将请求转发到后端的微服务。
Zuul 中默认实现的 Filter
Zuul 默认实现了很多 Filter,这些 Filter 如下面表格所示。
类型 | 顺序 | 过滤器 | 功能 |
---|---|---|---|
pre | -3 | ServletDetectionFilter | 标记处理 Servlet 的类型 |
pre | -2 | Servlet30WrapperFilter | 包装 HttpServletRequest 请求 |
pre | -1 | FormBodyWrapperFilter | 包装请求体 |
route | 1 | DebugFilter | 标记调试标志 |
route | 5 | PreDecorationFilter | 处理请求上下文供后续使用 |
route | 10 | RibbonRoutingFilter | serviceId 请求转发 |
route | 100 | SimpleHostRoutingFilter | url 请求转发 |
route | 500 | SendForwardFilter | forward 请求转发 |
post | 0 | SendErrorFilter | 处理有错误的请求响应 |
post | 1000 | SendResponseFilter | 处理正常的请求响应 |
禁用指定的 Filter
可以在 application.yml 中配置需要禁用的 filter,格式为zuul.<SimpleClassName>.<filterType>.disable=true
。
比如要禁用org.springframework.cloud.netflix.zuul.filters.post.SendResponseFilter
就设置如下。
zuul: SendResponseFilter: post: disable: true
自定义 Filter
实现自定义滤器需要继承 ZuulFilter,并实现 ZuulFilter 中的抽象方法。
public class MyFilter extends ZuulFilter { @Override String filterType() { return "pre"; // 定义 filter 的类型,有 pre、route、post、error 四种 }@Override </span><span style="color: rgba(0, 0, 255, 1)">int</span><span style="color: rgba(0, 0, 0, 1)"> filterOrder() { </span><span style="color: rgba(0, 0, 255, 1)">return</span> 5; <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 定义filter的顺序,数字越小表示顺序越高,越先执行</span>
}
@Override </span><span style="color: rgba(0, 0, 255, 1)">boolean</span><span style="color: rgba(0, 0, 0, 1)"> shouldFilter() { </span><span style="color: rgba(0, 0, 255, 1)">return</span> <span style="color: rgba(0, 0, 255, 1)">true</span>; <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 表示是否需要执行该filter,true表示执行,false表示不执行</span>
}
@Override Object run() { </span><span style="color: rgba(0, 0, 255, 1)">return</span> <span style="color: rgba(0, 0, 255, 1)">null</span>; <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> filter需要执行的具体操作</span>
}
}
实现案例
新建工程
新建一个项目 kitty-zuul 作为服务网关,工程结构如下图。
添加依赖
添加 consul、zuul 相关依赖。
pom.xml
<?xml version="1.0" encoding="UTF-8"?> <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"> <modelVersion>4.0.0</modelVersion><parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.4.RELEASE</version> <relativePath /> <!-- lookup parent from repository --> </parent> <groupId>com.louis</groupId> <artifactId>kitty-zuul</artifactId> <version>${project.version}</version> <packaging>jar</packaging> <name>kitty-zuul</name> <description>kitty-zuul</description> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <project.version>1.0.0</project.version> <java.version>1.8</java.version> <spring-cloud.version>Finchley.RELEASE</spring-cloud.version> </properties> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-zuul</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-consul-discovery</artifactId> </dependency> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${spring-cloud.version}</version> <type>pom</type> <scope><span style="color: rgba(0, 0, 255, 1)">import</span></scope> </dependency> </dependencies> </dependencyManagement> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build>
</project>
启动类
启动类添加 @EnableZuulProxy 注解,开启服务网关支持。
package com.louis.kitty.zuul;import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;@EnableZuulProxy
@SpringBootApplication
public class KittyZuulApplication {</span><span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">static</span> <span style="color: rgba(0, 0, 255, 1)">void</span><span style="color: rgba(0, 0, 0, 1)"> main(String[] args) { SpringApplication.run(KittyZuulApplication.</span><span style="color: rgba(0, 0, 255, 1)">class</span><span style="color: rgba(0, 0, 0, 1)">, args); }
}
配置文件
配置启动端口为 8010, 注册服务到注册中心,配置 zuul 转发规则。
这里配置在访问 locathost:8010/feigin/call 和 ribbon/call 时,调用消费者对应接口。
server: port: 8010 spring: application: name: kitty-zuul cloud: consul: host: localhost port: 8500 discovery: serviceName: ${spring.application.name} # 注册到 consul 的服务名称 zuul: routes: ribbon: path: /ribbon/** serviceId: kitty-consumer # 转发到消费者 /ribbon/ feign: path: /feign/** serviceId: kitty-consumer # 转发到消费者 /feign/
测试效果
依次启动注册中心、监控、服务提供者、服务消费者、服务网关等项目。
访问 http://localhost:8010/ribbon/call, 效果如下图。
访问 http://localhost:8010/feign/call, 效果如下图。
说明 Zuul 已经成功转发请求,并成功调用后端微服务。
配置接口前缀
如果想给每个服务的 API 接口加上一个前缀,可使用 zuul.prefix 进行配置。
例如 http://localhost:8010/v1/feign/call,即在所有的 API 接口上加一个 v1 作为版本号。
zuul: prefix: /v1 routes: ribbon: path: /ribbon/** serviceId: kitty-consumer # 转发到消费者 /ribbon/ feign: path: /feign/** serviceId: kitty-consumer # 转发到消费者 /feign/
默认路由规则
上面我们是通过添加路由配置进行请求转发的,内容如下。
zuul: routes: ribbon: path: /ribbon/** serviceId: kitty-consumer # 转发到消费者 /ribbon/ feign: path: /feign/** serviceId: kitty-consumer # 转发到消费者 /feign/
但是如果后端微服务服务非常多的时候,每一个都这样配置还是挺麻烦的,所以 Spring Cloud Zuul 已经帮我们做了默认配置。默认情况下,Zuul 会代理所有注册到注册中心的微服务,并且 Zuul 的默认路由规则如下:http://ZUUL_HOST:ZUUL_PORT/微服务在注册中心的serviceId/**
会被转发到 serviceId 对应的微服务,所以说如果遵循默认路由规则,基本上就没什么配置了。
我们移除上面的配置,直接通过 serviceId/feign/call 的方式访问。
访问 http://localhost:8010/kitty-consumer/feign/call,结果如下。
结果也是可用访问的,说明 ZUUL 默认路由规则正在产生作用。
路由熔断
Zuul 作为 Netflix 组件,可以与 Ribbon、Eureka 和 Hystrix 等组件相结合,实现负载均衡、熔断器的功能。默认情况下 Zuul 和 Ribbon 相结合,实现了负载均衡。实现熔断器功能需要实现 FallbackProvider 接口,实现该接口的两个方法,一个是 getRoute(),用于指定熔断器功能应用于哪些路由的服务;另一个方法 fallbackResponse() 为进入熔断器功能时执行的逻辑。
创建 MyFallbackProvider 类,getRoute()方法返回 "kitty-consumer",只针对 consumer 服务进行熔断。如果需要所有的路由服务都加熔断功能,需要在 getRoute()方法上返回”*“的匹配符。getBody() 方法返回发送熔断时的反馈信息,这里在发送熔断时返回信息:"Sorry, the service is unavailable now." 。
MyFallbackProvider.java
package com.louis.kitty.zuul;import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;import org.springframework.cloud.netflix.zuul.filters.route.FallbackProvider;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.stereotype.Component;@Component
public class MyFallbackProvider implements FallbackProvider {
@Override
public String getRoute() {
return "kitty-consumer";
}@Override </span><span style="color: rgba(0, 0, 255, 1)">public</span><span style="color: rgba(0, 0, 0, 1)"> ClientHttpResponse fallbackResponse(String route, Throwable cause) { System.out.println(</span>"route:"+<span style="color: rgba(0, 0, 0, 1)">route); System.out.println(</span>"exception:"+<span style="color: rgba(0, 0, 0, 1)">cause.getMessage()); </span><span style="color: rgba(0, 0, 255, 1)">return</span> <span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> ClientHttpResponse() { @Override </span><span style="color: rgba(0, 0, 255, 1)">public</span> HttpStatus getStatusCode() <span style="color: rgba(0, 0, 255, 1)">throws</span><span style="color: rgba(0, 0, 0, 1)"> IOException { </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> HttpStatus.OK; } @Override </span><span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">int</span> getRawStatusCode() <span style="color: rgba(0, 0, 255, 1)">throws</span><span style="color: rgba(0, 0, 0, 1)"> IOException { </span><span style="color: rgba(0, 0, 255, 1)">return</span> 200<span style="color: rgba(0, 0, 0, 1)">; } @Override </span><span style="color: rgba(0, 0, 255, 1)">public</span> String getStatusText() <span style="color: rgba(0, 0, 255, 1)">throws</span><span style="color: rgba(0, 0, 0, 1)"> IOException { </span><span style="color: rgba(0, 0, 255, 1)">return</span> "ok"<span style="color: rgba(0, 0, 0, 1)">; } @Override </span><span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">void</span><span style="color: rgba(0, 0, 0, 1)"> close() { } @Override </span><span style="color: rgba(0, 0, 255, 1)">public</span> InputStream getBody() <span style="color: rgba(0, 0, 255, 1)">throws</span><span style="color: rgba(0, 0, 0, 1)"> IOException { </span><span style="color: rgba(0, 0, 255, 1)">return</span> <span style="color: rgba(0, 0, 255, 1)">new</span> ByteArrayInputStream("Sorry, the service is unavailable now."<span style="color: rgba(0, 0, 0, 1)">.getBytes()); } @Override </span><span style="color: rgba(0, 0, 255, 1)">public</span><span style="color: rgba(0, 0, 0, 1)"> HttpHeaders getHeaders() { HttpHeaders headers </span>= <span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> headers; } }; }
}
重新启动,访问 http://localhost:8010/kitty-consumer/feign/call, 可以正常访问
停掉 kitty-consumer 服务, 再次访问 http://localhost:8010/kitty-consumer/feign/call, 效果如下。
说明我们自定义的熔断器已经起作用了。
自定义 Filter
创建一个 MyFilter, 继承 ZuulFilter 类,覆写 run() 方法逻辑,在转发请求前进行 token 认证,如果请求没有携带 token,返回 "there is no request token" 提示。
package com.louis.kitty.zuul;import java.io.IOException;
import javax.servlet.http.HttpServletRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;@Component
public class MyFilter extends ZuulFilter {</span><span style="color: rgba(0, 0, 255, 1)">private</span> <span style="color: rgba(0, 0, 255, 1)">static</span> Logger log=LoggerFactory.getLogger(MyFilter.<span style="color: rgba(0, 0, 255, 1)">class</span><span style="color: rgba(0, 0, 0, 1)">); @Override </span><span style="color: rgba(0, 0, 255, 1)">public</span><span style="color: rgba(0, 0, 0, 1)"> String filterType() { </span><span style="color: rgba(0, 0, 255, 1)">return</span> "pre"; <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 定义filter的类型,有pre、route、post、error四种</span>
}
@Override </span><span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">int</span><span style="color: rgba(0, 0, 0, 1)"> filterOrder() { </span><span style="color: rgba(0, 0, 255, 1)">return</span> 0; <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 定义filter的顺序,数字越小表示顺序越高,越先执行</span>
}
@Override </span><span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">boolean</span><span style="color: rgba(0, 0, 0, 1)"> shouldFilter() { </span><span style="color: rgba(0, 0, 255, 1)">return</span> <span style="color: rgba(0, 0, 255, 1)">true</span>; <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 表示是否需要执行该filter,true表示执行,false表示不执行</span>
}
@Override </span><span style="color: rgba(0, 0, 255, 1)">public</span> Object run() <span style="color: rgba(0, 0, 255, 1)">throws</span><span style="color: rgba(0, 0, 0, 1)"> ZuulException { </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> filter需要执行的具体操作</span> RequestContext ctx =<span style="color: rgba(0, 0, 0, 1)"> RequestContext.getCurrentContext(); HttpServletRequest request </span>=<span style="color: rgba(0, 0, 0, 1)"> ctx.getRequest(); String token </span>= request.getParameter("token"<span style="color: rgba(0, 0, 0, 1)">); System.out.println(token); </span><span style="color: rgba(0, 0, 255, 1)">if</span>(token==<span style="color: rgba(0, 0, 255, 1)">null</span><span style="color: rgba(0, 0, 0, 1)">){ log.warn(</span>"there is no request token"<span style="color: rgba(0, 0, 0, 1)">); ctx.setSendZuulResponse(</span><span style="color: rgba(0, 0, 255, 1)">false</span><span style="color: rgba(0, 0, 0, 1)">); ctx.setResponseStatusCode(</span>401<span style="color: rgba(0, 0, 0, 1)">); </span><span style="color: rgba(0, 0, 255, 1)">try</span><span style="color: rgba(0, 0, 0, 1)"> { ctx.getResponse().getWriter().write(</span>"there is no request token"<span style="color: rgba(0, 0, 0, 1)">); } </span><span style="color: rgba(0, 0, 255, 1)">catch</span><span style="color: rgba(0, 0, 0, 1)"> (IOException e) { e.printStackTrace(); } </span><span style="color: rgba(0, 0, 255, 1)">return</span> <span style="color: rgba(0, 0, 255, 1)">null</span><span style="color: rgba(0, 0, 0, 1)">; } log.info(</span>"ok"<span style="color: rgba(0, 0, 0, 1)">); </span><span style="color: rgba(0, 0, 255, 1)">return</span> <span style="color: rgba(0, 0, 255, 1)">null</span><span style="color: rgba(0, 0, 0, 1)">; }
}
OK,这样就行了,Zuul 会自动加载 Filter 执行过滤的。
重新启动 Zuul 项目,访问 http://localhost:8010/kitty-consumer/feign/call,效果如下。
请求时带上 token,访问 http://localhost:8010/kitty-consumer/feign/call?token=111,效果如下。
高可用性
Zuul 作为 API 服务网关,不同的客户端使用不同的负载将请求统一分发到后端的 Zuul,再有 Zuul 转发到后端服务。因此,为了保证 Zuul 的高可用性,前端可以同时开启多个 Zuul 实例进行负载均衡,另外,在 Zuul 的前端还可以使用 Nginx 或者 F5 再次进行负载转发,从而保证 Zuul 的高可用性。
源码下载
后端:https://gitee.com/liuge1988/kitty
前端:https://gitee.com/liuge1988/kitty-ui.git
作者:朝雨忆轻尘
出处:https://www.cnblogs.com/xifengxiaoma/
版权所有,欢迎转载,转载请注明原文作者及出处。