SpringCloud学习笔记(7)——API网关服务:Spring Cloud Zuul

一、背景介绍

在这里插入图片描述

通过前面几张的的学习,我们可以设计出类似上图的基础系统架构.
在该架构中,我们的服务集群包含内部服务ServiceA和ServiceB, 它们都会向Eureka Server集群进行注册与订阅服务,而OpenService是一个对外的RESTfulAPI服务,它通过FS、 Nginx等网络设备或工具软件实现对各个微服务的路由与负载均衡,并公开给外部的客户端调用。

在本章中,我们将把视线聚焦在对外服务这块内容.

问题:但是上面的架构对运维人员、开发人员都会带来很多问题,比如实力增减或者是IP地址变动的时候需要手工去同步修改这些信息、用户登录校验或者签名服务我们将原本的一个应用拆分成了多个应用,需要在这些多个应用中都添加校验逻辑。

为了解决上面的这些常见的架构问题,API网关的概念就出现了,API网关是一个更为智能的应用服务器,它的定义类似于面向对象设计模式中的Facade模式,它的存在就像是整个微服务架构系统的门面一样,所有的外部客户端访问都需要经过它来进行调度和过滤。它除了要实现请求路由、 负载均衡、 校验过滤等功能之外,还需要更多能力,比如与服务治理框架的结合、请求转发时的熔断机制、服务的聚合等一系列高级功能。

Spring Cloud Zuul:针对上述问题 Spring Cloud 提供了基于Netflix Zuul实现的API网关组件 —— Spring Cloud Zuul

  • 首先,对于路由规则与服务实例的维护间题。SpringCloud Zuul通过与SpringCloud Eureka进行整合,将自身注册为Eureka服务治理下的应用,同时从Eureka中获得了所有其他微服务的实例信息。这样的设计非常巧妙地将服务治理体系中维护的实例信息利用起来,使得将维护服务实例的工作交给了服务治理框架自动完成,不再需要人工介入。
  • 其次,对千类似签名校验、登录校验在微服务架构中的冗余问题。SpringCloud Zuul提供了一套过滤器机制,它可以 很好地支持这样的任务。开发者可以通过使用Zuul来创建各种校验过滤器,然后指定哪些规则的请求需要执行校验逻辑,只有通过校验的才会被路由到具体的微服务接口,不然就返回错误提示。通过这样的改造,各个业务层的微服务应用就不再需要非业务性质的校验逻辑了,这使得我们的微服务应用可以更专注千业务逻辑的开发,同时微服务的自动化测试也变得更容易实现。

二、快速入门

1.请求路由
  • 创建一个名为api-gateway的Spring Boot 工程,引入spring-cloud-starter-zuul 依赖,如下:
 <properties>
        <java.version>1.8</java.version>
        <spring-cloud.version>Greenwich.RC2</spring-cloud.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
            <version>2.0.2.RELEASE</version>
        </dependency>
        
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
  • 创建主类。使用@EnableZuulProxy注解开启Zuul 的 API 网关服务功能。
package com.gildata.apigateway;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;

@EnableZuulProxy
@SpringBootApplication
public class ApiGatewayApplication {

    public static void main(String[] args) {
        SpringApplication.run(ApiGatewayApplication.class, args);
    }

}


  • 在application。properties 中配置Zuul应用的基础信息,具体内容如下:
spring.application.name = api-gateway
server.port=5555
zuul.routes.api-a.path=/api-a/**
zuul.routes.api-a.service-id=HELLO-SERVICE

zuul.routes..api-b.path=/api-b/**
zuul.routes.api-b.service-id=FEIGN-CONSUMER

eureka.client.serviceUrl.defaultZone=http://localhost:8761/eureka/

面向服务的路由
:完成上面的工作后,通过Zuul 实现的API网关服务就构建完毕了.Spring Cloud Zuul 实现了与Spring Cloud Eureka 的无缝整合,我们可以让路由的path 不是映射具体的url,而是让它映射到某个具体的服务

完成了上面的路由配置后,我们启动eureka-server、hello-service、feign-consumer 以及 上面的api-gateway

通过上面的搭建工作,我们可以通过服务网关来访问 hello-service 和 feign-consumer 这两个服务了:

  • 发送http://localhost:5555/api-a/hello,请求被映射到了 hello-service的服务上
  • 发送http://localhost:5555/api-a/feign-consumer,请求被映射到了feign-consumer上.
2.请求过滤

Zuul 允许开发者在API网关上通过定义过滤器来实现对请求的拦截与过滤,实现方法非常简单,只需要继承ZuulFilter抽象类并实现它定义的4个抽象函数就可以完成对请求的拦截和过滤了。

下面的代码定义了一个简单的Zuul 过滤器,实现了检查HttpServletRequest 中是否含有accessToken 参数,若有就进行路由,没有就拒绝访问,返回401 Unauthorized 错误。

package com.gildata.apigateway.filter;

import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.http.HttpServletRequest;

/**
 * Created by liaock on 2019/1/14
 **/
