为了降低服务之间接口集成的成本,避免由于接口签名验证方式不同导致的系统互通障碍,因此约定了服务接口签名规范。此接口规范适用于服务与服务之间的接口调用使用。
HTTP接口签名规范
适用于1.0、1.1、2.0版本的HTTP协议。
签名请求参数
发起一次HTTP请求时,需要随请求提供以下请求参数:
名称 | 类型 | 说明 | 备注 |
---|---|---|---|
accessKeyId | string | 标识请求发起方身份的AccessKeyId | 必须提供,不超过128字节 |
nonce | string | 请求随机值,用于混淆待签字符串或者标识请求的唯一性 | 必须提供,不少于8字节,不超过128字节 |
timestamp | int64 | 请求发起的Unix时间戳,单位为秒 | 必须提供,服务端需要检查此时间戳和本地时间的差(防止请求一直生效),不超过±120s |
expires | int64 | 请求过期间隔时间,单位为秒 | 可选,服务端需要检查 timestamp 参数和本地时间的差是否大于 expires(timestamp的±120s校验无效),适用于预授权、预签名等场景 |
signedHeaders | string | 参与签名的HTTP头名称,多个以“,”分隔 | 可选,不超过255字节 |
principal | string | 代理的身份,用于代理他人身份访问 | 可选 |
signature | string | 请求签名 | 必须提供,签名计算方法见下节 |
在HTTP请求中可以通过以下任一方式携带这些请求参数,推荐使用Authorization头携带请求参数,优先使用Authorization头作为请求参数。
通过HTTP Header携带请求参数
使用Authorization头在HTTP Header中携带请求参数。Authorization头是由一个字符串组成的,请求参数需要按以下规则进行构造:
字符串由以下部分组成:
- 签名类型和签名参数之间使用一个空格分隔
- 签名参数使用key=value的方式进行连接,多个参数之间使用“; ”分隔(注意:分号后有空格)
通过HTTP QueryString携带请求参数
使用QueryString携带请求参数,请求参数和QueryString参数的对应规则如下:
请求参数名称 | QueryString参数名称 | 备注 |
---|---|---|
- | X-Auth-Algorithm | 签名类型,例如“LJ-HMAC-SHA256” |
accessKeyId | X-Auth-AccessKeyId | |
nonce | X-Auth-Nonce | |
timestamp | X-Auth-Timestamp | |
expires | X-Auth-Expires | |
signedHeaders | X-Auth-SignedHeaders | |
principal | X-Auth-Principal | |
signature | X-Auth-Signature |
一个使用QueryString携带请求参数的请求行示例如下:
GET /hello?X-Auth-Algorithm=LJ-HMAC-SHA256&X-Auth-AccessKeyId=your-ak&X-Auth-Nonce=some-nonce&X-Auth-Timestamp=1519800000&X-Auth-SignedHeaders=X-Request-Id%2CX-Info&X-Auth-Signature=some-signature HTTP/1.1 |
待签字符串
生成请求签名时,需要将请求参数构造成待签字符串。待签字符串由以下几部分组成:
- 签名请求参数:accessKeyId、nonce、timestamp、signedHeaders、principal
- HTTP Method
- HTTP Path
- HTTP Query请求参数(需要排除掉X-Auth-Signature参数)
- HTTP Headers
- Host头
- Content-MD5头(参考RFC2616.14.15)若存在需参与签名,用于签名请求体,防止被篡改
- 在signedHeaders参数中指定的HTTP头
待签字符串的各组成部分的生成规则各有要求,具体如下表所列:
组成部分 | 生成规则 | 示例 | |
---|---|---|---|
签名请求参数 | 以参数名为key,参数值为value | ||
HTTP Method | 以method为key,大写的HTTP Method为value | method=GET | |
HTTP Path | 以path为key,不包括查询字符串部分的Path为value | path=/hello | |
HTTP Query | 以query为key,将查询字符串按“&”切分为数组,排除这个字符串数组中以X-Auth-Signature为参数名的项和为空的项,按字母正序排序后重新以“&”连接的值为value,无需进行URL解码 | query=a=b&foo=bar | |
HTTP Headers | 以Header头的小写名称为key(下划线必须替换为中横线),Header值为value
|
将各个部分的key-value对以“=”连接,并将所有的key-value对存入一个字符串数组,再把数组以字符序正序排序,最后“&”连接每个key-value对的排序字符串,得到的结果即为待签字符串。生成待签字符串时,若相应部分的参数不存在或者值为空时,此key-value对不出现在待签字符串数组中。
对于下面的请求
GET /hello?foo=bar HTTP/1.0 Host: localhost X-Request-Id: some-request-id Authorization: LJ-HMAC-SHA256 accessKeyId=your-ak; nonce=some-nonce; timestamp=1519800000; signedHeaders=X-Request-Id; signature=some-signature |
拼装的待签字符串数组为:
[ "accessKeyId=your-ak", "nonce=some-nonce", "timestamp=1519800000", "signedHeaders=X-Request-Id", "method=GET", "path=/hello", "query=foo=bar", "host=localhost", "x-request-id=some-request-id" ] |
对其按字符正序排序后的结果为:
[ "accessKeyId=your-ak", "host=localhost", "method=GET", "nonce=some-nonce", "path=/hello", "query=foo=bar", "signedHeaders=X-Request-Id", "timestamp=1519800000", "x-request-id=some-request-id" ] |
生成的待签字符串为:
accessKeyId=your-ak&host=localhost&method=GET&nonce=some-nonce&path=/hello&query=foo=bar&signedHeaders=X-Request-Id×tamp=1519800000&x-request-id=some-request-id |
签名算法
签名算法是一种hash函数,将待签字符串和accessKeySecret一起通过hash函数进行计算,结果以Base64编码字符串来表示。必须实现的签名算法是HMAC-SHA256。
以下是几种常见语言的HMAC-SHA256参考实现:
Node版本的签名计算示例:
const crypto = require('crypto'); function sign(stringToSign, accessKeySecret) { const hmac = crypto.createHmac('sha256', accessKeySecret); hmac.update(stringToSign); return hmac.digest('base64'); } |
PHP版本的签名计算示例:
function sign($string, $secret) { if (!function_exists('hash_hmac')) { throw new \BadMethodCallException('This PHP environment does not support hash library.'); } $hash = hash_hmac('sha256', $string, $secret, true); if ($hash === false) { throw new \BadMethodCallException('This PHP environment does not support hmac-sha256.'); } return base64_encode($hash); } |
Java版本的签名计算示例,依赖commons-codec、commons-lang:
import org.apache.commons.codec.binary.Base64; import org.apache.commons.codec.digest.HmacAlgorithms; import org.apache.commons.codec.digest.HmacUtils; import org.apache.commons.lang.StringUtils; public static String sign(String stringToSign, String accessKeySecret) { byte[] hmac = new HmacUtils(HmacAlgorithms.HMAC_SHA_256, accessKeySecret).hmac(stringToSign); return Base64.encodeBase64String(hmac); } |
签名验证结果
服务器端收到请求后应当验证签名的正确性。若当前请求不包含任何可用的身份签名,服务器端应当应答HTTP 401 Unauthorized;若签名不正确,服务器端应当应答HTTP 403 Forbidden。签名验证不通过时,需要在应答Header中通过X-Auth-Result-Code、X-Auth-Result-Message来携带详细的错误信息。应答体中也可携详情信息,但不做具体要求,可根据HTTP的内容协商确定应答体的格式类型。
签名验证不通过时参考的错误码如下:
错误码 | 错误说明 |
---|---|
INVALID_PARAMETERS | 参数不存在或者提供了错误的值 |
INVALID_PRINCIPAL | 请求身份不正确 |
INVALID_SIGNATURE | 签名不正确 |
EXPIRED | 请求时间戳已过期 |
FORBIDDEN | 没有访问此资源的权限 |
SDK
- api-signature-java(标准SDK,支持SpringMVC、ApacheHttpClient、OkHttp等,推荐使用)
- api-signature-node (标准SDK,支持express、koa、request,推荐使用)
- api-signature-php (标准SDK,支持GuzzleHTTP、Yii2、Symfony、Yaf,推荐使用)
- api-signature-go (标准SDK,推荐使用)
- api-signature-python (标准SDK,支持作为客户端调用,推荐使用)
调试
使用Postman发送签名请求
在Postman中,可以使用Pre-request Script来对发送的请求进行加签名,使用下面的脚本,并将签名密钥替换为你自己的密钥:
const accessKeyId = '<your-access-key-id>'; const accessKeySecret = '<your-access-key-secret>'; const nonce = this.request.id; const timestamp = parseInt(new Date().getTime() / 1000); const stringToSignArray = [ `accessKeyId=${accessKeyId}`, `nonce=${nonce}`, `timestamp=${timestamp}`, `method=${this.request.method}`, `path=/${pm.request.url.path.join('/')}` ]; if (pm.request.headers.has('Host')) { stringToSignArray.push(`host=${pm.request.headers.get("Host")}`); } else if (pm.environment.has('HOST')) { stringToSignArray.push(`host=${pm.environment.get("HOST")}`); } else { stringToSignArray.push(`host=${pm.request.url.host}`); } const query = `${pm.request.url.query}`; var queryArray = query.split("&") var newQueryArray = [] for (var i=0,len=queryArray.length; i<len; i++) { if(queryArray[i]){ newQueryArray.push(encodeURI(queryArray[i])) } } var newQueryStr = newQueryArray.sort().join("&") console.log(newQueryStr) if (newQueryStr) { stringToSignArray.push(`query=${newQueryStr}`); } const stringToSign = stringToSignArray.sort().join('&'); const signature = CryptoJS.HmacSHA256(stringToSign, accessKeySecret).toString(CryptoJS.enc.Base64); console.log(stringToSign); const authorization = 'LJ-HMAC-SHA256 ' + [ `accessKeyId=${accessKeyId}`, `nonce=${nonce}`, `timestamp=${timestamp}`, `signature=${signature}` ].join('; '); console.log(authorization); pm.globals.set("Authorization", authorization); |
设置脚本后,还需要在Headers中指定Authorization头的值为{{Authorization}},以引用签名脚本计算出来的签名值。