参考 soul 官方文档,sign插件

1.启动 soul-admin, 开启 sign 插件,添加 sign 插件的选择器和规则,这里和 divide 插件的一致

2.soul-bootstrap 引入依赖,启动 soul-bootstrap,启动 soul-examples-http

<dependency>
    <groupId>org.dromara</groupId>
    <artifactId>soul-spring-boot-starter-plugin-sign</artifactId>
    <version>${project.version}</version>
</dependency>

3.soul-admin 认证管理菜单添加一条数据

添加完后,自动帮我们生成好了 AppKey 和秘钥。

image-20210201224033351

请求头不加参数,去访问就报401了

请求头加上这些参数就能正常访问了。

这个 sign 值的生成,我是使用了 soul 网关的 SignUtilsTest 类,这里的 signKey 是秘钥。

不过这个有效期只有5分钟,超过5分钟,就报错了。

我们 debug 看下 soul 是怎么使用 sign 插件的。直接看 DefaultSignService 的 signVerify 方法

        PluginData signData = BaseDataCache.getInstance().obtainPluginData(PluginEnum.SIGN.getName());
        //sign 插件开启才走执行
        if (signData != null && signData.getEnabled()) {
            final SoulContext soulContext = exchange.getAttribute(Constants.CONTEXT);
            assert soulContext != null;
            //从 exchange 获取header数据验证
            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);
        //这里就和当前时间比较,超时的话,就返回 false
        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);
        }
        //根据header传的参数生成sign,验证生成的sign是否和head 的sign一致
        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, "");
            }
            //这里如果我们配置的认证名称和contextPath一致的话,会把appParam放到请求头里,这里还不知道放的目的何在。
            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, "");
    }

总体来看,sign 插件这边的源码还是比较好理解的。