API 验证签名时,若 payload 的 json value 为复杂对象时该如何处理?

2020-01-16 22:15:15 +08:00
 LinJunzhu

提供 API 给客户端使用时,为了验证是客户端发起的请求,并且保证请求中不被用户恶意串改,我们一般会这么做:

客户端:

1、将请求参数按照 key 字典序排序,将所有 key=value 进行拼接 2、将 [ secret ] [ nonce ] [ timestamp ] 和上述的参数进行拼接

最终将这个字符串通过 sha1/或其他算法 生成一个 token

服务端:

接受到对应的参数时,根据同样的逻辑,生成 token,验证 token 是否一致。

若是下面的参数,会拼接成:aello=world&bello=java

{
	"aello": "world",
	"bello": "java
}

但是若 value 是数组,或是一个对象时,该如何拼接呢?

{
	"aello": "world",
	"bello": {
    	"hi": "hi"
    }
}

翻看了微信和支付宝的文档,两者的参数类型都是基本类型,无数组和对象,但是我们自己在实现过程中,偶尔难免会有复杂结构的 json 参数。

不知道大家是如何做的?

8919 次点击
所在节点    程序员
70 条回复
index90
2020-01-17 15:43:42 +08:00
@LinJunzhu 你发的那个是链接我看了,人家之所以这样做,是因为把 signature 放在了 body 中传送,可能由于他们协议的要求不能在 query 参数或者 header 中存放额外字段。

如果你们没有这种限制,最好的办法是把 signature 放在 header 中( aws 就是这样做),签名时针对 body 进行签名。这样做得好处是,api 网关做签名校验时,不需要反序列化,性能更高,更通用,无论什么 content 类型都支持。
index90
2020-01-17 15:56:29 +08:00
给一个我正在使用的方案:

Signature = HMAC-SHA1( '您的 SecretKey', UTF-8-Encoding-Of( $StringToSign ) ) );

StringToSign = $HTTP-Verb + "\n" +
$URL + "\n" +
$Query + "\n" +
$Content-Type + "\n" +
$Content-MD5 + "\n" +
$Date + "\n" +
$AccessKey;

HTTP-Verb: 指 HTTP 请求方法如, GET, POST, PUT, DELETE
URL: 指所访问的资源路径如, /your/resource/path
Query: 指请求中的 Query 参数, 其构成规则如下:
对参数 key 进行升序排序
对于所有参数以 key+value 方式串联
Content-Type: 请求 Header 中的 Content-Type 值
Content-MD5: 请求 Header 中的 Content-MD5 值, 等同于对 body 数据的 md5sum
Date: 整形, 请求发生时的时间戳(用于防重放的,不需要可省略)
AccessKey: 用户自己的 AccessKey

最后在 request header 中增加:
x-singnature: $Signature
x-accesskey: $AccessKey
x-date: $Date
fkdog
2020-01-17 16:22:42 +08:00
客户端搞什么接口签名啊,多此一举。
怕中间人篡改那就上 https。
如果怕用户自己篡改,那这个没辙,签名算法、加密逻辑完全能反编译出来。
面对这种情况,服务端需要做的是保证请求结果处理正确即可。
index90
2020-01-17 16:39:17 +08:00
@fkdog lz 的需求其实是身份认证,是想知道这个请求是谁发出的,并且要有数字签名证明。
https 解决不了 C 公司冒充 A 公司向 B 公司发起转账申请,除非你启用 tls 的双向验证,这时候需要给每个请求方发放 client 证书,服务端还需要实现从证书中提取请求方 ID,才知道是谁发起的请求,实现难度比数字签名大一点。
LinJunzhu
2020-01-17 16:42:47 +08:00
@fkdog Android 内的 so 可反不出来。

若客户端有 [不想将 API 简单的被暴露出去调用] 需求,这个就有用了,如 酒店类 APP 拿房价的接口
LinJunzhu
2020-01-17 16:51:22 +08:00
@index90 你这个方案没有把 payload 放进去生成 Signature 的逻辑是吧?

其实我纠结的点在于:

若是在 application/json 的基础上进行交互

1、若 payload 内的 value 为数组、json 对象 时,那么就递归遍历排序, 这就满足需求了,但是在网上也没看到这样的实现方案,大家的 value 都是简单类型,所以不确定这样的实现是否合理
2、忽略 value 为数组、json 对象的逻辑计算,这就跟你的方案差不多。

