戳蓝字“CSDN云计算”关注我们哦!
作者:爱撒谎的男孩
原文:https://chenjiabing666.github.io/2018/12/25/zuul%E6%9C%8D%E5%8A%A1%E7%BD%91%E5%85%B3/
本文系读者投稿,已获作者原创授权。
如果你有好文章,可以戳这里投稿。
Zuul
简介
Zuul包含了对请求的路由和过滤两个主要的功能,其中路由功能负责将外部的请求转发到具体的微服务实例上,是实现外部访问统一入口的基础上,而过滤功能则负责对请求的处理过程进行干预,是实现请求校验,服务聚合等功能的基础。
Zuul和Eureka进行整合,将Zuul自身注册为Eureka服务治理下的应用,同时从Eureka中获取其他微服务的信息,也即以后访问微服务都是通过Zuul跳转后获得。
代理+路由+过滤三大功能。
使用
需要和Eureka客户端结合使用,依赖如下:
1 2 3 4 5 6 7 8 9 10 | <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <!--zuul的依赖--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-zuul</artifactId> </dependency> |
添加配置,将其注册到eureka中,如下:
1 2 3 4 5 6 7 8 9 10 11 12 | server: port: 9001 eureka: client: serviceUrl: defaultZone: http://localhost:7001/eureka # eureka的暴露地址,直接注册 instance: instance-id: zuul.com prefer-ip-address: true spring: application: name: zuul #应用的名称,在同一个eureka中必须不重复 |
在主启动类上添加@EnableZuulProxy
这个注解,如下:
1 2 3 4 | @SpringBootApplication @EnableEurekaClient //开启eurkea客户端 @EnableZuulProxy //开启zuul public class DeptGetWayZuul9001Application { |
启动即可,在eureka中看到注册进入即可
之后只需要通过zuul访问其他的微服务提供者即可,比如服务提供者的实例名称为dept-provider
,那么通过zuul访问的路径为
http://localhost:9001/dept-provider/dept/1
路由映射规则
代理名称
之前的配置访问的还是需要带上微服务的实例名称,但是我们不想微服务的实例名称暴露,那么此时就需要使用代理名称替代,配置如下:
使用
ignored-services
忽略真实的服务名称访问,可以同时指定多个,其中服务名称必须和服务配置文件中一样。在routes下指定多个路由映射规则。
1 2 3 4 5 6 7 8 9 10 11 12 | zuul: # 忽略真实的服务名称实例访问,是一个Set集合,可以指定多个,取消全部使用 "*"即可 ignored-services: - order-provider #routes下面指定代理规则,可以同时指定多个 routes: #指定第一个规则,这里的名称任意 api-order: #指定的实例名称 serviceId: order-provider #指定可以访问的路由 path: /api-order/** |
按照上面的配置完成之后就可以直接使用映射的路由访问即可,如:
http://zuul.com:9001/api-order/order/1
设置统一前缀
我们可以在所有的访问uri前面加上统一的前缀,配置如下:
使用zuul.prefix
加上统一的前缀即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | zuul: #加上统一的前缀,那么访问的时候一定要加上这个前缀才可以访问到 prefix: /chenjiabing # 忽略真实的服务名称实例访问,是一个Set集合,可以指定多个,取消全部使用 "*"即可 ignored-services: - order-provider #routes下面指定代理规则,可以同时指定多个 routes: #指定第一个规则,这里的名称任意 api-dept: #指定的实例名称 serviceId: order-provider #指定可以访问的路由 path: /api-order/** |
通过上面的配置,此时的访问路径变成
http://zuul.com:9001/chenjiabing/api-order/order/1
某个uri取消路由
使用zuul.ignored-services
是忽略一个或者多个微服务的全部接口,但是如果我们可以更细化
如果我们需要隐藏一些敏感的接口不给访问,我们可以在yml文件中配置,如下:
1 2 3 4 | zuul: ignored-patterns: - /api-order/order/list # 取消指定的一个 - /api-order/order/** # 使用通配符去掉order下的全部接口 |
传递敏感头信息
默认zuul是不能传递头信息的,比如cookie,默认的设置了三个字段,如下:
1 2 | private Set<String> sensitiveHeaders = new LinkedHashSet<>( Arrays.asList("Cookie", "Set-Cookie", "Authorization")); |
如果我们想让它不过滤,只需要将其设置为空,或者不配置其他的,如下:
将
sensitive-headers
这个值设置为空即可这个配置只是针对
order-provider
这个微服务起作用
1 2 3 4 5 6 7 8 9 | zuul: routes: #指定第一个规则,这里的名称任意 api-order: #指定的实例名称 serviceId: order-provider #指定可以访问的路由 path: /api-order/** sensitive-headers: # 设置为空即可,那么就可以传递敏感头信息了 |
上面的配置是针对单个服务的设置,我们也可以配置针对所有的服务,如下:
1 2 | zuul: sensitive-headers: # 设置所有的服务都取消敏感头信息 |
过滤器
生命周期
Filter 的生命周期有 4 个,分别是 “PRE”、“ROUTING”、“POST” 和“ERROR”,整个生命周期可以用下图来表示
生命周期解释如下:
PRE
:这种过滤器在请求被路由之前调用。我们可利用这种过滤器实现身份验证、鉴权、限流、参数校验、请求转发,在集群中选择请求的微服务、记录调试信息等。ROUTING
:这种过滤器将请求路由到微服务。这种过滤器用于构建发送给微服务的请求,并使用 Apache HttpClient 或 Netfilx Ribbon 请求微服务。POST
:这种过滤器在路由到微服务以后执行。这种过滤器可用来为响应添加标准的 HTTP Header、收集统计信息和指标、将响应从微服务发送给客户端等。ERROR
:在其他阶段发生错误时执行该过滤器。 除了默认的过滤器类型,Zuul 还允许我们创建自定义的过滤器类型。例如,我们可以定制一种 STATIC 类型的过滤器,直接在 Zuul 中生成响应,而不将请求转发到后端的微服务。
前置过滤器的使用
利用前置过滤器实现检测token是否正确,如果不正确,那么直接返回权限不足401状态码,不路由微服务。
继承ZuulFilter。
注入到ioc容器中。
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 | /** * 自定义过滤器,用于实现鉴权,前置过滤器 * 继承ZuulFilter */ @Component //一定要注入到ioc容器中 public class TokenFilter extends ZuulFilter { /** * 判断过滤器是否被执行,返回true表示被会被执 * 在这里我们可以限制过滤器的执行范围,可以根据指定的条件判断这个请求是否被过滤 */ @Override public boolean shouldFilter() { return true; } /** * 过滤器的具体实现逻辑 * @return * @throws ZuulException */ @Override public Object run() throws ZuulException { RequestContext requestContext = RequestContext.getCurrentContext(); //获取请求上下文 HttpServletRequest request = requestContext.getRequest(); //获取HttpServletRequest String token = request.getParameter("token"); //获取传递过来的请求参数 //如果token是空的,返回权限不足,一般返回的状态码是401 if (StringUtils.isEmpty(token)) { requestContext.setSendZuulResponse(false); //设置false,此时的zuul不对此路由 requestContext.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value()); //设置401 // requestContext.setResponseBody("no power"); //设置响应的消息 } return null; } /** * 指定过滤器的类型,前置,后置................. * 1、其中FilterConstants这个常量类中定义了过滤器常用的变量 * public static final String ERROR_TYPE = "error"; public static final String POST_TYPE = "post"; public static final String PRE_TYPE = "pre"; public static final String ROUTE_TYPE = "route"; * @return */ @Override public String filterType() { return FilterConstants.PRE_TYPE; //前置过滤器 pre } /** * 过滤器执行的顺序,数字越小优先级越高 * @return */ @Override public int filterOrder() { //一般前置过滤器放在org.springframework.cloud.netflix.zuul.filters.pre.PreDecorationFilter这个过滤器之前即可,只需要将其对应的顺序-1 return FilterConstants.PRE_DECORATION_FILTER_ORDER-1; } } |
后置过滤器的使用
利用后置过滤器在响应头中添加内容,和前置过滤器的使用一样,只是使用的过滤器的类型不用,如下:
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 | /** * 后置过滤器,在响应头中添加一些内容 */ @Component //注入 public class AddResponseHeaderFilter extends ZuulFilter { @Override public boolean shouldFilter() { return true; } @Override public Object run() throws ZuulException { RequestContext requestContext = RequestContext.getCurrentContext(); //获取请求上下文 HttpServletResponse response = requestContext.getResponse(); //获取HttpServletResponse response.addHeader("X-Foo", "add header"); //添加头信息 return null; } @Override public String filterType() { return FilterConstants.POST_TYPE; //后置过滤器 } @Override public int filterOrder() { //在org.springframework.cloud.netflix.zuul.filters.post.SendResponseFilter#filterOrder()这个过滤一起之前执行即可 return FilterConstants.SEND_RESPONSE_FILTER_ORDER-1; } } |
禁用某种过滤器
如果我们想要禁用某种过滤器(自定义或者zuul自身的),我们可以在配置中设置,格式:zuul.<SimpleClassName>.<filterType>.disable=true
,比如禁用我们TokenFilter,如下:
1 2 3 4 | zuul: TokenFilter: # 类的名字 pre: # 类型 disable: true |
限流
令牌桶算法
https://blog.csdn.net/tianyaleixiaowu/article/details/74942405
https://baike.baidu.com/item/%E4%BB%A4%E7%89%8C%E6%A1%B6%E7%AE%97%E6%B3%95/6597000?fr=aladdin
系统按照恒定的速率往指定大小的桶里添加令牌,每来一个请求就消耗一个令牌,如果桶内没有令牌表示此事的请求流量已经超过设置的大小了,应该做出相应的响应或者直接抛出异常
实现
使用前置过滤器,在请求被转发之前调用,限流的过滤器应该是所有过滤器中优先级最大的
使用google开源的组件Guava
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 | import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Component; import com.google.common.util.concurrent.RateLimiter; import com.netflix.zuul.ZuulFilter; import com.netflix.zuul.context.RequestContext; import com.netflix.zuul.exception.ZuulException; /** * 限流 ,前置过滤器 * 限流的过滤器的优先级应该是最高,数字最小 */ @Component public class RateFilter extends ZuulFilter { private static final RateLimiter RATE_LIMITER=RateLimiter.create(100); //程每秒钟往桶里放置100个令牌
@Override public boolean shouldFilter() { return true; } @Override public Object run() throws ZuulException {
/** * tryAcquire():如果获取不到一个令牌,表示流量超时了,没有等待时间 * tryAcquire(int permits, long timeout, TimeUnit unit):获取permits个令牌,如果在指定的时间timeout内,还是没有获取到指定的permits个令牌,那么就返回false */ if (!RATE_LIMITER.tryAcquire()) { RequestContext requestContext = RequestContext.getCurrentContext(); requestContext.setSendZuulResponse(false); //不路由 requestContext.setResponseStatusCode(HttpStatus.FORBIDDEN.value()); //403拒绝访问 }
//也可以直接抛出异常 // if (!RATE_LIMITER.tryAcquire()) { // throw new RuntimeException(); //抛出异常 // } return null; } @Override public String filterType() { return FilterConstants.PRE_TYPE; //前置 } @Override public int filterOrder() { //org.springframework.cloud.netflix.zuul.filters.pre.ServletDetectionFilter#filterOrder()这个过滤器的优先级是最高的,只需要-1即可 return FilterConstants.SERVLET_DETECTION_FILTER_ORDER-1; } } |
多维度限流
https://segmentfault.com/a/1190000012252677
鉴权
https://www.jianshu.com/p/f89f5557990f
一些api只有具有某些权限的时候才可以被调用,比如用户的一些相关信息,只有在用户登录之后才可以调用,否则将会提示没有权限
实现
我们在用户登录成功之后会在返回头中添加cookie的值为openId=random(随机数)
,并且将其保存在redis
中(key=openId_userId,value=random)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | /** * 登录的方法,登录成功响应头返回添加cookie * @param response * @return */ @GetMapping("/user/login") public String login(HttpServletResponse response) { //登录的逻辑。。。。。
//设置cookie的值 Cookie cookie=new Cookie("openId", UUID.randomUUID().toString()); cookie.setMaxAge(60*60); response.addCookie(cookie); //添加到响应头中
//添加到redis中,key=openId_userId,value=uuid的值
return "登录成功"; } |
我们事先将指定权限的接口uri存放在数据库中,在zuul中定义一个鉴权的过滤器,如果请求过来了,判断这个uri是否需要某种权限才能调用,如果不需要直接路由即可,如果需要那么判断cookie
中是否有openId,如果没有表示没有登录,权限不够,如果有,需要判断和redis中的值是否相同,如果相同,表示有权限,直接路由到服务即可。
这里将部分逻辑写在shouldFilter()
方法中,限制范围(判断请求的uri是否需要鉴权),run()
方法中只需要判断是否具有权限即可。
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 */ @Component //一定要注入到ioc容器中 public class TokenFilter extends ZuulFilter {
@Resource private UriService uriservice; //注入 @Resource private RedisTemplate redisTemplate; //redis
/** * 判断过滤器是否被执行,返回true表示被会被执行(经过run方法) * 只需要判断请求的uri是存在数据库中即可 */ @Override public boolean shouldFilter() { RequestContext context=RequestContext.getCurrentContext(); //获取上下文 HttpServletRequest request = context.getRequest(); //获取request String uri=request.getRequestURI(); //获取请求的uri /** * 伪代码如下: * 1、List<URL> uriList=uriservice.getUrlList(); //获取需要权限访问的uri列表 * 2、判断请求的uri是否在uriList中 * 1、如果不存在,return false,表示不用执行过滤的逻辑(run方法)直接路由到指定的服务即可 * 2、如果不存在返回true,表示执行过滤的逻辑(run方法) */ return true; } /** * 过滤器的具体实现逻辑,经过shouldFilter方法之后,能够执行到这里的表示这个请求的uri需要验证权限 * @return * @throws ZuulException */ @Override public Object run() throws ZuulException { RequestContext requestContext = RequestContext.getCurrentContext(); //获取请求上下文 HttpServletRequest request = requestContext.getRequest(); //获取HttpServletRequest Cookie[] cookies = request.getCookies(); //获取cookie
/** * 伪代码如下: * 1、判断cookie中是否存在openId * 1、如果不存在,返回权限不足的提示信息 * 2、如果存在,需要判断redis中存储的openId的值是否和携带过来的cookie值相同 * 1、如果不相同,返回权限不足的提示信息 * 2、如果相同,表示这个请求具有相应的权限 */ return null; } @Override public String filterType() { return FilterConstants.PRE_TYPE; //前置过滤器 pre } @Override public int filterOrder() { //一般前置过滤器放在org.springframework.cloud.netflix.zuul.filters.pre.PreDecorationFilter这个过滤器之前即可,只需要将其对应的顺序-1 return FilterConstants.PRE_DECORATION_FILTER_ORDER-1; } } |
跨域
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 | import java.util.Arrays; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import org.springframework.web.filter.CorsFilter; /** * 跨域的配置类 */ @Configuration //配置类 public class CorsConfig { @Bean public CorsFilter corsFilter() { final UrlBasedCorsConfigurationSource source=new UrlBasedCorsConfigurationSource(); final CorsConfiguration config=new CorsConfiguration(); config.setAllowCredentials(true); //支持cookie跨域 config.setAllowedOrigins(Arrays.asList("*")); //配置允许跨域访问的域名,这里*表示全部 config.setAllowedHeaders(Arrays.asList("*")); //设置允许的头 config.setAllowedMethods(Arrays.asList("*")); //设置允许跨域的方法,GET,POST....,这里表示允许全部 config.setMaxAge(300l); //缓存时间,在指定的时间内,对于相同的请求就不需要再次检查了 source.registerCorsConfiguration("/**", config); return new CorsFilter(source); } } |
超时时间设置
我们在使用zuul访问服务的时候,一旦服务超过很短的时间没有响应,那么zuul就会自动熔断,默认的时间是2秒,但是可以通过配置修改,如下:由于zuul使用ribbon实现负载均衡,因此这里还需要配置ribbon的超时时间,否则配置将不会生效
1 2 3 4 5 6 7 8 | zuul: host: # 配置zuul的超时时间 connect-timeout-millis: 60000 # 默认2秒, socket-timeout-millis: 60000 ribbon: # zuul使用服务发现的时候,要想让上面的配置生效,必须配置ribbon的超时时间 ReadTimeout: 60000 # 请求处理时间。 ConnectTimeout: 60000 # 请求连接时间。 |
服务熔断
当请求的服务响应时间超时或者服务不可用的时候zuul会直接响应异常,我们可以设置熔断,只需要在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 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 | package cn.tedu.zuul.hystrix; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import org.springframework.cloud.netflix.zuul.filters.route.FallbackProvider; 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; /** * 设置zuul的熔断 * 实现FallbackProvider接口 * 出现熔断的情况如下: * 1、当请求的服务响应超时 * 2、当请求的服务不能正常提供服务 */ @Component //注入到IOC容器 public class OrderFallback implements FallbackProvider { /** * 这个方法返回的是serviceId,如果返回的单个服务,那么只针对一个服务熔断 * 如果想要针对所有的服务进行配置熔断,只需要返回*即可 */ @Override public String getRoute() { return "order-provider"; } /** * 发生熔断的响应方法 */ @Override public ClientHttpResponse fallbackResponse(String route, Throwable cause) { return new ClientHttpResponse() { @Override public HttpStatus getStatusCode() throws IOException { return HttpStatus.OK; } @Override public int getRawStatusCode() throws IOException { return 200; } @Override public String getStatusText() throws IOException { return "OK"; } @Override public void close() { }
//设置响应的内容 @Override public InputStream getBody() throws IOException { return new ByteArrayInputStream("fallback".getBytes()); } @Override public HttpHeaders getHeaders() { HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); return headers; } }; } } |
zuul的重试
有时候因为网络或者其它原因,服务可能会暂时的不可用,这个时候我们希望可以再次对服务进行重试,Zuul也帮我们实现了此功能,需要结合Spring Retry 一起来实现
依赖:
1 2 3 4 5 | <!-- 超时重试 --> <dependency> <groupId>org.springframework.retry</groupId> <artifactId>spring-retry</artifactId> </dependency> |
在配置文件中配置如下:
在zuul中开启重试机制
配置ribbon的重试次数
默认请求超时时间很短,还可以配置ribbon的超时时间
1 2 3 4 5 6 | zuul: retryable: true # 开启重试机制 ribbon: # zuul内部使用的是ribbon实现负载均衡的,因此配置ribbon的重试次数 MaxAutoRetries: 2 # 同一个服务的最大重试次数 MaxAutoRetriesNextServer: 2 # 对于切换的下一个实例的重试次数 |
Zuul的高可用
将多个zuul的微服务注册到Eureka中的(集群)
Nginx和Zuul混搭的方式,可以将Nginx的请求转发到多个zuul中,zuul再路由给指定的微服务
完整的配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | zuul: # TokenFilter: # pre: # disable: true # 忽略真实的服务名称实例访问,是一个Set集合,可以指定多个,取消全部使用 "*"即可 sensitive-headers: # 设置所有的服务都取消敏感头信息 ignored-services: - order-provider # ignored-patterns: # - /api-order/order/list # 取消指定的一个 # - /api-order/order/** # 使用通配符去掉order下的全部接口 #routes下面指定代理规则,可以同时指定多个 routes: #指定第一个规则,这里的名称任意 api-order: #指定的实例名称 serviceId: order-provider #指定可以访问的路由 path: /api-order/** |
消费端的使用
前提:
zuul微服务(zuul-server)注册到eureka注册中心
微服务提供者注册到Eureka注册中心,zuul-server配置的路由是api-order
服务消费者注册到Eureka中
那么如果消费者想用通过zuul-server访问到服务提供者,那么可以直接写http://zuul-server/api-order/order/{id}
1 2 3 4 5 6 7 8 9 10 11 12 | @RestController public class OrderController { private final static String URI_PRFIX="http://zuul-server/api-order"; //直接使用zuul网管连接订单的服务提供者
@Resource private RestTemplate restTemplate;
@GetMapping("/order/{id}") public Order getOrder(@PathVariable("id")Integer id) { return restTemplate.getForObject(URI_PRFIX+"/order/"+id, Order.class); } } |
源码
https://gitee.com/chenjiabing666/zuul-server.git
参考文章
https://windmt.com/2018/04/23/spring-cloud-11-zuul-filter/
https://cloud.spring.io/spring-cloud-netflix/2.0.x/single/spring-cloud-netflix.html#_router_and_filter_zuul
https://mp.weixin.qq.com/s/FsvZgkvpI0S6rposacGiiQ
推荐阅读
5G大规模商用来临之前,你必须知道的几个知识点
“离开360时,它只给了我一块钱”
AI找Bug,一键快速预测
原子互换:一统公链江湖的神来之笔
春晚鬼畜 B 站日排行最高,赵本山:我的时代还没有结束!
PDF翻译神器,再也不担心读不懂英文Paper了
新闻联播也可以拿来做数据分析?
高晓松侃5G!2019开年大讲揭示运营商的秘密
1.微信群:
添加小编微信:color_ld,备注“进群+姓名+公司职位”即可,加入【云计算学习交流群】,和志同道合的朋友们共同打卡学习!
2.征稿:
投稿邮箱:liudan@csdn.net;微信号:color_ld。请备注投稿+姓名+公司职位。
喜欢就点击“好看”吧!