抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

Spring Cloud Zuul 服务网关配置

Zuul配置

zuul通配符

Zuul中的路由匹配规则使用了Ant风格定义,一共有三种不同的通配符:

通配符 含义 举例 解释
? 匹配任意单个字符 /feign-consumer/? 匹配/feign-consumer/a,/feign-consumer/b,/feign-consumer/c等
* 匹配任意数量的字符 /feign-consumer/* 匹配/feign-consumer/aaa,feign-consumer/bbb,/feign-consumer/ccc等,无法匹配/feign-consumer/a/b/c
** 匹配任意数量的字符 /feign-consumer/* 匹配/feign-consumer/aaa,feign-consumer/bbb,/feign-consumer/ccc等,也可以匹配/feign-consumer/a/b/c

路由配置详解

或许你会觉得神奇,之前我们什么也没有配置,通过http://localhost:9999/user-server/invokInfo已经可以正确的访问到我们的微服务了,这就是Zuul的默认路由映射功能在起作用,那么接下来具体来看看Zuul是怎么进行路由配置的。

服务路由默认规则

当我们构建API服务网关时引入Eureka时,那么Zuul会自动为每个服务都创建一个默认路由规则: 访问路径的前缀为serviceId配置的服务名称,也就是之前为什么我们能够所使用:

1
http://localhost:9999/user-server/invokInfo

来访问user-server中所提供的invokInfo服务端点的原因。

可以通过 http://localhost:9999/actuator/routes zuul提供的映射端点

1
2
3
4
5
{
/order-server/**: "order-server",
/config-server/**: "config-server",
/user-server/**: "user-server"
}
自定义微服务访问路径

配置格式为: zuul.routes.微服务Id = 指定路径

例如:

1
zuul.routes.user-service = /user/**

这样,我们后面就可以通过/user/来访问user-service所提供的服务,比如之前的访问可以更改为:

1
http://localhost:9999/user/invokInfo

所要配置的路径可以指定一个正则表达式来匹配路径,因此,/user/*只能匹配一级路径,但是通过/user/**可以匹配所有以/user/开头的路径。

服务屏蔽与路径屏蔽

​ 默认情况下,Eureka上所有注册的服务都会被Zuul创建映射关系来进行路由,但是对于我这里的例子来说,我希望提供服务的是user-server,config-server作为服务提供者只对服务消费者提供服务,不对外提供服务,如果使用默认的路由规则,则Zuul也会自动为config-server创建映射规则,这个时候我们可以采用如下方式来让Zuul跳过config-server服务

配置格式为: **zuul.ignored-services=微服务Id1,微服务Id2…**,多个微服务之间使用逗号分隔。

例如:

1
2
zuul.ignored-services=config-server #忽略服务,防止服务入侵
zuul.ignored-patterns=/config-server/** #忽略接口,屏蔽接口

访问端点测试

1
2
3
4
5
{
/user/**: "user-server",
/order-server/**: "order-server",
/user-server/**: "user-server"
}

有时候为了避免有些服务或者路径入侵,可以将它们屏蔽掉。

同时指定微服务Url和对应路径
1
2
zuul.routes.user-server.path=/user/**
zuul.routes.user-server.url=http://127.0.0.1:8082

如之前所述,通过url配置的路由不会由HystrixCommand来执行,自然,也就得不到Ribbon的负载均衡、降级、断路器等功能。所以在实施尽量使用serviceId进行配置,也可以采用下面的配置方式。

同时指定微服务Id和对应路径

如果需要配置多个服务实例,则配置如下:

1
2
zuul.routes.user-server.path=/user/**
zuul.routes.user-server.service-id=user-server
本地轮询方式

有些情况下需要进行测试,但是又不想使用eureka的注册列表

1
2
3
4
5
zuul.routes.user-server.path=/user/**
zuul.routes.user-server.service-id=user-server
#禁用eureka,并使用本地轮询列表
ribbon.eureka.enabled=false
user.ribbon.listOfServers: http://192.168.1.10:8081, http://192.168.1.11:8081
forward跳转到本地url
1
2
zuul.routes.user.path=/user/**
zuul.routes.user.url=forward:/user
路由前缀

可以通过zuul.prefix可为所有的映射增加统一的前缀。如: /api。默认情况下,代理会在转发前自动剥离这个前缀。如果需要转发时带上前缀,可以配置: zuul.stripPrefix=false来关闭这个默认行为。

例如:

1
2
3
4
5
# zuul 请求前缀
zuul.prefix=/api
#zuul 路由配置
zuul.routes.user-server.path=/user/**
zuul.routes.user-server.url=http://127.0.0.1:8082

访问端点返回

1
2
3
4
5
6
{
/api/user/**: "http://127.0.0.1:8082",
/api/order-server/**: "order-server",
/api/config-server/**: "config-server",
/api/user-server/**: "user-server"
}

请求需要加上/api 例如

1
http://localhost:9999/api/user-server/invokInfo

可以通过zuul.stripPrefix=false 关闭默认设置

1
zuul.routes.user-server.strip-prefix=false

注意: zuul.stripPrefix只会对zuul.prefix的前缀起作用。对于path指定的前缀不会起作用。

路由配置顺序

如果想按照配置的顺序进行路由规则控制,则需要使用YAML,如果是使用propeties文件,则会丢失顺序。

1
2
3
4
5
6
zuul:
routes:
user-server:
path: /user/**
order-server:
path: /**

上例如果是使用properties文件进行配置,则legacy就可能会先生效,这样users就没效果了。

自定义转换器

我们也可以一个转换器,让serviceId和路由之间使用正则表达式来自动匹配。例如:

1
2
3
4
5
6
@Bean
public PatternServiceRouteMapper serviceRouteMapper() {
return new PatternServiceRouteMapper(
"(?<name>^.+)-(?<version>v.+$)",
"${version}/${name}");
}

这样,serviceId为“users-v1”的服务,就会被映射到路由为“/v1/users/”的路径上。任何正则表达式都可以,但是所有的命名组必须包括servicePattern和routePattern两部分。如果servicePattern没有匹配一个serviceId,那就会使用默认的。在上例中,一个serviceId为“users”的服务,将会被映射到路由“/users/”中(不带版本信息)。这个特性默认是关闭的,而且只适用于已经发现的服务。

重试机制

生产环境中,由于各种原因,可能会使一次请求偶然失败,考虑到某些业务的体验,不能通过有感知的操作来触发,这时候就会用到重试机制了,Zuul可以配合Ribbon(默认个集成)来做重试。

1
2
3
4
5
6
#开启重试
zuul.retryable=true
#同一个服务重试的次数(除去首次)
zuul.ribbon.MaxAutoRetries=1
#切换相同服务数量
zuul.ribbon.MaxAutoRetriesNextServer=1

当然,此功能要慎用,有一些接口要保证幂等性,一定要做好相关工作。

Zuul的Header设置

敏感Header设置

​ 同一个系统中各个服务之间通过Headers来共享信息是没啥问题的,但是如果不想Headers中的一些敏感信息随着HTTP转发泄露出去话,需要在路由配置中指定一个忽略Header的清单。

​ 默认情况下,Zuul在请求路由时,会过滤HTTP请求头信息中的一些敏感信息,默认的敏感头信息通过zuul.sensitiveHeaders定义,包括CookieSet-CookieAuthorization。配置的sensitiveHeaders可以用逗号分割。

对指定路由的可以用下面进行配置:

1
2
3
# 对指定路由开启自定义敏感头
zuul.routes.[route].customSensitiveHeaders=true
zuul.routes.[route].sensitiveHeaders=[这里设置要过滤的敏感头]

设置全局:

1
zuul.sensitiveHeaders=[这里设置要过滤的敏感头]
忽略Header设置

如果每一个路由都需要配置一些额外的敏感Header时,那你可以通过zuul.ignoredHeaders来统一设置需要忽略的Header。

1
zuul.ignoredHeaders=[这里设置要忽略的Header]

​ 在默认情况下是没有这个配置的,如果项目中引入了Spring Security,那么Spring Security会自动加上这个配置,默认值为: Pragma,Cache-Control,X-Frame-Options,X-Content-Type-Options,X-XSS-Protection,Expries

​ 此时,如果还需要使用下游微服务的Spring Security的Header时,可以增加下面的设置:

1
zuul.ignoreSecurityHeaders=false

Zuul Http Client

Zuul的Http客户端支持Apache Http、Ribbon的RestClient和OkHttpClient,默认使用Apache HTTP客户端。可以通过下面的方式启用相应的客户端:

1
2
3
4
5
# 启用Ribbon的RestClient
ribbon.restclient.enabled=true

# 启用OkHttpClient
ribbon.okhttp.enabled=true

如果需要使用OkHttpClient需要注意在你的项目中已经包含com.squareup.okhttp3相关包。

Zuul 上传文件

对于小文件(1M以内)上传无需任何处理,对于大文件(10M以上)上传,需要为上传路径添加 /zuul 前缀,也可以使用 zuul.servlet-path 自定义前缀

​ 假设 zuul.routes.file-upload = /file-upload/**,如果 http://{HOST}:{PORT}/uoload 是微服务 file-upload 上传路径,则可以使用 Zuul 的 /zuul/file-upload/upload 路径上传大文件

​ 如果 Zuul 使用 Ribbon 做负载均衡,那么对于超大文件(例如500M)需要提升超时时间

准备工作

添###### 加上传接口

在 user-server服务添加上传接口

1
2
3
4
5
6
7
@PostMapping("/upload")
public String handleFileUpLoad(@RequestParam(value = "file") MultipartFile file) throws IOException {
byte[] bytes = file.getBytes();
File fileToSave = new File(file.getOriginalFilename());
FileCopyUtils.copy(bytes, fileToSave);
return fileToSave.getAbsolutePath();
}
上传服务配置

需要在 user-server服务的 application.properties 配置一些上传大小的配置

1
2
3
4
# 上传文件总的最大值
spring.servlet.multipart.max-request-size=1000MB
# 单个文件的最大值
spring.servlet.multipart.max-file-size=500MB
上传服务测试

使用postman测试文件上传,发现能够成功上传

使用zuul上传

通过配置的 zuul进行上传文件测试

服务超时

我们先用一个1k的小文件测试上传发现服务调用超时

配置超时时间

在zuul的application.properties中添加hystrix和ribbon的超时设置

1
2
3
4
5
6
# hystrix的超时时间必须大于ribbon的超时时间
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=60000
# 请求连接的超时时间
ribbon.connectTimeout=2000
# 请求处理的超时时间
ribbon.readTimeout=5000

再次测试发现成功上传

文件过大

我们刚刚测试的是小文件上传,我们这次使用一个大文件上传

我们发现大文件上传会报错误,文件超过了大小设置,明明刚才能够直接上传的。

添加zuul前缀

添加 /zuul 前缀,通过http://localhost:9999/zuul/user/upload 来进行测试

这次发现文件上传成功

小结
  1. 通过zuul上传文件 小于1m的可以直接进行上传
  2. 对于大文件通过zuul上传需要加上前缀 zuul
  3. zuul上传一般需要进行hystrix和ribbon的超时设置

Zuul 过滤器

Zuul 大部分功能都是通过过滤器实现的,Zuul 中定义了 4 种标准过滤器类型,对应了典型的生命周期

PRE:在请求被路由之前调用,可用于身份验证、集群中选择请求的微服务、记录调试信息等

ROUTING:请求路由到微服务时执行,用于构建发送给微服务的请求,使用 HttpClient 或 Ribbon 请求微服务

POST:在路由到微服务后执行,可用来为响应添加 Header,收集统计信息和指标、将相应从微服务发送给客户端等

ERROR:在其他阶段发生错误时执行该过滤器

Zuul 还允许创建自定义过滤器类型,例如定制一种 STATIC 类型过滤器,直接在 Zuul 中生存响应,而不将请求转发到后端的微服务

请求生命周期

​ 外部http请求到达api网关服务的时候,首先它会进入第一个阶段pre,在这里它会被pre类型的过滤器进行处理。该类型过滤器的主要目的是在进行请求路由之前做一些前置加工,比如请求的校验等。在完成了pre类型的过滤器处理之后,请求进入第二个阶段routing,也就是之前说的路由请求转发阶段,请求将会被routing类型的处理器处理。这里的具体处理内容就是将外部请求转发到具体服务实例上去的过程,当服务实例请求结果都返回之后,routing阶段完成,请求进入第三个阶段post。此时请求将会被post类型的过滤器处理,这些过滤器在处理的时候不仅可以获取到请求信息,还能获取到服务实例的返回信息,所以在post类型的过滤器中,我们可以对处理结果进行一些加工或转换等内容。另外,还有一个特殊的阶段error,该阶段只有在上述三个阶段中发生异常的时候才会触发,但是它的最后流向还是post类型的过滤器,因为它需要通过post过滤器将最终结果返回给请求客户端(对于error过滤器的处理,在spring cloud zuul的过滤链中实际上有一些不同)

核心过滤器

​ 在spring cloud zuul中,为了让api网关组件可以被更方便的使用,它在http请求生命周期的各个阶段默认实现了一批核心过滤器,它们会在api网关服务启动的时候被自动加载和启动。我们可以在源码中查看和了解它们,它们定义与spring-cloud-netflix-core模块的org.springframework.cloud.netflix.zuul.filters包下。在默认启动的过滤器中包含三种不同生命周期的过滤器,这些过滤器都非常重要,可以帮组我们理解zuul对外部请求处理的过程,以及帮助我们在此基础上扩展过滤器去完成自身系统需要的功能。

pre过滤器

在请求被路由之前调用,可用于身份验证、集群中选择请求的微服务、记录调试信息等

ServletDetectionFilter

​ ServletDetectionFilter:它的执行顺序为-3,是最先被执行的过滤器。该过滤器总是会被执行,主要用来检测当前请求是通过Spring的DispatcherServlet处理运行的,还是通过ZuulServlet来处理运行的。它的检测结果会以布尔类型保存在当前请求上下文的isDispatcherServletRequest参数中,这样后续的过滤器中,我们就可以通过RequestUtils.isDispatcherServletRequest()RequestUtils.isZuulServletRequest()方法来判断请求处理的源头,以实现后续不同的处理机制。一般情况下,发送到api网关的外部请求都会被Spring的DispatcherServlet处理,除了通过/zuul/*路径访问的请求会绕过DispatcherServlet(比如之前我们说的大文件上传),被ZuulServlet处理,主要用来应对大文件上传的情况。另外,对于ZuulServlet的访问路径/zuul/*,我们可以通过zuul.servletPath参数进行修改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class ServletDetectionFilter extends ZuulFilter {
public ServletDetectionFilter() {
}

public String filterType() {
return "pre";
}

public int filterOrder() {
return -3;
}

public boolean shouldFilter() {
return true;
}
......
}
FormBodyWrapperFilter

​ FormBodyWrapperFilter:它的执行顺序为-1,是第三个执行的过滤器。该过滤器仅对两类请求生效,第一类是Context-Typeapplication/x-www-form-urlencoded的请求,第二类是Context-Type为multipart/form-data并且是由String的DispatcherServlet处理的请求(用到了ServletDetectionFilter的处理结果)。而该过滤器的主要目的是将符合要求的请求体包装成FormBodyRequestWrapper对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class FormBodyWrapperFilter extends ZuulFilter {
private FormHttpMessageConverter formHttpMessageConverter;
private Field requestField;
private Field servletRequestField;

public String filterType() {
return "pre";
}

public int filterOrder() {
return -1;
}
.....
}
DebugFilter

​ DebugFilter:它的执行顺序为1,是第四个执行的过滤器,该过滤器会根据配置参数zuul.debug.request和请求中的debug参数来决定是否执行过滤器中的操作。而它的具体操作内容是将当前请求上下文中的debugRoutingdebugRequest参数设置为true。由于在同一个请求的不同生命周期都可以访问到这二个值,所以我们在后续的各个过滤器中可以利用这二个值来定义一些debug信息,这样当线上环境出现问题的时候,可以通过参数的方式来激活这些debug信息以帮助分析问题,另外,对于请求参数中的debug参数,我们可以通过zuul.debug.parameter来进行自定义。

PreDecorationFilter

​ PreDecorationFilter:执行顺序是5,是pre阶段最后被执行的过滤器,该过滤器会判断当前请求上下文中是否存在forward.doserviceId参数,如果都不存在,那么它就会执行具体过滤器的操作(如果有一个存在的话,说明当前请求已经被处理过了,因为这二个信息就是根据当前请求的路由信息加载进来的)。而当它的具体操作内容就是为当前请求做一些预处理,比如说,进行路由规则的匹配,在请求上下文中设置该请求的基本信息以及将路由匹配结果等一些设置信息等,这些信息将是后续过滤器进行处理的重要依据,我们可以通过RequestContext.getCurrentContext()来访问这些信息。另外,我们还可以在该实现中找到对HTTP头请求进行处理的逻辑,其中包含了一些耳熟能详的头域,比如X-Forwarded-Host,X-Forwarded-Port。另外,对于这些头域是通过zuul.addProxyHeaders参数进行控制的,而这个参数默认值是true,所以zuul在请求跳转时默认会为请求增加X-Forwarded-*头域,包括X-Forwarded-Host,X-Forwarded-PortX-Forwarded-ForX-Forwarded-Prefix,X-Forwarded-Proto。也可以通过设置zuul.addProxyHeaders=false关闭对这些头域的添加动作。

Routing过滤器

请求路由到微服务时执行,用于构建发送给微服务的请求,使用 HttpClient 或 Ribbon 请求微服务

RibbonRoutingFilter

​ RibbonRoutingFilter:它的执行顺序为10,是route阶段的第一个执行的过滤器。该过滤器只对请求上下文中存在serviceId参数的请求进行处理,即只对通过serviceId配置路由规则的请求生效。而该过滤器的执行逻辑就是面向服务路由的核心,它通过使用ribbon和hystrix来向服务实例发起请求,并将服务实例的请求结果返回。

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
public class RibbonRoutingFilter extends ZuulFilter {
private static final Log log = LogFactory.getLog(RibbonRoutingFilter.class);
protected ProxyRequestHelper helper;
protected RibbonCommandFactory<?> ribbonCommandFactory;
protected List<RibbonRequestCustomizer> requestCustomizers;

public String filterType() {
return "route";
}

public int filterOrder() {
return 10;
}

public boolean shouldFilter() {
RequestContext ctx = RequestContext.getCurrentContext();
return ctx.getRouteHost() == null && ctx.get("serviceId") != null && ctx.sendZuulResponse();
}

public Object run() {
RequestContext context = RequestContext.getCurrentContext();
this.helper.addIgnoredHeaders(new String[0]);

try {
//使用hystrix和ribbon发起请求调用
RibbonCommandContext commandContext = this.buildCommandContext(context);
ClientHttpResponse response = this.forward(commandContext);
this.setResponse(response);
return response;
} catch (ZuulException var4) {
throw new ZuulRuntimeException(var4);
} catch (Exception var5) {
throw new ZuulRuntimeException(var5);
}
}
}
SimpleHostRoutingFilter

​ SimpleHostRoutingFilter:它的执行顺序为100,是route阶段的第二个执行的过滤器。该过滤器只对请求上下文存在routeHost参数的请求进行处理,即只对通过url配置路由规则的请求生效。而该过滤器的执行逻辑就是直接向routeHost参数的物理地址发起请求,从源码中我们可以知道该请求是直接通过httpclient包实现的,而没有使用Hystrix命令进行包装,所以这类请求并没有线程隔离和断路器的保护。

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
public class SimpleHostRoutingFilter extends ZuulFilter implements ApplicationListener<EnvironmentChangeEvent> {
private static final Log log = LogFactory.getLog(SimpleHostRoutingFilter.class);
private static final Pattern MULTIPLE_SLASH_PATTERN = Pattern.compile("/{2,}");


public String filterType() {
return "route";
}

public int filterOrder() {
return 100;
}

public boolean shouldFilter() {
return RequestContext.getCurrentContext().getRouteHost() != null && RequestContext.getCurrentContext().sendZuulResponse();
}

public Object run() {
RequestContext context = RequestContext.getCurrentContext();
HttpServletRequest request = context.getRequest();
MultiValueMap<String, String> headers = this.helper.buildZuulRequestHeaders(request);
MultiValueMap<String, String> params = this.helper.buildZuulRequestQueryParams(request);
String verb = this.getVerb(request);
InputStream requestEntity = this.getRequestBody(request);
if (this.getContentLength(request) < 0L) {
context.setChunkedRequestBody();
}

String uri = this.helper.buildZuulRequestURI(request);
this.helper.addIgnoredHeaders(new String[0]);

try {
CloseableHttpResponse response = this.forward(this.httpClient, verb, uri, request, headers, params, requestEntity);
this.setResponse(response);
return null;
} catch (Exception var9) {
throw new ZuulRuntimeException(this.handleException(var9));
}
}
.....
}

知道配置类似zuul.routes.user-server.url=http://localhost:8080/这样的底层都是通过httpclient直接发送请求的,也就知道为什么这样的情况没有做到负载均衡的原因所在。

SendForwardFilter

​ SendForwardFilter:它的执行顺序是500,是route阶段第三个执行的过滤器。该过滤器只对请求上下文中存在的forward.do参数进行处理请求,即用来处理路由规则中的forward本地跳转装配。

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
public class SendForwardFilter extends ZuulFilter {
protected static final String SEND_FORWARD_FILTER_RAN = "sendForwardFilter.ran";

public SendForwardFilter() {
}

public String filterType() {
return "route";
}

public int filterOrder() {
return 500;
}

public boolean shouldFilter() {
RequestContext ctx = RequestContext.getCurrentContext();
return ctx.containsKey("forward.to") && !ctx.getBoolean("sendForwardFilter.ran", false);
}

public Object run() {
try {
RequestContext ctx = RequestContext.getCurrentContext();
String path = (String)ctx.get("forward.to");
RequestDispatcher dispatcher = ctx.getRequest().getRequestDispatcher(path);
if (dispatcher != null) {
ctx.set("sendForwardFilter.ran", true);
if (!ctx.getResponse().isCommitted()) {
dispatcher.forward(ctx.getRequest(), ctx.getResponse());
ctx.getResponse().flushBuffer();
}
}
} catch (Exception var4) {
ReflectionUtils.rethrowRuntimeException(var4);
}

return null;
}
}

post过滤器

在路由到微服务后执行,可用来为响应添加 Header,收集统计信息和指标、将相应从微服务发送给客户端等

SendErrorFilter

​ SendErrorFilter:它的执行顺序是0,是post阶段的第一个执行的过滤器。该过滤器仅在请求上下文中包含error.status_code参数(由之前执行的过滤器设置的错误编码)并且还没有被该过滤器处理过的时候执行。而该过滤器的具体逻辑就是利用上下文中的错误信息来组成一个forward到api网关/error错误端点的请求来产生错误响应。

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

public class SendErrorFilter extends ZuulFilter {
private static final Log log = LogFactory.getLog(SendErrorFilter.class);
protected static final String SEND_ERROR_FILTER_RAN = "sendErrorFilter.ran";
@Value("${error.path:/error}")
private String errorPath;

public SendErrorFilter() {
}

public String filterType() {
return "error";
}

public int filterOrder() {
return 0;
}



public Object run() {
try {
RequestContext ctx = RequestContext.getCurrentContext();
SendErrorFilter.ExceptionHolder exception = this.findZuulException(ctx.getThrowable());
HttpServletRequest request = ctx.getRequest();
request.setAttribute("javax.servlet.error.status_code", exception.getStatusCode());
log.warn("Error during filtering", exception.getThrowable());
request.setAttribute("javax.servlet.error.exception", exception.getThrowable());
if (StringUtils.hasText(exception.getErrorCause())) {
request.setAttribute("javax.servlet.error.message", exception.getErrorCause());
}
//forward到api网关的错误端点
RequestDispatcher dispatcher = request.getRequestDispatcher(this.errorPath);
if (dispatcher != null) {
ctx.set("sendErrorFilter.ran", true);
if (!ctx.getResponse().isCommitted()) {
ctx.setResponseStatusCode(exception.getStatusCode());
dispatcher.forward(request, ctx.getResponse());
}
}
} catch (Exception var5) {
ReflectionUtils.rethrowRuntimeException(var5);
}

return null;
}
.....
}
SendResponseFilter

​ SendResponseFilter:它的执行顺序为1000,是post阶段最后执行的过滤器,该过滤器会检查请求上下文中是否包含请求响应相关的头信息,响应数据流或是响应体,只有在包含它们其中一个的时候执行处理逻辑。而该过滤器的处理逻辑就是利用上下文的响应信息来组织需要发送回客户端的响应内容。

自定义 Zuul 过滤器

创建Zuul 过滤器类

创建过滤器类,继承抽象类 ZuulFilter 并实现抽象方法

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
/**
* @description: Zuul日志过滤器
* filterType:返回过滤器类型,有 pre、route、post、error 等几种取值
* filterOrder:返回一个 int 值指定过滤器执行顺序,不同过滤器允许返回相同的数字
* shouldFilter:true 表示过滤器执行、false表示不执行
* run:过滤器具体逻辑,下面代码打印了请求方法和URL
*/
public class PreRequestLogFilter extends ZuulFilter {

private static final Logger LOGGER = LoggerFactory.getLogger(PreRequestLogFilter.class);

@Override
public String filterType() {
return "pre";
}

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

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

@Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
PreRequestLogFilter.LOGGER.info(String.format("send %s request to %s", request.getMethod(), request.getRequestURL().toString()));
return null;
}
}
加入到 IOC 容器

将过滤器通过 @Bean 注入到 IOC 容器

1
2
3
4
@Bean
public PreRequestLogFilter preRequestLogFilter() {
return new PreRequestLogFilter();
}
测试

访问 http://localhost:9999/user/invokInfo 查看日志

1
2020-05-24 10:04:41.853  INFO 15576 --- [nio-9999-exec-6] c.s.zuul.filter.PreRequestLogFilter      : send GET request to http://localhost:9999/user/invokInfo

禁用 Zuul 过滤器

​ Spring Cloud 默认为 Zuul 启用了一些过滤器,如 DebugFilter、FormBodyWrapperFilter、PreDecorationFilter 等,这些过滤器存放在 spring-cloud-netflix-core 这个 Jar 包的 org.springframwork.cloud.netflix.zuul.filters 包中

​ 只需配置 zuul.<SimpleClassName>.<filterType>.disable = true 即可禁用 SimpleClassName 对应的过滤器,例如

1
zuul.SendResponseFilter.post.disable = true

为 Zuul 添加回退

很多时候,由于服务的重启、宕机或者网络的不佳,Zuul进行路由时会出现异常,然后,异常信息直接展示给用户是不友好的, 需要我们提示一些通俗易懂的信息告知用户为什么会出现失败,这时就可以用到回退处理,SpringCloud中使用Hystrix实现微服务的容错与回退,其实Zuul默认已经整合了Hystrix