public class AccessFilter extends ZuulFilter {

    private static Logger log = LoggerFactory.getLogger(AccessFilter.class);
    @Override
    public String filterType() {
        return "pre";
    }

    @Override
    public int filterOrder() {
        return 0;
    }

    @Override
    public boolean shouldFilter() {
        return true;
    }

    @Override
    public Object run() throws ZuulException {
        RequestContext ctx = RequestContext.getCurrentContext();
        HttpServletRequest request = ctx.getRequest();
        log.info("send {} request to {}",request.getMethod(),request.getRequestURL().toString());
        Object accessToken = request.getParameter("accessToken");
        if(accessToken == null){
            log.warn("access token is empty");
            ctx.setSendZuulResponse(false);
            ctx.setResponseStatusCode(401);
            return null;
        }
        log.info("access token ok");
        return null;
    }
}

  • filterType: 过滤器的类型,它决定过滤器在请求的哪个生命周期中执行。这里定义为pre,代表会在请求被路由之前执行。
  • filterOrder: 过滤器的执行顺序。当请求在一个阶段中存在多个过滤器时,需要根据该方法返回的值来依次执行。
  • shouldFilter: 判断该过滤器是否需要被执行。这里我们直接返回了true,因此该过滤器对所有请求都会生效。实际运用中我们可以利用该函数来指定过滤器的有效范围。
  • run: 过滤器的具体逻辑。这里我们通过ctx.setSendZuulResponse(false)令zuul过滤该请求,不对其进行路由,然后通过ctx.setResponseStatusCode(401)设置了其返回的错误码,当然也可以进 一步优化我们的返回,比如,通过ctx.setResponseBody(body)对返回的body内容进行编辑等。

在实现了自定义过滤器之后,它并不会直接生效,我们还需要将其注册到容器中,才能启动该过滤器,如下:

package com.gildata.apigateway;

import com.gildata.apigateway.filter.AccessFilter;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
import org.springframework.context.annotation.Bean;

@EnableZuulProxy
@SpringBootApplication
public class ApiGatewayApplication {

    public static void main(String[] args) {
        SpringApplication.run(ApiGatewayApplication.class, args);
    }
    @Bean
    public AccessFilter accessFilter(){
        return new AccessFilter();
    }

}


重新启动 api-gateway 测试:

测试成功 !

三、路由详解

1.传统路由配置

传统路由配置就是不依赖于服务发现机制的情况,通过在配置文件中具体指定每个路由表达式与服务实例的映射关系来实现API网关对外部请求的路由。

  • 单实例配置:通过zuul.routes..path 与 zuul.routes..url参数的方式进行配置,比如:
