Soul 网关源码学习(14) - Soul 网关插件 Global, Sign

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 信息, 查看源码可以看到 SoulPluginorder 信息定义在了 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 请求在网关中的开始时间

插件处理过程解析

插件的处理过程定义在了 SoulPluginexecute 方法中, GlobalPluginexecute 方法中完成了 SoulContext 的构建:

@Override
public Mono<Void> execute(final ServerWebExchange exchange, final SoulPluginChain chain) {
final ServerHttpRequest request = exchange.getRequest();
final HttpHeaders headers = request.getHeaders();
// 获取请求头 "Upgrade" 字段信息
final String upgrade = headers.getFirst("Upgrade");
SoulContext soulContext;

if (StringUtils.isBlank(upgrade) || !"websocket".equals(upgrade)) {
// 请求头不包含 “Upgrade” 或者 “Upgrade” 请求头信息不等于 “websocket” 则调用 DefaultSoulContextBuilder 的 build方法来构建上下文对象
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 对象实例的方法在 DefaultSoulContextBuilderbuild 方法中:

@Override
public SoulContext build(final ServerWebExchange exchange) {
final ServerHttpRequest request = exchange.getRequest();
// 获取实际的请求路径
String path = request.getURI().getPath();
// 从元数据缓存信息中获取请求路径的元数据, 所谓元数据可以理解为 soul 网关中的请求路由信息
MetaData metaData = MetaDataCache.getInstance().obtain(path);
// 获取的元数据不为空,并且已启用元数据
if (Objects.nonNull(metaData) && metaData.getEnabled()) {
// 将 “metaData” 放到当前 exchange 的 attribute Map 中
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 {

//可以自定义sign过期时间,单位为分钟
@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());
// 检查是否配置 sign 插件且 sign 插件是激活状态
if (signData != null && signData.getEnabled()) {
final SoulContext soulContext = exchange.getAttribute(Constants.CONTEXT);
assert soulContext != null;
// 执行 verify 方法校验签名
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);
// 跟 delay 做比对
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());
// 判断 sign 校验依赖数据
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);
}
// 查看 path 是否在配置列表中
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());
// 校验 sign 是否相等
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 请求执行的前置插件, 结合前面分析的数据同步和内存缓存完成了网关请求执行的前置逻辑。

文章作者: David Liu
文章链接: https://davidliu.now.sh/2021/01/29/soul_plugin_source_discovery/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 David Liu's Blog