zuul简介

zuul简介

clouds234@163.com

2018-7-27


微服务架构体系中,通常一个业务系统会有很多的微服务,比如:OrderService、ProductService、UserService...,为了让调用更简单,一般会在这些服务前端再封装一层,类似下面这样:

 

 

前面这一层俗称为“网关层”,其存在意义在于,将"1对N"问题 转换成了"1对1”问题,同时在请求到达真正的微服务之前,可以做一些预处理,比如:来源合法性检测,权限校验,反爬虫之类...

Zuul做为网关层,自身也是一个微服务,跟其它服务Service-1,Service-2, ... Service-N一样,都注册在eureka server上,可以相互发现,zuul能感知到哪些服务在线,同时通过配置路由规则(后面会给出示例),可以将请求自动转发到指定的后端微服务上,对于一些公用的预处理(比如:权限认证,token合法性校验,灰度验证时部分流量引导之类),可以放在所谓的过滤器(ZuulFilter)里处理,这样后端服务以后新增了服务,zuul层几乎不用修改。

 

1 添加zuul到springboot项目;因为zuul同样也是一个eureka客户端.所以还是需要添加eureka依赖的;

<dependency>

<groupId>org.springframework.cloud</groupId>

<artifactId>spring-cloud-starter-zuul</artifactId>

</dependency>

 

2 application.yml配置

zuul:

routes:

api-a:

path: /api-user/**

service-id: service-provider

sensitive-headers:

api-b:

path: /api-order/**

service-id: service-consumer 

解释一下:上面这段配置表示,/api-user/开头的url请求,将转发到service-provider这个微服务

上,/api-order/开头的url请求,将转发到service-consumer这个微服务上。

 

注意:这的path我使用过

zuul:

routes:

api-a:

path: /zuul/1/**

service-id: service-provider

sensitive-headers:

api-b:

path: /zuul/2/**

service-id: service-consumer 

 

这种双层模式,结果是无法匹配到;说找不到对应的uri;改回一层就可以了;

 

 

 

3 熔断处理

 

如果网关后面的微服务挂了,zuul还允许定义一个fallback类,用于熔断处理,参考下面的代码:

package com.cnblogs.yjmyzz.spring.cloud.study.gateway;

import org.springframework.cloud.netflix.zuul.filters.route.ZuulFallbackProvider;

import org.springframework.http.HttpHeaders;

import org.springframework.http.HttpStatus;

import org.springframework.http.MediaType;

import org.springframework.http.client.ClientHttpResponse;

import org.springframework.stereotype.Component;

import java.io.ByteArrayInputStream;

import java.io.IOException;

import java.io.InputStream;

import java.nio.charset.Charset;

/**

* Created by yangjunming on 2017/7/14.

*/

@Component

public class ServiceConsumerFallbackProvider implements ZuulFallbackProvider {

@Override

public String getRoute() {

return "service-consumer";

}

@Override

public ClientHttpResponse fallbackResponse() {

return new ClientHttpResponse() {

@Override

public HttpStatus getStatusCode() throws IOException {

return HttpStatus.OK;

}

@Override

public int getRawStatusCode() throws IOException {

return this.getStatusCode().value();

}

@Override

public String getStatusText() throws IOException {

return this.getStatusCode().getReasonPhrase();

}

@Override

public void close() {

}

@Override

public InputStream getBody() throws IOException {

return new ByteArrayInputStream("Service-Consumer不可用".getBytes());

}

@Override

public HttpHeaders getHeaders() {

HttpHeaders headers = new HttpHeaders();

MediaType mt = new MediaType("application", "json", Charset.forName("UTF-8"));

headers.setContentType(mt);

return headers;

}

};

}

}

 

开发人员只要在getRoute这个方法里指定要处理的微服务实例,然后重写fallbackResponse即可。

 

此时,如果观察/health端点,也可以看到hystrix处于融断开启状态

4 ZuulFilter过滤器

过滤器是一个很有用的机制,下面分几种经典场景演示下:

