Spring Cloud创设微服务架构:服务网关

图片 1图片发自简书App

前言

在微服务架构中,后端服务往往不直接开放给调用端,而是通过一个公共网关根据请求的url,路由到相应的服务。在网关中可以做一些服务调用的前置处理,比如权限验证。也可以通过动态路由,提供多个版本的api接口。spring
cloud
提供的技术栈中,使用netflix zuul来作为服务网关。

Spring Cloud构建微服务架构:服务网关

通过之前几篇Spring
Cloud中几个核心组件的介绍,我们已经可以构建一个简略的(不够完善)微服务架构了。比如下图所示:
图片 2

我们使用Spring Cloud
Netflix中的Eureka实现了服务注册中心以及服务注册与发现;而服务间通过Ribbon或Feign实现服务的消费以及均衡负载;通过Spring
Cloud
Config实现了应用多环境的外部化配置以及版本管理。为了使得服务集群更为健壮,使用Hystrix的融断机制来避免在微服务架构中个别服务出现异常时引起的故障蔓延。

在该架构中,我们的服务集群包含:内部服务Service A和Service
B,他们都会注册与订阅服务至Eureka Server,而Open
Service是一个对外的服务,通过均衡负载公开至服务调用方。本文我们把焦点聚集在对外服务这块,这样的实现是否合理,或者是否有更好的实现方式呢?

先来说说这样架构需要做的一些事儿以及存在的不足:

  • 首先,破坏了服务无状态特点。为了保证对外服务的安全性,我们需要实现对服务访问的权限控制,而开放服务的权限控制机制将会贯穿并污染整个开放服务的业务逻辑,这会带来的最直接问题是,破坏了服务集群中REST
    API无状态的特点。从具体开发和测试的角度来说,在工作中除了要考虑实际的业务逻辑之外,还需要额外可续对接口访问的控制处理。
  • 其次,无法直接复用既有接口。当我们需要对一个即有的集群内访问接口,实现外部服务访问时,我们不得不通过在原有接口上增加校验逻辑,或增加一个代理调用来实现权限控制,无法直接复用原有的接口。

面对类似上面的问题,我们要如何解决呢?下面进入本文的正题:服务网关!

为了解决上面这些问题,我们需要将权限控制这样的东西从我们的服务单元中抽离出去,而最适合这些逻辑的地方就是处于对外访问最前端的地方,我们需要一个更强大一些的均衡负载器,它就是本文将来介绍的:服务网关。

服务网关是微服务架构中一个不可或缺的部分。通过服务网关统一向外系统提供REST
API的过程中,除了具备服务路由、均衡负载功能之外,它还具备了权限控制等功能。Spring
Cloud
Netflix中的Zuul就担任了这样的一个角色,为微服务架构提供了前门保护的作用,同时将权限控制这些较重的非业务逻辑内容迁移到服务路由层面,使得服务集群主体能够具备更高的可复用性和可测试性。

下面我们通过实例例子来使用一下Zuul来作为服务的路有功能。

本文完全参照并抄写了翟永超博客: Spring
Cloud构建微服务架构(五)服务网关
服务网关是微服务架构中一个非常重要的部分。通过服务网关统一向外系统提供REST
API的过程中,除了具备服务路由、均衡负载功能之外,它还具备了权限控制等功能。Spring
Cloud
Netflix中的Zuul就担任了这样的一个角色,为微服务架构提供了前门保护的作用,同时将权限控制这些较重的非业务逻辑内容迁移到服务路由层面,使得服务集群主体能够具备更高的可复用性和可测试性。

服务网关简介

服务网关是微服务架构中一个不可或缺的部分。通过服务网关统一向外系统提供REST
API的过程中,除了具备服务路由、均衡负载功能之外,它还具备了权限控制等功能。Spring
Cloud
Netflix中的Zuul就担任了这样的一个角色,为微服务架构提供了前门保护的作用,同时将权限控制这些较重的非业务逻辑内容迁移到服务路由层面,使得服务集群主体能够具备更高的可复用性和可测试性。

简介

创建服务网关

新建一个maven工程,修改pom.xml引入 spring cloud 依赖:

<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.4.2.RELEASE</version></parent><dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-zuul</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-eureka</artifactId> </dependency></dependencies><dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Camden.SR2</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies></dependencyManagement>

resources 目录中创建 application.yml 配置文件,在配置文件内容:

spring: application: name: @project.artifactId@server: port: 80eureka: client: serviceUrl: defaultZone: http://localhost:8000/eureka/zuul: routes: add-service-demo: path: /add-service/** serviceId: add-service-demo

