springcloud介绍

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

为了保证对外服务的安全性,需要实现对服务访问的权限控制,如果通过开放服务权限控制的方式去实现权限控制会入侵业务逻辑,破坏了服务集群无状态的特点,还无法直接复用既有接口,所以,通过服务网关的形式来解决以上问题。

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

构建服务网关

使用zuul之前,创建好服务端和消费端分别为:zuul-eureka-consumer、zuul-eureka-previder。
创建网关模块:zuul-gateway,引入依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?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>

<groupId>com.hu</groupId>
<artifactId>zuul-gateway</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-parent</artifactId>
<version>Dalston.SR1</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>
</project>

创建配置文件application.yml,并加入服务名、端口号、eureka注册中心的地址。

1
2
3
4
5
6
7
8
9
10
11
12
server:
port: 7903
spring:
application:
name: zuul-gateway
eureka:
client:
serviceUrl:
defaultZone: http://user:123456@localhost:8761/eureka
logging:
level:
root: INFO

启动类:

1
2
3
4
5
6
7
8
//开启Zuul的功能
@EnableZuulProxy
@SpringCloudApplication
public class ZuulApplication {
public static void main(String[] args){
SpringApplication.run(ZuulApplication.class, args);
}
}

这样,基于Spring Cloud Zuul服务网关就已经构建完毕。

由于Spring Cloud Zuul在整合了Eureka之后,具备默认的服务路由功能,当zuul-gateway应用启动并注册到eureka之后,服务网关会发现已经启动的两个服务zuul-eureka-consumer、zuul-eureka-previder,这时候Zuul就会创建两个路由规则。每个路由规则都包含:外部请求的匹配规则和路由的服务ID。

