SpringCloud 整合 Zuul 实现服务网关接口统一鉴权,实战讲解!
一、背景介绍
在之前的文章中,我们介绍了 Spring Cloud Zuul 的基础用法,本文将继续探讨 Zuul 的更高级用法。
二、Zuul 高级特性介绍
在之前的文章中,我们提到过 Zuul 除了提供服务路由、均衡负载等基础功能之外,还可以利用它来实现权限控制、接口限流等功能。
如何实现接口权限控制呢?
在 Zuul 中有一个非常核心的功能:过滤器,用于实现对请求进行全程控制。
此外,Zuul 除了默认内置的一些必要的过滤器组件之外,还允许开发者自定义过滤器来实现对请求进行拦截、过滤,我们可以利用它来实现权限控制、接口限流等操作。
整个过滤器的生命周期可以用下图来表示。

可以这么说,Zuul 的核心处理流程主要是通过过滤器来实现,每个阶段都有相应的过滤器来执行对应的任务。如果将其进行归类,可以划分成如下四个类型。
- pre 过滤器:会在请求被路由之前调用。开发者可以利用这种过滤器实现请求的校验,权限控制、接口限流等操作,类似于请求前的拦截。
- routing 过滤器:会在路由请求时候被调用。此阶段会将外部请求转发到具体服务实例上,并等待目标服务实例的请求返回结果。
- post 过滤器:会在请求被路由之后调用。开发者可以利用这种过滤器实现统一的请求返回格式,类似于请求后的拦截。
- error 过滤器:其它阶段在处理请求发生错误时被调用。
默认情况下,Zuul 为开发者内置了一些过滤器实现,其内容如下表所示。
类型 | 顺序 | 过滤器 | 功能 |
---|---|---|---|
pre | -3 | ServletDetectionFilter | 标记处理Servlet的类型 |
pre | -2 | Servlet30WrapperFilter | 包装HttpServletRequest请求 |
pre | -1 | FormBodyWrapperFilter | 包装请求体 |
pre | 1 | DebugFilter | 标记调试标志 |
pre | 5 | PreDecorationFilter | 标记调试标志 |
route | 10 | RibbonRoutingFilter | serviceId请求转发 |
route | 100 | SimpleHostRoutingFilter | url请求转发 |
route | 500 | SendForwardFilter | forward请求转发 |
post | 1000 | SendResponseFilter | 处理正常的请求响应 |
error | 0 | SendErrorFilter | 处理有错误的请求响应 |
因此,当我们集成 Zuul 的时候,无需添加任何过滤器就可以正常转发,其实内置过滤器起了很大的作用。
如果我们想要禁止某些过滤器,可以在application.properties
文件中添加相应的配置即可,示例如下:
# 禁止过滤器启用
zuul.DebugFilter.pre.disable=true
实际上,为了简化开发,Zuul 将具体过滤器的方法进行抽象化了,这些抽象方法分别是:过滤类型、执行顺序、执行条件、具体操作。
/**
* 过滤类型,枚举指:pre、route、post、error
*/
String filterType();
/**
* 执行顺序,数字越小表示顺序越高,越先执行
*/
int filterOrder();
/**
* 执行条件,true表示执行,false表示不执行
*/
boolean shouldFilter();
/**
* 具体操作
*/
Object run();
在开发业务的时候,如果我们想自定义过滤器,只需要继承ZuulFilter
抽象类并重写以上的抽象方法即可。
下面我们通过具体的例子,看看如何使用自定义 Zuul 过滤器来实现对请求拦截和返回参数的包装操作。
三、方案实践
3.1、自定义 Filter
下面我们以之前构建的eureka-zuul
服务网关工程为例,实现对请求头部的 token 进行鉴权和请求返回格式的统一封装,采用自定义 Filter 来实现,具体示例如下。
3.1.1、自定义 pre 过滤器
首先,自定义一个 pre 类型的过滤器,执行顺序需要排在内置 pre 过滤器的后面,以便于所有的前置过滤器都能被执行到。
token 鉴权操作示例如下!
@Component
public class TokenFilter extends ZuulFilter {
private final Logger LOGGER = LoggerFactory.getLogger(TokenFilter.class);
@Override
public String filterType() {
return "pre";
}
@Override
public int filterOrder() {
return 10;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
// 获取请求token
String token = request.getParameter("accessToken");
LOGGER.info("TokenFilter, 请求accessToken参数:{}", token);
// 检查token是否合法
if(!"123456789".equals(token)){
// 不对请求进行路由
ctx.setSendZuulResponse(false);
// 设置返回状态码
ctx.setResponseStatusCode(500);
// 设置返回响应体
ctx.setResponseBody(JSONObject.toJSONString(buildRsg(500, "token 无效!")));
// //设置返回响应体格式
ctx.getResponse().setContentType("application/json;charset=UTF-8");
return null;
}
return null;
}
private Map<String, Object> buildRsg(Integer code, String msg){
Map<String, Object> result = new HashMap();
result.put("code", code);
result.put("message", msg);
return result;
}
}
3.1.2、自定义 post 过滤器
接着,自定义一个 post 类型的过滤器,执行顺序需要排在内置 post 过滤器的前面,否则可能无法被成功执行。
对请求返回信息进行统一封装的示例如下!
@Component
public class ResponseFilter extends ZuulFilter {
private final Logger LOGGER = LoggerFactory.getLogger(ResponseFilter.class);
@Override
public String filterType() {
return "post";
}
@Override
public int filterOrder() {
return 1;
}
@Override
public boolean shouldFilter() {
RequestContext ctx = RequestContext.getCurrentContext();
// 如果前面的拦截器不进行路由,不执行具体操作
if (!ctx.sendZuulResponse()) {
return false;
}
// 如果有抛异常,不执行具体操作
if(ctx.getThrowable() != null){
return false;
}
return true;
}
@Override
public Object run() {
LOGGER.info("ResponseFilter....");
RequestContext ctx = RequestContext.getCurrentContext();
try {
// 读取返回的内容
String responseResult = StreamUtils.copyToString(ctx.getResponseDataStream(), Charset.forName("UTF-8"));
// 统一包装返回格式
ctx.setResponseBody(JSONObject.toJSONString(buildRsg(200, responseResult)));
// 设置json返回格式
ctx.addZuulResponseHeader("Content-Type", "application/json;charset=UTF-8");
} catch (IOException e) {
LOGGER.error("Error reading body", e);
}
return null;
}
private Map<String, Object> buildRsg(Integer code, String msg){
Map<String, Object> result = new HashMap();
result.put("code", code);
result.put("message", msg);
return result;
}
}
3.1.3、自定义 error 过滤器
然后,自定义一个 error 类型的过滤器,执行顺序需要排在内置 error 过滤器的前面,否则可能无法被执行。
对各个阶段执行的异常信息进行统一封装,操作示例如下。
@Component
public class ErrorFilter extends ZuulFilter {
private final Logger LOGGER = LoggerFactory.getLogger(ErrorFilter.class);
@Override
public String filterType() {
return "error";
}
@Override
public int filterOrder() {
//需要在默认的 SendErrorFilter 之前
return -1;
}
@Override
public boolean shouldFilter() {
// 只有在抛出异常时才会进行拦截
return RequestContext.getCurrentContext().containsKey("throwable");
}
@Override
public Object run() {
try {
RequestContext requestContext = RequestContext.getCurrentContext();
Object e = requestContext.get("throwable");
if (e != null && e instanceof Exception) {
Exception error = (Exception) e;
LOGGER.info("ErrorFilter...,错误信息:", e);
// 响应给客户端信息
HttpServletResponse response = requestContext.getResponse();
response.setHeader("Content-type", "application/json;charset=UTF-8");
response.setCharacterEncoding("UTF-8");
PrintWriter pw = response.getWriter();
pw.write(JSONObject.toJSONString(buildRsg(9999, error.getMessage())));
pw.flush();
pw.close();
// 重新标记参数,不然会被 sendErrorFilter 执行处理
requestContext.set("sendErrorFilter.ran", true);
}
} catch (Exception ex) {
LOGGER.error("Exception filtering in custom error filter", ex);
ReflectionUtils.rethrowRuntimeException(ex);
}
return null;
}
private Map<String, Object> buildRsg(Integer code, String msg){
Map<String, Object> result = new HashMap();
result.put("code", code);
result.put("message", msg);
return result;
}
}
3.1.4、服务测试
最后,将服务注册中心、服务网关、订单服务依次启动起来,测试过滤器是否能正常工作。
1)测试 pre 前置过滤器
在浏览器中访问http://localhost:9030/order/hello
,看看结果如何。