4.1、token校验/安全认证

网关直接暴露在公网上时,终端要调用某个服务,通常会把登录后的token传过来,网关层对token进行有效性验证,如果token无效(或没传token),提示重新登录或直接拒绝。另外,网关后面的微服务,如果设置了spring security中的basic Auth(即:不允许匿名访问,必须提供用户名、密码),也可以在Filter中处理。参考下面的代码:

 

package com.cnblogs.yjmyzz.spring.cloud.study.gateway;

import com.netflix.zuul.ZuulFilter;

import com.netflix.zuul.context.RequestContext;

import org.apache.commons.codec.binary.Base64;

import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;

import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;

/**

* Created by yangjunming on 2017/7/13.

*/

@Component

public class AccessFilter extends ZuulFilter {

private static Logger logger = LoggerFactory.getLogger(AccessFilter.class);

@Override

public String filterType() {

return FilterConstants.PRE_TYPE;

}

@Override

public int filterOrder() {

return 0;

}

@Override

public boolean shouldFilter() {

return true;

}

@Override

public Object run() {

RequestContext ctx = RequestContext.getCurrentContext();

HttpServletRequest request = ctx.getRequest();

Object token = request.getParameter("token");

//校验token

if (token == null) {

logger.info("token为空,禁止访问!");

ctx.setSendZuulResponse(false);

ctx.setResponseStatusCode(401);

return null;

} else {

//TODO 根据token获取相应的登录信息,进行校验(略)

}

//添加Basic Auth认证信息

ctx.addZuulRequestHeader("Authorization", "Basic " + getBase64Credentials("app01", "*****"));

return null;

}

private String getBase64Credentials(String username, String password) {

String plainCreds = username + ":" + password;

byte[] plainCredsBytes = plainCreds.getBytes();

byte[] base64CredsBytes = Base64.encodeBase64(plainCredsBytes);

return new String(base64CredsBytes);

}

}

 

 

 

 

Filter一共有4种类型,其常量值在org.springframework.cloud.netflix.zuul.filters.support.FilterConstants 中定义

 

// Zuul Filter TYPE constants -----------------------------------

/**

* {@link ZuulFilter#filterType()} error type.

*/

String ERROR_TYPE = "error";

/**

* {@link ZuulFilter#filterType()} post type.

*/

String POST_TYPE = "post";

/**

* {@link ZuulFilter#filterType()} pre type.

*/

String PRE_TYPE = "pre";

/**

* {@link ZuulFilter#filterType()} route type.

*/

String ROUTE_TYPE = "route"; 

 

安全校验,一般放在请求真正处理之前,所以上面的示例filterType指定为pre,剩下的只要在shouldFilter()、run()方法中重写自己的逻辑即可。

 

4.2 动态修改请求参数

 

zuulFilter可以拦截所有请求参数,并对其进行修改,比如:终端发过来的数据,出于安全要求,可能是经过加密处理的,需要在网关层进行参数解密,再传递到后面的服务;再比如:用户传过来的token值,需要转换成userId/userName这些信息,再传递到背后的微服务。参考下面的run方法:

 

public Object run() {

try {

RequestContext context = getCurrentContext();

InputStream in = (InputStream) context.get("requestEntity");

if (in == null) {

in = context.getRequest().getInputStream();

}

String body = StreamUtils.copyToString(in, Charset.forName("UTF-8"));

body = "动态增加一段内容到body中: " + body;

byte[] bytes = body.getBytes("UTF-8");

context.setRequest(new HttpServletRequestWrapper(getCurrentContext().getRequest()) {

@Override

public ServletInputStream getInputStream() throws IOException {

return new ServletInputStreamWrapper(bytes);

}

@Override

public int getContentLength() {

return bytes.length;

}

@Override

public long getContentLengthLong() {

return bytes.length;

}

});

} catch (IOException e) {

rethrowRuntimeException(e);

}

return null;

}

 

