Zuul是Spring Cloud全家桶中的微服务API网关。
所有从设备或网站来的请求都会经过Zuul到达后端的Netflix应用程序。作为一个边界性质的应用程序,Zuul提供了动态路由、监控、弹性负载和安全功能。Zuul底层利用各种filter实现如下功能:
•认证和安全 识别每个需要认证的资源,拒绝不符合要求的请求。
•性能监测 在服务边界追踪并统计数据,提供精确的生产视图。
•动态路由 根据需要将请求动态路由到后端集群。
•负载卸载 预先为每种类型的请求分配容量,当请求超过容量时自动丢弃。
•静态资源处理 直接在边界返回某些响应。
zuul入门
新建一个服务zuul-service作为路由网关服务。
pom.xml中引入依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zuul</artifactId>
</dependency>
使用@EnableZuulProxy启用Zuul(使用@EnableZuulServer也可以启用Zuul,只是不会自动从Eureka中获取并自动代理服务,也不会自动加载部分Zuul过滤器,但是可以选择性地替换代理平台的各个部分)。
@SpringBootApplication
@EnableEurekaClient
@EnableZuulProxy
public class ZuulApplicationStarter {
public static void main(String[] args) {
SpringApplication.run(ZuulApplicationStarter.class, args);
}
}
路由配置
server:
port: 9000
spring:
application:
name: zuul Service
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8771/eureka/
fetch-registry: false
zuul:
strip-prefix: false
routes:
users: #你自定义规则名字
path: /user/** #url地址
serviceId: one #路由到serviceid
ignored-patterns: /user/one #url拦截 防止外面通过url请求内部通信路径
#简便写法
#one: /user/**
ribbon:
eureka:
enabled: false
one:
ribbon:
listOfServers: http://localhost:8773
management:
security:
enabled: false
相关参数
Zuul会自动读取注册中心的已经注册的服务。user-service服务会自动设置/user-service/**这样的路由,即/user-service/users会被代理到user-service服务的/users请求。
zuul.ignoredServices
可以指定忽略注册中心获取的服务
zuul.routes.=
zuul.routes.<key>.serviceId=<serviceId>
指定一个服务对应路由路径为
zuul.routes.<key>.path
zuul.routes.<key>.url=<url>
指定一个服务的url或者使用forward转向Zuul服务的接口,对应路由路径为
zuul.routes.<key>.path
zuul.routes.<ribbon>=<path>
使用自定义Ribbon实现路由
Zuul服务启动完成后,可以访问http://localhost:9000/routes获取路由列表
动态路由
Zuul结合SpringCloud配置中心,在修改路由配置信息后刷新配置可立即生效,无需重启Zuul服务,这样就实现了动态路由。
Zuul Filter
Zuul进行代理时,会有一系列的Zuul Filter对Http请求的request和response进行封装和操作。
一个Zuul Filter有下面四个要素:
Type:类型。Zuul Filter的类型包括pre,routing,post和error。routing过滤器是在路由阶段执行的,负责寻找原服务、请求转发和返回接收。pre和post分别在routing之前和之后执行。如果Zuul执行代理的过程中抛出ZuulException异常,则会被error过滤器捕获并进行相应处理。
Execution Order:执行顺序。通过一个整型的值从小到大依次执行(相同类型过滤器间互相比较)。
Criteria:执行条件。当满足一定条件时,才会执行该过滤器。
Action:执行动作。当执行条件满足时,进行的操作。
实现一个过滤器只要继承ZuulFilter,并实现filterType(),filterOrder(),shouldFilter()和run()四个方法。这些方法与上面的四个要素对应。
如果要禁用一个Zuul过滤器,只需要配置zuul.<SimpleClassName>.<filterType>.disable=true
,比如需要禁用org.springframework.cloud.netflix.zuul.filters.post.SendResponseFilter需要配置zuul.SendResponseFilter.post.disable=true
。
下面我们使用一个pre过滤器实现token验证,如果Http header里面没有一个固定的token,则禁止访问。
禁用Zuul默认的error过滤器,设置固定的token和需要验证的路由key名单
zuul:
# 禁用SpringCloud自带的error filter
SendErrorFilter:
error:
disable: true
zuul-filter:
token-filter:
# 访问时,需要进行认证的路由key
un-auth-routes:
- users
- smsApi
# 固定的token
static-token: xF2fdi8M
读取自定义token配置信息
@Component
@ConfigurationProperties("zuulFilter.tokenFilter")
public class TokenValidateConfiguration {
// 在这个列表里面存储的routeId都是需要使用TokenValidateFilter过滤的
private List<String> unAuthRoutes;
// 给定的token
private String staticToken;
public List<String> getUnAuthRoutes() {
return unAuthRoutes;
}
public void setUnAuthRoutes(List<String> unAuthRoutes) {
this.unAuthRoutes = unAuthRoutes;
}
public String getStaticToken() {
return staticToken;
}
public void setStaticToken(String staticToken) {
this.staticToken = staticToken;
}
}
自定义过滤器
1、 shouldFilter:返回一个Boolean值,判断该过滤器是否需要执行。返回true表示要执行此过虑器,否则不执行。
2、 run:过滤器的业务逻辑。
3、 filterType:返回字符串代表过滤器的类型,如下
pre:请求在被路由之前执行
routing:在路由请求时调用
post:在routing和errror过滤器之后调用
error:处理请求时发生错误调用
4、 filterOrder:此方法返回整型数值,通过此数值来定义过滤器的执行顺序,数字越小优先级越高。
@Component
public class TokenValidateFilter extends ZuulFilter {
protected static final Logger logger = LoggerFactory.getLogger(TokenValidateFilter.class);
@Autowired
private TokenValidateConfiguration tvConfig;
@Override
public String filterType() {//类型
return FilterConstants.PRE_TYPE;
}
@Override
public int filterOrder() {//执行顺序
return FilterConstants.PRE_DECORATION_FILTER_ORDER;
}
@Override
public boolean shouldFilter() {//是否拦截
RequestContext ctx = RequestContext.getCurrentContext();
return tvConfig.getUnAuthRoutes().contains(ctx.get(FilterConstants.PROXY_KEY));
}
@Override
public Object run() {//拦截后操作
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
String token = request.getHeader("Authorization");
if (token == null) {
logger.warn("Http Header Authorization is null");
forbidden();
return null;
}
String staticToken = tvConfig.getStaticToken();
if (StringUtils.isBlank(staticToken)) {
logger.warn("property zuulFilter.tokenFilter.staticToken was not set");
forbidden();
} else if (!staticToken.equals(token)) {
logger.warn("token is not valid");
forbidden();
}
return null;
}
/**
* 设置response的状态码为403
*/
private void forbidden() {
// zuul中,将请求附带的信息存在线程变量中。
RequestContext.getCurrentContext().setResponseStatusCode(HttpStatus.FORBIDDEN.value());
ReflectionUtils.rethrowRuntimeException(new ZuulException("token is not valid", HttpStatus.FORBIDDEN.value(),
"token校验不通过"));
}
}
注意:如果使用zuul.routes.=方式配置的路由,则ctx.get(FilterConstants.PROXY_KEY)会得到去掉头尾的url(/smsApi/**会得到smsApi,/smsApi/target/**会得到smsApi/target),而并非路由key。所以之前配置文件中的路由
测试:携带正确的token访问成功
新建一个error过滤器,当捕获到ZuulException时,返回一个JSON对象
@Component
public class SendErrorRestFilter extends ZuulFilter {
private static final Logger logger = LoggerFactory.getLogger(SendErrorRestFilter.class);
@Override
public String filterType() {
return FilterConstants.ERROR_TYPE;
}
@Override
public int filterOrder() {
return FilterConstants.SEND_ERROR_FILTER_ORDER;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() {
RequestContext context = RequestContext.getCurrentContext();
Throwable throwable = getCause(context.getThrowable());
// 获取response状态码
int status = context.getResponseStatusCode();
JSONObject info = new JSONObject();
info.put("code", "异常码" + status);
info.put("message", throwable.getMessage());
// 记录日志
logger.warn("请求异常,被error filter拦截", context.getClass());
// 设置response
context.setResponseBody(info.toJSONString());
context.getResponse().setContentType("application/json;charset=UTF-8");
context.getResponse().setStatus(HttpStatus.OK.value());
// 处理了异常之后清空异常
context.remove("throwable");
return null;
}
private Throwable getCause(Throwable throwable) {
while (throwable.getCause() != null) {
throwable = throwable.getCause();
}
return throwable;
}
}