​ Zuul 的 Hystrix 监控粒度是微服务,而不是某个 API

​ 要为 Zuul 添加回退,需要实现 ZuulFallbackProvider 接口,在实现类中指定为哪个微服务提供回退,并提供一个 ClientHttpResponse 作为回退响应

编写 Zuul 回退类

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
@Component
public class UserFallbackProvider implements ZuulFallbackProvider {

@Override
public String getRoute() {
//表明为哪个微服务提供回退
return "flim-user";
}

@Override
public ClientHttpResponse fallbackResponse() {
return new ClientHttpResponse() {
@Override
public HttpStatus getStatusCode() throws IOException {
//回退时的状态码
return HttpStatus.OK;
}

@Override
public int getRawStatusCode() throws IOException {
//数字类型的状态码,本文返回的是200
return this.getStatusCode().value();
}

@Override
public String getStatusText() throws IOException {
//状态文本,本文返回的是OK
return this.getStatusCode().getReasonPhrase();
}

@Override
public void close() {

}

@Override
public InputStream getBody() throws IOException {
//响应体
return new ByteArrayInputStream("用户微服务不可用".getBytes());
}

@Override
public HttpHeaders getHeaders() {
//headers设定
HttpHeaders httpHeaders = new HttpHeaders();
MediaType mt = new MediaType("application","json", Charset.forName("UTF-8"));
httpHeaders.setContentType(mt);
return httpHeaders;
}
};
}
}
相关方法介绍
方法名 说明
getRoute 为哪个服务提供回退,*号代表所有服务
fallbackResponse 回退响应
getStatusCode 回退时的状态码
getRawStatusCode 数字类型状态码
getStatusText 状态文本
close 这个不用管
getBody 响应体
getHeaders 返回的响应头

配置超时时间

1
2
3
4
# 请求连接的超时时间
ribbon.connectTimeout=2000 #读超时时间(单位毫秒)
# 请求处理的超时时间
ribbon.readTimeout=5000 #连接超时时间(单位毫秒)

注意!!!:如果zuul配置了熔断fallback的话,熔断超时也要配置,不然如果你配置的ribbon超时时间大于熔断的超时(Hystirx超时默认1秒),那么会先走熔断,相当于你配的ribbon超时就不生效了,ribbon和hystrix是同时生效的,哪个值小哪个生效,另一个就看不到效果了

1
2
3
# hystrix的超时时间必须大于ribbon的超时时间
# default为默认所有,可以配置指定服务名
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=60000 #超时时间(单位毫秒)

测试

web服务接口设置线程休眠,模拟超时,当请求实际达到设置的超时时间后会进行回退

Zuul 高可用

  1. 将多个 Zuul 客户端也注册到 Eureka Server 上,就可以实现 Zuul 高可用

  2. 部署多个 Zuul,Zuul 客户端会自动从 Eureka Server 中查询 Zuul Server 列表,并使用 Ribbon 负载均衡地请求 Zuul 集群

  3. 如果 Zuul 客户端未注册到 Eureka Server 上,可借助 Nginx 等实现负载均衡

修改 ServiceId 与路由映射规则

可以自定义 serviceId 和路由之间的相互映射,通过正则表达式进行匹配

​ 例如将 serviceId 为 users-v1 的服务映射到路由 /v1/users/ 的路径上,当请求 /v1/users/ 时等同于请求 /users-v1/

1
2
3
4
5
6
@Bean
public PatternServiceRouteMapper serviceRouteMapper() {
return new PatternServiceRouteMapper(
"(?<name>^.+)-(?<version>.+$)",
"${version}/${name}");
}

配置HTTPS

生成证书

使用java自带的 keytool

1
keytool -genkeypair -alias hellowood -keyalg RSA -keysize 2048 -storetype PKCS12 -keystore https.keystore -validity 3650

配置证书

将https.keystore证书放在项目目录下,然后配置配置文件

1
2
3
4
5
6
server.port=8443
server.ssl.protocol=TLS
server.ssl.key-store=classpath:ssl/https.keystore
server.ssl.key-store-password=123456
server.ssl.key-store-type=PKCS12
server.ssl.key-alias=tomcat

启动测试

点击高级继续访问

兼容HTTP请求

在这种情况下http的9999端口无法访问了

http://127.0.0.1:9999/user/order/version

增加端口配置

在application.properties 中增加

1
server.port.http=9999
连接器配置
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
@Configuration
public class ConnectorConfig {

@Value("${server.port.http}")
private int serverPortHttp;

@Value("${server.port}")
private int serverPortHttps;

@Bean
public ServletWebServerFactory servletWebServerFactory() {
TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory() {
@Override
protected void postProcessContext(Context context) {
SecurityConstraint securityConstraint = new SecurityConstraint();
securityConstraint.setUserConstraint("CONFIDENTIAL");
SecurityCollection securityCollection = new SecurityCollection();
securityCollection.addPattern("/*");
securityConstraint.addCollection(securityCollection);
context.addConstraint(securityConstraint);
}
};
factory.addAdditionalTomcatConnectors(redirectConnector());
return factory;
}

private Connector redirectConnector() {
Connector connector = new Connector(Http11NioProtocol.class.getName());
connector.setScheme("http");
connector.setPort(serverPortHttp);
connector.setSecure(false);
connector.setRedirectPort(serverPortHttps);
return connector;
}
}
测试

访问http://127.0.0.1:9999/user/order/version

他会自动跳转到https://127.0.0.1/user/order/version

到这个整合https完成

跨域配置

​ 大家都知道spring boot 可以通过@CrossOrigin实现跨域。但是在spring cloud 里,如果要粒度那么细的去控制跨域,这个就太繁琐了,所以一般来说,会在路由zuul里实现。

未设置跨域测试

跨域测试页面
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

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<link type="test/css" href="css/style.css" rel="stylesheet">
<script type="text/javascript" src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
<script type="text/javascript">
$(function(){
$("#cors").click(
function(){
$.ajax({
//headers:{"token":"","Content-Type":"application/json;charset=UTF-8","Access-Control-Allow-Origin":"*"},
url:"http://127.0.0.1:9999/user/order/version",
success:function(data){
console.log("start")
console.log(data)
alert(data);
}
})
});
});
</script>
<body>
<input type="button" id="cors" value="core跨域测试"
</body>
</html>
测试

corsFilter方式

在zuul服务下添加一个corsFilter实现跨域,实现起来方便。代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Configuration
public class GateWayCorsConfig {
@Bean
public FilterRegistrationBean corsFilter() {
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
final CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.addAllowedOrigin("*");
config.addAllowedHeader("*");
config.addAllowedMethod("*");
//这个请求头在https中会出现,但是有点问题,下面我会说
//config.addExposedHeader("X-forwared-port, X-forwarded-host");
source.registerCorsConfiguration("/**", config);
FilterRegistrationBean bean = new FilterRegistrationBean(new CorsFilter(source));
bean.setOrder(Ordered.HIGHEST_PRECEDENCE);
return bean;
}
}

