Soul网关源码学习(19)- 认证:SignPlugin

文章目录

前言

在上一篇文章《Soul网关源码学习(18)- 防火墙:WafPlugin》中,我们学习了 WafPlugin 的使用,并且分析了其实现原理。那这一篇我们再来学习如何使用 suol 网关来进行签名认证。

Ak/SK鉴权技术简介

在介绍 SignPlugin 之前,有必要先简单介绍一下Ak/SK鉴权技术方案。在我们对接一些 PASS 平台和支付平台时,会要求我们预先生成一个 access key(AK) 和 secure key(SK),然后通过签名的方式完成认证请求,这种方式可以避免传输 secure key,且大多数情况下签名只允许使用一次,避免了重放攻击。

这种基于 AK/SK 的认证方式主要是利用散列的消息认证码 (Hash-based MessageAuthentication Code) 来实现的,因此有很多地方叫 HMAC 认证,实际上不是非常准确。HMAC 只是利用带有 key 值的哈希算法生成消息摘要,在设计 API 时有具体不同的实现。HMAC 在作为网络通信的认证设计中作为凭证生成算法使用,避免了口令等敏感信息在网络中传输。基本过程如下:

  • 客户端需要在认证服务器中预先设置 access key(AK 或叫 app ID) 和 secure key(SK)
  • 在调用 API 时,客户端需要对参数和 access key 进行自然排序后并使用 secure key 进行签名生成一个额外的参数 digest
  • 服务器根据预先设置的 secure key 进行同样的摘要计算,并要求结果完全一致

注意:secure key 不能在网络中传输,以及在不受信任的位置存放(浏览器等)。例如做微信支付的时候,我们的支付逻辑一般是由业务后台来负责,而不是由浏览器或者手机端直接发起,浏览器或者手机端只负责向业务后台传输必要的数据,业务后台拿到这些诸如价格、数量的数据后,再使用 secure key 向微信支付平台发起支付请求,支付完成后,支付平台回调业务后台,业务后端再通知前端。对于微信支付平台来说,我们的业务服务就是一个客户端。

SignPlugin 的使用

和其他插件一样,要使用首先需要在网关添加依赖:

<dependency>
   <groupId>org.dromara</groupId>
   <artifactId>soul-spring-boot-starter-plugin-sign</artifactId>
   <version>${last.version}</version>
</dependency>
  • 登录控制台 -> 系统管理 -> 插件管理 -> 插件列表选择 sign -> 右边点击编辑打开插件

  • 控制台-> 系统管理 -> 认证管理 -> 添加数据:
    Soul网关源码学习(19)- 认证:SignPlugin

  • 添加认证数据后,会再列表中生成 Appkey 和 secure key,记住它们,后面测试请求会用到:
    Soul网关源码学习(19)- 认证:SignPlugin

  • 控制台 -> 插件列表 -> 插件列表 -> sign -> 添加选择器:
    Soul网关源码学习(19)- 认证:SignPlugin

  • 控制台 -> 插件列表 -> 插件列表 -> sign -> 添加规则:
    Soul网关源码学习(19)- 认证:SignPlugin

ps: 上面几个步骤都记得点击相应页面的同步数据按钮,能让数据及时同步到网关服务

由于 secure key 一般不会存储在浏览器或者APP,所以我们这次不使用 postman 来测试,而是手写一个简单的 java 程序:

public static void main(String[] args) throws IOException {
	//下面是 soul sign 通用参数
    Map<String, String> map = new HashMap<>(3);
    //当前时间的毫秒数(网关会过滤掉5分钟之前的请求)
    String timestamp = String.valueOf(LocalDateTime.now().toInstant(ZoneOffset.of("+8")).toEpochMilli());
    map.put("timestamp",timestamp); 
    //你需要访问的接口路径(根据你访问网关接口自己变更)
    map.put("path", "/http/order/findById");
    //目前定位1.0.0 写死,String类型
    map.put("version", "1.0.0");
    
	//对参数和 access key 进行自然排序后并使用 secure key 进行签名生成一个额外的参数 digest
    List<String> storedKeys = Arrays.stream(map.keySet()
            .toArray(new String[]{}))
            .sorted(Comparator.naturalOrder())
            .collect(Collectors.toList());
    final String sign = storedKeys.stream()
            .map(key -> String.join("", key, map.get(key)))
            .collect(Collectors.joining()).trim()
            //soul admin 生成的 secure key,这东西保存在你本地,不进行网络传输
            //就是直接从 soul admin 粘贴下来保持到你的业务服务端
            .concat("F256701CA81F4FBFAA4400E7062E9CE6");
    String digest =  DigestUtils.md5DigestAsHex(sign.getBytes()).toUpperCase();

    String url = "http://localhost:9195/http/order/findById?id=1";
    Headers headers = new Headers.Builder()
            .add("timestamp",timestamp)
            //soul admin 生成的 sappKey
            .add("appKey","12ED6479082D4B818DFCE62A0CDCD836")
            .add("sign",digest)
            ///目前定位1.0.0 写死,String类型
            .add("version","1.0.0")
            .build();
    Request request = new Request.Builder().url(url).headers(headers).get().build();
    Response response = OK_HTTP_CLIENT.newCall(request).execute();
    System.out.println(response.body().string());
}

测试结果:
Soul网关源码学习(19)- 认证:SignPlugin
接着我们把 digest 从请求头中剔除:
Soul网关源码学习(19)- 认证:SignPlugin

SignPlugin 源码分析

SignPlugin 和之前介绍的大多数插件一样,都是继承于模板类 AbstractSoulPlugin,所以我们直奔其核心方法:

protected Mono<Void> doExecute(final ServerWebExchange exchange, final SoulPluginChain chain, final SelectorData selector, final RuleData rule) {
    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 对象来实现。signService 是 DefaultSignService 对象,其实现了 SignService 接口,如果用户想扩展签名算法,也可以实现该接口来替换 DefaultSignService。

public class DefaultSignService implements SignService {
    ...    
    // 只有插件开启了,才会进行签名认证,否则直接通过。
    @Override
    public Pair<Boolean, String> signVerify(final ServerWebExchange exchange) {
        PluginData signData = BaseDataCache.getInstance().obtainPluginData(PluginEnum.SIGN.getName());
        if (signData != null && signData.getEnabled()) {
        	...
            return verify(soulContext, exchange);
        }
        return Pair.of(Boolean.TRUE, "");
    }
    //如果是一定时间范围之前的请求,则直接丢弃,不需要进行验证
    private Pair<Boolean, String> verify(final SoulContext soulContext, final ServerWebExchange exchange) {
    	...
        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) {
    	//前面省略代码主要是三个方面
        //1、appKey 对应的认证配置是否为空,空则拒接请求
        //2、认证配置的资源路径列表是否为空,空则拒绝请求
        //3、认证配置的资源路径列表是否存能和请求匹配上,如果不难则拒绝请求
        ...
       	//签名认证算法,和上面测试的java代码中计算 digest 的逻辑是一样的
        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 {
			...
        }
        //一致就通过
        return Pair.of(Boolean.TRUE, "");
    }
}

总结

我们再小结一下本篇文章所讲的内容:Ak/SK鉴权技术的简介和使用场景;soul 网关 sign 的使用和测试;sign 插件的源码分析,其真正认证逻辑默认由 DefaultSignService 实现。到这里 SignPlugin 的学习就先告一段落,后面有可能会带来 SignService 的自定义扩展实现,希望小伙伴们多多关注!

上一篇:输出1到100 之间的奇数


下一篇:Node.js中的express-session中间件的简单安装使用