Spring Cloud Zuul 除了可以实现请求的路由功能,还有一个重要的功能就是过滤器。Zuul 的路由功能让所有的微服务提供的接口有统一的网关入口,但并不是所有的接口都是对外完全开发的,它们的访问权限一般都有一定的限制。那我们可以在每个服务都加上对应的校验和权限鉴定,那这些通常都是用过滤器或拦截器实现的,而且一个系统的各个服务的校验也大都是相似,这些相似的校验逻辑打码在每个服务都会有一份,不仅冗余且维护麻烦。更好的办法就是在请求的最前端统一去做这样的事情,而统一的 API 服务网关入口就是合适的选择
Zuul 可以通过定义过滤器来实现请求的拦截和过滤,而它本身的大部分功能也是通过过滤器实现的
过滤器
在 Zuul 中自定义过滤器需要继承抽象类 ZuulFilter
,需要实现以下4个方法:
String filterType(); // 过滤器类型int filterOrder(); // 执行顺序,数值越小优先级越高 boolean shouldFilter(); // 执行过滤器的条件Object run() throws ZuulException; // 具体的过滤操作
从名字我们也能知道各个方法的作用,下面来了解下过滤器类型和过滤器的生命周期,以及自定义过滤器的使用
过滤器类型和生命周期
Zuul 定义了4种不同的过滤器类型,对应着请求的典型生命周期
PRE
: 在请求被路由之前执行。可以用于请求身份验证、选择源服务器和记录调试信息ROURING
: 该过滤器将请求路由到服务。用于构建和发送给微服务的请求(使用 Apache HttpClient 或 Netflix Ribbon 请求服务)POST
: 在请求被路由到服务之后执行。可以用于向响应添加标准的 HTTP Header、收集统计数据和指标,以及将响应从源服务发送到客户端ERROR
: 该过滤器在其他阶段发生错误时执行
除了默认的过滤器流,Zuul 还允许我们创建自定义过滤器类型并显式地执行它们。例如,我们可以自定义一个 STATIC
类型的过滤器,它在Zuul 中生成响应,而不是将请求转发到后端的服务
下面是 Zuul 的生命周期图,描述着各种类型的过滤器的执行顺序(图片来源于 )
Spring Cloud Zuul 的过滤器
Spring Cloud Zuul 作为服务网关的大部分功能都是通过过滤器实现的,它在请求的各个阶段实现了一系列的过滤器,在 Spring Cloud Zuul 网关服务启动时自动加载和启用。实现的这些过滤器是在 spring-cloud-netflix-zuul
模块中的 org.springframework.cloud.netflix.zuul.filters
包下面
下面介绍部分过滤器的功能
Pre filters
filter | order | 说明 |
---|---|---|
ServletDetectionFilter | -3 | 检测请求是否通过 Spring 调度程序,即判断请求是交由 Spring DispatcherServlet 处理,还是 ZuulServlet 处理(主要是用于大文件上传) |
Servlet30WrapperFilter | -2 | 把原始 HttpServletRequest 包装成 Servlet30RequestWrapper 对象 |
FormBodyWrapperFilter | -1 | 解析表单数据并为下游服务重新编码 |
DebugFilter | 1 | 如果设置 debug 请求参数,则此过滤器将RequestContext.setDebugRouting() 和 RequestContext.setDebugRequest() 设置为true |
PreDecorationFilter | 5 | 根据提供的 RouteLocator 确定路由的位置和方式,它还为下游请求设置各种与代理相关的头文件 |
Route filters
filter | order | 说明 |
---|---|---|
RibbonRoutingFilter | 10 | 使用 Ribbon、Hystrix 和 可插拔 HTTP客户机发送请求。只对 RequestContext 存在 serviceId 参数的请求进行处理,即只对通过 serviceId配置路由规则的请求路由。可以使用不同的 HTTP 客户端:HttpClient、OkHttpClient、Netflix Ribbon HTTP client |
SimpleHostRoutingFilter | 100 | 通过 Apache HttpClient 发送请求到预定的 url,这些 url 可以在 RequestContext.getRouteHost() 中找到,即只对通过 url 配置路由规则的请求路由 |
SendForwardFilter | 500 | 通过使用 Servlet RequestDispatcher 转发请求。用于转发请求到当前应用的端点 |
Post filters
filter | order | 说明 |
---|---|---|
LocationRewriteFilter | 900 | 负责将 Location header 重写为 Zuul URL |
SendResponseFilter | 1000 | 将代理请求的响应写入当前响应 |
Error filters
filter | order | 说明 |
---|---|---|
SendErrorFilter | 0 | 利用请求上下文中的错误信息来组织成一个 forward 到 /error 错误端点的请求来产生错误响应 |
禁用过滤器
默认情况下,这些过滤器在代理和服务器模式下都是启用的。如果在某些场景下,禁用某个过滤器,可以设置
zuul.<SimpleClassName>.<filterType>.disable=true
。例如,要禁用org.springframework.cloud.netflix.zuul.filter.post.sendresponsefilter
,设置zuul.SendResponseFilter.post.disable=true
自定义过滤器
创建一个 Spring Boot 项目 zuul-filters
,Zuul 的服务网关路由配置可以见
spring: application: name: zuul-filtersserver: port: 8090eureka: client: service-url: defaultZone: http://localhost:8761/eureka/zuul: ignoredServices: '*' routes: product: path: /product/** serviceId: product-servicemanagement: endpoints: web: exposure: include: '*'
自定义一个过滤器 AccessFilter
,对请求中没有 accessToken 参数的请求,返回 401拒绝访问
@Log4j2public class AccessFilter extends ZuulFilter { @Override public int filterOrder() { // run before PreDecoration return FilterConstants.PRE_DECORATION_FILTER_ORDER - 1; } @Override public String filterType() { return FilterConstants.PRE_TYPE; } @Override public boolean shouldFilter() { RequestContext ctx = RequestContext.getCurrentContext(); // a filter has already forwarded // a filter has already determined serviceId return !ctx.containsKey(FilterConstants.FORWARD_TO_KEY) && !ctx.containsKey(FilterConstants.SERVICE_ID_KEY); } @Override public Object run() { RequestContext ctx = RequestContext.getCurrentContext(); HttpServletRequest request = ctx.getRequest(); log.info("send {} request to {}", request.getMethod(), request.getRequestURL().toString()); String token = request.getParameter("accessToken"); if(StringUtils.isBlank(token)) { log.warn("access token is empty"); // 过滤该请求,不对其进行路由 ctx.setSendZuulResponse(false); ctx.setResponseStatusCode(401); return null; } log.info("access token ok"); return null; }}
实现自定义过滤器后,把它添加到 Spring 的 Beans 中
@SpringBootApplication@EnableZuulProxypublic class ZuulFiltersApplication { public static void main(String[] args) { SpringApplication.run(ZuulFiltersApplication.class, args); } @Bean public AccessFilter accessFilter() { return new AccessFilter(); }}
下面我们来测试下结果,启动项目 eureka-server
、product-serivce
(作为代理的服务)、zuul-filters
访问 http://localhost:8090/product/product/1
返回 401
http://localhost:8090/product/product/1?accessToken=111
会正常路由到 product-service 的 /product/1
参考代码见:
过滤器管理端点
@EnableZuulProxy
注解配合 Spring Boot Actuator,Zuul 会暴露额外的两个管理端点:Routes
和 Filters
。分别是关于路由和过滤器的端点(服务路由的端点在这里介绍 )
spring-cloud-starter-netflix-zuul
已经依赖了 spring-boot-starter-actuator
,所以上面的工程已经包含了路由管理的功能。关于过滤器的管理端点的路径为 /filters
访问路径
{ "error": [ { "class": "org.springframework.cloud.netflix.zuul.filters.post.SendErrorFilter", "order": 0, "disabled": false, "static": true } ], "post": [ { "class": "org.springframework.cloud.netflix.zuul.filters.post.SendResponseFilter", "order": 1000, "disabled": false, "static": true } ], "pre": [ { "class": "org.springframework.cloud.netflix.zuul.filters.pre.DebugFilter", "order": 1, "disabled": false, "static": true }, { "class": "org.springframework.cloud.netflix.zuul.filters.pre.FormBodyWrapperFilter", "order": -1, "disabled": false, "static": true }, { "class": "com.turbosnail.zuul.filter.AccessFilter", "order": 4, "disabled": false, "static": true }, { "class": "org.springframework.cloud.netflix.zuul.filters.pre.Servlet30WrapperFilter", "order": -2, "disabled": false, "static": true }, { "class": "org.springframework.cloud.netflix.zuul.filters.pre.ServletDetectionFilter", "order": -3, "disabled": false, "static": true }, { "class": "org.springframework.cloud.netflix.zuul.filters.pre.PreDecorationFilter", "order": 5, "disabled": false, "static": true } ], "route": [ { "class": "org.springframework.cloud.netflix.zuul.filters.route.SimpleHostRoutingFilter", "order": 100, "disabled": false, "static": true }, { "class": "org.springframework.cloud.netflix.zuul.filters.route.RibbonRoutingFilter", "order": 10, "disabled": false, "static": true }, { "class": "org.springframework.cloud.netflix.zuul.filters.route.SendForwardFilter", "order": 500, "disabled": false, "static": true } ]}
访问404是因为没有暴露端点,可以设置 management.endpoints.web.exposure.include: '*'