一、目标
1、解读Soul网关sign插件签名算法;
2、解读sign插件主要业务罗逻处理代码;
二、内容
2.1 背景
上一节,我们一起学习了sign插件的使用,体验了sign插件的主要功能,具体的使用流程可以参考:
Soul源码解析(18)-Soul网关sign插件使用:https://blog.csdn.net/qq_38314459/article/details/113658890
2.2 SignPlugin插件分析
SignPlugin插件继承了AbstractSoulPlugin,这里核心的方法是doExecute()方法,就一起来看一下这个方法,这里主要是调用了SignService里面的signVerify()方法进行插件验证操作,并对返回的结果进行判断:
protected Mono<Void> doExecute(final ServerWebExchange exchange, final SoulPluginChain chain, final SelectorData selector, final RuleData rule) {
// 调用了SignService里面的signVerify()方法进行插件验证操作
Pair<Boolean, String> result = signService.signVerify(exchange);
if (!result.getLeft()) {
Object error = SoulResultWrap.error(SoulResultEnum.SIGN_IS_NOT_PASS.getCode(), result.getRight(), null);
return WebFluxResultUtils.result(exchange, error);
}
return chain.execute(exchange);
}
SignService接口定义了一个signVerify()方法,其实现类是DefaultSignService。接下来一起看一下这个方法:
public Pair<Boolean, String> signVerify(final ServerWebExchange exchange) {
// 从缓存中获取sign插件配置参数
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, "");
}
verify()方法分析,主要是对参数进行判断以及超时的操作,如果这些都校验通过,则调用sign()方法进行逻辑判断:
private Pair<Boolean, String> verify(final SoulContext soulContext, final ServerWebExchange exchange) {
//appkey、sign、Timestamp任一参数为空,则抛出错误信息
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做比对,如果间隔时间超出delay的值,则需要重新验证。delay的可以配置,默认5分钟
if (between > delay) {
return Pair.of(Boolean.FALSE, String.format(SoulResultEnum.SING_TIME_IS_TIMEOUT.getMsg(), delay));
}
return sign(soulContext, exchange);
}
sign()方法分析
private Pair<Boolean, String> sign(final SoulContext soulContext, final ServerWebExchange exchange) {
// 从缓存中获取appKey值
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);
}
// 获取配置的path并进行校验
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);
}
// 生成signKey
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, "");
}
2.3 sign签名算法
sign插件的主要处理逻辑就是这些了,这里再看一下生成signKey的方法:
第一步,先构建一个Map,
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;
}
第二步,进行Key的自然排序,然后Key,Value值拼接最后再拼接分配给你的Sk
List<String> storedKeys = Arrays.stream(params.keySet()
.toArray(new String[]{}))
.sorted(Comparator.naturalOrder())
.collect(Collectors.toList());
final String sign = storedKeys.stream()
.filter(key -> !Objects.equals(key, Constants.SIGN))
.map(key -> String.join("", key, params.get(key)))
.collect(Collectors.joining()).trim()
.concat(signKey);
第三步,Md5加密后转成大写
DigestUtils.md5DigestAsHex(sign.getBytes()).toUpperCase()
总体如下所示:
public static String generateSign(final String signKey, final Map<String, String> params) {
List<String> storedKeys = Arrays.stream(params.keySet()
.toArray(new String[]{}))
.sorted(Comparator.naturalOrder())
.collect(Collectors.toList());
final String sign = storedKeys.stream()
.filter(key -> !Objects.equals(key, Constants.SIGN))
.map(key -> String.join("", key, params.get(key)))
.collect(Collectors.joining()).trim()
.concat(signKey);
return DigestUtils.md5DigestAsHex(sign.getBytes()).toUpperCase();
}
三、总结
今天我们一起解读了Soul网关sign插件源码,并看了相关的签名算法,总体流程并不复杂,主要关注sign和verify方法。