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 条回复
chenqh
2020-01-17 23:54:52 +08:00
@index90 你里面的 AccessKey 是什么?相当于服务器给客户端分配的 client_id?
rioshikelong121
2020-01-18 00:08:51 +08:00
also24 讲得不错。刷 v2 涨姿势系列。
index90
2020-01-18 00:25:03 +08:00
@chenqh 对,有些地方叫 appid,有些地方叫 clientid
index90
2020-01-18 01:21:42 +08:00
我猜,这是 LZ 看到的:
jsonObject -> sendRequest(jsonObject) -> client -> server -> handleRequest(jsonObject)

LZ 对 request body 签名的理解:
jsonObject -> sign(encode(jsonObject)) -> sendRequest(jsonObject) -> client -> server -> handleRequest(jsonObject) -> verify(encode(jsonObject), sign)

但实际上是:
jsonObject -> sendRequest(jsonObject) -> body = encode(jsonObject) -> post(body) -> client -> server -> recv(body) -> jsonObject = decode(body) -> handleRequest(jsonObject)

对 request body 签名的正确理解:
jsonObject -> sign(encode(jsonObject)) -> sendRequest(encode(jsonObject)) -> client -> server -> handleRequest(jsonString) -> verify(jsonString, sign)

@LinJunzhu 我尽力了
既然你上来是找答案的,别人已经给了你经过实践验证的答案,那就先实践,遇到问题再提问。你都没解决方案了,你还没试过别人给的方案就忙于反驳,这是真的如你提问中所说,想看看别人怎么做的么?
HaoLan
2020-01-18 03:09:42 +08:00
有一堆闲着没事干的程序员也想到这个问题,于是他们规范这种复杂 JSON,取名叫 JWT(JSON Web Token),可以去看看。不仅涉及到规范化,还有防止 JSON 被改动
xuanbg
2020-01-18 03:29:27 +08:00
先加密,加密一下就和顺序没关系了。然后对密文做签名,服务端使用同样的签名方法进行验证。
xcstream
2020-01-18 04:13:55 +08:00
把子对象变成 json 字符串
chenuu
2020-01-18 06:52:34 +08:00
在解析成 JSON 之前,是 string 的阶段。
wd
2020-01-18 07:01:13 +08:00
我感觉你是不是自己给自己设置障碍?你就把整个 json 这个字符串当作一个 vlaue 有什么问题吗?你只需要验证这个字符串没有被改就可以了?何必需要自己解析后再序列化比较呢?
wd
2020-01-18 07:01:50 +08:00
就是 1 楼给你的方案。
LinJunzhu
2020-01-18 09:49:01 +08:00
@index90

我自己实现过 HTTP 代理服务,所以对 HTTP 协议自认为还是理解的。

你说的 request body,我完全懂,并没有误会成 object -> request body。

为什么不想直接对 request body 进行签名,是因为部分 Http 库并不支持直接设置 json string 作为 request body,而是通过设置对象,再由 Http 库自己转为 json string -> request body。

因为想要一个约定俗成的方案,所以想看看大家的做法,现在我知道了哈哈

非常感谢你的答复 :)
LinJunzhu
2020-01-18 09:50:08 +08:00
@also24

我自己实现过 HTTP 代理服务,所以对 HTTP 协议自认为还是理解的。

你说的 request body,我完全懂,并没有误会成 object -> request body。

为什么不想直接对 request body 进行签名,是因为部分 Http 库并不支持直接设置 json string 作为 request body,而是通过设置对象,再由 Http 库自己转为 json string -> request body。

因为想要一个约定俗成的方案,所以想看看大家的做法,现在我知道了哈哈

非常感谢你的答复 :)
Jackin
2020-01-18 09:56:50 +08:00
// 这个应该是你想要的

private static String sign(HashMap<String, Object> parameters) throws Exception {
List<String> keys = new ArrayList<String>(parameters.keySet());
Collections.sort(keys, new Comparator<String>() {
@Override
public int compare(String s1, String s2) {
return s1.compareTo(s2);
}
});

StringBuilder signature = new StringBuilder("");
for (String key : keys) {
if (key.equalsIgnoreCase("sign")) {
continue;
}
signature.append(key);
Object value = parameters.get(key);

if (value instanceof HashMap) {
signature.append(sign((HashMap<String, Object>) value));
continue;
}

if (((String) value).equals("")) {
continue;
}

signature.append((String) value);
}
return signature.toString();
}
also24
2020-01-18 10:49:19 +08:00
@LinJunzhu #52
无奈脸,所谓的设置 JSON 对象设置的是什么?

就是一个正常的 http post,以 JSON String 作为字符串 body,
再覆盖 header 里的 Content-Type 为 "application/json" 嘛……
自己手动处理下 header 不就好了么…………

requests / okhttp 它们自己也是这样干的啊……

https://github.com/psf/requests/blob/master/requests/models.py#L463

https://github.com/square/okhttp/blob/master/samples/guide/src/main/java/okhttp3/guide/PostExample.java#L15

如果不想这么处理,一定要传 JSON Object 给框架,那么相同的思路我在 6 楼和 10 楼提到的 JSON String 的方式,实质上是一致的。
mxalbert1996
2020-01-18 11:02:11 +08:00
这年头还有不支持直接设置 raw data 作为 body 的 http 库?涨知识了
fuis
2020-01-18 11:30:25 +08:00
@LinJunzhu 通常

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

这样的拼成 aello=world&bello.hi=hi
chinvo
2020-01-18 11:45:23 +08:00
@mxalbert1996 #55 可能楼主试过直接传 json 字符串,但是服务器端不解析(因为没设置 content type
dallaslu
2020-01-18 11:48:19 +08:00
参考苹果支付接口,payload 是一串转码加密后的信息。可以用 base64 编码一下啊
walpurgis
2020-01-18 12:49:56 +08:00
json 排序后签名实属蛋疼,此类功能应在拦截器 /中间件上实现,对业务是完全透明的
hst001
2020-01-18 12:55:18 +08:00
想太多了,拿最原始的数据计算签名不就好了,为什么非要解析完

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

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

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

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

© 2021 V2EX