1 介绍
API 网关是对外服务的一个入口,其聚合内部服务,提供统一对外的 API 接口给前端系统,屏蔽内部实现细节,可以为我们管理大量的 API 接口,还可以对接客户、适配协议、进行安全认证、转发路由、限制流量、监控日志、防止爬虫、进行灰度发布等。
Zuul 是一个基于 JVM 路由和服务端的负载均衡器,提供路由、监控、弹性、安全等方面的服务框架。其核心是过滤器,通过这些过滤器我们可以扩展出很多功能,比如:
- 动态路由:动态地将客户端的请求路由到后端不同的服务器,做一些逻辑处理,比如聚合多个服务的数据返回。
- 请求监控:可以对整个系统的请求进行监控,记录详细的请求响应日志,可以实时统计出当前系统的访问量以及监控状态。
- 认证鉴权:对每一个访问的请求做认证,拒绝非法请求,保护好后端的服务。
- 压力测试:压力测试是一项很重要的工作,像一些电商公司需要模拟更多真实的用户并发量来保证重大活动时系统的稳定。通过 Zuul 可以动态地将请求转发到后端服务的集群中,还可以识别测试流量和真实流量,从而做一些特殊处理。
- 灰度发布:灰度发布可以保证整体系统的稳定,在初始灰度的时候就可以发现、调整问题,以保证其影响度。
2 整合 Zuul
在实际开发中通常使用 Zuul 来代理请求转发到内部的服务上去,统一为外部提供服务。内部服务的数量会很多,而且可以随时拓展,我们不可能增加一个服务就改一次路由配置,所以也得通过结合 Eureka 来实现动态的路由转发功能。
2.1 pom.xml
<dependencies>
<!-- springcloud begin -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
<version>1.4.6.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zuul</artifactId>
<version>1.4.6.RELEASE</version>
</dependency>
<!-- springcloud end -->
</dependencies>
2.2 application.yml
server:
port: 5001
spring:
application:
name: springcloud-zuul
# eureka 配置
eureka:
client:
service-url:
# eureka 服务端地址(即注册地址),若 eureka 开启了权限认证,则需要携带账号密码
defaultZone: http://pkyShare:123456@eureka7001.com:7001/eureka/
# defaultZone: http://pkyShare:123456@eureka7001.com:7001/eureka/,http://pkyShare:123456@eureka7002.com:7002/eureka/,http://pkyShare:123456@eureka7003.com:7003/eureka/
# 网关
zuul:
routes: # 路由转发
api-consumer: # 自定义名字
path: /consumer/** # 路径,**表示匹配 /consumer 路径下的所有路径
serviceId: springclud-consumer # 路由到指定服务器的服务名(spring.application.name)
api-hystri-consumer:
path: /hy/consumer/**
serviceId: springclud-consumer-hystrix
2.3 启动类
@SpringBootApplication
@EnableZuulProxy // 开启路由代理功能
public class ZuulApplication {
public static void main(String[] args) {
SpringApplication.run(ZuulApplication.class, args);
}
}
@ EnableZuulProxy 已经自带了 @EnableDiscoveryClient。
2.4 启动测试
分别启动 Eureka 服务、两个服务消费者或服务提供者和 Zuul 服务。
-
Eureka
-
浏览器输入:localhost:5001/consumer/users/feign/pkyShare
这样就完成的 Zuul 的整合。
2.5 Zuul 转发规则
Zuul 结合 Eureka 后,就是可以为 Eureka 中所有的服务进行路由操作了,默认的转发规则是“Zuul 网关地址 + 访问的服务名称 + 接口 UR”。,其中 “springclud-consumer” 是一个服务消费者的服务名
如果给服务指定名称的时候,应尽量短一点,这样的话我们就可以用默认的路由规则进行请求,不需要为每个服务都顶一个路由规则,这样就算新增了服务,API 网关也不用修改和重启了。
默认规则举例:
- Zuul 网关地址:http://localhost:5001
- 服务1名称:springclud-consumer (apring.application.name)
- 服务1 获取某用户信息 UR:/users/feign/{username}
因此浏览器也可以将 “localhost:5001/consumer/users/feign/pkyShare” 改为 “localhost:5001/springclud-consumer/users/feign/pkyShare” 进行访问。
3 Zuul 中过滤器类型
Zuul 中的过滤器总共有 4 中类型,每种类型都有对应的使用场景。
- pre:可以在请求被路由之前调用。适用于身份认证的场景,认证通过后再继续执行下面的流程。
- route:在路由请求时被调用。适用于灰度发布场景,在将要路由的时候可以做一些自定义的逻辑。
- post:在 route 和 error 过滤之后被调用。这种过滤器将请求路由到达具体的服务之后执行。适用于需要添加响应头,记录响应日志等场景。
- error:处理请求时发生错误时被调用。在执行过程中发送错误时会进入 error 过滤器,可以用来统一记录错误信息。
4 Zuul 生命周期
上图可知,请求发过来首先到 pre 过滤器,再到 routing 过滤器,最后到 post 过滤器,任何一个过滤器有异常都会进入 error 过滤器。
通过 com.netflix.zuul.http.ZuulServlet 也可以看出完整执行顺序,ZuulServlet 类似 Spring MVC 中的 DispatcherServlet,所有的 Request 都要进过 ZuulServlet 的处理,如下代码所示:
public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
try {
this.init((HttpServletRequest)servletRequest, (HttpServletResponse)servletResponse);
RequestContext context = RequestContext.getCurrentContext();
context.setZuulEngineRan();
try {
this.preRoute();
} catch (ZuulException var12) {
this.error(var12);
this.postRoute();
return;
}
try {
this.route();
} catch (ZuulException var13) {
this.error(var13);
this.postRoute();
return;
}
try {
this.postRoute();
} catch (ZuulException var11) {
this.error(var11);
}
} catch (Throwable var14) {
this.error(new ZuulException(var14, 500, "UNHANDLED_EXCEPTION_" + var14.getClass().getName()));
} finally {
RequestContext.getCurrentContext().unset();
}
}
5 自定义过滤器实现 Token 认证
package com.pky.springcloud.zuul.filter;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.apache.http.HttpStatus;
import org.codehaus.jettison.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Map;
import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.PRE_TYPE;
/**
* Token 过滤器
*/
@Component
public class TokenFilter extends ZuulFilter {
private static final Logger logger = LoggerFactory.getLogger(TokenFilter.class);
/**
* 配置过滤类型,有四种不同生命周期的过滤器类型
* 1. pre:路由之前
* 2. routing:路由之时
* 3. post:路由之后
* 4. error:发生错误调用
* @return
*/
@Override
public String filterType() {
return PRE_TYPE;
}
@Override
public int filterOrder() {
return 1;
}
/**
* 配置是否需要过滤:true/需要,false/不需要
* @return
*/
@Override
public boolean shouldFilter() {
//此方法可以根据请求的url进行判断是否需要拦截
return true;
}
/**
* 过滤器的具体业务代码
* @return
* @throws ZuulException
*/
@Override
public Object run() throws ZuulException {
/**RequestContext 用于记录 Request 的 context。前面也分析了,由于 Servlet 是单例多线程的,而 Request 由唯一 worker 线程处理,
这里的RequestContext使用`ThreadLocal`实现,其本身简单wrap了`ConcurrentHashMap`
**/
RequestContext currentContext = RequestContext.getCurrentContext();
HttpServletRequest request = currentContext.getRequest();
//避免中文乱码
currentContext.addZuulResponseHeader("Content-type", "text/json;charset=UTF-8");
currentContext.getResponse().setCharacterEncoding("UTF-8");
//打印日志
logger.info("{} >>> {}", request.getMethod(), request.getRequestURL().toString());
String token = request.getParameter("token");
if (token == null) {
logger.warn("Token is empty");
//设置为false则不往下走(不调用api接口)
currentContext.setSendZuulResponse(false);
//响应一个状态码:401
currentContext.setResponseStatusCode(HttpStatus.SC_UNAUTHORIZED);
try {
setResponse(currentContext.getResponse(), HttpStatus.SC_UNAUTHORIZED,"Token 不可为空");
}
catch (Exception e) {
}
}
else {
logger.info("OK");
}
return null;
}
/**
* 设置响应信息
* @param resp
* @param code
* @param msg
* @return
* @throws Exception
*/
private void setResponse(HttpServletResponse resp, int code, String msg) throws Exception {
resp.setCharacterEncoding("UTF-8");
resp.setContentType("application/json;charset=utf-8");
PrintWriter writer = resp.getWriter();
JSONObject resultJson = new JSONObject();
resultJson.put("code", code);
resultJson.put("msg", msg);
writer.append(resultJson.toString());
}
}
不携带 token 访问: