Soul 网关源码分析(四)sign 插件

在上一节中,我们尝试dubbo插件将http协议转换成dubbo协议,今天我们来看看 sign 插件是如何实现请求鉴权的。

流程演示

我们先试试没有添加sign插件时,是否能正常请求接口:

➜  ~ curl localhost:9195/dubbo/findAll
{"code":200,"message":"Access to success!","data":{"name":"hello world Soul Alibaba Dubbo , findAll","id":"-803129909"}}

成功。

首先我们需要在插件管理中打开sign插件的开关。然后我们在认证管理中,新增一条 AK/SK 记录:
在这里插入图片描述
添加完毕后,我们可以看到AppKey 和加密密钥已经为我们生成好了:
在这里插入图片描述
现在我们再试试刚刚代理dubbo 服务中的 /dubbo/findAll 接口:

➜  ~ curl localhost:9195/dubbo/findAll
{"code":200,"message":"Access to success!","data":{"name":"hello world Soul Alibaba Dubbo , findAll","id":"-541002115"}}

发现没有生效,原因是没有配置sign的选择器:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
按照上图所示配置好sign选择器后再请求我们配置了鉴权校验的接口:

➜  ~ curl localhost:9195/dubbo/findAll
{"code":401,"message":"sign parameters are incomplete!","data":null}

成了,提示我们签名参数不完整,下表展示了我们需要在headers中添加的header:

字段描述
timestamp1571711067186上述你进行签名的时候使用的时间值
appKey1TEST123456781AK值
signA90E66763793BDBC817CF3B52AAAC041上述得到的签名值
version1.0.0固定默认值

签名插件会默认过滤5分钟之后的请求。

现在直奔单元测试类,把现有的时间戳,appkey 和 sign 改写成自己的:

	private SignService signService;
    
    private ServerWebExchange exchange;
    
    //这里改成自己的
    private final String appKey = "E841FEDBF8EE4912878993EC84F70947";
    //这里改成自己的
    private final String secretKey = "4FBB99E1B97B45D79D8F891D45DBB97E";
    
    private SoulContext passed;
    
    @Value("${soul.sign.delay:5}")
    private int delay;
    
    @Before
    public void setup() {
        this.signService = new DefaultSignService();
        this.exchange = MockServerWebExchange.from(MockServerHttpRequest.get("localhost").build());
        //这里改成自己的,路径是默认签名的一部分
        final String path = "/dubbo/findAll";
        PluginData signData = new PluginData();
        signData.setId("1");
        signData.setName(PluginEnum.SIGN.getName());
        signData.setEnabled(true);
        signData.setRole(1);
        BaseDataCache.getInstance().cachePluginData(signData);
        
        AppAuthData authData = new AppAuthData();
        authData.setAppKey(appKey);
        authData.setAppSecret(secretKey);
        authData.setEnabled(true);
        AuthPathData authPathData = new AuthPathData();
        authPathData.setAppName("test-api");
        authPathData.setPath(path);
        authPathData.setEnabled(true);
        authData.setPathDataList(Lists.newArrayList(authPathData));
        SignAuthDataCache.getInstance().cacheAuthData(authData);
        
        this.passed = new SoulContext();
        this.passed.setModule("/dubbo");
        this.passed.setMethod("/findAll");
        this.passed.setRpcType("dubbo");
        this.passed.setHttpMethod("GET");
        this.passed.setPath(path);
        final String timestamp = String.valueOf(System.currentTimeMillis());
        this.passed.setTimestamp(timestamp);
        this.passed.setSign(buildSign(secretKey, timestamp, this.passed.getPath()));
        //在这里打断点,获取到 this.getpassed 中的sign
        this.passed.setAppKey(appKey);
        this.passed.setContextPath("/");
        this.passed.setRealUrl("/dubbo/findAll");
    }

下面我们在postman 请求的 headers 中添加签名相关的header并请求:
在这里插入图片描述
成功通过了校验。

源码分析

/**
 * The type Default sign service.
 *
 * @author xiaoyu
 */
@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);
    }
    
    /**
     * verify sign .
     *
     * @param soulContext {@linkplain SoulContext}
     * @return result : True is pass, False is not pass.
     */
    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);
        }
   		//查看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;
    }
}

总结

其实签名插件的核心逻辑比较简单,就是实现了 sign 、 verify等方法,再实现 SignService 接口的 signVerify函数完成整个签名的流程。如果有需要我们可以加入自己的签名校验逻辑,比如用JWT之类的来实现,只需要实现 SignService 接口的 signVerify 即可。

已标记关键词 清除标记
相关推荐
©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页