Spring Cloud网关服务Zuul基本原理

在我们的系统中会有各种各样的微服务,当我们想要控制权限、限制登录,那么就需要在所有微服务中添加登录校验;因此就有了网关,网关顾名思义就是用来做调度和过滤的

1.快速入门

  1. 创建一个基础SpringBoot网关服务,引入zuul依赖
<dependencies> 
	<dependency> 
		<groupid>org.springframework.cloud</groupid> 
		<artifactId>spring-cloud-starter-zuul</artifactId> 
	</dependency> 
</dependencies>
  1. 注解开启zuul网关服务
// 开启zuul
@EnableZuulProxy 
@SpringCloudApplication 
public class Application { 
	public static void main(String[] args) { 
		new SpringApplicationBuilder(Application.class).web(true).run(args);
	}
}
  1. 在配置文件application.properties等加入应用名、端口等
spring.application.name=api-gateway 
server.port=5555
  1. 配置一些简单路由规则(请求转发地址)
// 符合api-a-url规则的请求都会被转发到localhost:8080机器上
zuul.routes.api-a-url.path=/api-a-url/** 
zuul.routes.api-a-url.url=http://localhost:8080/ 

意思是我们请求 http://localhost:5555/api-a-url/getUser就会将请求转发到http://localhost:8080/getUser上。

  1. 面向服务的路由规则

上面的url因为是个具体的地址,我们如果更新ip或者端口就需要更改,这会带来很大不便,所以需要我们引入eureka注册中心,只有这样我们才能发现服务拿到服务具体地址。

// 加入eureka包
<dependency> 
	<groupId>org.springframework.cloud</groupId> 
	<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>

修改路由配置

// 同理将满足 /api-a/**请求转发到 hello-service/**上,然后在RestTemplate和ribbon处理后请求具体ip:port/**
zuul.routes.api-a.path=/api-a/** 
zuul.routes.api-a.serviceId=hello-service 
zuul.routes.api-b.path=/api-b/**
zuul.routes.api-b.serviceid=feign-consumer
// erueka注册中心配置
eureka.client.serviceUrl.defaultZone=http://localhost:1111/eureka/ 

2.请求过滤

我们在登录鉴权的时候,需要对一些不符合的请求过滤掉,或者对不符合的地区ip进行拦截等,那么我们就需要使用到网关过滤功能

Zuul 允许开发者在API网关上通过定义过滤器来实现对请求的拦截与过滤,实现的方法非常简单,我们只需要继承 ZuulFilter 抽象类并实现它定义的4个抽象函数就可以完成对请求的拦截和过滤了。

public class AccessFilter extends ZuulFilter { 
	private static Logger log = LoggerFactory.getLogger(AccessFilter.class); 
	@Override 
	public String filterType() { 
		return "pre"; 
	}
	@Override 
	public int filterOrder () { 
		return O; 
	}
	@Override 
	public boolean shouldFilter() { 
		return true;
	}
	@Override 
	public Object run() { 
		RequestContext ctx = RequestContext.getCurrentContext();
		HttpServletRequest request = ctx.getRequest(); 
		log.info("send {} request to{}", request.getMethod(),request.getRequestURL().toString()); 
		Object accessToken = request.getParameter("accessToken");
		if (accessToken == null) { 
			log.warn("access token is empty"); 
			ctx.setSendZuulResponse(false); 
			ctx.setResponseStatusCode(401);
			return null; 
		}
		log.info("access token ok"); 
		return null;
	}
}
  • FilterType: 过滤器的类型, 它决定过滤器在请求的哪个生命周期中执行。 这里定义为pre, 代表会在请求被路由之前执行。
  • FilterOrder: 过滤器的执行顺序。 当请求在一个阶段中存在多个过滤器时, 需要根据该方法返回的值来依次执行。
  • shouldFilter: 判断该过滤器是否需要被执行。 这里我们直接返回了true, 因 此该过滤器对所有请求都会生效。 实际运用中我们可以利用该函数来指定过滤器的有效范围
  • run: 过滤器的具体逻辑。 这里我们通过ctx.setSendZuulResponse(false) 令zuul过滤该请求, 不对其进行路由, 然后通过ctx.setResponseStatusCode (401)设置了其返回的错误码, 当然也可以进 一步优化我们的返回, 比如, 通过ctx.setResponseBody(body)对返回的body内容进行编辑等。

具体过滤器,打算在下篇文章详解,对四种类型的过滤器作用时间和作用规则讲解一下。

3. 路径匹配

通配符说明URL路径匹配路径
匹配任意单个字符/user/?/user/a, /user/g 等单个字符
*匹配任意数量字符,单级目录/user/*/user/a, /user/ab, /user/endwas 等单级路径
**匹配任意数量字符,且支持多级目录/user/**/user/a/b, /user/e/n/d/was等多级路径

