API签名认证机制

本文档主要针对RESTful API做访问认证。

API认证简介

对于认证方式,需要通过使用AccessKeyId/SecretAccessKey加密的方法来验证某个请求的发送者身份。

AccessKeyId(AK)用于标示用户,
SecretAccessKey(SK)是用户用于加密认证字符串和Server端用来验证认证字符串的密钥

其中SK必须保密,只有用户和Dayu知道。

当Server端接收到用户的请求后,系统将使用相同的SK和同样的认证机制生成认证字符串,并与用户请求中包含的认证字符串进行比对。如果认证字符串相同,系统认为用户拥有指定的操作权限,并执行相关操作;如果认证字符串不同,系统将忽略该操作并返回错误码。

名词解释

本章节涉及的核心名词解释如下:

  • 认证字符串:非匿名请求中必须携带的认证信息。包含生成待签名串CanonicalRequest所必须的信息以及签名摘要signature。

  • authStringPrefix:认证字符串的前缀部分。

  • canonicalRequest:待签名串。携带经过规范化处理后的请求信息。

  • signingKey:签名Key。server不直接使用SK对待签名串生成摘要。相反的,server端首先使用SK和认证字符串前缀生成signingKey,然后用signingKey对待签名串生成摘要。

  • signature:签名摘要。Server端使用signingKey对canonicalRequest使用HMAC算法计算签名。

API认证优势

API认证将为用户带来以下优势:

  • 对于请求者的身份进行验证。认证字符串使用指定用户的AK/SK对HTTP请求进行签名,可以起到验证用户身份的作用。

  • 对被传输内容进行保护,防止非法篡改。用户基于HTTP请求的指定内容生成认证字符串,如果在传输过程中遭到非法篡改,将导致系统生成的认证字符串与用户生成的认证字符串不匹配,最终导致认证失败。

  • 防止重放攻击。认证字符串都具有指定的生效时间,一个请求必须要在指定时间内到达百度云,否则系统将拒绝该请求。

  • 为了保护用户的SK信息,百度云不直接使用SK信息,而是使用SK生成SigningKey,同时在SigningKey中包含有效时间范围。这样可以减少用户因SigningKey丢失带来的安全隐患。

API认证方式

通过认证算法对HTTP请求的指定内容进行计算并输出认证字符串用于认证。开发者需要首先将HTTP请求的指定内容连接成字符串,结合服务分配的SK,通过HMAC算法计算密文摘要,这个过程也就是对HTTP请求进行签名过程。基于认证字符串的HTTP请求签名机制来验证用户身份。对于每个HTTP请求,都需要携带一个认证字符串然后通过以下两种方式将这个认证字符串包含在请求中:

  • 在HTTP Header中包含认证字符串
    通常使用的方法是在HTTP请求的Authorization头域中包含认证字符串,除了匿名请求之外,所有与Server的交互都应该包含该字段。即:在HTTP Header中加入Authorization: <认证字符串>。

  • 在URL中包含认证字符串
    用户也可以认证字符串放在HTTP请求Query String的authorization参数中。常用于生成URL给第三方使用的场景,例如要临时把某个数据开放给他人下载。即在URL的Query String中加入authorization = <认证字符串>。

认证字符串生成简介

认证字符串的生成机制如下图所示:

API使用基于认证字符串的HTTP请求签名机制来验证用户身份,对于非匿名方式的HTTP请求,都应携带一个认证字符串。当Dayu收到用户的HTTP请求后,系统将按照下图所示流程进行处理。

  • (1)判断用户的HTTP请求是否为匿名请求,即用户的请求中是否包含Authorization认证字符串。如果不包含认证字符串,则需要根据不同业务情况,参考其他相关流程进行处理;如果包含认证字符串,则执行下一步操作。

  • (2)判断用户的请求是否超时,即服务器收到请求的时间需要符合以下要求:

1
{timestamp} - 5分钟 < 服务器接收到请求时间 < {timestamp} + {expirationPeriodInSeconds} + 5分钟

timestamp代表签名生效UTC时间,expirationPeriodInSeconds代表签名有效期限。

为了防止用户时钟与服务器时钟不同步而导致的认证失败,此处引入5分钟的宽松系数。如果服务器收到请求的时间不符合以上时间要求,则认为请求超时,拒绝该请求;如果符合上述要求,则执行下一步操作。

  • (3)基于HTTP请求信息,使用相同的算法,生成Signature字符串。

  • (4)使用服务器生成的Signature字符串与用户提供的字符串进行比对,如果内容不一致,则认为认证失败,拒绝该请求;如果内容一致,则表示认证成功,系统将按照用户的请求内容进行操作。

生成认证字符串

UriEncode函数

RFC3986规定,”URI非保留字符”包括以下字符:字母(A-Z,a-z)、数字(0-9)、连字号(-)、点号(.)、下划线(_)、波浪线(~),算法实现如下:

1
2
3
1. 将字符串转换成UTF-8编码的字节流
2. 保留所有“URI非保留字符”原样不变
3. 对其余字节做一次RFC 3986中规定的百分号编码(Percent-encoding),即一个“%”后面跟着两个表示该字节值的十六进制字母,字母一律采用大写形式。

