1. 概述
Zuul是一种提供动态路由、监视、弹性、安全性等功能的边缘服务,是一个基于JVM路由和服务端的负载均衡器,在Spring Cloud框架中,Zuul的角色是网关,负责接收所有REST请求,然后进行内部转发,是微服务提供者集群的流量入口。
1.1. 主要功能
路由:将不同REST请求转发至不同的微服务提供者,其作用类似于Nginx的反向代理。同时,也起到了统一端口的作用,将很多微服务提供者的不同端口统一到了Zuul的服务端口
认证:网关直接暴露在公网上时,终端要调用某个服务,通常会把登录后的token(令牌)传过来,网关层对token进行有效性验证。如果token无效(或没有token),就不允许访问REST服务。可以结合Spring Security中的认证机制完成Zuul网关的安全认证
限流:高并发场景下瞬时流量不可预估,为了保证服务对外的稳定性,限流成为每个应用必备的一道安全防火墙。如果没有这道安全防火墙,那么请求的流量超过服务的负载能力时很容易造成整个服务的瘫痪
负载均衡:在多个微服务提供者之间按照多种策略实现负载均衡
2. 搭建Zuul网关服务
Zuul作为网关层微服务,跟其他服务提供者一样都注册在Eureka Server上,可以相互发现。Zuul能感知到哪些服务Provider实例在线,同时通过配置路由规则可以将REST请求自动转发到指定的后端微服务提供者
2.1. 引入核心依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
2.2. 编写主启动类
@EnableZuulProxy
@SpringBootApplication
public class ZuulApplication {
public static void main(String[] args) {
SpringApplication.run(ZuulApplication.class, args);
}
}
在启动类中添加注解@EnableZuulProxy,声明这是一个网关服务提供者
2.3. 编写application.yml基础配置
server:
port: 8810
spring:
application:
name: cloud-zuul
eureka:
client:
register-with-eureka: false
fetch-registry: true
service-url:
defaultZone: http://localhost:8761/eureka
instance:
prefer-ip-address: true
ip-address: ${spring.cloud.client.ip-address}
instance-id: ${spring.cloud.client.ip-address}:${server.port}
zuul:
routes:
cloud-provider:
path: /cloud-provider/**
serviceId: cloud-provider
2.4. 验证
依次启动服务Eureka Server、Provider和Zuul,在浏览器地址输入http://localhost:8770/provider/getProviderInfo/world
再次在浏览器地址栏输入http://localhost:8810/cloud-provider/provider/getProviderInfo/world
3. 路由规则配置
路由规则通常有两种方式,其一是路由到直接URL,其二是路由到微服务提供者
两种方式的区别如下:
- 第一种方式使用url属性来指定直接的上游URL的前缀,第二种方式使用serviceId属性来指定上游服务提供者的名称
- 第二种方式需要结合Eureka Client客户端来实现动态的路由转发功能,需要配置Eureka相关配置信息
3.1. 过滤敏感请求头部
防止请求头泄露的方式之一是,在Zuul的路由配置中指定要忽略的请求头列表,并且多个敏感头部之间可以用逗号隔开,默认情况,Zuul转发请求会把header清空,如果在微服务集群内部转发请求,上游Provider就会收不到任何头部,如果需要传递原始的header信息到最终的上游,就需要添加如下敏感头部设置
zuul:
routes:
cloud-provider:
sensitiveHeaders:
如何需要屏蔽头信息,需要如下配置
zuul:
routes:
cloud-provider:
sensitiveHeaders: Cookie,Set-Cookie,token,backend,Authorization
3.2. 路径前缀处理
默认情况下Zuul会去掉路由的路径前缀,如果上游微服务提供者没有配置路径前缀,Zuul这种默认处理和转发就不会有问题,如果上游微服务提供者配置了统一的路径前缀,前缀去掉后,上游服务提供者就会报404错误,找不到URL对应的资源服务。可以设置配置项stripPrefix的值为false
zuul:
routes:
cloud-provider:
path: /cloud-provider/**
serviceId: cloud-provider
stripPrefix: true #是否取消请求前缀
如果需要对访问网关的所有请求都加上前缀,可以设置配置prefix,具体配置如下:
zuul:
prefix: /native #所有请求添加前缀
routes:
cloud-provider:
path: /cloud-provider/**
serviceId: cloud-provider
stripPrefix: true #是否取消请求前缀
sensitiveHeaders: Cookie,Set-Cookie,token,backend,Authorization
4. 查看路由信息
4.1. 引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
4.2. 开启路由监控端口
management:
endpoints:
web:
exposure:
include: 'routes' #开启查看路由端口
4.3. 验证
重新启动服务,在浏览器地址栏输入http://localhost:8810/actuator/routes
5. 过滤器
通过定义过滤器来实现请求的拦截和过滤
5.1. 过滤器类型
pre类型过滤器
请求路由之前调用,用于实现身份验证、记录调试信息
route类型过滤器
发送请求到上游服务,例如使用Apache HttpClient或Netflix Ribbon请求上游服务
post类型过滤器
上游服务返回之后调用,为响应添加HTTP响应头、收集统计信息和指标、将响应回复给客户端
error类型过滤器
在其他阶段发生错误时执行
5.2. 请求处理流程
- 当外部请求到达Zuul网关时,首先会进入pre处理阶段,在这个阶段请求将被pre类型的过滤器处理,以完成再请求路由的前置过滤处理,比如请求的校验等。在完成pre类型的过滤处理之后,请求进入第二个阶段:route路由请求转发阶段
- 在route路由请求转发阶段,请求将被route类型的过滤器处理,route类型的过滤器将外部请求转发到上游的服务。当服务实例的结果返回之后,route阶段完成,请求进入第三个阶段:post处理阶段
- 在post处理阶段,请求将被post类型的过滤器处理,post类型的过滤器在处理的时候不仅可以获取请求信息,还能获取服务实例的返回信息,所以post阶段可以对处理结果进行一些加工或转换等
- 还有一个特殊的阶段error,在该阶段请求将被error类型的过滤器处理,在上述3个阶段发生异常时才会触发,但是error过滤器也能将最终结果返回给请求客户端
5.3. 实例
Zuul提供一个过滤器ZuulFilter抽象基类,可以作为自定义过滤器的父类,需要实现的方法主要有4个
filterType方法
返回自定义过滤器类型,以常量的形式定义在FilterConstants类中
filterOrder方法
返回过滤器顺序,值越小优先级越高
shouldFilter方法
返回过滤器是否生效,返回true表示生效,返回false表示不生效
run方法
过滤器业务逻辑处理,可以进行当前的请求拦截和参数定制,后续路由定制,返回结果定制
例如可以使用前置过滤器打印日志和黑名单处理过滤,具体代码如下
@Component
public class BlackListFilter extends ZuulFilter {
private static final Logger logger = LoggerFactory.getLogger(BlackListFilter.class);
static List<String> blackList = Arrays.asList("");
/**
* 过滤器类型pre:过滤之前;routing:路由之时;post:路由之后;error:发送错误调用
* @return
*/
@Override
public String filterType() {
return "pre";
}
/**
* 过滤执行次序
* @return
*/
@Override
public int filterOrder() {
return 0;
}
/**
* 是否执行过滤
* @return
*/
@Override
public boolean shouldFilter() {
RequestContext context = RequestContext.getCurrentContext();
if (!context.sendZuulResponse()) {
return false;
}
//返回true,表示需要执行run方法
HttpServletRequest request = context.getRequest();
if (request.getRequestURI().startsWith("/native")) {
return true;
}
return false;
}
/**
* 过滤器具体执行方法
* @return
* @throws ZuulException
*/
@Override
public Object run() throws ZuulException {
RequestContext context = RequestContext.getCurrentContext();
HttpServletRequest request = context.getRequest();
String host = request.getRemoteHost();
String method = request.getMethod();
String uri = request.getRequestURI();
logger.info("=====>Remote host:{},method:{},uri:{}", host, method, uri);
String username = request.getParameter("username");
if (null != username && blackList.contains(username)) {
logger.info(username + " is forbidden: " + request.getRequestURL().toString());
context.setSendZuulResponse(false);
try {
context.getResponse().setContentType("text/html;charset=utf-8");
context.getResponse().getWriter().write("对不起,您已进入黑名单!");
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
return null;
}
}