文章目录
前言
在上一篇文章《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 -> 右边点击编辑打开插件
-
控制台-> 系统管理 -> 认证管理 -> 添加数据:
-
添加认证数据后,会再列表中生成 Appkey 和 secure key,记住它们,后面测试请求会用到:
-
控制台 -> 插件列表 -> 插件列表 -> sign -> 添加选择器:
-
控制台 -> 插件列表 -> 插件列表 -> sign -> 添加规则:
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());
}
测试结果:
接着我们把 digest 从请求头中剔除:
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 的自定义扩展实现,希望小伙伴们多多关注!