所以才发了贴,问下大家的做法 :)
LinJunzhu
2020-01-17 16:53:06 +08:00
@index90 更正: payload 为 request body
index90
2020-01-17 17:03:28 +08:00
@LinJunzhu
注意这一行:“Content-MD5: ……, 等同于对 body 数据的 md5sum”(简单理解,payload 实际上时一个 byte 数组,可以直接求 md5 )

劝你先忘掉微信 API 那个方案,它那个场景很有可能是因为安全原因,网关过滤了 header 和 query 的自定义字段,导致 signature 只能放在 body 中。

如果你还是坚持按照微信 API 那种模式,有个笨方法,就是把你要传递的 content 对象序列化后,当作字符串参数看待,即在原来的 body 上再包多一层:
{
content: string // "{\"key\":\"value\"}"
appid: string
sign: string
}
fkdog
2020-01-17 17:12:13 +08:00
@LinJunzhu 是谁给你的错觉说这类二进制反编译不出来。。。??
只不过难度比 java 成本高而已。不然一堆堆的破解软件补丁是怎么出来的?
Erroad
2020-01-17 17:17:07 +08:00
算 sign 前先按 key 升序或降序排序下 json 在各语言对应的可排序数据结构
Erroad
2020-01-17 17:18:13 +08:00
看错了,复杂结构递归排下序吧
also24
2020-01-17 19:10:44 +08:00
@LinJunzhu #19
直接对 request body 进行验签是可取的且并不会被乱序干扰到,只能说你确实对 http 协议不够熟悉……
ratazzi
2020-01-17 19:50:10 +08:00
千万别学那些恶心的排序然后 urlencode 什么的,直接对 body 做 HMAC
LinJunzhu
2020-01-17 22:27:41 +08:00
@also24 只能说你考虑不周到...
LinJunzhu
2020-01-17 22:34:42 +08:00
@fkdog 搜索了下,so 确实可以反编译, 但这不是 API 不加任何防护的理由:)
also24
2020-01-17 23:14:34 +08:00
@LinJunzhu
request body 就是一串纯文本,可以理解为一个 String,在被解析之前,它是固定不变的,完全不涉及到底是 json 还是 xml 还是 form 的问题。

你的思维被 json 禁锢住了,request body 是更高一个层面的东西。
以及,这样算出来的 hash,需要加在 header 里面,不能动 request body
also24
2020-01-17 23:23:57 +08:00
换一个方式说:
JSON 的无序,体现在 JSON Object 被 序列化 为 JSON String 的时候,无法保证不同的序列化方式序列化出的 JSON String 是一致的。

而 request body,本身就是一段已经被 序列化 好了的 JSON String,这段已经被 序列化 后的字符串,是可以确定不变的。

如果你经常使用一些 web 请求框架的话,由于框架的封装,你也许会以为自己发送和接收的是 JSON 对象。
但是要搞清楚的是,你收发的只是一串符合 JSON 规范的字符串而已,对 HTTP 来说这和 “hello world” 没有区别。

除非你的请求中间有被篡改过,否则,客户端发送的 request body,和服务端收到的 request body,应当是完全一致的字符串,HTTP 是不会把你的 “hello world” 变成 “world hello” 的。

为什么说你对 HTTP 的了解确实不够深,因为你把下面两件事搞混淆了:
[可能不一致] A:JSON Object 被 序列化 为 JSON String
[确定一致] B:JSON String 被填充在 request body 发送给服务端
also24
2020-01-17 23:25:53 +08:00
另外,request body 的方式,和我在 6 楼 10 楼 提到的 “JSON String” 的方式,其实思路上是一致的。

都是通过传递已经被 序列化 后的 JSON String 来保证签名和验签一致的。
auser
2020-01-17 23:28:39 +08:00
最近这段时间接触了一些程序员和 API,有些感触:本应该是跟编程语言特性无关的接口,却被设计成跟某语言特性 /官方库强相关。

also24 是真的在帮楼主理解 👍
phx13ye
2020-01-17 23:49:52 +08:00
我以为是我发的,我昨天也问了这个问题

纠结的点在于两边实现起来都麻烦,继续来学习一下

发送方请求时,在代码层面,requeset body 是由某个库为你把请求对象序列化成 json 字符串,你只要构造这个请求对象,但是涉及到签名就需要把最终 request body 拿到,digest 并设置签名请求头

接收方验证时,也是直接由框架转成请求对象,他要验签,不可以直接用这个请求对象序列化成 json 字符串,不然两边会不一致。所以要把 request body 当作字符串接收并签名,然后和签名请求头判断是否一致

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/638565

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX