Soul 网关插件执行的顺序
我们已经知道了 Soul 网关中, 定义的插件会组装成插件链, 在 SoulWebHandler 中按顺序构建好, 在 SoulWebHandler 执行网关的请求处理的时候会将插件已固定的顺序构建好, 关于网关插件排序的代码如下:
@Bean("webHandler") public SoulWebHandler soulWebHandler(final ObjectProvider<List<SoulPlugin>> plugins) { List<SoulPlugin> pluginList = plugins.getIfAvailable(Collections::emptyList); final List<SoulPlugin> soulPlugins = pluginList.stream() .sorted(Comparator.comparingInt(SoulPlugin::getOrder)).collect(Collectors.toList()); soulPlugins.forEach(soulPlugin -> log.info("load plugin:[{}] [{}]", soulPlugin.named(), soulPlugin.getClass().getName())); return new SoulWebHandler(soulPlugins); }
|
SoulPlugin 接口中定义了标记接口 int getOrder(), SoulPlugin 的实现类返回各自的 order 信息, 查看源码可以看到 SoulPlugin 的 order 信息定义在了 org.dromara.soul.common.enums.PluginEnum 中。
GlobalPlugin 插件是 soul 网关中所有插件执行前,最先执行的一个插件。
Global 插件
GlobalPlugin 的作用是构建 Soul 网关的请求上下文环境 SoulContext, SoulContext 对象的源码定义在了 org.dromara.soul.plugin.api.context.SoulContext 中, 它包含的字段有:
| 字段名 |
字段说明 |
| module |
模块名,也就是当前的contextPath、当前执行的选择器名 |
| method |
方法名,contextPath之后的路径名 |
| rpcType |
远程调用类型,目前支持:http、dubbo、SpringCloud、sofa |
| httpMethod |
http请求方法,目前支持:get和post |
| sign |
请求头携带的签名 |
| timestamp |
请求时的时间戳 |
| appKey |
请求头携带的appKey |
| path |
请求的真实路径 |
| contextPath |
请求的根路径 |
| realUrl |
请求的真实路径 |
| dubboParams |
请求的dubbo参数 |
| startDateTime |
请求在网关中的开始时间 |
插件处理过程解析
插件的处理过程定义在了 SoulPlugin 的 execute 方法中, GlobalPlugin 在 execute 方法中完成了 SoulContext 的构建:
@Override public Mono<Void> execute(final ServerWebExchange exchange, final SoulPluginChain chain) { final ServerHttpRequest request = exchange.getRequest(); final HttpHeaders headers = request.getHeaders(); final String upgrade = headers.getFirst("Upgrade"); SoulContext soulContext; if (StringUtils.isBlank(upgrade) || !"websocket".equals(upgrade)) { soulContext = builder.build(exchange); } else { final MultiValueMap<String, String> queryParams = request.getQueryParams(); soulContext = transformMap(queryParams); } exchange.getAttributes().put(Constants.CONTEXT, soulContext); return chain.execute(exchange); }
|
构建 SoulContext 对象实例的方法在 DefaultSoulContextBuilder 的 build 方法中:
@Override public SoulContext build(final ServerWebExchange exchange) { final ServerHttpRequest request = exchange.getRequest(); String path = request.getURI().getPath(); MetaData metaData = MetaDataCache.getInstance().obtain(path); if (Objects.nonNull(metaData) && metaData.getEnabled()) { exchange.getAttributes().put(Constants.META_DATA, metaData); } return transform(request, metaData); }
|
在 transform 方法中, 根据元信息将 Http 请求信息转换为 SoulContext 对象放在了 ServerWebExchange 的 attributes 中,转换的过程会根据请求路径对应的元信息是否存在或者开启和元数据中的 Rpc 类型在 SoulContext
对象实例中组装不同的数据信息,结束了 SoulContext 的组装后也就完成了 GlobalPlugin 插件的处理,接着执行插件链中的下一个插件,后序的插件会从 ServerWebExchange 的 attributes 中获取 SoulContext 中组装的数据来完成网关插件功能的处理。
Sign 插件
soul 网关中 Sign 插件是用来检验访问是否有效的前置插件,它会根据请求参数中的 timestamp, module, method, rpcType 4 个字段做 Key,Value 值的拼接,再拼接上 appSecret,再进行MD5加密生成一个签名,然后通过对比传入的签名和预期的签名值是否相等来检测请求是否合法。Sign 插件主要依赖
SignService 完成 sign 校验逻辑, 其源码如下:
@Slf4j public class DefaultSignService implements SignService { @Value("${soul.sign.delay:5}") private int delay; @Override public Pair<Boolean, String> signVerify(final ServerWebExchange exchange) { PluginData signData = BaseDataCache.getInstance().obtainPluginData(PluginEnum.SIGN.getName()); if (signData != null && signData.getEnabled()) { final SoulContext soulContext = exchange.getAttribute(Constants.CONTEXT); assert soulContext != null; return verify(soulContext, exchange); } return Pair.of(Boolean.TRUE, ""); } private Pair<Boolean, String> verify(final SoulContext soulContext, final ServerWebExchange exchange) { if (StringUtils.isBlank(soulContext.getAppKey()) || StringUtils.isBlank(soulContext.getSign()) || StringUtils.isBlank(soulContext.getTimestamp())) { log.error("sign parameters are incomplete,{}", soulContext); return Pair.of(Boolean.FALSE, Constants.SIGN_PARAMS_ERROR); } final LocalDateTime start = DateUtils.formatLocalDateTimeFromTimestampBySystemTimezone(Long.parseLong(soulContext.getTimestamp())); final LocalDateTime now = LocalDateTime.now(); final long between = DateUtils.acquireMinutesBetween(start, now); if (between > delay) { return Pair.of(Boolean.FALSE, String.format(SoulResultEnum.SING_TIME_IS_TIMEOUT.getMsg(), delay)); } return sign(soulContext, exchange); } private Pair<Boolean, String> sign(final SoulContext soulContext, final ServerWebExchange exchange) { final AppAuthData appAuthData = SignAuthDataCache.getInstance().obtainAuthData(soulContext.getAppKey()); if (Objects.isNull(appAuthData) || !appAuthData.getEnabled()) { log.error("sign APP_kEY does not exist or has been disabled,{}", soulContext.getAppKey()); return Pair.of(Boolean.FALSE, Constants.SIGN_APP_KEY_IS_NOT_EXIST); } List<AuthPathData> pathDataList = appAuthData.getPathDataList(); if (CollectionUtils.isEmpty(pathDataList)) { log.error("You have not configured the sign path:{}", soulContext.getAppKey()); return Pair.of(Boolean.FALSE, Constants.SIGN_PATH_NOT_EXIST); } boolean match = pathDataList.stream().filter(AuthPathData::getEnabled) .anyMatch(e -> PathMatchUtils.match(e.getPath(), soulContext.getPath())); if (!match) { log.error("You have not configured the sign path:{},{}", soulContext.getAppKey(), soulContext.getRealUrl()); return Pair.of(Boolean.FALSE, Constants.SIGN_PATH_NOT_EXIST); } String sigKey = SignUtils.generateSign(appAuthData.getAppSecret(), buildParamsMap(soulContext)); boolean result = Objects.equals(sigKey, soulContext.getSign()); if (!result) { log.error("the SignUtils generated signature value is:{},the accepted value is:{}", sigKey, soulContext.getSign()); return Pair.of(Boolean.FALSE, Constants.SIGN_VALUE_IS_ERROR); } else { List<AuthParamData> paramDataList = appAuthData.getParamDataList(); if (CollectionUtils.isEmpty(paramDataList)) { return Pair.of(Boolean.TRUE, ""); } paramDataList.stream().filter(p -> ("/" + p.getAppName()).equals(soulContext.getContextPath())) .map(AuthParamData::getAppParam) .filter(StringUtils::isNoneBlank).findFirst() .ifPresent(param -> exchange.getRequest().mutate().headers(httpHeaders -> httpHeaders.set(Constants.APP_PARAM, param)).build() ); } return Pair.of(Boolean.TRUE, ""); } private Map<String, String> buildParamsMap(final SoulContext dto) { Map<String, String> map = Maps.newHashMapWithExpectedSize(3); map.put(Constants.TIMESTAMP, dto.getTimestamp()); map.put(Constants.PATH, dto.getPath()); map.put(Constants.VERSION, "1.0.0"); return map; } }
|
总结
从今天分析源码插件的逻辑可以看出, 这两个插件都是 RPC 请求执行的前置插件, 结合前面分析的数据同步和内存缓存完成了网关请求执行的前置逻辑。