签名认证算法

    该算法主要依据 HTTP 签名草案 建立,使用的 Kong 原生的

    Authorization 请求头的构成

    首先来看一个标准的 Authorization 请求头:

    逐个部分来看具体含义

    第1部分:hmac

    表明使用了 hmac 签名,这是个静态字段,所有请求都是一样的,无需变化

    第2部分:appkey=”wsK8t77fvAAs3i7878NSkC0j95ib3oVu”

    即凭证中的 App Key 字段,取如图所示的值:

    需要与第1部分之间用 ASCII 空格分隔

    第3部分:algorithm=”hmac-sha256”

    表示使用的签名算法,这部分也无需变化

    需要与第2部分之间用 ASCII 字符 , 和 ASCII 空格 分隔

    第4部分:headers=”date request-line”

    参与签名的请求头,都需要是小写的,注意这里是有序的,表明了签名过程中字段拼接的顺序,具体会在签名算法中介绍

    ::: tip request-line request-line 这个字段比较特殊,表示请求行,例如GET /api?name=bob HTTP/1.1,虽然这里写在 headers 里,但其实并不是 header :::

    需要与第3部分之间用 ASCII 字符 , 和 ASCII 空格 分隔

    第5部分:signature=”gaweQbATuaGmLrUr3HE0DzU1keWGCt3H96M28sSHTG8=”

    基于签名算法生成的签名值

    需要与第4部分之间用 ASCII 字符 , 和 ASCII 空格 分隔

    签名算法

    1. 不存在请求 body 时

    必须请求头

    • Date
    • Authorization

    Date 请求头

    其中 Date 请求头需要遵循 RFC1123 HTTP 规范,例如Thu, 10 Dec 2020 08:47:43 GMT

    Unix 命令生成:

    1. env LANG=eng TZ=GMT date '+%a, %d %b %Y %T %Z'
    1. System.out.println(DateTimeFormatter.RFC_1123_DATE_TIME.format(ZonedDateTime.now(ZoneOffset.UTC)));

    ::: tip 如果 Date 请求头里的时间与服务器时间的差值绝对值超过 5 分钟,会被认为是请求重放,拒绝请求 :::

    Authorization 请求头

    对于请求头的构造,上面已经阐述。这里说明其中的 signature 部分是如何生成的

    首先是待签名字符串的生成,生成规则如下:

    1. 如果不是request-line,拼接小写的请求头的 key,并跟上 ASCII 字符 : 和 ASCII 空格
    2. 如果不是request-line, 拼接上请求头的 value; 如果是 request-line,拼接上 HTTP request line
    3. 如果不是最后,拼接上 ASCII 换行符 \n

    通过具体例子来说明,对于以下请求:

    1. curl -i -X GET http://localhost/requests?name=bob \
    2. -H 'Host: hmac.com' \
    3. -H 'Date: Thu, 22 Jun 2017 21:12:36 GMT' \
    4. -H 'Authorization: hmac appkey="wsK8t77fvAAs3i7878NSkC0j95ib3oVu", algorithm="hmac-sha256", headers="date host request-line", signature="FiPTWoayUGvlaAk6HbnxEzlXo0JO2HhiDGEwsR4yKPo="'

    Authorization 头中指定了用于签名的请求头,date,host 以及特殊的请求行request-line,按顺序进行字符串拼接,获得待签名字符串

    1. date: Thu, 22 Jun 2017 21:12:36 GMT
    2. host: hmac.com
    3. GET /requests?name=bob HTTP/1.1

    接着,对待签名字符串进行签名,签名规则如下:

    1. signed_string=HMAC-SHA256(<signing_string>, "secret")
    2. signature=base64(<signed_string>)

    如果 App Secret 为 qdWre3pJxitNm9NOBRH3EpWeVYepnt3f

    可以得到签名值为 FiPTWoayUGvlaAk6HbnxEzlXo0JO2HhiDGEwsR4yKPo=

    对于上面的例子,使用 Unix 命令生成签名:

    1. echo -ne "date: Thu, 22 Jun 2017 21:12:36 GMT\nhost: hmac.com\nGET /requests?name=bob HTTP/1.1" | \
    2. openssl dgst -sha256 -hmac "qdWre3pJxitNm9NOBRH3EpWeVYepnt3f" -binary | base64

    使用 java 代码生成签名:

    1. import org.apache.commons.codec.binary.Base64;
    2. import org.apache.commons.codec.digest.HmacAlgorithms;
    3. import org.apache.commons.codec.digest.HmacUtils;
    4. // ...
    5. String digest =
    6. new String(
    7. Base64.encodeBase64String(
    8. new HmacUtils(HmacAlgorithms.HMAC_SHA_256, "qdWre3pJxitNm9NOBRH3EpWeVYepnt3f")
    9. .hmac("date: Thu, 22 Jun 2017 21:12:36 GMT\nhost: hmac.com\nGET /requests?name=bob HTTP/1.1")));

    必须请求头

    • Date
    • Digest
    • Authorization

    Date 请求头

    与不存在 body 时一致

    Digest 请求头

    需要使用 SHA-256 对请求 Body 进行签名,例如 body 为 ,则对应的 Digest 请求头为:Digest: SHA-256=956ba28434677d7d825157df180ef8123067cd58277c73f2c0f5e461a2830b52

    注意 Digest 请求头的 value 需要用 SHA-256= 开头

    使用 Unix 命令生成

    1. echo -n '{"name": "bob"}' | openssl dgst -sha256

    ::: warning 请求限制 请求 body 大小不能超过 10m :::

    Authorization 请求头

    与不存在 body 时的区别是,headers 部分必须带上 digest,举例如下:

    Authorization 头中指定了用于签名的请求头,date,host,特殊的请求行request-line,以及请求 body 的签名值digest,按顺序进行字符串拼接,获得待签名字符串

    1. date: Thu, 22 Jun 2017 21:12:36 GMT
    2. host: hmac.com
    3. GET /requests?name=bob HTTP/1.1
    4. digest: SHA-256=956ba28434677d7d825157df180ef8123067cd58277c73f2c0f5e461a2830b52

    生成 signature 的方式与不存在 body 时是一致的,不再赘述

    参数签名认证

    比如调用参数为:

    1. /api?appKey=foobar&name=dadu&abc=123

    则首先参数名按照字母序升序排列,得到:

    1. abc=123&appKey=foobar&name=dadu

    再假设调用凭证中的 App Secret 为 my.secret,则将其附加到参数末尾,得到:

    1. abc=123&appKey=foobar&name=dadumy.secret

    再计算这段字符串的SHA512,得到签名值为:

    1. f97efc239eef4eafe69bfe41438740199d939e2e123c4c5a6b5d0b5e58d295a2818d6444c5c7b9e5985e751ad93f9c854e1966e59a63a1eeceb31e46641e291a

    所以最后的请求为:

    1. /api?appKey=foobar&name=dadu&abc=123&sign=f97efc239eef4eafe69bfe41438740199d939e2e123c4c5a6b5d0b5e58d295a2818d6444c5c7b9e5985e751ad93f9c854e1966e59a63a1eeceb31e46641e291a

    对于 POST 等带了 body 的请求,会对 body 进行签名,此时分为两种情况:

    1、Content-Type 是 application/x-www-form-urlencoded

    此时与 URL 参数签名的方式一致,只不过将参数放到了 body 里面

    请求限制:

    • body 大小不能超过 10m
    • 参数个数不能大于 100 个

    2、Content-Type 是 application/json

    例如原始请求为:

    1. POST --header 'Content-Type: application/json'
    2. -d '
    3. {"userName":"abc","gender":"male"}
    4. '

    则将 body 整体作为名为 data 的参数,按照字母序升序排列,得到:

    1. appKey=foobar&data={"userName":"abc","gender":"male"}

    再假设调用凭证中的 App Secret 为 my.secret,则将其附加到参数末尾,得到:

    再计算这段字符串的 SHA512,得到签名值为:

    1. ec23eeda5f88abe26311ed020439172eea409e3475875c87e9abfa8a6856138e767608e8497435f573ccb417a90448c78abdca4a0de12c4da4583aa3add7bf52

    所以最终调用方需要发起的请求为:

    1. POST --header 'Content-Type: application/json'
    2. -d '
    3. {
    4. "data": "{\"userName\":\"abc\",\"gender\":\"male\"}",
    5. "appKey": "foobar",
    6. "sign": "ec23eeda5f88abe26311ed020439172eea409e3475875c87e9abfa8a6856138e767608e8497435f573ccb417a90448c78abdca4a0de12c4da4583aa3add7bf52"
    7. }'

    网关收到请求后,发给后端服务的真正请求和原始请求一致:

    1. POST --header 'Content-Type: application/json'
    2. -d '
    3. {"userName":"abc","gender":"male"}
    4. '

    请求限制:

    • body 大小不能超过 2m

    增加一个名为 “apiTimestamp” 的时间戳参数,和其他参数一起进行字符序升序排列之后,进行签名生成sign

    时间戳取值

    按照 Unix 时间戳标准:从 1970 年 1 月 1 日(UTC/GMT 的午夜 )开始所经过的秒数

    不同语言的获取方式:

    时间的校验

    在添加了 apiTimestamp 时间戳参数后,网关会判断是否和服务端时间接近,允许正负误差在 5 分钟内。如果超过这个时间范围,则会鉴权失败。

    example

    以 URL 参数的签名为例

    1. /api?appKey=foobar&name=dadu&abc=123

    加上参数 apiTimestamp=1581565619,进行字符序升序排列,并加上 App Secret( 假设为 my.secret)

    1. abc=123&apiTimestamp=1581565619&appKey=foobar&name=dadumy.secret

    再计算这段字符串的 SHA512,得到签名值为:

    1. 61cabbc719e5edff3021ab5047bd3c5981e6348066d0416254dd529241a7135d57498dac56d2400139bc1040c5759d1c0798f1673913c537d10769c149879edd

    所以最后的请求为:

    1. /api?appKey=foobar&name=dadu&abc=123&apiTimestamp=1581565619&sign=61cabbc719e5edff3021ab5047bd3c5981e6348066d0416254dd529241a7135d57498dac56d2400139bc1040c5759d1c0798f1673913c537d10769c149879edd
    1. POST --header 'Content-Type: application/json'
    2. -d '
    3. {
    4. "data": "{\"userName\":\"abc\",\"gender\":\"male\"}",
    5. "appKey": "foobar",
    6. "apiTimestamp": 1581565619,

    签名实现

    测试程序

    1. import java.util.HashMap;
    2. import java.util.Map;
    3. import static org.junit.Assert.assertEquals;
    4. /**
    5. * Main.java (JAVA 8+)
    6. */
    7. public class Main {
    8. public static void main(String[] args) {
    9. // Sign Request URL Parameter
    10. // GET https://domain.com?param1=123&param2=Abc&appKey=foobar&pampasCall=query.coupon
    11. Map<String, String> params = new HashMap<>(2);
    12. params.put("param1", "123");
    13. params.put("param2", "Abc");
    14. params.put("appKey", "foobar");
    15. params.put("pampasCall", "query.coupon");
    16. params = SignAuthHelper.sign(params, "my.secret");
    17. String expect = "d6fee3145be668425f70878084f9d"
    18. + "39fce3f7c5fca283ffc4c5d5a5568077334e9a505"
    19. + "26e7e806758a66b7647ae9951f9324a0f921e28417e07d69beed79f7ef";
    20. assertEquals(expect, params.get("sign"));
    21. System.out.println("Verify Success");
    22. // Sign Request Body
    23. // POST --header 'Content-Type: application/json'
    24. // --header 'Accept: application/json'
    25. // -d '{"userName":"abc","gender":"male"}'
    26. // 'https://domain.com'
    27. params = new HashMap<>(4);
    28. // request body use data as param name
    29. params.put("data", "{\"userName\":\"abc\",\"gender\":\"male\"}");
    30. params.put("appKey", "foobar");
    31. expect = "ec23eeda5f88abe26311ed020439172eea409e34"
    32. + "75875c87e9abfa8a6856138e767608e8497435f573c"
    33. + "cb417a90448c78abdca4a0de12c4da4583aa3add7bf52";
    34. params = SignAuthHelper.sign(params, "test-secret");
    35. assertEquals(expect, params.get("sign"));
    36. System.out.println("Verify Request Body Success");
    37. }