这里主要关注 zuul 相关的配置,path
定义了需要路由的url,serviceId 和注册中心中的application
相对应,定义了路由到哪个服务。(这里 serviceId 应该换行和 path
同级,oschina 的markdown格式显示有问题)在 java 目录中创建一个包
demo ,在包中创建启动入口 ServiceGatewayApplication.java

@EnableDiscoveryClient@SpringBootApplication@EnableZuulProxypublic class ServiceGatewayApplication { public static void main(String[] args) { SpringApplication.run(ServiceGatewayApplication.class, args); }}

到这里一个简单的服务网关就配置好了,先启动注册中心
service-registry-demo,然后启动两个 add-service-demo
工程,分别映射到 81008101
端口,然后再启动刚刚配置好的服务网关
service-gateway-demo。启动完成后访问注册中心页面
http://localhost:8000,可以看到注册了两个 add-service-demo 和一个
service-gateway-demo

图片 3服务注册中心

在浏览器中访问
http://localhost/add-service/add?a=1&b=2,可以看到返回结果:

{ msg: "操作成功", result: 3, code: 200}

多次访问,查看 add-service-demo
的控制台输出,可以看到服务网关对请求分发做了负载均衡。

准备工作

在使用Zuul之前,我们先构建一个服务注册中心、以及两个简单的服务,比如:我构建了一个service-A,一个service-B。然后启动eureka-server和这两个服务。通过访问eureka-server,我们可以看到service-A和service-B已经注册到了服务中心。

图片 4

如果您还不熟悉如何构建服务中心和注册服务,请先阅读Spring
Cloud构建微服务架构(一)服务注册与发现。

如果您不想自己动手准备,可以从这里获取示例代码:

我们通过实例例子来使用一下Zuul来作为服务的路有功能。

依赖

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-zuul</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>

配置

