V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
• 请不要在回答技术问题时复制粘贴 AI 生成的内容
LinJunzhu
V2EX  ›  程序员

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

  •  3
     
  •   LinJunzhu · 2020-01-16 22:15:15 +08:00 · 8984 次点击
    这是一个创建于 1801 天前的主题,其中的信息可能已经有所发展或是发生改变。

    提供 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 参数。

    不知道大家是如何做的?

    70 条回复    2020-08-11 16:51:42 +08:00
    billlee
        1
    billlee  
       2020-01-16 22:36:50 +08:00
    你不按 key=value 格式拼接,直接算 JSON 字符串的 digest 也行啊
    LinJunzhu
        2
    LinJunzhu  
    OP
       2020-01-16 22:43:31 +08:00
    @billlee Hash 对象的 key 是无序的, 同一个 json 文本, 在不同系统 /语言 解析成 对象后,再格式化为 json 文本,key 的位置是有可能发生变化的,所以这段 json 文本的 digest/md5 也是有可能不一样的。
    shoaly
        3
    shoaly  
       2020-01-16 22:57:14 +08:00
    微信和支付宝的做法都一样, 他们最外层的结构依然是 一层关系的, 当 value 是数组或者对象的时候, 他们把 value 的内容转换成了 json 字符串
    Vegetable
        4
    Vegetable  
       2020-01-16 23:05:11 +08:00
    递归呗.
    LinJunzhu
        5
    LinJunzhu  
    OP
       2020-01-16 23:11:29 +08:00
    @shoaly 哈希对象的 key 是无顺序的,同一段 json 文本,在最终解析出来的顺序是可能不一致的;

    若 value 为对象,那么客户端在生成 token 时,该 value 转换为 json 文本是:

    "{\"a\":\"a\",\"b\":\"b\"}"

    服务端在生成 token 时,该 value 转换为 json 文本可能是:

    "{\"b\":\"b\",\"a\":\"a\"}"

    所以最终验签会失败
    also24
        6
    also24  
       2020-01-16 23:12:39 +08:00   ❤️ 1
    首先,JSON array 是有序的,可以不用担心无序的问题。

    此类问题没有标准的解决方式,只能选择提供若干思路:

    思路一,参考 query string 对数组参数的解决方案:
    key=value 这种格式,可以认为是参考了 query string 的形式,那么我们可以翻一下 qs 是如何处理的,比如说看看 node 的文档
    https://nodejs.org/docs/latest-v12.x/api/querystring.html#querystring_querystring_parse_str_sep_eq_options

    可见,node 此处是使用了数组的 key 多次出现,以出现顺序作为数组顺序的方式来解决数组问题的。
    不过这种方式暂时无法解决复杂对象的问题。

    思路二,参考 form 对数组参数的解决方案
    https://stackoverflow.com/questions/9073690/post-an-array-from-an-html-form-without-javascript
    我没有仔细核查这种方式是一种标准,还是某个框架 /语言约定俗成。

    很显然它利用中括号,又构建了一层 k-v 体系,从而不但解决了数组问题,连复杂对象问题一起解决了。


    思路三,直接 JSON String 不香么?
    反正验签和业务代码肯定是分离开的,那干脆直接把业务数据都封在一起呗,还能顺便搞个 RSA 之类的加密。

    {
    "rsa_key_id": "24",
    "req_body": "ewogICJyc2Ffa2V5X2lkIjogIjI0IiwKICAicmVxX2JvZHkiOiAiIiwKICAibm9uY2UiOiAidjJleCIsCiAgInRpbWVzdGFtcCI6IDE1ODA1ODAxMjIKfQ==",
    "nonce": "v2ex",
    "timestamp": 1580580122,
    "sign": "82f7f51055326385bf9f7e151b212066"
    }

    注意:nonce 和 timestamp 同时也包含在 req_body 中,外面再放一份是方便你验签。
    LinJunzhu
        7
    LinJunzhu  
    OP
       2020-01-16 23:12:54 +08:00
    @Vegetable 这也是我一开始的想法,但在网上搜索了好一阵,都没有个约定说怎么做,所以上来问问大家的做法。
    also24
        8
    also24  
       2020-01-16 23:19:01 +08:00
    补充一下,关于第二种思路,我确认了一下,似乎不是标准实现,但是确实是 php 官方支持的,参见:
    https://www.php.net/manual/en/faq.html.php#faq.html.arrays
    LinJunzhu
        9
    LinJunzhu  
    OP
       2020-01-16 23:19:13 +08:00
    @also24 数组的有序性我倒是不担心, 唯一疑惑的点就在于 value 为对象时,该如何确保 客户端和服务端生成 sign 的逻辑是一致的。

    因为哈希对象的 key 是无顺序的,同一段 json 文本,在最终解析出来的顺序是可能不一致的;

    若 value 为对象,那么客户端在生成 token 时,该 value 转换为 json 文本是:

    "{\"a\":\"a\",\"b\":\"b\"}"

    服务端在生成 token 时,该 value 转换为 json 文本可能是:

    "{\"b\":\"b\",\"a\":\"a\"}"

    所以最终会导致验签失败。

    我能想到的就是对 value 为复杂对象时,也进行 key=value 的形式排序,如此递归下去
    also24
        10
    also24  
       2020-01-16 23:23:58 +08:00
    @LinJunzhu #9
    你没看我给出的第二种和第三种方法么?

    第二种方法参考了 php 对 form 的数组实现(同时可以应用于复杂对象)

    第三种方式则直接嵌套了一个 JSON String,可能因为我添加了一个虚假的 rsa 导致看起来不太直观,我再补充一下吧:
    {
    "req_body": "{\n \"title\": \"v2ex 发帖\",\n \"url\": \"/t/638565\",\n \"nonce\": \"v2ex\",\n \"timestamp\": 1580580122\n}",
    "nonce": "v2ex",
    "timestamp": 1580580122,
    "sign": "82f7f51055326385bf9f7e151b212066"
    }
    wangyzj
        11
    wangyzj  
       2020-01-17 00:02:09 +08:00
    为什么会有数组出现?
    dawniii
        12
    dawniii  
       2020-01-17 00:10:41 +08:00 via iPhone   ❤️ 1
    为什么要解析了签呢,直接用 http body 签不就好了。
    JCZ2MkKb5S8ZX9pq
        13
    JCZ2MkKb5S8ZX9pq  
       2020-01-17 00:12:33 +08:00
    碰到过几个 api,直接把 value 变成字符串了。
    xl224
        14
    xl224  
       2020-01-17 00:13:05 +08:00 via iPhone
    直接拿传过来的 json string 验证,通过了再去反序列化成内部对象
    geelaw
        15
    geelaw  
       2020-01-17 08:25:37 +08:00 via iPhone
    > 提供 API 给客户端使用时,为了验证是客户端发起的请求,并且保证请求中不被用户恶意篡改

    目的错误,用户就是客户端,客户端就是用户,运行在最终用户机器上的代码都是最终用户可以查看的,没有必要防范用户篡改——用户本来就有完全控制能力。

    > 翻看了微信和支付宝的文档

    这两个和你说的场景不同,考虑 A 公司向 B 用户通过支付宝收款,则收款请求是 A 的服务器发出的而不是 B 的设备发出的。要求 A 进行消息认证的目的是为了确保收款请求确实是 A 的意思而不是别人栽赃。

    注意是消息认证而不是数字签名。后者是可公开验证的,前者不一定要是这样。在楼主提出的机制里校验也需要 secret。

    简单来说,API 的消费者是不安全、成千上万的用户设备则无需画蛇添足,API 的消费者是受控制、安全的服务器才需要消息认证。
    StarUDream
        16
    StarUDream  
       2020-01-17 09:17:36 +08:00
    按照 key 的 ascii 递归拼接 value。
    1. worldjava
    2. worldhi
    qyvlik
        17
    qyvlik  
       2020-01-17 09:45:46 +08:00
    有些平台是不对 http body 为 JSON 字符串格式进行签名(其实是消息认证、数字签名也有),例如 火币的部分 open-api。

    如果想要对 JSON 字符串进行签名,可以剖析一下 HTTP 的请求格式

    ```
    METHOD PATH[QUERY] HTTP 1.1
    [HEADERS]
    [BODY]
    ```

    在**部分请求头不参与签名**情况下,对一个 HTTP 请求进行签名,第一步是整理出需要签名的原始内容,然后确保原始内容和内容格式,客户端和服务端都可以**轻易**获取和处理,例如一种可能的原始内容格式如下:

    ```
    METHOD\n
    PATH\n
    QUERY\n
    CONTENT-TYPE\n
    PAYLOAD\n
    NONCE
    ```

    - `METHOD`: http 请求方法,例如 GET, POST 等
    - `PATH`: 请求路径,例如 `/`, `/api/v1/time`
    - `QUERY`: 查询字符串,例如 `?userId=1` 或者 `?userId=1&state=active`
    - `CONTENT-TYPE`: http body 的格式,例如 `application/x-www-form-urlencoded`,或者:`application/json`
    - `PAYLOAD`: http body,例如是表单,或者 JSON 字符
    - `NONCE`:一般是时间戳,防止重放的

    然后客户端使用 HMAC-SHA256 的消息认证算法进行签名,**并将签名的字符串放到请求头中**,服务端从请求头获取签名串,然后从请求按照上诉格式拼接处原始内容,在服务端再签名一次,比对两次的签名串是否一致,即可做到**验证签名**的效果。

    由于签名串是放在请求头,而不是写回到 `QUERY`(查询字符串),所以不用在客户端重新排序 `QUERY` 或者表单,服务端也不用。

    当然,这是其中一种处理方式,更多时候,可能是要求 客户端将 POST JSON 方式修改为 POST FORM,也就是将 JSON 格式转为表单中的一个字段,类似:`&json_payload={"a":"1", "b":"2"}`,然后在对其进行 url 编码。

    可以看看如何在 spring 中处理 hmac 消息认证的,[qyvlik/spring-hmac-rest-verify]( https://github.com/qyvlik/spring-hmac-rest-verify)
    index90
        18
    index90  
       2020-01-17 12:15:32 +08:00
    你签名的目的是保证 request 的 body 不被篡改,至于 body 的 content-type 是什么没有关系。
    直接对 request 的 body 进行签名就好了。

    我猜 lz 主要是把 json 字符串和 json 对象搞混了,json 对象是具体语言对 json 字符串的面向对象表达方式。
    http request 的 payload 实际上只能是一个 byte 数组,你之所以觉得会有 json value,other value 之分,是因为你所使用的库抽象封装了。查看源码你会发现,json 对象是需要 encode 成 json 字符串,才放进 request payload 里面。

    回到问题,lz 应该自行对 json 对象进行 encode,并对 json 字符串进行签名,你所使用的库应该提供 set raw payload 的方法。
    引申问题,lz 你对 http 协议的理解可能还只停留在你所使用的库层面上,建议你还是更仔细学一下 http 协议。
    LinJunzhu
        19
    LinJunzhu  
    OP
       2020-01-17 14:09:05 +08:00
    @index90

    你看我上面的回复,就能看到,我指出了 JSON 文本,和 JSON 对象(各个语言有各自的类型),我对 http 的协议还是蛮熟悉的 :)

    至于为什么我一直在说 json, 是因为现在 API 交互中, 双方基本约定成俗为 content_type: application/json

    直接对 request body 进行验签不可取,还是那句话,hash key 是无序的,不同语言、不同库 解析和反解析,顺序不一定一致。

    你可以看下微信支付商户的 API 文档: [https://pay.weixin.qq.com/wiki/doc/api/micropay.php?chapter=4_3] 也是进行 键值对的 排序组合, 只不过微信支付的 API 参数 value,都是基本类型
    LinJunzhu
        20
    LinJunzhu  
    OP
       2020-01-17 14:14:47 +08:00
    @geelaw

    >这两个和你说的场景不同,考虑 A 公司向 B 用户通过支付宝收款,则收款请求是 A 的服务器发出的而不是 B 的设备发出的。要求 A 进行消息认证的目的是为了确保收款请求确实是 A 的意思而不是别人栽赃。

    那我们回到这个场景,微信支付商户文档也是对参数进行了键值对的排序组合。

    若 参数值为 一个 json 对象,而非基本类型(String, int),该如何做解析?
    index90
        21
    index90  
       2020-01-17 15:43:42 +08:00
    @LinJunzhu 你发的那个是链接我看了,人家之所以这样做,是因为把 signature 放在了 body 中传送,可能由于他们协议的要求不能在 query 参数或者 header 中存放额外字段。

    如果你们没有这种限制,最好的办法是把 signature 放在 header 中( aws 就是这样做),签名时针对 body 进行签名。这样做得好处是,api 网关做签名校验时,不需要反序列化,性能更高,更通用,无论什么 content 类型都支持。
    index90
        22
    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
        23
    fkdog  
       2020-01-17 16:22:42 +08:00
    客户端搞什么接口签名啊,多此一举。
    怕中间人篡改那就上 https。
    如果怕用户自己篡改,那这个没辙,签名算法、加密逻辑完全能反编译出来。
    面对这种情况,服务端需要做的是保证请求结果处理正确即可。
    index90
        24
    index90  
       2020-01-17 16:39:17 +08:00
    @fkdog lz 的需求其实是身份认证,是想知道这个请求是谁发出的,并且要有数字签名证明。
    https 解决不了 C 公司冒充 A 公司向 B 公司发起转账申请,除非你启用 tls 的双向验证,这时候需要给每个请求方发放 client 证书,服务端还需要实现从证书中提取请求方 ID,才知道是谁发起的请求,实现难度比数字签名大一点。
    LinJunzhu
        25
    LinJunzhu  
    OP
       2020-01-17 16:42:47 +08:00
    @fkdog Android 内的 so 可反不出来。

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

    其实我纠结的点在于:

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

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

    所以才发了贴,问下大家的做法 :)
    LinJunzhu
        27
    LinJunzhu  
    OP
       2020-01-17 16:53:06 +08:00
    @index90 更正: payload 为 request body
    index90
        28
    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
        29
    fkdog  
       2020-01-17 17:12:13 +08:00
    @LinJunzhu 是谁给你的错觉说这类二进制反编译不出来。。。??
    只不过难度比 java 成本高而已。不然一堆堆的破解软件补丁是怎么出来的?
    Erroad
        30
    Erroad  
       2020-01-17 17:17:07 +08:00
    算 sign 前先按 key 升序或降序排序下 json 在各语言对应的可排序数据结构
    Erroad
        31
    Erroad  
       2020-01-17 17:18:13 +08:00
    看错了,复杂结构递归排下序吧
    also24
        32
    also24  
       2020-01-17 19:10:44 +08:00
    @LinJunzhu #19
    直接对 request body 进行验签是可取的且并不会被乱序干扰到,只能说你确实对 http 协议不够熟悉……
    ratazzi
        33
    ratazzi  
       2020-01-17 19:50:10 +08:00 via iPhone
    千万别学那些恶心的排序然后 urlencode 什么的,直接对 body 做 HMAC
    LinJunzhu
        34
    LinJunzhu  
    OP
       2020-01-17 22:27:41 +08:00
    @also24 只能说你考虑不周到...
    LinJunzhu
        35
    LinJunzhu  
    OP
       2020-01-17 22:34:42 +08:00
    @fkdog 搜索了下,so 确实可以反编译, 但这不是 API 不加任何防护的理由:)
    also24
        36
    also24  
       2020-01-17 23:14:34 +08:00 via Android
    @LinJunzhu
    request body 就是一串纯文本,可以理解为一个 String,在被解析之前,它是固定不变的,完全不涉及到底是 json 还是 xml 还是 form 的问题。

    你的思维被 json 禁锢住了,request body 是更高一个层面的东西。
    以及,这样算出来的 hash,需要加在 header 里面,不能动 request body
    also24
        37
    also24  
       2020-01-17 23:23:57 +08:00   ❤️ 2
    换一个方式说:
    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
        38
    also24  
       2020-01-17 23:25:53 +08:00
    另外,request body 的方式,和我在 6 楼 10 楼 提到的 “JSON String” 的方式,其实思路上是一致的。

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

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

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

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

    接收方验证时,也是直接由框架转成请求对象,他要验签,不可以直接用这个请求对象序列化成 json 字符串,不然两边会不一致。所以要把 request body 当作字符串接收并签名,然后和签名请求头判断是否一致
    chenqh
        41
    chenqh  
       2020-01-17 23:54:52 +08:00
    @index90 你里面的 AccessKey 是什么?相当于服务器给客户端分配的 client_id?
    rioshikelong121
        42
    rioshikelong121  
       2020-01-18 00:08:51 +08:00
    also24 讲得不错。刷 v2 涨姿势系列。
    index90
        43
    index90  
       2020-01-18 00:25:03 +08:00 via iPhone
    @chenqh 对,有些地方叫 appid,有些地方叫 clientid
    index90
        44
    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
        45
    HaoLan  
       2020-01-18 03:09:42 +08:00 via Android
    有一堆闲着没事干的程序员也想到这个问题,于是他们规范这种复杂 JSON,取名叫 JWT(JSON Web Token),可以去看看。不仅涉及到规范化,还有防止 JSON 被改动
    xuanbg
        46
    xuanbg  
       2020-01-18 03:29:27 +08:00
    先加密,加密一下就和顺序没关系了。然后对密文做签名,服务端使用同样的签名方法进行验证。
    xcstream
        47
    xcstream  
       2020-01-18 04:13:55 +08:00
    把子对象变成 json 字符串
    chenuu
        48
    chenuu  
       2020-01-18 06:52:34 +08:00 via Android
    在解析成 JSON 之前,是 string 的阶段。
    wd
        49
    wd  
       2020-01-18 07:01:13 +08:00 via iPhone
    我感觉你是不是自己给自己设置障碍?你就把整个 json 这个字符串当作一个 vlaue 有什么问题吗?你只需要验证这个字符串没有被改就可以了?何必需要自己解析后再序列化比较呢?
    wd
        50
    wd  
       2020-01-18 07:01:50 +08:00 via iPhone
    就是 1 楼给你的方案。
    LinJunzhu
        51
    LinJunzhu  
    OP
       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
        52
    LinJunzhu  
    OP
       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
        53
    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
        54
    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
        55
    mxalbert1996  
       2020-01-18 11:02:11 +08:00 via Android
    这年头还有不支持直接设置 raw data 作为 body 的 http 库?涨知识了
    fuis
        56
    fuis  
       2020-01-18 11:30:25 +08:00
    @LinJunzhu 通常

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

    这样的拼成 aello=world&bello.hi=hi
    chinvo
        57
    chinvo  
       2020-01-18 11:45:23 +08:00 via iPhone
    @mxalbert1996 #55 可能楼主试过直接传 json 字符串,但是服务器端不解析(因为没设置 content type
    dallaslu
        58
    dallaslu  
       2020-01-18 11:48:19 +08:00
    参考苹果支付接口,payload 是一串转码加密后的信息。可以用 base64 编码一下啊
    walpurgis
        59
    walpurgis  
       2020-01-18 12:49:56 +08:00 via Android
    json 排序后签名实属蛋疼,此类功能应在拦截器 /中间件上实现,对业务是完全透明的
    hst001
        60
    hst001  
       2020-01-18 12:55:18 +08:00 via Android
    想太多了,拿最原始的数据计算签名不就好了,为什么非要解析完
    niubee1
        61
    niubee1  
       2020-01-18 13:00:08 +08:00
    用类似 Merkle tree 的方式嘛,如果一个属性的值是一个对象的话,就替换为对象的 hash 值,而 hash 这个子对象的规则呢就用属性值按照字典序排列 hash 的方式,逐级递归。 这样子就能够保证签名的值不会因为字典解析的 key 是无序的而导致实际内容一致而签名不一致的情况
    momocraft
        62
    momocraft  
       2020-01-18 13:30:23 +08:00
    有 https 后还要这样做 图什么
    gamexg
        63
    gamexg  
       2020-01-18 14:55:38 +08:00 via Android
    body 存放 json,
    签名放到 http 头里面
    tanszhe
        64
    tanszhe  
       2020-01-18 14:59:33 +08:00
    @LinJunzhu 字符串是有序的啊,解析成对象的时候 保持不变就可以了
    Orenoid
        65
    Orenoid  
       2020-01-18 15:24:15 +08:00
    楼主能不能给我解下惑,我真的没明白你说的防篡改是出于什么需求。。
    微信支付我也做过,它那里头的签名的核心部分是商户密钥,本质上是为了身份认证。
    如果你只是单纯给业务数据做个签名发给服务端,任何人知道签名算法的话,完全可以脱离客户端伪造一个请求啊。
    真心没理解,望解惑。
    jss
        66
    jss  
       2020-01-18 16:42:08 +08:00
    没有任何意义
    jss
        67
    jss  
       2020-01-18 16:43:28 +08:00
    不会改参数的不说,会改的防不住...
    tairan2006
        68
    tairan2006  
       2020-01-18 21:18:35 +08:00 via Android
    这玩意儿其实没啥意义,你不用非要抄微信,自己写一个也行…
    aaronlam
        69
    aaronlam  
       2020-02-12 02:25:18 +08:00
    我之前也纠结过这个问题,持续关注
    huobazi
        70
    huobazi  
       2020-08-11 16:51:42 +08:00
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2862 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 27ms · UTC 12:46 · PVG 20:46 · LAX 04:46 · JFK 07:46
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.