spring-cloud-gateway 原理赏析

906 阅读3分钟

1. 介绍

spring-cloud-gateway 作为一款优秀的api 网关,它的运行流程是怎样的,又是如何与其他框架整合的呢,本文就此展开;

关于如何使用,请参见前两篇:spring-cloud-gateway 入门使用spring-cloud-gateway 原理赏析-准备篇

准备:

本文spring-cloud-gateway 版本为:spring-cloud-gateway-2.1.0.RELEASE,

项目示例地址:github链接

注:想要了解源码,即对框架有自己的理解,一定要debug !!! ,原创不易,转载请标注出处!!!

2. 查看前准备

示例需求:

实现一个网关功能:
1)除配置白名单uri 外,其他接口引入认证服务(用户必须登录);

登录判定:header 中含有 user-token 且有效;

2.1 环境

eurake : 快速部署项目-github

服务提供方:快速部署项目-github

gateway网关项目:快速部署项目-github

2.2 配置

认证过滤器工厂🏭

@Component
public class AuthGatewayFilterFactory extends AbstractGatewayFilterFactory<AuthGatewayFilterFactory.Config> {

    private Logger logger = LoggerFactory.getLogger(AuthGatewayFilterFactory.class);

    /**
     * 用户登录状态token
     */
    private static final String USER_TOKEN = "user_token";

    public AuthGatewayFilterFactory(){
        super(Config.class);
        logger.info("AuthGatewayFilterFactory init");
    }

    @Override
    public List<String> shortcutFieldOrder() {
        return Collections.singletonList("ignoreUrlListStr");
    }

    @Override
    public GatewayFilter apply(Config config) {
        return (exchange, chain) -> {
            ServerHttpRequest request = exchange.getRequest();
            // 校验是否是 不用登录的URL
            String path = request.getPath().toString();
            logger.info("AuthGatewayFilterFactory.apply path:{}",path);


            String ignoreUrlListStr = config.ignoreUrlListStr;
            logger.info("AuthGatewayFilterFactory.apply ignoreUrlListStr={}",ignoreUrlListStr);

            boolean ignoreOk = Arrays.asList(ignoreUrlListStr.split("\|")).contains(path);
            if(ignoreOk){
                return chain.filter(exchange);
            }

            // 校验是否登录
            HttpHeaders headers = request.getHeaders();
            String userToken = headers.getFirst(USER_TOKEN);
            if(StringUtils.isEmpty(userToken)){
                // 返回未登录提示
                ServerHttpResponse response = exchange.getResponse();
                response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
                response.setStatusCode(HttpStatus.UNAUTHORIZED);

                Map<String, Object> bodyMap = new HashMap<>();
                bodyMap.put("code",-1000003);
                bodyMap.put("message","未登录");

                byte[] responseByteArray = JSON.toJSONBytes(bodyMap);
                DataBuffer responseBuffer = response.bufferFactory().allocateBuffer(responseByteArray.length).write(responseByteArray);
                return response.writeWith(Mono.just(responseBuffer));
            }
            logger.info("AuthGatewayFilterFactory.apply  user-token={}",userToken);
            return chain.filter(exchange);
        };
    }

    public static class Config {
        private String ignoreUrlListStr;

        public String getIgnoreUrlListStr() {
            return ignoreUrlListStr;
        }

        public void setIgnoreUrlListStr(String ignoreUrlListStr) {
            this.ignoreUrlListStr = ignoreUrlListStr;
        }
    }
}

application.properties