这里就会创建两个规则:
zuul-eureka-consumer服务的请求规则为:/consumer-user/**
zuul-eureka-previder服务的请求规则为:/provider-user/**

通过访问7903端口的服务网关来验证上述路由的正确性:
http://localhost:7903/provider-user/user,该请求将最终被路由到zuul-eureka-previder的/user接口上。
访问成功返回数据:江小白

传统路由配置

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

单实例配置:通过一组zuul.routes..path与zuul.routes..url参数对的方式配置

1
2
zuul.routes.user-service.path=/user-service/**
zuul.routes.user-service.url=http://localhost:8080/

实现了对符合/user-service/**规则的请求路径转发到http://localhost:8080/地址的路由规则。
当有一个请求http://localhost:7903/user-service/hello被发送到API网关上,由于/user-service/hello能够被上述配置的path规则匹配,所以API网关会转发请求到http://localhost:8080/hello地址。

多实例配置:通过一组zuul.routes..path与zuul.routes..serviceId参数对的方式配置

1
2
3
4
zuul.routes.user-service.path=/user-service/**
zuul.routes.user-service.serviceId=user-service
ribbon.eureka.enabled=false
user-service.ribbon.listOfServers=http://localhost:8080/,http://localhost:8081/

实现了对符合/user-service/**规则的请求路径转发到http://localhost:8080/和http://localhost:8081/两个实例地址的路由规则。
serviceId是由用户手工命名的服务名称,配合.ribbon.listOfServers参数实现服务与实例的维护。
由于存在多个实例,API网关在进行路由转发时需要实现负载均衡策略,还需要Spring Cloud Ribbon的配合,Spring Cloud Zuul中自带了对Ribbon的依赖,只需要做一些配置即可。

ribbon.eureka.enabled:由于zuul.routes..serviceId指定的是服务名称,默认情况下Ribbon会根据服务发现机制来获取配置服务名对应的实例清单。
但是,该示例并没有整合类似Eureka之类的服务治理框架,所以需要将该参数设置为false,不然配置的serviceId是获取不到对应实例清单的。
user-service.ribbon.listOfServers:该参数内容与zuul.routes..serviceId的配置相对应,开头的user-service对应了serviceId的值,这两个参数的配置相当于在该应用内部手工维护了服务与实例的对应关系。

不论哪种方式,都需要为每一对映射关系指定一个名称,也就是上面配置中的,每一个就对应了一条路由规则。每条路由规则都需要通过path属性来定义一个用来匹配客户端请求的路径表达式,并通过url或serviceId属性来指定请求表达式映射具体实例地址或服务名。

服务路由配置

Spring Cloud Zuul通过与Spring Cloud Eureka的整合,实现了对服务实例的自动化维护,使用服务路由配置的时候,不需要向传统路由配置方式那样为serviceId去指定具体的服务实例地址,只需要通过一组zuul.routes..path与zuul.routes..serviceId参数对的方式配置即可。

例如:

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

实现了对符合/user-service/规则的请求路径转发到名为user-service的服务实例上去的路由规则。
还有一种更简洁的配置方式:zuul.routes.=,其中用来指定路由的具体服务名,用来配置匹配的请求表达式。
例如:zuul.routes.user-service=/user-service/

等价于上面两条的组合。

那么当采用path与serviceId以服务路由方式实现时候,没有配置任何实例地址的情况下,外部请求经过API网关的时候,它是如何被解析并转发到服务具体实例的呢?
在Spring Cloud Netflix中,Zuul巧妙的整合了Eureka来实现面向服务的路由。实际上,可以直接将API网关也看做是Eureka服务治理下的一个普通微服务应用。它除了会将自己注册到Eureka服务注册中心上之外,也会从注册中心获取所有服务以及它们的实例清单。
所以,在Eureka的帮助下,API网关服务本身就已经维护了系统中所有serviceId与实例地址的映射关系。当有外部请求到达API网关的时候,根据请求的URL路径找到最佳匹配的path规则,API网关就可以知道要将该请求路由到哪个具体的serviceId上去。由于在API网关中已经知道serviceId对应服务实例的地址清单,那么只需要通过Ribbon的负载均衡策略,直接在这些清单中选择一个具体的实例进行转发就能完成路由工作了。

过滤器

微服务应用提供的接口就可以通过统一的API网关入口被客户端访问。但是当请求时对它们的访问权限进行限制,系统不会把所有接口全部开放。当前的服务路由并没有限制权限这样的功能,实现对客户端请求的安全校验和权限控制,最简单的方法就是为每个微服务应用都实现过滤器或拦截器。但是这样并不好,增加日后的系统维护难度。所以直接把这些校验剥离出去,构建出一个独立的服务。在完成了剥离之后,还可能会在微服务应用中通过调用校验服务来实现校验,这种方式校验在本质上并没有完全脱离微服务应用,冗余的拦截器或过滤器依然会存在。

由于网关服务的加入,外部客户端访问我们的系统已经有了统一入口,通过在网关中完成校验和过滤,微服务应用端就可以去除各种复杂的过滤器和拦截器了,这使得微服务应用的接口开发和测试复杂度也得到了相应的降低。

Spring Cloud Zuul的另外一个核心功能:过滤器。允许开发者在API网关上通过定义过滤器来实现对请求的拦截与过滤。只需要继承ZuulFilter抽象类并实现它定义的四个抽象函数就可以完成对请求的拦截和过滤了。

过滤器的实现

定义Zuul过滤器,请求被路由之前检查HttpServletRequest中是否有某个参数,若有就进行路由,若没有就拒绝访问,自定义过滤器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
/**
* 继承ZuulFilter抽象类并重写了下面的四个方法来实现自定义的过滤器
* 定请求被路由之前检查HttpServletRequest中是否有某个参数,若有就进行路由,若没有就拒绝访问。
*/
public class ZuulFilter extends com.netflix.zuul.ZuulFilter {

private static Logger log = LoggerFactory.getLogger(ZuulFilter.class);

/**
* 过滤器的类型,它决定过滤器在请求的哪个生命周期中执行。这里定义为pre,代表会在请求被路由之前执行。
* @return
*/
@Override
public String filterType() {
return "pre";
}

/**
* 过滤器的执行顺序。当请求在一个阶段中存在多个过滤器时,需要根据该方法返回的值来依次执行。
* @return
*/
@Override
public int filterOrder() {
return 0;
}

/**
* 判断该过滤器是否需要被执行。
* 这里直接返回了true,该过滤器对所有请求都会生效。
* @return
*/
@Override
public boolean shouldFilter() {
return true;
}

/**
* 过滤器具体逻辑
* @return
*/
@Override
public Object run() {
//获取请求信息
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();

log.info(request.getMethod(), request.getRequestURL().toString());

Object accessToken = request.getParameter("token");
if(accessToken == null) {
log.warn("不通过");
//令zuul过滤该请求,不对其进行路由
ctx.setSendZuulResponse(false);
//设置了其返回的错误码
//还可以通过ctx.setResponseBody(body)对返回body内容进行编辑等。
ctx.setResponseStatusCode(401);
return null;
}
//TODO 还有一些校验信息略过
log.info("通过");
return null;
}

}

