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>
&lt;parent&gt;
    &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
    &lt;artifactId&gt;spring-boot-starter-parent&lt;/artifactId&gt;
    &lt;version&gt;2.0.4.RELEASE&lt;/version&gt;
    &lt;relativePath /&gt; &lt;!-- lookup parent from repository --&gt;
&lt;/parent&gt;

&lt;groupId&gt;com.louis&lt;/groupId&gt;
&lt;artifactId&gt;kitty-zuul&lt;/artifactId&gt;
&lt;version&gt;${project.version}&lt;/version&gt;
&lt;packaging&gt;jar&lt;/packaging&gt;

&lt;name&gt;kitty-zuul&lt;/name&gt;
&lt;description&gt;kitty-zuul&lt;/description&gt;

&lt;properties&gt;
    &lt;project.build.sourceEncoding&gt;UTF-8&lt;/project.build.sourceEncoding&gt;
    &lt;project.reporting.outputEncoding&gt;UTF-8&lt;/project.reporting.outputEncoding&gt;
    &lt;project.version&gt;1.0.0&lt;/project.version&gt;
    &lt;java.version&gt;1.8&lt;/java.version&gt;
    &lt;spring-cloud.version&gt;Finchley.RELEASE&lt;/spring-cloud.version&gt;
&lt;/properties&gt;

&lt;dependencies&gt;
    &lt;dependency&gt;
        &lt;groupId&gt;org.springframework.cloud&lt;/groupId&gt;
        &lt;artifactId&gt;spring-cloud-starter-netflix-zuul&lt;/artifactId&gt;
    &lt;/dependency&gt;
    &lt;dependency&gt;
        &lt;groupId&gt;org.springframework.cloud&lt;/groupId&gt;
        &lt;artifactId&gt;spring-cloud-starter-consul-discovery&lt;/artifactId&gt;
    &lt;/dependency&gt;
&lt;/dependencies&gt;

&lt;dependencyManagement&gt;
    &lt;dependencies&gt;
        &lt;dependency&gt;
            &lt;groupId&gt;org.springframework.cloud&lt;/groupId&gt;
            &lt;artifactId&gt;spring-cloud-dependencies&lt;/artifactId&gt;
            &lt;version&gt;${spring-cloud.version}&lt;/version&gt;
            &lt;type&gt;pom&lt;/type&gt;
            &lt;scope&gt;<span style="color: rgba(0, 0, 255, 1)">import</span>&lt;/scope&gt;
        &lt;/dependency&gt;
    &lt;/dependencies&gt;
&lt;/dependencyManagement&gt;

&lt;build&gt;
    &lt;plugins&gt;
        &lt;plugin&gt;
            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
            &lt;artifactId&gt;spring-boot-maven-plugin&lt;/artifactId&gt;
        &lt;/plugin&gt;
    &lt;/plugins&gt;
&lt;/build&gt;

</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/ 
版权所有,欢迎转载,转载请注明原文作者及出处。