服务网关是微服务架构中一个不可或缺的部分。通过服务网关统一向外系统提供REST API的过程中,除了具备服务路由、均衡负载功能之外,它还具备了权限控制等功能。Spring Cloud Netflix中的Zuul就担任了这样的一个角色,为微服务架构提供了前门保护的作用,同时将权限控制这些较重的非业务逻辑内容迁移到服务路由层面,使得服务集群主体能够具备更高的可复用性和可测试性。
服务网关 = 路由转发 + 过滤器
Netflix使用Zuul进行以下操作:
Zuul、Ribbon以及Eureka结合可以实现智能路由和负载均衡的功能;网关将所有服务的API接口统一聚合,统一对外暴露。外界调用API接口时,不需要知道微服务系统中各服务相互调用的复杂性,保护了内部微服务单元的API接口;网关可以做用户身份认证和权限认证,防止非法请求操作API接口;网关可以实现监控功能,实时日志输出,对请求进行记录;网关可以实现流量监控,在高流量的情况下,对服务降级;API接口从内部服务分离出来,方便做测试。
Zuul通过Servlet来实现,通过自定义的ZuulServlet来对请求进行控制。核心是一系列过滤器,可以在Http请求的发起和响应返回期间执行一系列过滤器。Zuul采取了动态读取、编译和运行这些过滤器。过滤器之间不能直接通信,而是通过RequestContext对象来共享数据,每个请求都会创建一个RequestContext对象。
Zuul生命周期如下图。 当一个客户端Request请求进入Zuul网关服务时,网关先进入”pre filter“,进行一系列的验证、操作或者判断。然后交给”routing filter“进行路由转发,转发到具体的服务实例进行逻辑处理、返回数据。当具体的服务处理完成后,最后由”post filter“进行处理,该类型的处理器处理完成之后,将Request信息返回客户端。 在其他阶段发生错误时执行”error filter“。 除了默认的过滤器类型,Zuul还允许我们创建自定义的过滤器类型。例如,我们可以定制一种STATIC类型的过滤器,直接在Zuul中生成响应,而不将请求转发到后端的微服务。
4.0.0
com.springcloud
springcloud-root
0.0.1-SNAPSHOT
springcloud-zuul
springcloud-zuul
http://maven.apache.org
UTF-8
org.springframework.cloud
spring-cloud-starter-netflix-eureka-client
org.springframework.cloud
spring-cloud-starter-netflix-zuul
org.springframework.boot
spring-boot-starter-test
test
junit
junit
3.8.1
test
org.springframework.boot
spring-boot-maven-plugin
spring:
application:
name: springcloud-zuul
freemarker:
prefer-file-system-access: false
security:
user:
name: admin
password: 123456
server:
port: 8120
eureka:
instance:
hostname: eureka-zuul.com
instance-id: eureka-zuul
client:
service-url:
defaultZone: http://${spring.security.user.name}:${spring.security.user.password}@eureka-peer1.com:8897/eureka/,http://${spring.security.user.name}:${spring.security.user.password}@eureka-peer2.com:8898/eureka/,http://${spring.security.user.name}:${spring.security.user.password}@eureka-peer3.com:8899/eureka/
zuul:
#接口前缀(v1作为版本号)
prefix: /v1
routes:
hiapi:
path: /hiapi/**
serviceId: springcloud-eureka-provider
ribbonapi:
path: /ribbonapi/**
serviceId: springcloud-ribbon
feignapi:
path: /feignapi/**
serviceId: springcloud-feign
修改C:\Windows\System32\drivers\etc\hosts
127.0.0.1 eureka-zuul.com
禁用指定的Filter
可以在 application.yml 中配置需要禁用的 filter,格式为
zuul...disable=true
比如要禁用org.springframework.cloud.netflix.zuul.filters.post.SendResponseFilter就设置
zuul:
SendResponseFilter:
post:
disable: true
加上注解@EnableZuulProxy
package org.springcloud.zuul;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
@SpringBootApplication
@EnableZuulProxy
public class ZuulApplication {
public static void main(String[] args) {
SpringApplication.run(ZuulApplication.class, args);
}
}
springcloud-eureka-cluster-peer1
springcloud-eureka-cluster-peer2
springcloud-eureka-cluster-peer3
springcloud-eureka-provider1
springcloud-eureka-provider2
springcloud-eureka-provider3
springcloud-ribbon
springcloud-feign
springcloud-zuul
浏览器多次访问http://eureka-zuul.com:8120/v1/hiapi/hi?name=zhaojq
zuul在路由转发做了负载均衡
浏览器多次访问http://eureka-zuul.com:8120/v1/ribbonapi/hi?name=zhaojq
Zuul和Ribbon相结合,实现了负载均衡(随机策略)
浏览器多次访问http://eureka-zuul.com:8120/v1/feignapi/hi?name=zhaojq
Zuul和Feign相结合,实现了负载均衡(随机策略)
实现熔断器功能需要实现FallbackProvider接口,实现该接口的两个方法,一个是getRoute(),用于指定熔断器功能应用于哪些路由的服务;另一个方法fallbackResponse()为进入熔断器功能时执行的逻辑。
如果需要所有的路由服务都加熔断功能,需要在getRoute()方法上返回”*“的匹配符
package org.springcloud.zuul;
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;
@Component
public class ZuulHystrix implements FallbackProvider {
@Override
//指定熔断器功能应用于哪些路由的服务
public String getRoute() {
//return "springcloud-eureka-provider";
return "*";
}
@Override
//进入熔断器功能时执行的逻辑
public ClientHttpResponse fallbackResponse(String route, Throwable cause) {
System.out.println("route:"+route);
System.out.println("exception:"+cause.getMessage());
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("eureka-provider is down!! this is the fallback.".getBytes());
}
@Override
public HttpHeaders getHeaders() {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
return headers;
}
};
}
}
全部正常启动后,停止 springcloud-eureka-provider2 提供者,端口为:8002服务
访问命令窗口curl http://eureka-zuul.com:8120/v1/hiapi/hi?name=zhaojq,断路器已经生效,提示:提供者服务挂了
Zuul包括以下4中过滤器
Zuul中默认实现的filter
类型 | 顺序 | 过滤器 | 功能 |
---|---|---|---|
pre | -3 | ServletDetectionFilter | 标记处理Servlet的类型 |
pre | -2 | Servlet30WrapperFilter | 包装HttpServletRequest请求 |
pre | -1 | FormBodyWrapperFilter | 包装请求体 |
route | 1 | DebugFilter | 标记调试标志 |
route | 5 | PreDecorationFilter | 处理请求上下文供后续使用 |
route | 10 | RibbonRoutingFilter | serviceId请求转发 |
route | 100 | SimpleHostRoutingFilter | url请求转发 |
route | 500 | SendForwardFilter | forward请求转发 |
post | 0 | SendErrorFilter | 处理有错误的请求响应 |
post | 1000 | SendResponseFilter | 处理正常的请求响应 |
实现自定义滤器需要继承ZuulFilter,实现ZuulFilter中的抽象方法,包括filterType(),filterOrder()以及IZuulFilter的shouldFilter()和run()方法。
package org.springcloud.zuul;
import java.io.IOException;
import javax.servlet.http.HttpServletRequest;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
public class TokenFilter extends ZuulFilter {
private static Logger LOGGER=LoggerFactory.getLogger(TokenFilter.class);
@Override
//定义filter的类型,有pre、route、post、error四种
public String filterType() {
//可以在请求被路由之前调用
return "pre";
}
@Override
//定义filter的顺序,数字越小表示顺序越高,越先执行
public int filterOrder() {
return 0;
}
@Override
//表示是否需要执行该filter,true表示执行,false表示不执行
public boolean shouldFilter() {
return true;
}
@Override
//filter需要执行的具体操作
public Object run() throws ZuulException {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
LOGGER.info("--->>> TokenFilter {},{}", request.getMethod(), request.getRequestURL().toString());
//获取请求的参数
String token = request.getParameter("token");
if (StringUtils.isNotBlank(token)) {
//对请求进行路由
ctx.setSendZuulResponse(true);
ctx.setResponseStatusCode(200);
ctx.set("isSuccess", true);
return null;
} else {
LOGGER.warn("token is empty");
//不对请求进行路由
ctx.setSendZuulResponse(false);
ctx.setResponseStatusCode(401);
try {
ctx.getResponse().getWriter().write("token is empty");
} catch (IOException e) {
e.printStackTrace();
}
ctx.set("isSuccess", false);
return null;
}
}
}
开启过滤器,在程序的启动类 ZuulFilterApplication 添加 Bean
package org.springcloud.zuul;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
import org.springframework.context.annotation.Bean;
@SpringBootApplication
@EnableZuulProxy
public class ZuulApplication {
public static void main(String[] args) {
SpringApplication.run(ZuulApplication.class, args);
}
@Bean
public TokenFilter TokenFilter() {
return new TokenFilter();
}
}
重启项目后,访问http://eureka-zuul.com:8120/v1/hiapi/hi?name=zhaojq
提示 token is empty。
访问http://eureka-zuul.com:8120/v1/hiapi/hi?name=zhaojq&token=cc
可见,TokenFilter这个Bean注入IOC容器后,对请求进行了过滤,并在请求路由转发之前进行了逻辑判断。
把自定义TokenFilter的@Component取消掉就可以了,不让它被Spring容器管理。
package org.springcloud.zuul;
import java.io.IOException;
import javax.servlet.http.HttpServletRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
public class PasswordFilter extends ZuulFilter {
private final Logger LOGGER = LoggerFactory.getLogger(PasswordFilter.class);
@Override
public String filterType() {
//请求已被路由到微服务后执行
return "post";
}
@Override
public int filterOrder() {
return 1;
}
@Override
public boolean shouldFilter() {
RequestContext ctx = RequestContext.getCurrentContext();
//判断上一个过滤器结果为true,否则就不走下面过滤器,直接跳过后面的所有过滤器并返回 上一个过滤器不通过的结果
return (boolean) ctx.get("isSuccess");
}
@Override
public Object run() throws ZuulException {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
LOGGER.info("--->>> PasswordFilter {},{}", request.getMethod(), request.getRequestURL().toString());
String username = request.getParameter("password");
if (null != username && username.equals("123456")) {
ctx.setSendZuulResponse(true);
ctx.setResponseStatusCode(200);
ctx.set("isSuccess", true);
return null;
} else {
ctx.setSendZuulResponse(false);
ctx.setResponseStatusCode(401);
try {
ctx.getResponse().getWriter().write("The password cannot be empty");
} catch (IOException e) {
e.printStackTrace();
}
ctx.set("isSuccess", false);
return null;
}
}
}
开启过滤器,在程序的启动类 ZuulFilterApplication 添加 Bean
package org.springcloud.zuul;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
import org.springframework.context.annotation.Bean;
@SpringBootApplication
@EnableZuulProxy
public class ZuulApplication {
public static void main(String[] args) {
SpringApplication.run(ZuulApplication.class, args);
}
@Bean
public TokenFilter TokenFilter() {
return new TokenFilter();
}
@Bean
public PasswordFilter PasswordFilter() {
return new PasswordFilter();
}
}
访问http://eureka-zuul.com:8120/v1/hiapi/hi?name=zhaojq&token=cc&password=123456