可以清晰的看到,提示 token 无效!结果以json
格式返回!
2)测试 post 后置过滤器
继续在浏览器中访问http://localhost:9030/order/hello?token=123456789
,看看结果如何。

可以清晰的看到,自定义的 post 过滤器成功实现对返回结果进行统一格式封装。
3) 测试 error 异常过滤器
将订单微服务停机,再次访问http://localhost:9030/order/hello?token=123456789
,看看结果如何。


因为目标服务已经停止服务,因此 zuul 在进行请求转发的时候会抛异常。
可以清晰的看到,自定义的 error 过滤器成功实现对请求异常信息的处理。
3.2、路由重试
有时候因为网络抖动,服务会出现短暂的不可用,此时我们希望可以再次对服务进行重试。
Zuul 也可以帮助我们实现此功能,不过需要结合 Spring Retry 一起来实现,具体操作步骤如下。
3.2.1、添加 Spring Retry 依赖
在pom.xml
文件中,添加 Spring Retry 依赖包。
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
3.2.2、添加 Zuul Retry 配置
在application.properties
配置文件中,添加 Zuul Retry 配置,示例如下:
#是否开启重试功能
zuul.retryable=true
#同一个Server重试的次数(除去首次)
ribbon.MaxAutoRetries=2
#切换相同Server的次数
ribbon.MaxAutoRetriesNextServer=0
3.2.3、服务测试
最后,对订单服务中的/hello
接口进行改造,添加一段阻塞时间,以便测试 Zuul 是否能自动重试。
/hello
接口改造后的内容如下。
@GetMapping("/hello")
public String index() {
System.out.println("收到客户端发起的rpc请求!");
try{
Thread.sleep(1000000);
}catch ( Exception e){
e.printStackTrace();
}
return "hello,我是服务提供方";
}
最后,将服务注册中心、服务网关、订单服务依次启动起来,在此访问http://localhost:9030/order/hello?token=123456789
,看看结果如何。

可以清晰的看到,目标服务一共收到3次请求,后两次是路由重试的效果。
四、小结
最后总结一下,正如之前所介绍的,Zuul 是一个强大的服务网关组件,不仅能实现服务路由、均衡负载等网关的基本要求,还可以利用自定义过滤器实现权限控制、接口限流等操作。
尽管之后 Netflix 宣布对其停止维护,但是 Zuul 仍然是一个值得学习和了解的重要工具,后续诞生的很多服务网关组件的设计思路都有 Zuul 的影子。
五、参考
作者:潘志的技术笔记
出处:https://pzblog.cn/
版权归作者所有,转载请注明出处
