跳转至

HMAC(AK/SK)验证使用详解

固定请求头说明

请求头 是否必填 说明
X-GS-API-AK 填写AK密钥
X-GS-API-DATE 填写发送请求的时间(GMT 格式)
X-GS-API-ALGORITHM 可选算法"hmac-sha1", "hmac-sha256", "hmac-sha512",建议"hmac-sha256"
X-GS-API-SIGNATURE 需要组合HTTP Method、URI等生成,详见X-GS-API-SIGNATURE 签名生成公式
X-GS-API-SIGNED-HEADERS 可选 headers 中加入X-GS-API-SIGNATURE计算的请求头,注意顺序不能错!
X-GS-API-BODY-DIGEST 可选 当网关路由开启 body 做签名校验时必传,详见X-GS-API-BODY-DIGEST 签名生成公式

X-GS-API-SIGNATURE 签名生成公式

在使用HMAC(AK/SK)验证鉴权方式时,会涉及到签名。签名的计算公式为 X-GS-API-SIGNATURE = HMAC-SHAx-HEX(SK密钥, signing_string)。

为了生成签名需要两个参数:SK密钥 和 signing_string。其中 SK密钥 由界面自动生成,也可手动修改,signing_string 的计算公式为 signing_string = HTTP Method + \n + HTTP URI + \n + canonical_query_string + \n + access_key + \n + Date + \n + signed_headers_string。以下是对计算公式中各个字段的释义:

  • HTTP Method:指 HTTP 协议中定义的 GET、PUT、POST 等请求方法,必须使用全大写的形式。
  • HTTP URI:HTTP URI。必须以 / 开头,/ 表示空路径。
  • Date:请求头中的日期(GMT 格式)。
  • canonical_query_string:对 URL 中的 query(query 即 URL 中 ? 后面的 key1=valve1&key2=valve2 字符串)进行编码后的结果。
  • signed_headers_string:从请求头中获取客户端指定的字段,并按顺序拼接字符串的结果。

! 如果 signing_string 中的任意一项不存在,则需要使用一个空字符串代替。 由于签名计算时,会区分大小写字母,在使用时,请规范其参数命名。

生成 canonical_query_string 的算法描述如下:

1、提取 URL 中的 query 项。

2、使用 & 作为分隔符,将 query 拆分成键值对。 - 当该项有 key 时,转换公式为 url_encode(key) + "="。 - 当该项同时有 key 和 value 时,转换公式为 url_encode(key) + "=" + url_encode(value) 。此处 value 可以是空字符串。 - 将每一项转换后,以 key 按照字典顺序(ASCII 码由小到大)排序,并使用 & 符号连接起来,生成相应的 canonical_query_string 。

生成 signed_headers_string 的算法如下:

1.从请求头中获取指定的 headers 加入计算中,指定传输请求头为X-GS-API-SIGNED-HEADERS

2.从请求头中按顺序取出 SIGNED_HEADERS 指定的 headers,并按顺序用 name:value 方式拼接起来,拼接完后就生成了 signed_headers_string。

HeaderKey1 + ":" + HeaderValue1 + "\n"+ HeaderKey2 + ":" + HeaderValue2 + "\n"+ ... HeaderKeyN + ":" + HeaderValueN + "\n"

X-GS-API-BODY-DIGEST 签名生成公式

如果没有请求 body,你可以将 X-GS-API-ALGORITHM 的值设置为空字符串的 HMAC-SHA。

X-GS-API-ALGORITHM: base64(hmac-sha())

签名生成公式过程详解

接下来,我们将以下述请求为例,为你介绍签名生成公式的具体计算过程:

curl -i http://127.0.0.1:9080/index.html?name=james&age=36 \ -H "X-GS-API-SIGNED-HEADERS: User-Agent;x-custom-a" \ -H "x-custom-a: test" \ -H "User-Agent: curl/7.29.0"

1、上文请求默认的 HTTP Method 是 GET,得到 signing_string 为:

"GET"

2、请求的 URI 是 /index.html,根据 HTTP Method + \n + HTTP URI 得到 signing_string 为:

"GET /index.html"

3、URL 中的 query 项是 name=james&age=36,假设 encode_uri_params 为 false,根据 canonical_query_string 的算法,重点是对 key 进行字典排序,得到 age=36&name=james;根据 HTTP Method + \n + HTTP URI + \n + canonical_query_string 得到 signing_string 为:

"GET /index.html age=36&name=james"

4、access_key 是 user-key,根据 HTTP Method + \n + HTTP URI + \n + canonical_query_string + \n + access_key 得到 signing_string 为:

"GET /index.html age=36&name=james user-key"