随着功能迭代,我们不仅仅是会将服务拆分到不同路由路径,也有可能在服务内部需要拆分部分接口到新的url路径下,那么这个时候如何配置路由呢

zuul.routes.user-service.path=/user-service/** 
zuul.routes.user-service.serviceId=user-service 
// 相当于在/user-service/下的/ext路径所有请求路由到新的服务上
zuul.routes.user-service-ext.path=/user-service/ext/**
zuul.routes.user-service-ext.serviceId= user-service-ext

但这样配置是无法保证能够路由到/ext下的,因为/user-service/也能匹配/user-service/ext/规则的。这样配置是无法保证路由顺序

3.1 路由匹配算法

@Override 
public Route getMatchingRoute{final String adjustedPath) { 
	ZuulRoute route = null; 
	// 当前的path不需要被排除
	if (!matchesIgnoredPatterns(adjustedPath)) { 
		// 遍历所有配置的规则和路由
		for (Entry<String, ZuulRoute> entry : this.routes.get().entrySet()) { 
			String pattern = entry.getKey() ; 
			log.debug("Matching pattern:" + pattern);
			// 用成员属性pathMatcher来匹配路径和规则 
			if (this.pathMatcher.match(pattern, adjustedPath)) { 
				//匹配上则当前路径使用此路由
				route = entry.getValue(); 
				break; 
			}
		}
	}
	log.debug("route matched=" + route); 
	return getRoute(route, adjustedPath);
}

我们可以看到它在使用路由规则匹配请求路径的时候是通过遍历的方式,在请求路径获取到第一个匹配的路由规则之后就返回并结束匹配过程。 所以当存在多个匹配的路由规则时, 匹配结果完全取决于路由规则的保存顺序

路由加载规则,读取先后map保存也会先后

protected Map<String, ZuulRoute> locateRoutes() { 
	// 路由规则保存是有序的
	LinkedHashMap<String, ZuulRoute> routesMap = new LinkedHashMap<String, ZuulRoute>(); 
	// 遍历配置文件
	for (ZuulRoute route : this.properties.getRoutes().values()) { 
		routesMap.put(route.getPath(), route);
	} 
	return routesMap; 
}

所以为了保证读取顺序,我们要使用yaml代替properties的配置文件

zuul: 
	routes: 
		user-service-ext: 
			path: /user-service/ext/** 
			serviceld: user-service-ext 
		user-service: 
			path : / user-service/** 
			serviceld: user-service 

前面在路由匹配的时候,可以配置忽略表达式,让该url表达式不被网关路由

// 忽略所有路由 /hello/**路由
zuul.ignored-patterns=/**/hello/** 
zuul.routes.api-a.path=/api-a/** 
zuul.routes.api-a.serviceid=hello-service

虽然该路由规则满足 /api-a/**但是因为被忽略,所以在路由匹配算法中将不会获得zuulroute属性。


结论:

zuul是netflix提供的一个网关组件,他采用过滤器模式来完成类似设计模式中门面的功能,它的功能很强大,包括但不限于登录鉴权、动态路由、过滤拦截、性能监控、负载均衡等

end
  • 作者:Endwas(联系作者)
  • 发表时间:2021-06-29 12:45
  • 版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)
  • 转载声明:如果是转博主转载的文章,请附上原文链接
  • 公众号转载:请在文末添加作者名字和博客地址
  • 评论