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源码编译过程中出现Kotlin: Language version 1.1 is no longer supported; please, use version 1.2