# 网关配置 -- 免登录认证配置
spring.cloud.gateway.routes[1].id = login_auth
spring.cloud.gateway.routes[1].uri = lb://yiyi-example-api
#spring.cloud.gateway.routes[1].uri = localhost:18083
spring.cloud.gateway.routes[1].predicates[0] = Path=/eat/**
spring.cloud.gateway.routes[1].filters[0] = Auth=/login|/send_code

3. 运💣行

注: spring-cloud-gateway 依赖 web-flux 框架 ,debug 时我们可以先把断点打在 DispatcherHandler#hanlde() 、自定AuthGatewayFilterFactory.apply()上;

3.1 基础回顾

spring-cloud-gateway 里几个重要的属性如下:

🆔: 确定唯一性(根据业务配置);

uri : 跳转后的uri;

predicate : 谓语断言;判断是否满足跳转条件;【可以为多个】

filter : 过滤器,对请求进行拦截,在请求前处理(例:增加header)【可以为多个】

3.2 目标定位

debug前我们先梳理一下我们的目标是啥?

1) 定位 route 加载的位置;

2)定位route 匹配的位置;

3)定位过滤器执行的开始位置;

4)定位负载均衡 lb://yiyi-example-api, 服务域名替换的位置;

3.2 庖丁解牛

访问: curl --location --request GET 'localhost/eat/apple'

debug 后得到如下时序图:

如上图所示:

spring-cloud-gateway 通过 RoutePredicateHandlerMapping 完成对web-flux 的接入;

getHandlerInternal(ServerWebExchange exchange) 完成对请求处理程序的查找;

下面看一下这个方法的官方描述

//查找给定请求的处理程序,如果未找到特定请求,则返回空 Mono。 该方法由 getHandler 调用。
protected abstract Mono<?> getHandlerInternal(ServerWebExchange exchange);

RoutePredicateHandlerMapping#getHandlerInternal 带注释源码

@Override
protected Mono<?> getHandlerInternal(ServerWebExchange exchange) {
	// don't handle requests on the management port if set
	if (managmentPort != null && exchange.getRequest().getURI().getPort() == managmentPort) {
		return Mono.empty();
	}
	exchange.getAttributes().put(GATEWAY_HANDLER_MAPPER_ATTR, getSimpleName());

	/**
	 * 根据当前的request 匹配出相应的 Route,放入attributes, key: GATEWAY_ROUTE_ATTR
	 * 在 {@link FilteringWebHandler#handle(org.springframework.web.server.ServerWebExchange)} 处进行调用处理
	 */
	return lookupRoute(exchange)
			.flatMap((Function<Route, Mono<?>>) r -> {
				exchange.getAttributes().remove(GATEWAY_PREDICATE_ROUTE_ATTR);

				if (logger.isDebugEnabled()) {
					logger.debug("Mapping [" + getExchangeDesc(exchange) + "] to " + r);
				}

				exchange.getAttributes().put(GATEWAY_ROUTE_ATTR, r);

				return Mono.just(webHandler);
			}).switchIfEmpty(Mono.empty().then(Mono.fromRunnable(() -> {
				exchange.getAttributes().remove(GATEWAY_PREDICATE_ROUTE_ATTR);
				if (logger.isTraceEnabled()) {
					logger.trace("No RouteDefinition found for [" + getExchangeDesc(exchange) + "]");
				}
			})));
}

FilteringWebHandler#handle 带注释源码

@Override
public Mono<Void> handle(ServerWebExchange exchange) {

	/**
	 * 根据当前的request 匹配出相应的 Route,放入attributes, key: GATEWAY_ROUTE_ATTR
	 * 在 {@link RoutePredicateHandlerMapping#getHandlerInternal(org.springframework.web.server.ServerWebExchange)} 处进行存储
	 */

	Route route = exchange.getRequiredAttribute(GATEWAY_ROUTE_ATTR);

	List<GatewayFilter> gatewayFilters = route.getFilters();

	// 获取gatewayFiler 数据 包含route.filter 和 globalFilter
	List<GatewayFilter> combined = new ArrayList<>(this.globalFilters);
	combined.addAll(gatewayFilters);

	//TODO: needed or cached?
	AnnotationAwareOrderComparator.sort(combined);

	if (logger.isDebugEnabled()) {
		logger.debug("Sorted gatewayFilterFactories: "+ combined);
	}

	return new DefaultGatewayFilterChain(combined).filter(exchange);
}

RouteToRequestUrlFilter#filter 带注释源码

public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
		// 获得Route
		Route route = exchange.getAttribute(GATEWAY_ROUTE_ATTR);
		if (route == null) {
			return chain.filter(exchange);
		}
		log.trace("RouteToRequestUrlFilter start");
		URI uri = exchange.getRequest().getURI();
		boolean encoded = containsEncodedParts(uri);
		URI routeUri = route.getUri();

		if (hasAnotherScheme(routeUri)) {
			// this is a special url, save scheme to special attribute
			// replace routeUri with schemeSpecificPart
			exchange.getAttributes().put(GATEWAY_SCHEME_PREFIX_ATTR, routeUri.getScheme());
			routeUri = URI.create(routeUri.getSchemeSpecificPart());
		}

		if("lb".equalsIgnoreCase(routeUri.getScheme()) && routeUri.getHost() == null) {
			//Load balanced URIs should always have a host.  If the host is null it is most
			//likely because the host name was invalid (for example included an underscore)
			throw new IllegalStateException("Invalid host: " + routeUri.toString());
		}

		// 拼接url
		URI mergedUrl = UriComponentsBuilder.fromUri(uri)
				// .uri(routeUri)
				.scheme(routeUri.getScheme())
				.host(routeUri.getHost())
				.port(routeUri.getPort())
				.build(encoded)
				.toUri();
		/**
		 * 在 {@link LoadBalancerClientFilter#filter(org.springframework.web.server.ServerWebExchange, org.springframework.cloud.gateway.filter.GatewayFilterChain)} 处使用
		 */
		exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, mergedUrl);

		return chain.filter(exchange);
	}

LoadBalancerClientFilter#filter带注释源码

@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {

	//  获得URL

	/**
	 * 在{@link RouteToRequestUrlFilter#filter(org.springframework.web.server.ServerWebExchange, org.springframework.cloud.gateway.filter.GatewayFilterChain)} 处 存入此值
	 */
	URI url = exchange.getAttribute(GATEWAY_REQUEST_URL_ATTR);
	String schemePrefix = exchange.getAttribute(GATEWAY_SCHEME_PREFIX_ATTR);

	if (url == null || (!"lb".equals(url.getScheme()) && !"lb".equals(schemePrefix))) {
		return chain.filter(exchange);
	}

	// 添加原始请求URI 到 GATEWAY_ORIGINAL_REQUEST_URL_ATTR
	//preserve the original url
	addOriginalRequestUrl(exchange, url);

	log.trace("LoadBalancerClientFilter url before: " + url);

	// 获得一个服务实例( ServiceInstance ) ,从而实现负载均衡
	final ServiceInstance instance = choose(exchange);

	if (instance == null) {
		String msg = "Unable to find instance for " + url.getHost();
		if(properties.isUse404()) {
			throw new FourOFourNotFoundException(msg);
		}
		throw new NotFoundException(msg);
	}

	URI uri = exchange.getRequest().getURI();

	// if the `lb:<scheme>` mechanism was used, use `<scheme>` as the default,
	// if the loadbalancer doesn't provide one.
	String overrideScheme = instance.isSecure() ? "https" : "http";
	if (schemePrefix != null) {
		overrideScheme = url.getScheme();
	}

	URI requestUrl = loadBalancer.reconstructURI(new DelegatingServiceInstance(instance, overrideScheme), uri);

	log.trace("LoadBalancerClientFilter url chosen: " + requestUrl);

	// 设置 requestUrl 到 GATEWAY_REQUEST_URL_ATTR 。后面 Routing 相关的 GatewayFilter 会通过该属性,发起请求。
	exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, requestUrl);

	// 提交过滤器链继续过滤
	return chain.filter(exchange);
}

4. 汇总

参见3.2 的时序图,得到如下的请求流程图

由此,spring-cloud-gateway 整体的运行流程,框架交互逻辑我们以基本掌握,在使用时 可以根据实际需求随机应变

参见

spring-cloud魔芋源码分析

Spring源码编译过程中出现Kotlin: Language version 1.1 is no longer supported; please, use version 1.2

spring-cloud-gateway 带注释源码-github地址