前言
每个企业都有很多核心接口,比如商品信息,比如订单交易,这些接口要么是涉及到企业数据资产,要么直接和GMV挂钩,如果接口没有任何保护,在互联网上裸奔,很可能你的数据早就被人爬得一干二净,然后各种虚假交易/信用卡盗刷/一大堆机器人注册等情况了。
要应对这种问题,我们有很多办法,比如直接使用较为全面的安全网关(Kong/APISix/AWS Waf/Curiefense等等),比如在架构设计上做好各种反作弊措施。
我们今天来介绍一下接口设计上的一个较为常见的安全措施:API签名
。
API签名介绍
API签名是一种安全措施,用于验证API请求的真实性和完整性。它通常涉及将请求的一部分(如请求参数、路径、HTTP方法和时间戳)与一个秘密密钥结合使用,通过加密算法生成一串独特的签名字符串。这个签名随后会附加到API请求中(通常在HTTP头部或请求参数中)发送给服务器。
当服务器收到一个带有签名的请求时,它会使用相同的算法和秘密密钥重新生成签名,并将其与请求中提供的签名进行比较。如果两个签名相匹配,服务器就可以确认请求是由持有秘密密钥的合法客户端发起的,并且请求的内容在传输过程中没有被篡改。如果签名不匹配,服务器可以拒绝请求。
API签名通常用于:
-
验证请求者身份:只有知道秘密密钥的请求者才能生成有效的签名,因此签名可以作为一种身份验证手段。
-
确保数据完整性:签名是基于请求的内容生成的,任何对请求数据的更改都会导致签名无效,因此签名还可以验证数据在传输过程中是否被篡改。
-
防止重放攻击:通过在签名中包含时间戳和/或随机数,可以确保即使攻击者截获了一个签名的请求,他们也不能在以后重新发送相同的请求。
一般的API签名流程
以下是一个典型的 API 签名流程:
-
请求准备:客户端准备发起一个 API 请求,包括设置所有必要的参数和请求体。
-
选择签名方法:选择一个加密算法(如 HMAC-SHA256)来创建签名。
-
构造待签名字符串:根据服务端要求的格式,组合请求中的各个部分(如请求方法、请求路径、时间戳、请求参数、API 密钥等)来构造一个待签名的字符串。这通常需要按照一定的顺序,并可能需要对参数进行排序。
-
生成签名:使用你的 API 密钥和选择的加密算法对待签名字符串进行加密,生成签名。
-
添加签名到请求:将生成的签名添加到 HTTP 请求的头部或者请求参数中。签名的具体位置取决于 API 的设计。
-
发送请求:客户端发送包含签名的 API 请求。
-
签名验证:服务端收到请求后,会从请求中提取签名和相关信息(如时间戳和请求参数)。
-
重构待签名字符串:服务端使用相同的规则构造待签名字符串。
-
服务端生成签名:服务端使用存储的 API 密钥和相同的加密算法生成签名。
-
比较签名:服务端比较自己生成的签名和请求中提供的签名。如果两者相同,说明请求是合法的。
-
处理请求:如果签名验证成功,服务端继续处理请求。如果验证失败,服务端拒绝请求并返回错误。
-
响应客户端:服务端处理完请求后,向客户端发送响应。有时候响应也会包含签名,特别是在响应体中包含敏感数据时。
周边的步骤很多,但其实主要就是签名+验签,简单的示意图如下:
- 我们有不同的客户端平台,各自需要做签名
- 然后我们需要有一个统一验签的地方,最好是统一的安全网关
- 只有通过验证的请求才能最终抵达后台服务(ec-xxx这种是后台服务)
便利性思考
上面的图片中,我们看到每个端都需要有对应的API签名,这里面一般要有自己封装的一套加密算法,各个端的接入者需要有一个好的文档,按照这个文档来进行加密。另外安全网关也需要按照这个算法进行验签。
各个平台做好接入之后,分别单独的测试验签效果,验证不通过的回去改吧改吧再测试。每个平台都在仔细揣摩测试、接入成功后,才能上线。
算法相关的东西,很麻烦,大家一般接入后就再也不想再次接入了。
看起来都挺美好,但是如果,某一天老板觉得加密算法不够好,需要换一套算法呢?
每个端再重新接入一遍,按照新的算法仔细比对各个参数的顺序,算法的正确性等等,重新每个平台都测试一遍,都验签通过才能上线,这个是很折磨人的。
那有没有什么比较好的方法,不那么周折,修改算法后,只需要该一个地方即可?
当然有,那就需要一个跨平台的库。
如何制作一个跨平台的加密库?
如果要跨各个平台,都能使用,首先要兼容性好,其次要性能高,这么一说,其实就只有三种选择了:C, C++, Rust
考虑到C/C++的可维护性和发现Bug的成本问题,我们最终选择Rust,因为基本上能编译通过就可以解决95%以上的问题,性能还特别高,不用它用谁?
可能有些朋友还不清楚怎么弄,我这里先贴一张图片,再解释:
对于不同的平台,直接使用相同的代码很显然是不可能的,我Web环境的Javascript还能和你iOS的OC/Swift直接使用相同的代码不成?那怎么做呢,我们这个时候就要利用Rust的跨平台编译的便利性了,它可以针对不同的平台有不同的编译手段:
-
Web平台 :统一使用WebAssemably技术,接入wasm二进制,Rust天然支持wasm的编译
-
iOS平台: 使用Lipo针对iOS目标平台进行编译,生成静态库(.a),方便平台接入,支持armv7和arm64两种指令集
-
Android平台:使用Android官方提供的NDK进行跨平台编译,生成动态链接库(.so),支持armv7,armv8a,arm64等指令架构
编译之后,比如Web环境,我们生成了apisign.wasm
文件,就可以使用它进行API签名接入了,什么?还不知道WASM
是什么吗?那可以先学习一下这个知识,毕竟它很可能是Web的未来。
同理,iOS和Android熟悉的伙伴,对于接入静态库和动态库应该是手到擒来了,这里不做过多表述。
那还有一个问题?验签呢?
说的好,如果你有安全网关,大部分的安全网关是基于Nginx
的,那么它比如有Lua插件扩展支持,我们的示意图中最右一条就是编译这个的,Rust + mlua
是可以直接编译Lua识别的桥接库的哦。
如果你没有安全网关,想要后台直接使用,比如你是直接用的Spring全家桶,那照样可以通过JNI
的方式调用Rust
的动态库,只需要编译成你的服务运行的平台库即可(一般是linux)。
现在看看流程图更新之后的样子:
小结
企业数据很重要,因此早上加上API签名很有必要,如果有必要,可以做跨平台的方案,本文主要是讲解其中的技术方案,后面会给出一个详细的例子。
评论区