spring.application.name=api-gateway
server.port=5555
#通过url的方式做服务转发(不推荐)
#zuul.routes.api-a-url.path=/api-a-url/**
#zuul.routes.api-a-url.url=http://localhost:2222/
#通过serviceId的方式
#api-a api-b只是自定义的名称
zuul.routes.api-a.path=/api-a/**
zuul.routes.api-a.serviceId=compute-service-a
zuul.routes.api-b.path=/api-b/**
zuul.routes.api-b.serviceId=compute-service-b
#注册为服务
eureka.client.serviceUrl.defaultZone=http://localhost:1111/eureka/

所有的/api-a/**路径的访问转发到compute-service-a服务
所有的/api-b/**路径的访问转发到compute-service-b服务
服务网关实现转发有两种方式,一种是通过url的方式转发,另一种是通过serviceId的方式转发。通过url转发的方式,则不需要eureka依赖。
推荐使用serviceId的映射方式,除了对Zuul维护上更加友好之外,serviceId映射方式还支持了断路器,对于服务故障的情况下,可以有效的防止故障蔓延到服务网关上而影响整个系统的对外服务

Application.java

@EnableZuulProxy
@SpringCloudApplication
public class Application {
    public static void main(String[] args) {
        new SpringApplicationBuilder(Application.class).web(true).run(args);
    }

    @Bean
    public AccessFilter accessFilter(){
        return new AccessFilter();
    }
}

自定义过滤器AccessFilter.java

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() {
        RequestContext currentContext = RequestContext.getCurrentContext();
        HttpServletRequest request = currentContext.getRequest();
        log.info(String.format("%s request to %s", request.getMethod(), request.getRequestURL().toString()));
        String accessToken = request.getParameter("accessToken");
        if(accessToken==null){
            log.warn("access token is empty");
            currentContext.setSendZuulResponse(false);
            currentContext.setResponseStatusCode(401);
            currentContext.setResponseBody("accessToken is null");
            return null;
        }
        log.info("access token ok");
        return null;
    }
}

自定义过滤器的实现,需要继承ZuulFilter,需要重写实现下面四个方法:

  • filterType:返回一个字符串代表过滤器的类型,在zuul中定义了四种不同生命周期的过滤器类型,具体如下:

    • pre:可以在请求被路由之前调用
    • routing:在路由请求时候被调用
    • post:在routing和error过滤器之后被调用
    • error:处理请求时发生错误时被调用
  • filterOrder:通过int值来定义过滤器的执行顺序

  • shouldFilter:返回一个boolean类型来判断该过滤器是否要执行,所以通过此函数可实现过滤器的开关。在上例中,我们直接返回true,所以该过滤器总是生效。

  • run:过滤器的具体逻辑。需要注意,这里我们通过ctx.setSendZuulResponse(false)令zuul过滤该请求,不对其进行路由,然后通过ctx.setResponseStatusCode(401)设置了其返回的错误码,当然我们也可以进一步优化我们的返回,比如,通过ctx.setResponseBody(body)对返回body内容进行编辑等。

将服务网关注册为服务,将权限系统做成另外一个服务,在网关里面调用这个服务,来实现权限控制。

我们已经知道,在微服务架构中,不同的微服务可以有不同的网络地址,各个微服务之间通过互相调用完成用户请求,客户端可能通过调用N个微服务的接口完成一个用户请求。比如:用户查看一个商品的信息,它可能包含商品基本信息、价格信息、评论信息、折扣信息、库存信息等等,而这些信息获取则来源于不同的微服务,诸如产品系统、价格系统、评论系统、促销系统、库存系统等等,那么要完成用户信息查看则需要调用多个微服务,这样会带来几个问题:

使用corsFilter解决前端跨域问题

在对外提供rest接口时,经常会遇到跨域问题,尤其是使用前后端分离架构时。可以在服务端使用cors技术,解决前端的跨域问题。这里我们在网关层解决跨域问题。修改
ServiceGatewayApplication.java ,增加一个CorsFilter,代码如下:

[@Bean](https://my.oschina.net/bean)public CorsFilter corsFilter() { final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); final CorsConfiguration config = new CorsConfiguration(); config.setAllowCredentials; config.addAllowedOrigin; config.addAllowedHeader; config.addAllowedMethod("OPTIONS"); config.addAllowedMethod; config.addAllowedMethod; config.addAllowedMethod; config.addAllowedMethod; config.addAllowedMethod; config.addAllowedMethod; source.registerCorsConfiguration("/**", config); return new CorsFilter;}

这样服务端的接口就可以支持跨域访问了。

开始使用Zuul

  • 引入依赖spring-cloud-starter-zuul、spring-cloud-starter-eureka,如果不是通过指定serviceId的方式,eureka依赖不需要,但是为了对服务集群细节的透明性,还是用serviceId来避免直接引用url的方式吧。


    org.springframework.cloud
    spring-cloud-starter-zuul


    org.springframework.cloud
    spring-cloud-starter-eureka

  •  应用主类使用@EnableZuulProxy注解开启Zuul

    @EnableZuulProxy
    @SpringCloudApplication
    public class Application {

    public static void main(String[] args) {
        new SpringApplicationBuilder(Application.class).web(true).run(args);
    }
    

    }

 

这里用了@SpringCloudApplication注解,之前没有提过,通过源码我们看到,它整合了@SpringBootApplication@EnableDiscoveryClient@EnableCircuitBreaker,主要目的还是简化配置。这几个注解的具体作用这里就不做详细介绍了,之前的文章已经都介绍过。

  • application.properties中配置Zuul应用的基础信息,如:应用名、服务端口等。

    spring.application.name=api-gateway
    server.port=5555

 

准备工作

1. 客户端多次请求不同的微服务,增加客户端的复杂性,

使用自定义filter过滤请求

自定义filter也很简单,只需要继承 ZuulFilter
就可以了。在demo包下新建一个filter的子包,用来存放自定义的filter类。新建一个filter类
MyFilter 继承 ZuulFilter

@Componentpublic class MyFilter extends ZuulFilter { @Override public String filterType() { return "pre"; } @Override public int filterOrder() { return 0; } @Override public boolean shouldFilter() { return true; } @Override public Object run() { RequestContext ctx = RequestContext.getCurrentContext(); HttpServletRequest request = ctx.getRequest(); String ip = getIpAddr; System.out.println("收到来自IP为: '" + ip + "'的请求"); return null; } private String getIpAddr(HttpServletRequest request) { String ip = request.getHeader("x-forwarded-for"); if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase { ip = request.getHeader("Proxy-Client-IP"); } if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase { ip = request.getHeader("WL-Proxy-Client-IP"); } if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase { ip = request.getRemoteAddr(); } return ip; }}

这个filter的逻辑很简单,就是从 HttpServletRequest
对象中获取请求IP,并打印出来。然后在 ServiceGatewayApplication.java
中增加 MyFilter 的配置:

@Beanpublic MyFilter myFilter() { return new MyFilter();}

重新启动 service-gateway-demo,再次发起请求,会看到控制台打印的IP。

我们回过头看一下,MyFilter 这个类从 ZuulFilter
继承之后做了哪些处理。首先覆写 ZuulFilter 中的4个方法,分别为
filterTypefilterOrdershouldFilter
run。这4个方法看名字就知道有什么作用:

  • filterType 定义了filter的类型
    • pre 表示在请求被路由之前调用
    • route 请求被路由时调用,时机比pre
    • post 在路由完成后调用
    • error 发生错误时调用
  • filterOrder 定义过滤器的执行顺序,值小的先执行
  • shouldFilter 是否需要过滤
  • run 过滤器的具体执行逻辑

demo源码 spring-cloud-1.0/service-gateway-demo

Zuul配置

完成上面的工作后,Zuul已经可以运行了,但是如何让它为我们的微服务集群服务,还需要我们另行配置,下面详细的介绍一些常用配置内容。

在使用Zuul之前,需要先构建一个服务注册中心、以及两个简单的服务,构建了一个service-A,一个service-B。然后启动eureka-server和这两个服务。通过访问eureka-server,我们可以看到service-A和service-B已经注册到了服务中心。

2. 认证复杂,每个服务都要进行认证;

使用docker-maven-plugin打包并生成docker镜像

复制 application.yml,重命名为 application-docker.yml,修改
defaultZone为:

eureka: client: serviceUrl: defaultZone: http://service-registry:8000/eureka/

这里修改了 defaultZone 的访问url,如何修改取决于部署docker容器时的
–link 参数, –link 可以让两个容器之间互相通信。

修改 application.yml 中的 spring 节点为:

spring: application: name: @project.artifactId@ profiles: active: @activatedProperties@

这里增加了 profiles
的配置,在maven打包时选择不同的profile,加载不同的配置文件。

在pom.xml文件中增加:

<properties> <java.version>1.8</java.version> <!-- 指定java版本 --> <!-- 镜像前缀,推送镜像到远程库时需要,这里配置了一个阿里云的私有库 --> <docker.image.prefix> registry.cn-hangzhou.aliyuncs.com/ztecs </docker.image.prefix> <!-- docker镜像的tag --> <docker.tag>demo</docker.tag> <!-- 激活的profile --> <activatedProperties></activatedProperties></properties><profiles> <!-- docker环境 --> <profile> <id>docker</id> <properties> <activatedProperties>docker</activatedProperties> <docker.tag>docker-demo-${project.version}</docker.tag> </properties> </profile></profiles><build> <defaultGoal>install</defaultGoal> <finalName>${project.artifactId}</finalName> <resources> <resource> <directory>src/main/resources</directory> <filtering>true</filtering> </resource> </resources> <plugins> <!-- 配置spring boot maven插件,把项目打包成可运行的jar包 --> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <executable>true</executable> </configuration> </plugin> <!-- 打包时跳过单元测试 --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> <configuration> <skipTests>true</skipTests> </configuration> </plugin> <!-- 配置docker maven插件,绑定install生命周期,在运行maven install时生成docker镜像 --> <plugin> <groupId>com.spotify</groupId> <artifactId>docker-maven-plugin</artifactId> <version>0.4.13</version> <executions> <execution> <phase>install</phase> <goals> <goal>build</goal> <goal>tag</goal> </goals> </execution> </executions> <configuration> <!-- 修改这里的docker节点ip,需要打开docker节点的远程管理端口2375, 具体如何配置可以参照之前的docker安装和配置的文章 --> <dockerHost>http://docker节点ip:2375</dockerHost> <imageName>${docker.image.prefix}/${project.build.finalName}</imageName> <baseImage>java</baseImage> <!-- 这里的entryPoint定义了容器启动时的运行命令,容器启动时运行 java -jar 包名 , -Djava.security.egd这个配置解决tomcat8启动时,因为需要收集环境噪声来生成安全随机数导致启动过慢的问题--> <entryPoint> ["java", "-Djava.security.egd=file:/dev/./urandom", "-jar", "/${project.build.finalName}.jar"] </entryPoint> <resources> <resource> <targetPath>/</targetPath> <directory>${project.build.directory}</directory> <include>${project.build.finalName}.jar</include> </resource> </resources> <image>${docker.image.prefix}/${project.build.finalName}</image> <newName>${docker.image.prefix}/${project.build.finalName}:${docker.tag}</newName> <forceTags>true</forceTags> <!-- 如果需要在生成镜像时推送到远程库,pushImage设为true --> <pushImage>false</pushImage> </configuration> </plugin> </plugins></build>

选择 docker profile,运行 mvn install -P docker
,打包项目并生成docker镜像,注意docker-maven-plugin中的 <entryPoint>
标签里的内容不能换行,否则在生成docker镜像的时候会报错
。运行成功后,登录docker节点,运行
docker images 应该可以看到刚才打包生成的镜像了。

服务路由

通过服务路由的功能,我们在对外提供服务的时候,只需要通过暴露Zuul中配置的调用地址就可以让调用方统一的来访问我们的服务,而不需要了解具体提供服务的主机信息了。

在Zuul中提供了两种映射方式:

  • 通过url直接映射,我们可以如下配置:

    # routes to url
    zuul.routes.api-a-url.path=/api-a-url/**
    zuul.routes.api-a-url.url=

 

该配置,定义了,所有到Zuul的中规则为:/api-a-url/**的访问都映射到http://localhost:2222/上,也就是说当我们访问http://localhost:5555/api-a-url/add?a=1&b=2的时候,Zuul会将该请求路由到:http://localhost:2222/add?a=1&b=2上。

其中,配置属性zuul.routes.api-a-url.path中的api-a-url部分为路由的名字,可以任意定义,但是一组映射关系的path和url要相同,下面讲serviceId时候也是如此。

  • 通过url映射的方式对于Zuul来说,并不是特别友好,Zuul需要知道我们所有为服务的地址,才能完成所有的映射配置。而实际上,我们在实现微服务架构时,服务名与服务实例地址的关系在eureka
    server中已经存在了,所以只需要将Zuul注册到eureka
    server上去发现其他服务,我们就可以实现对serviceId的映射。例如,我们可以如下配置:

    zuul.routes.api-a.path=/api-a/
    zuul.routes.api-a.serviceId=service-A
    zuul.routes.api-b.path=/api-b/

    zuul.routes.api-b.serviceId=service-B
    eureka.client.serviceUrl.defaultZone=

 

针对我们在准备工作中实现的两个微服务service-A和service-B,定义了两个路由api-a和api-b来分别映射。另外为了让Zuul能发现service-A和service-B,也加入了eureka的配置。

接下来,我们将eureka-server、service-A、service-B以及这里用Zuul实现的服务网关启动起来,在eureka-server的控制页面中,我们可以看到分别注册了service-A、service-B以及api-gateway

图片 5

尝试通过服务网关来访问service-A和service-B,根据配置的映射关系,分别访问下面的url

  • http://localhost:5555/api-a/add?a=1&b=2:通过serviceId映射访问service-A中的add服务
  • http://localhost:5555/api-b/add?a=1&b=2:通过serviceId映射访问service-B中的add服务
  • http://localhost:5555/api-a-url/add?a=1&b=2:通过url映射访问service-A中的add服务

推荐使用serviceId的映射方式,除了对Zuul维护上更加友好之外,serviceId映射方式还支持了断路器,对于服务故障的情况下,可以有效的防止故障蔓延到服务网关上而影响整个系统的对外服务

开始使用Zuul

3. http请求增加,效率不高;

启动docker容器并注册服务

在前一篇中,已经启动了 service-registry-demo
add-service-demo,并且在两个容器之间建立了连接。这里启动
service-gateway-demo

docker run -d --name service-gateway-demo --publish 80:80 --link service-registry-demo:service-registry \ --link add-service-demo --volume /etc/localtime:/etc/localtime \ registry.cn-hangzhou.aliyuncs.com/ztecs/service-gateway-demo:docker-demo-1.0

这里比起上一篇启动 add-service-demo 容器的命令,有两个 –link
,分别连接了 service-registry-demo
add-service-demo,因为服务网关不仅需要注册到服务注册中心,还需要和后端提供的服务进行连接。启动完成之后,访问注册中心的页面
http://宿主机IP:8000 查看服务注册信息,可以发现 service-gateway-demo
也注册成功了。这时候就可以通过网关访问 add-service-demo
提供的服务了。注意:在前一篇启动 add-service-demo 时使用了
--publish把端口映射到了宿主机,在部署服务网关的情况下,后端服务就不需要映射到宿主机了,所有对服务的访问都通过网关进行路由,避免透过网关直接访问。

可以把3条启动命令封装到一个shell里:

docker run -d --name service-registry-demo --publish 8000:8000 \ --volume /etc/localtime:/etc/localtime \ registry.cn-hangzhou.aliyuncs.com/ztecs/service-registry-demo:docker-demo-1.0echo 'sleep 30s to next step...'sleep 30sdocker run -d --name add-service-demo --link service-registry-demo:service-registry \ --volume /etc/localtime:/etc/localtime \ registry.cn-hangzhou.aliyuncs.com/ztecs/add-service-demo:docker-demo-1.0docker run -d --name service-gateway-demo --publish 80:80 --link service-registry-demo:service-registry \ --link add-service-demo --volume /etc/localtime:/etc/localtime \ registry.cn-hangzhou.aliyuncs.com/ztecs/service-gateway-demo:docker-demo-1.0

这里的 sleep 30s 是为了让 service-registry-demo 启动完成之后再启动
add-service-demo
service-gateway-demo。在启动完成之后,通过网关访问接口时,可能会报错Load balancer does not have available server for client: add-service-demo
这是因为 service-gateway-demoadd-service-demo
同时启动,service-gateway-demo 在向注册中心注册时,add-service-demo
可能还没有来得及注册,导致 service-gateway-demo 获取不到
add-service-demo 的注册信息,过个几十秒再访问就可以了。

服务过滤

在完成了服务路由之后,我们对外开放服务还需要一些安全措施来保护客户端只能访问它应该访问到的资源。所以我们需要利用Zuul的过滤器来实现我们对外服务的安全控制。

在服务网关中定义过滤器只需要继承ZuulFilter抽象类实现其定义的四个抽象函数就可对请求进行拦截与过滤。

比如下面的例子,定义了一个Zuul过滤器,实现了在请求被路由之前检查请求中是否有accessToken参数,若有就进行路由,若没有就拒绝访问,返回401 Unauthorized错误。

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() {
        RequestContext ctx = RequestContext.getCurrentContext();
        HttpServletRequest request = ctx.getRequest();
        log.info(String.format("%s request to %s", 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;
    }
}

 

自定义过滤器的实现,需要继承ZuulFilter,需要重写实现下面四个方法:

  • filterType:返回一个字符串代表过滤器的类型,在zuul中定义了四种不同生命周期的过滤器类型,具体如下:
    • pre:可以在请求被路由之前调用
    • routing:在路由请求时候被调用
    • post:在routing和error过滤器之后被调用
    • error:处理请求时发生错误时被调用
  • filterOrder:通过int值来定义过滤器的执行顺序
  • shouldFilter:返回一个boolean类型来判断该过滤器是否要执行,所以通过此函数可实现过滤器的开关。在上例中,我们直接返回true,所以该过滤器总是生效。
  • run:过滤器的具体逻辑。需要注意,这里我们通过ctx.setSendZuulResponse(false)令zuul过滤该请求,不对其进行路由,然后通过ctx.setResponseStatusCode(401)设置了其返回的错误码,当然我们也可以进一步优化我们的返回,比如,通过ctx.setResponseBody(body)对返回body内容进行编辑等。

在实现了自定义过滤器之后,还需要实例化该过滤器才能生效,我们只需要在应用主类中增加如下内容:

@EnableZuulProxy
@SpringCloudApplication
public class Application {
    public static void main(String[] args) {
        new SpringApplicationBuilder(Application.class).web(true).run(args);
    }
    @Bean
    public AccessFilter accessFilter() {
        return new AccessFilter();
    }
}

 

启动该服务网关后,访问:

  • http://localhost:5555/api-a/add?a=1&b=2:返回401错误
  • http://localhost:5555/api-a/add?a=1&b=2&accessToken=token:正确路由到server-A,并返回计算内容

对于其他一些过滤类型,这里就不一一展开了,根据之前对filterType生命周期介绍,可以参考下图去理解,并根据自己的需要在不同的生命周期中去实现不同类型的过滤器。

图片 6alt

最后,总结一下为什么服务网关是微服务架构的重要部分,是我们必须要去做的原因:

  • 不仅仅实现了路由功能来屏蔽诸多服务细节,更实现了服务级别、均衡负载的路由。
  • 实现了接口权限校验与微服务业务逻辑的解耦。通过服务网关中的过滤器,在各生命周期中去校验请求的内容,将原本在对外服务层做的校验前移,保证了微服务的无状态性,同时降低了微服务的测试难度,让服务本身更集中关注业务逻辑的处理。
  • 实现了断路器,不会因为具体微服务的故障而导致服务网关的阻塞,依然可以对外服务。

本文完整示例可参考:Chapter9-1-5

 

  1. 引入依赖spring-cloud-starter-zuul、spring-cloud-starter-eureka,如果不是通过指定serviceId的方式,eureka依赖不需要,但是为了对服务集群细节的透明性,还是用serviceId来避免直接引用url的方式吧。

4. 存在跨域请求,比较复杂。

最后

目前我们已经成功搭建了 服务注册中心服务网关
后端服务,也创建了两个服务调用者 ribbon
feign配置中心断路器 还没有涉及,配置中心 由于
spring cloud bus 需要用到消息队列 rabbitmq
kafka,在进行配置中心的开发之前,需要先部署消息队列。断路器
的功能会和 hystrix-dashboard 断路监控一起放出,包括 turbine
zipkinspring cloud sleuth
服务调用追踪,这些都属于服务端异常监控范畴。由于配置中心和服务追踪都涉及到消息队列,下一篇先脱离
spring cloud,介绍一下 docker 环境下的 rabbitmq 部署、AMQP
协议、以及使用 spring AMQP 进行消息收发。

那么问题来了?我们如何解决这个问题呢,可以这么想,不要让前端直接知道后台诸多微服务的存在,我们的系统本身就是从业务领域的层次上进行划分,形成多个微服务,这是后台的处理方式,对于前台而言,仍然类似于单体应用一样,一次请求即可,于是我们可以在客户端和服务端之间增加一个API网关,所有的外部请求先通过这个微服务网关,它只需跟网关进行交互,而由网关进行各个微服务的调用,如图:

<properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
        <spring-cloud.version>Dalston.RELEASE</spring-cloud.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-zuul</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-eureka</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>

图片 7图片发自简书App

  1. 应用主类使用@EnableZuulProxy注解开启Zuul

这样的话,我们就可以解决上面提到的问题,同时开发就可以得到相应的简化,还可以有如下优点:

1. 减少客户端与微服务之间的调用次数,提高效率;

@EnableZuulProxy
@SpringCloudApplication
public class ZuulGatewayApplication {

    public static void main(String[] args) {
        //SpringApplication.run(ZuulGatewayApplication.class, args);
        new SpringApplicationBuilder(ZuulGatewayApplication.class).web(true).run(args);
    }

}

2. 便于监控,可在网关中监控数据,可以做统一切面任务处理;

注意:
@SpringCloudApplication注解,它整合了@SpringBootApplication、@EnableDiscoveryClient、@EnableCircuitBreaker,主要目的还是简化配置。

3. 便于认证,只需要在网关进行认证即可,无需每个微服务都进行认证;

  1. application.properties中配置Zuul应用的基础信息,如:应用名、服务端口等。

4. 降低客户端与服务端的耦合度。

这里可以联想到一个概念,面向对象设计中的门面模式,即对客户端隐藏细节,API网关也是类似的东西,只不过叫法不同而已。它是系统的入口,封装了应用程序的内部结构,为客户端提供统一服务,一些与业务本身功能无关的公共逻辑可以在这里实现,诸如认证、鉴权、监控、缓存、负载均衡、流量管控、路由转发等等,如图:

spring.application.name=api-gateway
server.port=5555

图片 8图片发自简书App

Zuul配置
完成上面的工作后,Zuul已经可以运行了,但是如何让它为我们的微服务集群服务,还需要我们另行配置,下面详细的介绍一些常用配置内容。
服务路由
通过服务路由的功能,我们在对外提供服务的时候,只需要通过暴露Zuul中配置的调用地址就可以让调用方统一的来访问我们的服务,而不需要了解具体提供服务的主机信息了。
在Zuul中提供了两种映射方式:
通过url直接映射,我们可以如下配置:

总结一下,服务网关大概就是四个功能:统一接入、流量管控、协议适配、安全维护,如图:

# routes to url
zuul.routes.api-a-url.path=/api-a-url/**
zuul.routes.api-a-url.url=http://localhost:2222/

图片 9图片发自简书App

该配置,定义了,所有到Zuul的中规则为:/api-a-url/**
的访问都映射到http://localhost:2222/
上,也就是说当我们访问http://localhost:5555/api-a-url/add?a=1&b=2
的时候,Zuul会将该请求路由到:http://localhost:2222/add?a=1&b=2
上。
其中,配置属性zuul.routes.api-a-url.path中的api-a-url部分为路由的名字,可以任意定义,但是一组映射关系的path和url要相同,下面讲serviceId时候也是如此。
通过url映射的方式对于Zuul来说,并不是特别友好,Zuul需要知道我们所有为服务的地址,才能完成所有的映射配置。而实际上,我们在实现微服务架构时,服务名与服务实例地址的关系在eureka
server中已经存在了,所以只需要将Zuul注册到eureka
server上去发现其他服务,我们就可以实现对serviceId的映射。例如,我们可以如下配置:

服务网关

zuul.routes.api-a.path=/api-a/**
zuul.routes.api-a.serviceId=service-A

zuul.routes.api-b.path=/api-b/**
zuul.routes.api-b.serviceId=service-B

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

在目前的网关解决方案里,有Nginx+ Lua、Kong、Spring Cloud
Zuul等等,这里以Zuul为例进行说明,它是Netflix公司开源的一个API网关组件,Spring
Cloud对其进行封装做到开箱即用,同时,Zuul还可以与Eureka、Ribbon、Hystrix等组件配合使用。简单说来,Zuul实现了两个功能:路由转发、过滤器:

针对我们在准备工作中实现的两个微服务service-A和service-B,定义了两个路由api-a和api-b来分别映射。另外为了让Zuul能发现service-A和service-B,也加入了eureka的配置。
接下来,我们将eureka-server、service-A、service-B以及这里用Zuul实现的服务网关启动起来,在eureka-server的控制页面中,我们可以看到分别注册了service-A、service-B以及api-gateway

1. 路由转发:接受请求,转发到后端服务;

图片.png

图片 10图片发自简书App

尝试通过服务网关来访问service-A和service-B,根据配置的映射关系,分别访问下面的url
http://localhost:5555/api-a/add?a=1&b=2:通过serviceId映射访问service-A中的add服务
http://localhost:5555/api-b/add?a=1&b=2:通过serviceId映射访问service-B中的add服务
http://localhost:5555/api-a-url/add?a=1&b=2:通过url映射访问service-A中的add服务

2. 过滤器:提供一系列过滤器完成权限、日志、限流等切面任务。

注意:推荐使用serviceId的映射方式,除了对Zuul维护上更加友好之外,serviceId映射方式还支持了断路器,对于服务故障的情况下,可以有效的防止故障蔓延到服务网关上而影响整个系统的对外服务

图片 11图片发自简书App

服务过滤
在完成了服务路由之后,我们对外开放服务还需要一些安全措施来保护客户端只能访问它应该访问到的资源。所以我们需要利用Zuul的过滤器来实现我们对外服务的安全控制。
在服务网关中定义过滤器只需要继承ZuulFilter
抽象类实现其定义的四个抽象函数就可对请求进行拦截与过滤。
下面的例子,定义了一个Zuul过滤器,实现了在请求被路由之前检查请求中是否有accessToken
参数,若有就进行路由,若没有就拒绝访问,返回401 Unauthorized错误。

原理

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() {
        RequestContext ctx = RequestContext.getCurrentContext();
        HttpServletRequest request = ctx.getRequest();

        log.info(String.format("%s request to %s", 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;
    }

}

Zuul的核心是一系列过滤器,开发者通过实现过滤器接口,可以做大量切面任务,即AOP思想的应用。Zuul的过滤器之间没有直接的相互通信,而是通过本地ThreadLocal变量进行数据传递的。

自定义过滤器的实现,需要继承ZuulFilter,需要重写实现下面四个方法:

图片 12图片发自简书App

1. filterType:返回一个字符串代表过滤器的类型,在zuul中定义了四种不同生命周期的过滤器类型,具体如下:
    pre:可以在请求被路由之前调用
    routing:在路由请求时候被调用
    post:在routing和error过滤器之后被调用
    error:处理请求时发生错误时被调用
2. filterOrder:通过int值来定义过滤器的执行顺序
3. shouldFilter:返回一个boolean类型来判断该过滤器是否要执行,所以通过此函数可实现过滤器的开关。在上例中,我们直接返回true,所以该过滤器总是生效。
4. run:过滤器的具体逻辑。需要注意,这里我们通过ctx.setSendZuulResponse(false)令zuul过滤该请求,不对其进行路由,然后通过ctx.setResponseStatusCode(401)设置了其返回的错误码,当然我们也可以进一步优化我们的返回,比如,通过ctx.setResponseBody(body)对返回body内容进行编辑等。

配置步骤

在实现了自定义过滤器之后,还需要实例化该过滤器才能生效,我们只需要在应用主类中增加如下内容:

添加Zuul依赖,非常简单;

@EnableZuulProxy
@SpringCloudApplication
public class ZuulGatewayApplication {

    public static void main(String[] args) {
        //SpringApplication.run(ZuulGatewayApplication.class, args);
        new SpringApplicationBuilder(ZuulGatewayApplication.class).web(true).run(args);
    }

    @Bean
    public AccessFilter accessFilter() {
        return new AccessFilter();
    }
}

图片 13图片发自简书App

启动该服务网关后,访问:

在启动类添加@EnableZuulProxy注解,开启路由功能:

http://localhost:5555/api-a/add?a=1&b=2:返回401错误
http://localhost:5555/api-a/add?a=1&b=2&accessToken=token:正确路由到server-A,并返回计算内容

图片 14图片发自简书App

工程请参见:
ComputeService-A
ComputeService-B
zuul-gateway

路由配置:

图片 15图片发自简书App

说明:凡是以/cjgl/打头的请求,路由到ly-microservice-cjgl微服务

测试,打开浏览器,输入:

图片 16图片发自简书App

图片 17图片发自简书App

ly-microservice-clgl微服务

安全认证

Zuul不仅实现路由转发,还可以充当过滤器,进行安全认证等操作,这里我们做一个示例,ZuulFilter是Zuul中核心组件,可以通过继承该抽象类,重写其中的方法实现。

我们这里要求用户请求时,必须传递一个token参数,没带token参数进行请求表示非法访问,我们给予拦截,并给客户端提示,操作步骤如下:

新建AccessFilter类,继承ZuulFilter类,重写其中的run方法,如图:

图片 18图片发自简书App

修改启动类,增加bean注入,如图:

图片 19图片发自简书App

重启服务,再次输入:

图片 20图片发自简书App

加上accessToken参数,再访问:

图片 21图片发自简书App

以上就是对服务网关的理解和简单应用,后续再进行补充完善。

如果觉得文章对你有启发,就请顺手点一下“❤”,或者关注一下再走啦。么么哒!

You may also like...

发表评论

电子邮件地址不会被公开。 必填项已用*标注

网站地图xml地图