zuul.routes.user-service.path = /user-service/**
zuul.routes.user-service.url=http://localhost:8080/
  • 多实例配置:通过zuul.routes..path 与 zuul.routes..serviceId参数对的方式进行配置,比如:
zuul.routes.user-service.path=/user-service/**
zuul.routes.user-service.serviceID=user-service
ribbon.eureka.enabled=false
user-service.ribbon.listOfServiers=http://localhost:8080/,http://localhost:8081/

由于在Spring Cloud Zuul中自带了对Ribbon的依赖,所以我们只需要做一些配置即可,它们具体作用如下:

  • ribbon.eureka.enabled:默认情况下 Ribbon会根据服务发现机制来获取配置服务名对应的实例清单,但是这里并没有整合 Eureka 所以需要设置成false,否则配置的serviceId 获取不到对应的实例清单。
  • user-service.ribbon.listOfServers
2.服务路由配置
zuul.routes.api-a.path=/api-a/**
zuul.routes.api-a.service-id=HELLO-SERVICE

除了使用如上的path与serviceId映射的方式之外,还有一种更简洁的配置方式:zuul.routes.= ,其中是具体服务名, 用来配置匹配的请求表达式,例如:

zuul.routes.HELLO-SERVICE=/api-a/**

3.服务路由的默认规则

zuul-routes.user-service.path=/user-service/**
zuul-routes.user-service.serviceId=user-service

当我们为Spring Cloud Zuul 构建的API网关服务引入Spring Cloud Eureka之后,它为Eureka中的每个服务都自动创建一个默认路由规则,这些默认规则的path会使用serviceId配置的服务名作为请求前缀,就如上面的例子

对于我们不希望对外开放的服务,我们可以用zuul.ignored-services 参数来设置一个服务名匹配表达式来定义不自动创建路由的规则.,比如设置为zuul.ignored-services=*, Zuul将对所有的服务不进行自动创建路由。

4.自定义路由映射规则

假如我们想通过路径/v1/userservice/** 的路径来访问 serviceId为 userservice-v1的服务实例,只需要在API网关中增加如下Bean创建即可:

 @Bean
    public PatternServiceRouteMapper serviceRouteMapper(){
        return new PatternServiceRouteMapper(
                "(?<name>^.+)-(?<version>v.+$)","${version}/${name}"
        );
    }
4.本地跳转

在Zuul 实现的 API 网关路由功能中,还支持forward 形式的服务端跳转配置。例如:

zuul.routes.api-a.path=/api-a/**
zuul.routes.api-a.url=http://localhost:8081

zuul.routes.api-b.path=/api-b/**
zuul.routes.api-b.url=forward:/local
5.Cookie与头信息

默认情况下,Spring Cloud Zuul 在请求路由时,会过滤掉 HTTP 请求头信息中的一些敏感信息,防止他们被传递到下游的服务器。 默认的敏感头信息通过 zuul.sensitiveHeaders 参数定义,包括Cookie、Set-Cookie、Authorization三个属性,这样就会引发一个常见的问题,如果我们使用 Spring Security、Shiro 等安全框架构建的Web应用通过Spring Cloud Zuul构建网关进行路由时,由于Cookie信息无法传递,我们的Web应用将无法实现登录和鉴权。

为了解决这些问题,配置的方法如下:

  • 通过设置全局参数为空来覆盖默认值,具体如下:
    zuul.sensitiveHeaders= (此种方法不推荐.)

  • 通过指定路由的参数来配置:

#方法一:对指定路由开启自定义敏感头
zuul.routes.<routes>.customSensitiveHeaders=true

#方法二:将指定路由的敏感头设置为空
zuul.routes.<router>.sensitiveHeaders= 
重定向问题

解决前面的Cookie问题后,我们已经可以通过网关来访问我们的Web应用了。 但是我们此时会遇到另外一个问题,我们可以通过网关访问登录页面并发起登录请求,但是登录成功后,我们跳转到的页面URL却是具体WEB应用的实例的地址,而不是通过网关的路由地址。这个问题非常严重,因为使用 API 网关的一个重要原因就是要将网关作为统一入口,从而不暴露内部服务细节

引发问题的大致原因是由于Spring Security 或 Shiro 在登录完成后,通过重定向方式跳转到登陆后的页面,此时登录后的请求结果状态码为 302,请求响应头中的 Location 指向了具体的服务实例地址,而请求头信息中的 Host 也指向了具体的服务实例IP地址和端口, 所以该问题的根本原因在于 Spring Cloud Zuul 在路由请求时,并没有将最初的Host信息设置正确

针对这个问题,在spring-cloud-netflix-core-1.2.x 版本中增加了一参数配置,能够使得网关在进行路由转发前为请求设置 Host 头信息,以表示最初的服务端请求地址。具体配置方式如下:

zuul.addHostHeader=true

四、过滤器详解

1.过滤器

基本特性:

  • Type: 定义在请求执行过程中何时被执行;
  • Execution Order: 当存在多个过滤器时,用来指示执行的顺序,值越小就会越早执行;
  • Criteria: 执行的条件,即该过滤器何时会被触发;
  • Action: 具体的动作。

即 4 个抽象方法:

String filterType();

int filterOrder();

boolean shouldFilter();

Object run();
  • filterType:该函数返回一个字符串来代表过滤器的类型,而这个类型就是在 HTTP 请求过程中定义的各个阶段。在Zuul 中默认定义了 4 种不同生命周期的过滤器类型,具体如下所示。
    • pre:可以在请求路由之前调用
    • routing:在路由请求时被调用
    • post:在routing 和 error 过滤器之后被调用
    • error:处理请求时发生错误时被调用.
  • filterOrder:通过 int 值来定义过滤器的执行顺序,数值越小优先级越高
  • shouldFilter:返回一个boolean 值来判断该过滤器是否要执行。我们可以通过此方法来指定过滤器的有效范围.
  • run:过滤器的具体逻辑。
2.请求生命周期

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值