Zuul
什么是网关 zuul
Zuul是Netflix开源的微服务网关,它可以和Eureka、Ribbon、Hystrix等组件配合使用。Zuul的核心是一系列的过滤器
主要功能
1、身份认证与安全:识别每个资源的验证要求,并拒绝那些与要求不符的请求
2、审查与监控:在边缘位置追踪有意义的数据和统计结果,从而为我们带来精确的生产视图;
3、动态路由:动态地将请求路由到不同的后端集群;
4、压力测试:逐渐增加指向集群的流量,以了解性能;
5、为每一种负载类型分配对应容量,并弃用超出限定值的请求;
为什么需要使用zuul
不同的微服务一般会有不同的网络地址,而外部客户端(例如手机APP)可能需要调用多个服务的接口才能完成一个业务需求
没有使用网关服务的时候缺点:
1、客户端会多次请求不同的微服务,增加了客户端的复杂性。
2、存在跨域请求,在一定场景下处理相对复杂
3、认证复杂,每个服务都需要独立认证
使用服务网关
优点:
1、易于监控。可在微服务网关收集监控数据并将其推送到外部系统进行分析
2、易于认证。可在微服务网关上进行认证,然后再将请求转发到后端的微服务,而无须在每个微服务中进行认证
3、减少了客户端与各个微服务之间的交互次数。
如何快速搭建网关服务
1、第一步(加入依赖)
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
<version>2.2.7.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
第二步(写配置文件)
server.port=9001
spring.application.name=ms-gateway-zuul
eureka.instance.prefer-ip-address=true
eureka.instance.instance-id=ms-gateway-zuul-9001
eureka.client.service-url.defaultZone=http://localhost:9000/eureka/
第三布(写注解)
@SpringBootApplication
@EnableZuulProxy
public class ZuulApplication {
public static void main(String[] args) {
SpringApplication.run(ZuulApplication .class, args);
}
}
通过网关地址来访问:http://localhost:9001/EUREKA-CLIENT1/eurekaClient1Test
zuul网关核心配置
关闭通过微服务名称路访问(暴露了微服务)
全部关闭
zuul.ignored-services=*
部分关闭
#关闭某个微服务的服务实例名称映射,若想关闭多个通过逗号隔开
#zuul.ignored-services=EUREKA-CLIENT1
通过微服务实例指定路映射
配置 zuul.routes.ms-provider-order=/order-service/**
访问:http://localhost:9001/order-service/EUREKA-CLIENT1/eurekaClient1Test
同时指定微服务的serviceId和对应路径
配置
#通过指定微服务名称 映射路径
zuul.routes.use-routing.serviceId=EUREKA-CLIENT1
zuul.routes.use-routing.path=/order-service/**
**访问路径:**http://localhost:9001/order-service/eurekaClient1Test
同时指定url和path
配置
#使用这种方式配置的路由不会作为HystrixCommand执行,同时也不能使用Ribbon来负载均衡多个URL
#zuul.routes.use-routing.url=http://localhost:8002/
#zuul.routes.use-routing.path=/order-service/**
**访问路径:**http://localhost:9001/order-service/eurekaClient1Test
同时指定path和URL,并且不破坏Zuul的Hystrix、Ribbon特性
#同时指定path和URL,并且不破坏Zuul的Hystrix、Ribbon特性
#zuul.routes.use-routing.serviceId=EUREKA-CLIENT1
#zuul.routes.use-routing.path=/order-service/**
#关闭ribbon的负载均衡
#ribbon.eureka.enabled=false
#ms-provider-order.ribbon.listOfServers=http://localhost:8002
统一设置路由前缀
zuul.prefix=/order-api
#是否剥离前缀(默认是true)
zuul.strip-prefix=false
zuul.routes.use-routing.serviceId=EUREKA-CLIENT1
zuul.routes.use-routing.path=/order-service/**
局部配置
zuul.strip-prefix=false
zuul.routes.use-routing.serviceId=EUREKA-CLIENT1
zuul.routes.use-routing.path=/order-api/**
zuul.routes.use-routing.stripPrefix=false
过滤敏感路径
#保护敏感路径
zuul.ignored-patterns=/**/admin/**
http://localhost:9001/order-api/order/admin是不会被路由的
设置敏感头信息
private Set<String> sensitiveHeaders = new LinkedHashSet(Arrays.asList(new String[]{"Cookie", "Set-Cookie", "Authorization"}));
若在没设置的情况下,通过zuul网关的时候,会过滤掉敏感的头信息,比如cookie等其他的
全局设置
zuul.sensitive-headers=
部分设置
zuul.routes.use-routing.serviceId=EUREKA-CLIENT1
zuul.routes.use-routing.path=/order-service/**
zuul.routes.use-routing.sensitiveHeaders=
zuul过滤器详解
Zuul过滤器类型与请求生命周期
zuul过滤器的类型
PRE:
这种过滤器在请求被路由之前调用。我们可利用这种过滤器实现身份验证、在集群中选择请求的微服务、记录调试信息等
ServletDetectionFilter:它的执行顺序为-3
是最先被执行的过滤器。该过滤器总是会被执行,主要用来检测当前请求是通过Spring的DispatcherServlet处理运行,还是通过ZuulServlet来处理运行的。
它的检测结果会以布尔类型保存在当前请求上下文的isDispatcherServletRequest参数中,这样在后续的过滤器中,我们就可以通过RequestUtils.isDispatcherServletRequest()和RequestUtils.isZuulServletRequest()方法判断它以实现做不同的处理。
一般情况下,发送到API网关的外部请求都会被Spring的DispatcherServlet处理,除了通过/zuul/路径访问的请求会绕过DispatcherServlet,被ZuulServlet处理,主要用来应对处理大文件上传的情况。另外,对于ZuulServlet的访问路径/zuul/,我们可以通过zuul.servletPath参数来进行修改
Servlet30WrapperFilter:执行顺序是-2
是第二个执行的过滤器。目前的实现会对所有请求生效,主要为了将原始的HttpServletRequest包装成Servlet30RequestWrapper对象
FormBodyWrapperFilter 它的执行顺序为-1
该过滤器仅对两种类请求生效,第一类是Content-Type为application/x-www-form-urlencoded的请求,第二类是Content-Type为multipart/form-data并且是由Spring的DispatcherServlet处理的请求(用到了ServletDetectionFilter的处理结果
而该过滤器的主要目的是将符合要求的请求体包装成FormBodyRequestWrapper对象
PreDecorationFilter 他的执行顺序是5
是pre阶段最后被执行的过滤器。该过滤器会判断当前请求上下文中是否存在forward.to和serviceId参数,如果都不存在,那么它就会执行具体过滤器的操作(如果有一个存在的话,说明当前请求已经被处理过了,因为这两个信息就是根据当前请求的路由信息加载进来的)。
而它的具体操作内容就是为当前请求做一些预处理,比如:进行路由规则的匹配、在请求上下文中设置该请求的基本信息以及将路由匹配结果等一些设置信息等,这些信息将是后续过滤器进行处理的重要依据,我们可以通过RequestContext.getCurrentContext()来访问这些 信息。
另外,我们还可以在该实现中找到一些对HTTP头请求进行处理的逻辑,其中包含了一些耳熟能详的头域,比如:X-Forwarded-Host、X-Forwarded-Port。
另外,对于这些头域的记录是通过zuul.addProxyHeaders参数进行控制的,而这个参数默认值为true,所以Zuul在请求跳转时默认地会为请求增加X-Forwarded-*头域,包括:X-Forwarded-Host、X-Forwarded-Port、X-Forwarded-For、X-Forwarded-Prefix、X-Forwarded- Proto。
我们也可以通过设置zuul.addProxyHeaders=false关闭对这些头域的添加动作
ROUTING:
这种过滤器将请求路由到微服务。这种过滤器用于构建发送给微服务的请求,并使用Apache HttpClient或Netfilx Ribbon请求微服
RibbonRoutingFilter:它的执行顺序为10,是route阶段第一个执行的过滤器
该过滤器只对请求上下文中存在serviceId参数的请求进行处理,即只对通过serviceId配置路由规则的请求生效。而该过滤器的执行逻辑就是面向服务路由的核心,它通过使用Ribbon和Hystrix来向服务实例发起请求,并将服务实例的请求结果返回
SimpleHostRoutingFilter:它的执行顺序为100
是route阶段第二个执行的过滤器。该过滤器只对请求上下文中存在routeHost参数的请求进行处理,即只对通过url配置路由规则的请求生效。而该过滤器的执行逻辑就是直接向routeHost参数的物理地址发起请求,从源码中我们可以知道该请求是直接通过httpclient包实现的,而没有使用Hystrix命令进行包装,所以这类请求并没有线程隔离和断路器的保护
SendForwardFilter:它的执行顺序为500,是route阶段第三个执行的过滤器
该过滤器只对请求上下文中存在forward.to参数的请求进行处理,即用来处理路由规则中的forward本地跳转配置
POST:
这种过滤器在路由到微服务以后执行。这种过滤器可用来为响应添加标准的HTTP Header、收集统计信息和指标、将响应从微服务发送给客户端
SendResponseFilter:它的执行顺序为1000
是post阶段最后执行的过滤器。该过滤器会检查请求上下文中是否包含请求响应相关的头信息、响应数据流或是响应体,只有在包含它们其中一个的时候就会执行处理逻辑。而该过滤器的处理逻辑就是利用请求上下文的响应信息来组织需要发送回客户端的响应内容
ERROR:
SendErrorFilter:它的执行顺序为0
该过滤器仅在请求上下文中包含error.status_code参数(由之前执行的过滤器设置的错误编码)并且还没有被该过滤器处理过的时候执行。而该过滤器的具体逻辑就是利用请求上下文中的错误信息来组织成一个forward到API网关/error错误端点的请求来产生错误响应
zuul过滤器的执行顺序
先执行pre>routing>post 然后再在同类型的过滤器按照order大小执行,越小的越先被执行
zuul生命周期
如何编写Zuul过滤器
/**
* 自定义的zuul的过滤器
*/
@Component
public class MyPreFilter extends ZuulFilter {
/**
* filterType:返回过滤器的类型。有pre、route、post、error等几种取值
* @return
*/
@Override
public String filterType() {
return "pre";
}
/**
* 返回一个int值来指定过滤器的执行顺序,不同的过滤器允许返回相同的数字。
* @return
*/
@Override
public int filterOrder() {
return 0;
}
/**
* 返回一个boolean值来判断该过滤器是否要执行,true表示执行,false表示不执行。
* @return
*/
@Override
public boolean shouldFilter() {
return true;
}
/**
* 真正的过滤顺序
* @return
* @throws ZuulException
*/
@Override
public Object run() throws ZuulException {
System.out.println("通过自定义的路由器.......");
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
Cookie[] cookies = request.getCookies();
if(cookies==null || cookies.length==0) {
shouldNotFilter(ctx);
return null;
}else {
//不进行路由
ctx.setSendZuulResponse(true);
//设置返回码
ctx.setResponseStatusCode(200);
ctx.set("isSuccess",false);
return null;
}
}
private void shouldNotFilter(RequestContext ctx) {
//不进行路由
ctx.setSendZuulResponse(false);
//设置返回码
ctx.setResponseStatusCode(401);
//设置返回体
ctx.setResponseBody("权限不够");
ctx.set("isSuccess",false);
}
}
如何禁用Zuul过滤器
禁用系统的过滤器org.springframework.cloud.netflix.zuul.filters包下的
zuul.SendResponseFilter.post.disable=true
禁用自定义的过滤器
#关闭自定义的filter
#zuul.TulingPreFilter.pre.disable=true
通过zuul上传文件
设置文件上传大小的设置
spring.servlet.multipart.max-file-size=4000MB
spring.servlet.multipart.max-request-size=4000MB
通过zuul上传
上传小文件直接上传
上传大文件:the request was rejected because its size (24324159) exceeds the configured maximum (10485760)
要么在网关设置文件的大小:默认是10M
spring.servlet.multipart.max-file-size=4000MB
spring.servlet.multipart.max-request-size=4000MB
要么设置使用zuul的默认网端点来绕过servlet的 :http://localhost:9001/zuul/zuul-fileupload/file/upload
@EnableZuulProxy您可以使用代理路径上传文件,只要文件很小,它就应该工作。对于大文件,有一个替代路径绕过“/ zuul / *”中的Spring DispatcherServlet(以避免多部分处理)。
上传大文件的时候,需要设置ribbon和hystrix的超时时间
#设置超时时间
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=600000
ribbon.ConnectTimeout=100000
ribbon.ReadTimeout=100000
zuul的回退
由于zuul 加入了 Hystrix 和ribbon进行调用,那么我们可以设置zuul的回退方法
在zuul工程中编写回退组件implements FallbackProvider
@Component
public class MyZuulFallBack implements FallbackProvider{
@Override
public String getRoute() {
/**
* *表示为所有微服务提供回退
* "微服务实例名称 指定为哪个微服务回退"
*/
return "*";
}
@Override
public ClientHttpResponse fallbackResponse(String s, Throwable throwable) {
return new ClientHttpResponse() {
@Override
public HttpStatus getStatusCode() throws IOException {
//请求网关是成功了 所以是OK
return HttpStatus.OK;
}
@Override
public int getRawStatusCode() throws IOException {
return HttpStatus.OK.value();
}
@Override
public String getStatusText() throws IOException {
return HttpStatus.OK.getReasonPhrase();
}
@Override
public void close() {
}
@Override
public InputStream getBody() throws IOException {
return new ByteArrayInputStream("后端服务不可用".getBytes());
}
@Override
public HttpHeaders getHeaders() {
HttpHeaders headers = new HttpHeaders();
//和body中的内容编码一致,否则容易乱码
headers.setContentType(MediaType.APPLICATION_JSON_UTF8);
return headers;
}
};
}
}
宕机回退
关闭目标服务
超时回退
先是设置hystrix的超时时间:hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=2000
zuul的全局异常处理
系统有一个error的过滤器SendErrorFilter
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;
@Override
public String filterType() {
return ERROR_TYPE;
}
@Override
public int filterOrder() {
return SEND_ERROR_FILTER_ORDER;
}
@Override
public boolean shouldFilter() {
RequestContext ctx = RequestContext.getCurrentContext();
// only forward to errorPath if it hasn't been forwarded to already
//请求上下问中有异常 并且 该异常没有被转发
return ctx.getThrowable() != null && !ctx.getBoolean(SEND_ERROR_FILTER_RAN, false);
}
@Override
public Object run() {
try {
RequestContext ctx = RequestContext.getCurrentContext();
ExceptionHolder exception = findZuulException(ctx.getThrowable());
HttpServletRequest request = ctx.getRequest();
//往请求上下文中设置 javax.servlet.error.status_code
request.setAttribute("javax.servlet.error.status_code", exception.getStatusCode());
log.warn("Error during filtering", exception.getThrowable());
//往请求上下文中设置 javax.servlet.error.exception
request.setAttribute("javax.servlet.error.exception", exception.getThrowable());
//往请求上下文中设置 javax.servlet.error.message
if (StringUtils.hasText(exception.getErrorCause())) {
request.setAttribute("javax.servlet.error.message", exception.getErrorCause());
}
//转发到BasicErrorController 中/error处理
RequestDispatcher dispatcher = request.getRequestDispatcher(this.errorPath);
if (dispatcher != null) {
ctx.set(SEND_ERROR_FILTER_RAN, true);
if (!ctx.getResponse().isCommitted()) {
ctx.setResponseStatusCode(exception.getStatusCode());
dispatcher.forward(request, ctx.getResponse());
}
}
}
catch (Exception ex) {
ReflectionUtils.rethrowRuntimeException(ex);
}
return null;
}
protected ExceptionHolder findZuulException(Throwable throwable) {
if (throwable.getCause() instanceof ZuulRuntimeException) {
Throwable cause = null;
if (throwable.getCause().getCause() != null) {
cause = throwable.getCause().getCause().getCause();
}
if (cause instanceof ClientException && cause.getCause() != null
&& cause.getCause().getCause() instanceof SocketTimeoutException) {
ZuulException zuulException = new ZuulException("", 504,
ZuulException.class.getName() + ": Hystrix Readed time out");
return new ZuulExceptionHolder(zuulException);
}
// this was a failure initiated by one of the local filters
if(throwable.getCause().getCause() instanceof ZuulException) {
return new ZuulExceptionHolder((ZuulException) throwable.getCause().getCause());
}
}
if (throwable.getCause() instanceof ZuulException) {
// wrapped zuul exception
return new ZuulExceptionHolder((ZuulException) throwable.getCause());
}
if (throwable instanceof ZuulException) {
// exception thrown by zuul lifecycle
return new ZuulExceptionHolder((ZuulException) throwable);
}
// fallback
return new DefaultExceptionHolder(throwable);
}
protected interface ExceptionHolder {
Throwable getThrowable();
default int getStatusCode() {
return HttpStatus.INTERNAL_SERVER_ERROR.value();
}
default String getErrorCause() {
return null;
}
}
protected static class DefaultExceptionHolder implements ExceptionHolder {
private final Throwable throwable;
public DefaultExceptionHolder(Throwable throwable) {
this.throwable = throwable;
}
@Override
public Throwable getThrowable() {
return this.throwable;
}
}
protected static class ZuulExceptionHolder implements ExceptionHolder {
private final ZuulException exception;
public ZuulExceptionHolder(ZuulException exception) {
this.exception = exception;
}
@Override
public Throwable getThrowable() {
return this.exception;
}
@Override
public int getStatusCode() {
return this.exception.nStatusCode;
}
@Override
public String getErrorCause() {
return this.exception.errorCause;
}
}
public void setErrorPath(String errorPath) {
this.errorPath = errorPath;
}
}
如何作定制化
自己写一个异常过滤器关闭系统的SendErrorFilter
zuul.SendErrorFilter.error.disable=true
自定义的ErrorFilter
public class MyErrorFilter extends SendErrorFilter {
public Object run() {
try {
RequestContext ctx = RequestContext.getCurrentContext();
ExceptionHolder exception = findZuulException(ctx.getThrowable());
HttpServletRequest request = ctx.getRequest();
HttpServletResponse response = ctx.getResponse();
response.setContentType("appliation/json;charset=UTF-8");
response.setCharacterEncoding("UTF-8");
Map<String,Object> errorMap = new HashMap<>();
errorMap.put("code","-1");
errorMap.put("errMsg",exception.getThrowable().getCause().getMessage());
response.getWriter().write(JSON.toJSONString(errorMap));
}
catch (Exception ex) {
ReflectionUtils.rethrowRuntimeException(ex);
}
return null;
}
}
使用系统的SendErrorFilter最终异常处理会交给BasicErrorController处理
如何处理跨越请求
在zuul上配置跨越filter的配置
@Configuration
public class CorsConfig {
@Bean
public CorsFilter corsFilter() {
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
final CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true); // 允许cookies跨域
config.addAllowedOrigin("*");// #允许向该服务器提交请求的URI,*表示全部允许,在SpringMVC中,如果设成*,会自动转成当前请求头中的Origin
config.addAllowedHeader("*");// #允许访问的头信息,*表示全部
config.setMaxAge(18000L);// 预检请求的缓存时间(秒),即在这个时间段里,对于相同的跨域请求不会再预检了
config.addAllowedMethod("*");// 允许提交请求的方法,*表示全部允许
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
}
<script>
$(function(){
$('#queryUserInfo').click(function(){
$.ajax({
type: "GET",
url: "http://localhost:9001/order-service/eurekaClient1Test",
dataType: "json",
success: function(data){
console.log(data);
alert(JSON.stringify(data));
}
});
});
});
</script>
zuul的高可用
zuul的客户端同时也注册到了eureka上
这种情况下,Zuul的高可用非常简单,只需将多个Zuul节点注册到Eureka Server上,就可 实现Zuul的高可用。此时,Zuul的高可用与其他微服务的高可用没什么区别。
Zuul客户端未注册到Eureka Server上
现实中,这种场景往往更常见,例如,Zuul客户端是一个手机APP——我们不可能让所有 的手机终端都注册到Eureka Server上。这种情况下,我们可借助一个额外的负载均衡器来 实现Zuul的高可用,例如Nginx、HAProxy、F5等。