​ 经过测试,这样的配置在http的情况下跨域是OK的,但是当我的环境切换的https的情况下就发生了奇怪的问题。说明一下我遇到的问题。
​ 前端 服务A后端服务B 在同一台服务器上,服务A 调用 服务B 时,服务A通过负载均衡进入服务B时:
http时,服务A的请求跨域成功,https时,服务A的请求跨域失败。
也就是端口为443的时候,会被认为跨域失败!!
​ 我一开始对比了请求头,以为是少了ExposedHeader的”X-forwared-port, X-forwarded-host”,但是添加后,还是失败。因为急着上线,所以我没有去深入测试到底什么原因引起的https请求跨域失败。(所以如果大家发现我哪里写的不对,请务必通知我,让我也明白为什么失败!谢谢!)

测试

跨域测试成功

继承ZuulFilter

因为第一种方式在https下失败后,我尝试了用zuulfilter实现cors的方式

一共需要两个filiter:一个pre, 一个post

FirstFilter
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

@Component
public class FirstFilter extends ZuulFilter {

private Logger logger = LoggerFactory.getLogger(FirstFilter.class);

@Override
public String filterType() {
/*
pre:可以在请求被路由之前调用
route:在路由请求时候被调用
post:在route和error过滤器之后被调用
error:处理请求时发生错误时被调用
* */
// 前置过滤器
return FilterConstants.PRE_TYPE;
}

@Override
public int filterOrder() {
//// 优先级为0,数字越大,优先级越低
return 0;
}
@Override
public boolean shouldFilter() {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
//只过滤OPTIONS 请求
if(request.getMethod().equals(RequestMethod.OPTIONS.name())){
return true;
}

return false;
}

@Override
public Object run() {
logger.debug("*****************FirstFilter run start*****************");
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletResponse response = ctx.getResponse();
HttpServletRequest request = ctx.getRequest();
response.setHeader("Access-Control-Allow-Origin",request.getHeader("Origin"));
response.setHeader("Access-Control-Allow-Credentials","true");
response.setHeader("Access-Control-Allow-Headers","authorization, content-type");
response.setHeader("Access-Control-Allow-Methods","POST,GET");
response.setHeader("Access-Control-Expose-Headers","X-forwared-port, X-forwarded-host");
response.setHeader("Vary","Origin,Access-Control-Request-Method,Access-Control-Request-Headers");
//不再路由
ctx.setSendZuulResponse(false);
ctx.setResponseStatusCode(200);
logger.debug("*****************FirstFilter run end*****************");
return null;
}
}