5、Date 是指 GMT 格式的日期,形如 Tue, 19 Jan 2021 11:33:20 GMT, 根据 HTTP Method + \n + HTTP URI + \n + canonical_query_string + \n + access_key + \n + Date 得到 signing_string 为:

"GET /index.html age=36&name=james user-key Tue, 19 Jan 2021 11:33:20 GMT"

6、signed_headers_string 用来制定参与到签名的 headers,在上面示例中包括 User-Agent: curl/7.29.0 和 x-custom-a: test。 根据 HTTP Method + \n + HTTP URI + \n + canonical_query_string + \n + access_key + \n + Date + \n + signed_headers_string + \n,得到完整的 signing_string 为:

"GET /index.html age=36&name=james user-key Tue, 19 Jan 2021 11:33:20 GMT User-Agent:curl/7.29.0 x-custom-a:test "

通过以下 Python 代码为上述请求生成签名 SIGNATURE:

import base64
import hashlib
import hmac

secret = bytes('my-secret-key', 'utf-8')
message = bytes("""GET
/index.html
age=36&name=james
user-key
Tue, 19 Jan 2021 11:33:20 GMT
User-Agent:curl/7.29.0
x-custom-a:test
""", 'utf-8')

hash = hmac.new(secret, message, hashlib.sha256)

# to lowercase base64
print(base64.b64encode(hash.digest()))

也可以参考文章末尾了解如何使用不同的编程语言生成签名。 SIGNATURE:8XV1GB7Tq23OJcoz6wjqTs4ZLxr9DiLoY4PxzScWGYg= 签名生成后,你可以通过以下示例使用生成的签名发起请求:

curl -i "http://127.0.0.1:9080/index.html?name=james&age=36" \ -H "X-GS-API-SIGNATURE: 8XV1GB7Tq23OJcoz6wjqTs4ZLxr9DiLoY4PxzScWGYg=" \ -H "X-GS-API-ALGORITHM: hmac-sha256" \ -H "X-GS-API-AK": user-key" \ -H "X-GS-API-DATE: Tue, 19 Jan 2021 11:33:20 GMT" \ -H "X-GS-API-SIGNED-HEADERS: User-Agent;x-custom-a" \ -H "x-custom-a: test" \ -H "User-Agent: curl/7.29.0"

HTTP/1.1 200 OK Content-Type: text/html; charset=utf-8 Transfer-Encoding: chunked Connection: keep-alive Date: Tue, 19 Jan 2021 11:33:20 GMT Server: APISIX/2.2 ......

不同语言样例

Java

Java版本Demo》,建议其他语言引入相关java语言模块,使用java版本Demo中GsApiSignature.java生成HTTP请求头。

python

Java版本Demo

import base64
import hashlib
import hmac

secret = bytes('the shared secret key here', 'utf-8')
message = bytes('this is signature string', 'utf-8')


hash = hmac.new(secret, message, hashlib.sha256)

# to lowercase hexits
hash.hexdigest()

# to lowercase base64
base64.b64encode(hash.digest())

GO

package main

import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/base64"
    "encoding/hex"
)

func main() {
    secret := []byte("the shared secret key here")
    message := []byte("this is signature string")

    hash := hmac.New(sha256.New, secret)
    hash.Write(message)

    // to lowercase hexits
    hex.EncodeToString(hash.Sum(nil))

    // to base64
    base64.StdEncoding.EncodeToString(hash.Sum(nil))
}

NodeJs

NodeJs版本Demo

var crypto = require('crypto');

var secret = 'the shared secret key here';
var message = 'this is signature string';

var hash = crypto.createHmac('sha256', secret).update(message);

// to lowercase hexits
hash.digest('hex');

// to base64
hash.digest('base64');

JavaScript ES6

const secret = 'the shared secret key here';
const message = 'this is signature string';

const getUtf8Bytes = str =>
  new Uint8Array(
    [...unescape(encodeURIComponent(str))].map(c => c.charCodeAt(0))
  );

const secretBytes = getUtf8Bytes(secret);
const messageBytes = getUtf8Bytes(message);

const cryptoKey = await crypto.subtle.importKey(
  'raw', secretBytes, { name: 'HMAC', hash: 'SHA-256' },
  true, ['sign']
);
const sig = await crypto.subtle.sign('HMAC', cryptoKey, messageBytes);

// to lowercase hexits
[...new Uint8Array(sig)].map(b => b.toString(16).padStart(2, '0')).join('');

// to base64
btoa(String.fromCharCode(...new Uint8Array(sig)));

Lua

local hmac = require("resty.hmac")
local secret = 'the shared secret key here'
local message = 'this is signature string'
local digest = hmac:new(secret, hmac.ALGOS.SHA256):final(message)

--to lowercase hexits
ngx.say(digest)

--to base64
ngx.say(ngx.encode_base64(digest))