UriEncode()函数Java版本参考代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static String uriEncode(CharSequence input, boolean encodeSlash) {
StringBuilder result = new StringBuilder();
for (int i = 0; i < input.length(); i++) {
char ch = input.charAt(i);
if ((ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9')
|| ch == '_' || ch == '-' || ch == '~' || ch == '.') {
result.append(ch);
} else if (ch == '/') {
result.append(encodeSlash ? "%2F" : ch);
} else {
result.append(toHexUTF8(ch)); // 转换成:%<两个表示该字节值的十六进制字母>
}
}
return result.toString();
}

2 生成CanonicalRequest

CanonicalRequest = HTTP Method + "\n" + CanonicalURI + "\n" + CanonicalQueryString + "\n" + CanonicalHeaders

其中:

  • HTTP Method:指HTTP协议中定义的GET、PUT、POST等请求,必须使用全大写的形式。例如:
    GET、POST、PUT、DELETE、HEAD

  • CanonicalURI:是对URL中的绝对路径进行编码后的结果。要求绝对路径必须以“/”开头,不以“/”开头的需要补充上,空路径为“/”,即CanonicalURI = uriEncode(Path, false)。

  • CanonicalQueryString:对于URL中的Query String(Query String即URL中“?”后面的“key1=valve1&key2=valve2 ”字符串)进行编码后的结果。
    编码方法为:

    1. 将Query String根据&拆开成若干项,每一项是key=value或者只有key的形式。
    2. 对拆开后的每一项进行如下处理:
        a. 对于key是authorization,直接忽略。
        b. 对于只有key的项,转换为uriEncode(key) + "="的形式。
        c. 对于key=value的项,转换为 uriEncode(key) + "=" + uriEncode(value) 的形式。这里value可以是空字符串。
    3. 将上面转换后的所有字符串按照字典顺序排序。
    4. 将排序后的字符串按顺序用 & 符号链接起来。
  • CanonicalHeaders:对HTTP请求中的Header部分进行选择性编码的结果。
    您可以自行决定哪些Header 需要编码。大多数情况下,我们推荐您对以下Header进行编码:

    • Host

    • Content-Type

      如果您不想对header进行编码,那么认证字符串中的 {signedHeaders} 可以直接留空,无需填写。您也可以自行选择自己想要编码的Header,只需要在认证字符串中填写 {signedHeaders} 。填写方法为,把所有在这一阶段进行了编码的Header名字转换成全小写之后按照字典序排列,然后用分号(;)连接。

      对于每个要编码的Header进行如下处理:

      1. 将Header的名字变成全小写。
      2. 将Header的值去掉开头和结尾的空白字符。
      3. 经过上一步之后值为空字符串的Header忽略,其余的转换为 uriEncode(name) + “:” + uriEncode(value) 的形式。
      4. 把上面转换后的所有字符串按照字典序进行排序。
      5. 将排序后的字符串按顺序用\n符号连接起来得到最终的CanonicalQueryHeaders。
        注意:很多发送HTTP请求的第三方库,会添加或者删除你指定的header(例如:某些库会删除content-length:0这个header),如果签名错误,请检查您真实发出的http请求的header,看看是否与签名时的header一样。

3 生成SigningKey

SigningKey = HMAC-SHA256-HEX(sk, authStringPrefix)

其中:

  • sk为用户的SecretAccessKey。
  • authStringPrefix代表认证字符串的前缀部分,即:{accessKeyId}/{timestamp}/{expirationPeriodInSeconds}

4 生成Signature

Signature = HMAC-SHA256-HEX(SigningKey, CanonicalRequest)

5 生成认证字符串

认证字符串 = {accessKeyId}/{timestamp}/{expirationPeriodInSeconds}/{signedHeaders}/{signature}
  • timestamp:签名生效UTC时间戳,格式为13位,例如:1543495783836,默认值为当前时间。

  • expirationPeriodInSeconds:签名有效期限,从timestamp所指定的时间开始计算,时间为秒,默认值为1800秒(30)分钟。

  • signedHeaders:签名算法中涉及到的HTTP头域列表。HTTP头域名字一律要求小写且头域名字之间用分号(;)分隔,如host;content-type。当signedHeaders为空时表示未对header签名。

附录

签名函数Java样例

HMAC-SHA256-HEX()实现样例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 调用HMAC SHA256算法,根据开发者提供的密钥(signingKey)和密文(stringToSign)输出密文摘要,并把结果转换为小写形式的十六进制字符串。
*
* @param signingKey 签名密钥
* @param stringToSign 待签名文本
*/
public static String hmacSha256Hex(String signingKey, String stringToSign)
throws InvalidKeyException, NoSuchAlgorithmException {
try {
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(signingKey.getBytes(UTF8), mac.getAlgorithm()));
return new String(Hex.encodeHex(mac.doFinal(stringToSign.getBytes(UTF8))));
} catch (Exception e) {
log.error("Fail to generate the signature, {}", e.getMessage());
throw e;
}
}