​ Pre-Filter 用来处理预处理OPTIONS请求,当发现是OPTIONS请求的时候,给出跨域响应头,并且不对其进行zuul路由,直接返回成功(200), 给前端服务允许跨域

PostFilter
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

@Component
public class PostFilter extends ZuulFilter {

private Logger logger = LoggerFactory.getLogger(PostFilter.class);

@Override
public String filterType() {
/*
pre:可以在请求被路由之前调用
route:在路由请求时候被调用
post:在route和error过滤器之后被调用
error:处理请求时发生错误时被调用
* */
// 前置过滤器
return FilterConstants.POST_TYPE;
}

@Override
public int filterOrder() {
//// 优先级为0,数字越大,优先级越低
return 2;
}

@Override
public boolean shouldFilter() {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
//过滤各种POST请求
if (request.getMethod().equals(RequestMethod.OPTIONS.name())) {
return false;
}
return true;
}

@Override
public Object run() {
logger.debug("*****************PostFilter run start*****************");
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletResponse response = ctx.getResponse();
HttpServletRequest request = ctx.getRequest();
response.setHeader("Access-Control-Allow-Origin", request.getHeader("Origin"));
response.setHeader("Access-Control-Allow-Credentials", "true");
response.setHeader("Access-Control-Expose-Headers", "X-forwared-port, X-forwarded-host");
response.setHeader("Vary", "Origin,Access-Control-Request-Method,Access-Control-Request-Headers");
//允许继续路由
ctx.setSendZuulResponse(true);
ctx.setResponseStatusCode(200);
logger.debug("*****************PostFilter run end*****************");
return null;
}
}

​ Post-Filter 用来处理 预处理OPTIONS以外的请求,对于正常的请求,不但要给出跨域请求头,还需要允许请求进行路由(否则你的请求到这儿就结束啦),然后返回状态码200。(emmmm……这里要不要返回200,我觉得可能还要想一想……)

​ 按照以上方式配置的话,方法一出现的问题,就得到了解决。服务A能够正常请求服务B了

测试

跨域测试成功

评论