自定义过滤器之后,不会直接生效,创建具体的Bean才能启动该过滤器,比如在主类中增加如下内容:

1
2
3
4
@Bean
public ZuulFilter zuulFilter(){
return new ZuulFilter();
}

浏览:http://localhost:7903/consumer-user/user 日志显示

1
2
 INFO 11872 --- [nio-7903-exec-2] com.hu.filter.ZuulFilter                 : GET
WARN 11872 --- [nio-7903-exec-2] com.hu.filter.ZuulFilter : 不通过

浏览:http://localhost:7903/consumer-user/user?token=11
显示出结果,并且日志中显示出通过的信息。

Zuul常见问题

有时候当我们将Spring Cloud Zuul作为API网关接入网站类应用时,会遇到会话无法保持或者重定向后的HOST错误。
1、会话无法保持
通过跟踪一个HTTP请求经过Zuul到具体服务,再到返回结果的全过程。在传递的过程中,HTTP请求头信息中的Cookie和Authorization都没有被正确地传递给具体服务,所以最终导致会话状态没有得到保持。
从Zuul进行路由转发的过滤器(如:RibbonRoutingFilter)中进行一步步查看:
从过滤器的核心逻辑run方法看,其中调用了内部方法buildCommandContext来构建上下文,而buildCommandContext中调用了helper对象的buildZuulRequestHeaders方法来处理请求头信息,helper对象是ProxyRequestHelper类的实例;
而进入到ProxyRequestHelper类中看到,构建头信息的方法buildZuulRequestHeaders通过isIncludedHeader函数来判断当前请求的各个头信息是否在忽略的头信息清单中,如果是的话就不组织到此次转发的请求中去;
如果是忽略的头信息,则也要进行初始化,在PreDecorationFilter源码中可以看到,通过调用ProxyRequestHelper的addIgnoredHeaders方法来添加需要忽略的信息到请求上下文中,供后续ROUTE阶段的过滤器使用。
if/else块分别用来处理全局设置的敏感头信息和指定路由设置的敏感头信息。而全局的敏感头信息定义于ZuulProperties中:

1
2
3
4
5
@Data
@ConfigurationProperties("zuul")
public class ZuulProperties {
private Set<String> sensitiveHeaders = new LinkedHashSet<>(Arrays.asList("Cookie", "Set-Cookie", "Authorization"));
}

所以解决该问题的方法只需要通过设置sensitiveHeaders即可:

1
2
3
4
5
全局设置:
zuul.sensitive-headers=
指定路由设置:
zuul.routes.<routeName>.sensitive-headers=
zuul.routes.<routeName>.custom-sensitive-headers=true

2、重定向问题
这是Zuul没有正确的处理HTTP请求头信息中的Host导致。在Brixton版本中,Spring Cloud Zuul的PreDecorationFilter过滤器实现时完全没有考虑这一问题,但是在Camden版本之后,Zuul增强了该功能,只需要通过配置属性zuul.add-host-header=true就能让有问题的重定向操作得到正确的处理。

Zuu统一异常处理

1、try-catch处理
异常后通过设置上下文参数信息,回传:
error.status_code:错误编码
error.exception:Exception异常对象
error.message:错误信息

1
2
3
4
5
catch (ZuulException ex) {
context.set(ERROR_STATUS_CODE, ex.nStatusCode);
context.set("error.message", ex.errorCause);
context.set("error.exception", ex);
}

2、ErrorFilter处理
使用error类型的过滤器
请求生命周期的pre、route、post三个阶段中有异常抛出的时候都会进入error阶段的处理,可以通过创建一个error类型的过滤器来捕获这些异常信息,并根据这些异常信息在请求上下文中注入需要返回给客户端的错误描述,这里我们可以直接沿用在try-catch处理异常信息时用的那些error参数,这样就可以让这些信息被SendErrorFilter捕获并组织成消息响应返回给客户端。

参考 http://blog.didispace.com/spring-cloud-learning/