4.3 灰度发布(Gated Launch/Gray Release) 

 

大型分布式系统中,灰度发布是保证线上系统安全生产的重要手段,一般的做法为:从集群中指定一台(或某几台)机器,每次做新版本发布前,先只发布这些机器上,先观察一下是否正常,如果稳定运行后,再发布到其它机器。这种策略(相当于按部分节点来灰度),大多数情况下可以满足要求,但是有一些特定场景,可能不太适用。

 

比如:笔者所在的“美味不用等”公司,主要B端用户为各餐饮品牌的商家,多数情况下,如果新上了一个功能,希望找一些规模较小的餐厅做试点,先看看上线后的运行情况,如果运行良好,再推广到其它商家。

 

再比如:后端服务有N多个版本在同时运行,比如V1、V2,现在新加了一个V3版本(这在手机app应用中很常见),希望只有部分升级了app的用户访问最新的V3版本服务,其它用户仍然访问旧版本,待系统稳定后,再大规模提示用户升级。

 

对于这些看上去需求各异的灰度需求,其实本质是一样的:将请求(根据参数内容+业务规则),将其转向到特定的灰度机器上。Spring Cloud MicroService中有一个metadata-map(元数据)设置,可以很好的满足这类需求。

 

首先要引入一个jar包:(这是github上开源的一个项目ribbon-discovery-filter-spring-cloud-starter)

compile 'io.jmnarloch:ribbon-discovery-filter-spring-cloud-starter:2.1.0'

 

示例如下:

 

在各个服务的application.yml中设置以下metadata-map

 

eureka:

instance:

metadata-map:

gated-launch: false

 

即:所有节点发布后,默认灰度模式为false。然后把特定的灰度机器上的配置,该参数改成true(表明这台机器是用于灰度验证的)。

然后在ZuulFilter中参考下面的代码:

 

@Override

public Object run() {

RequestContext ctx = RequestContext.getCurrentContext();

HttpServletRequest request = ctx.getRequest();

Object token = request.getParameter("token");

//校验token

if (token == null) {

logger.info("token为空,禁止访问!");

ctx.setSendZuulResponse(false);

ctx.setResponseStatusCode(401);

return null;

} else {

//TODO 根据token获取相应的登录信息,进行校验(略)

//灰度示例

RibbonFilterContextHolder.clearCurrentContext();

if (token.equals("1234567890")) {

RibbonFilterContextHolder.getCurrentContext().add("gated-launch", "true");

} else {

RibbonFilterContextHolder.getCurrentContext().add("gated-launch", "false");

}

}

//添加Basic Auth认证信息

ctx.addZuulRequestHeader("Authorization", "Basic " + getBase64Credentials("app01", "*****"));

return null;

}

 

注意18-23行,这里演示了通过特定的token参数值,将请求引导到gated-lanuch=true的机器上。(注:参考这个原理,大家可以把参数值,换成自己的version-版本号,shopId-商家Id之类)。只要请求参数中的token=1234567890,这次请求就会转发到灰度节点上。

 

如果有朋友好奇这是怎么做到的,可以看下io.jmnarloch.spring.cloud.ribbon.predicate.MetadataAwarePredicate 这个类:

 

@Override

protected boolean apply(DiscoveryEnabledServer server) {

final RibbonFilterContext context = RibbonFilterContextHolder.getCurrentContext();

final Set<Map.Entry<String, String>> attributes = Collections.unmodifiableSet(context.getAttributes().entrySet());

final Map<String, String> metadata = server.getInstanceInfo().getMetadata();

return metadata.entrySet().containsAll(attributes);

}  

 

大致原理就是拿上下文中,开发人员设置的属性 与 服务节点里的metadata-map 进行比较,如果metadata-map中包括开发人员设置的属性,就返回成功

 

部分内容来自以下文章;

作者:菩提树下的杨过

出处:http://yjmyzz.cnblogs.com

https://www.cnblogs.com/yjmyzz/p/spring-cloud-zuul-demo.html

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值