签名算法

修订记录

2018-09-19:合成开放平台的说明到本地文档。

签名生成总体说明

  1. 本文档仅适用于QQ轻游戏后台openapi接口的签名生成,由于是通用说明,本文中仅以/openapi/apollo_verify_openid_openkey的签名生成作为示例。
  2. 签名值sig是将请求源串以及密钥根据一定签名方法生成的签名值,用来提高传输过程参数的防篡改性。
  3. 签名值的生成共有3个步骤:构造源串,构造密钥,生成签名值。详见下面的描述。

Step 1. 构造源串

源串是由3部分内容用“&”拼接起来的: HTTP请求方式 & urlencode(uri) & urlencode(a=x&b=y&…)

源串构造步骤如下:第1步:将请求的URI路径进行URL编码(URI不含host,URI示例:/openapi/apollo_verify_openid_openkey)。请开发者关注:URL编码注意事项,否则容易导致后面签名不能通过验证。

第2步:将除“sig”外的所有参数按key进行字典升序排列。 注:除文档中特别标注了某参数不参与签名,否则除sig外的所有参数都要参与签名。

第3步:将第2步中排序后的参数(key=value)用&拼接起来,并进行URL编码。URLENCODE时,要求对字符串中除了“-”、“_”、“.”之外的所有非字母数字字符都替换成百分号(%)后跟两位十六进制数。十六进制数中字母必须为大写。否则会导致校验不过。

第4步:将HTTP请求方式(目前只支持POST)以及第1步和第3步中的字符串用&拼接起来。

源串构造示例如下(由于是通用说明,这里以/openapi/apollo_verify_openid_openkey作为示例,且示例中的请求串不可直接复制访问)

  1. 1. 原始请求信息:
  2. appkey228bf094169a40a3
  3. HTTP请求方式:POST
  4. 请求的URI路径(不含HOST):/openapi/apollo_verify_openid_openkey
  5. 请求参数:appid=1&gameid=2017&openid=222&openkey=1111&rnd=1512981097&sig=xxxxxxxx&ts=1111
  6. 2. 下面开始构造源串:
  7. 1步:将请求的URI路径进行URL编码,得到: %2Fopenapi%2Fapollo_verify_openid_openkey
  8. 2步:将除“sig”外的所有参数按key进行字典升序排列,排列结果为:appidgameidopenidopenkeyrndts
  9. 3步:将第2步中排序后的参数(key=value)用&拼接起来:
  10. appid=1&gameid=2017&openid=222&openkey=1111&rnd=1512981097&ts=1111
  11. 然后进行URL编码( 编码时请关注URL编码注意事项,否则容易导致后面签名不能通过验证),编码结果为:
  12. appid%3D1%26gameid%3D2017%26openid%3D222%26openkey%3D1111%26rnd%3D1512981097%26ts%3D1111
  13. 4步:将HTTP请求方式,第1步以及第3步中的到的字符串用&拼接起来,得到源串:
  14. POST&%2Fopenapi%2Fapollo_verify_openid_openkey&appid%3D1%26gameid%3D2017%26openid%3D222%26openkey%3D1111%26rnd%3D1512981097%26ts%3D1111

Step 2. 构造密钥

得到密钥的方式:在应用的appkey末尾加上一个字节的“&”,即appkey&,例如:

  1. 228bf094169a40a3&

Step 3. 生成签名值

  • 使用HMAC-SHA1加密算法,使用Step2中得到的密钥对Step1中得到的源串加密。 (注:一般程序语言中会内置HMAC-SHA1加密算法的函数,例如PHP5.1.2之后的版本可直接调用hash_hmac函数,注意输出格式要选择二进制输出)

  • 然后将加密后的字符串经过Base64编码。 (注:一般程序语言中会内置Base64编码函数,例如PHP中可直接调用 base64_encode() 函数。)

  • 得到的签名值结果如下:

  1. UUkRyyx0NVfIinwB8P/saj00df8=

C++签名样例

依赖的库

  • openssl

    使用GetSignByHttpParamsSha1接口获取签名,注意strSecKey传入前在尾部加"&"

  • UrlEncode

    1. std::string URLEncode(const std::string & sIn)
    2. {
    3. std::string sOut;
    4. for (size_t ix = 0; ix < sIn.size(); ix++)
    5. {
    6. unsigned char buf[4];
    7. memset(buf, 0, 4);
    8. if (isalnum((unsigned char)sIn[ix]))
    9. {
    10. buf[0] = sIn[ix];
    11. }
    12. else if (sIn[ix] == '.' || sIn[ix] == '-' || sIn[ix] == '_')
    13. {
    14. buf[0] = sIn[ix];
    15. }
    16. else if (isspace((unsigned char)sIn[ix]))
    17. {
    18. buf[0] = '+';
    19. }
    20. else
    21. {
    22. buf[0] = '%';
    23. buf[1] = toHex((unsigned char)sIn[ix] >> 4);
    24. buf[2] = toHex((unsigned char)sIn[ix] % 16);
    25. }
    26. sOut += (char *)buf;
    27. }
    28. return sOut;
    29. }
  • Base64Encode

    1. int Base64Encode(const unsigned char * buffer, unsigned int length, char ** b64text, unsigned int & outlen)
    2. {
    3. int iRet = 0;
    4. //Encodes a binary safe base 64 string
    5. BIO *bio, *b64;
    6. BUF_MEM *bufferPtr;
    7. b64 = BIO_new(BIO_f_base64());
    8. bio = BIO_new(BIO_s_mem());
    9. bio = BIO_push(b64, bio);
    10. BIO_set_flags(bio, BIO_FLAGS_BASE64_NO_NL);
    11. iRet = BIO_write(bio, buffer, length);
    12. iRet = BIO_flush(bio);
    13. BIO_get_mem_ptr(bio, &bufferPtr);
    14. iRet = BIO_set_close(bio, BIO_NOCLOSE);
    15. *b64text = new char[((bufferPtr->length + 1) * sizeof(char))];
    16. memcpy(*b64text, bufferPtr->data, bufferPtr->length);
    17. (*b64text)[bufferPtr->length] = '\0';
    18. outlen = bufferPtr->length;
    19. BIO_free_all(bio);
    20. return (0);
    21. }
  • GetSignByHttpParamsSha1

    1. int GetSignByHttpParamsSha1(const string & strMethod, const string & strCgi,
    2. const map<string, string> & mapParam, const string & strSecKey,
    3. string & strSign)
    4. {
    5. stringstream ssParam;
    6. // 将请求参数组装成key1=value1&key2=value2的形式
    7. for (map<string , string>::const_iterator iter = mapParam.begin(); iter != mapParam.end(); ++iter)
    8. {
    9. ssParam << iter->first << "=" << iter->second << "&";
    10. }
    11. // 组装方法名和域名
    12. // 这里的strMethod是"POST"
    13. // strCgi是api名称,比如"/openapi/apollo_game_item_proxy"或"/openapi/apollo_verify_openid_openkey"
    14. stringstream ss;
    15. ss << strMethod << "&" << URLEncode(strCgi)
    16. << "&" << URLEncode(ssParam.str().substr(0, ssParam.str().size() - 1));
    17. string strBuff = ss.str();
    18. // 计算HMAC_HASH,采用SHA1
    19. HMAC_CTX ctx;
    20. HMAC_CTX_init(&ctx);
    21. // 初始化
    22. const EVP_MD * engine = EVP_sha1();
    23. HMAC_Init_ex(&ctx, strSecKey.c_str(), strSecKey.size(), engine, NULL);
    24. // 扣除最后一个&字符
    25. HMAC_Update(&ctx, (const unsigned char *)strBuff.data(), strBuff.size());
    26. // 计算结果
    27. unsigned int uiLen = MAX_SIGN_RESULT_LEN;
    28. unsigned char * ucResult = new unsigned char[uiLen];
    29. memset(ucResult, 0x00, uiLen);
    30. HMAC_Final(&ctx, ucResult, &uiLen);
    31. unsigned int uiBaseLen = 0;
    32. char * cstrResult = NULL;
    33. Base64Encode(ucResult, uiLen, &cstrResult, uiBaseLen);
    34. strSign.clear();
    35. strSign.assign(cstrResult, uiBaseLen);
    36. HMAC_CTX_cleanup(&ctx);
    37. delete [] cstrResult;
    38. delete [] ucResult;
    39. return 0;
    40. }

php签名样例

php签名类,引用自腾讯开放平台的openapi接口,使用makeSig接口获取签名,注意secret传入前在尾部加"&"

  1. <?php
  2. /**
  3. * 生成签名类
  4. *
  5. * @version 3.0.3
  6. * @author open.qq.com
  7. * @copyright © 2012, Tencent Corporation. All rights reserved.
  8. * @ History:
  9. * 3.0.3 | nemozhang | 2012-08-28 16:40:20 | support cpay callback sig verifictaion.
  10. * 3.0.2 | sparkeli | 2012-03-06 17:58:20 | add statistic fuction which can report API's access time and number to background server
  11. * 3.0.1 | nemozhang | 2012-02-14 17:58:20 | resolve a bug: at line 108, change 'post' to $method
  12. * 3.0.0 | nemozhang | 2011-12-12 11:11:11 | initialization
  13. */
  14. /**
  15. * 生成签名类
  16. */
  17. class SnsSigCheck
  18. {
  19. /**
  20. * 生成签名
  21. *
  22. * @param string $method 请求方法 "POST"
  23. * @param string $url_path
  24. * @param array $params 表单参数
  25. * @param string $secret 密钥
  26. */
  27. static public function makeSig($method, $url_path, $params, $secret)
  28. {
  29. $mk = self::makeSource($method, $url_path, $params);
  30. $my_sign = hash_hmac("sha1", $mk, strtr($secret, '-_', '+/'), true);
  31. $my_sign = base64_encode($my_sign);
  32. return $my_sign;
  33. }
  34. static private function makeSource($method, $url_path, $params)
  35. {
  36. $strs = strtoupper($method) . '&' . rawurlencode($url_path) . '&';
  37. ksort($params);
  38. $query_string = array();
  39. foreach ($params as $key => $val )
  40. {
  41. array_push($query_string, $key . '=' . $val);
  42. }
  43. $query_string = join('&', $query_string);
  44. return $strs . str_replace('~', '%7E', rawurlencode($query_string));
  45. }
  46. /**
  47. * 验证URL的签名 (注意和普通的OpenAPI签名算法不一样,详见@refer的说明)
  48. *
  49. * @param string $method 请求方法 "get" or "post"
  50. * @param string $url_path
  51. * @param array $params 腾讯调用发货回调URL携带的请求参数
  52. * @param string $secret 密钥
  53. * @param string $sig 腾讯调用发货回调URL时传递的签名
  54. *
  55. * @refer
  56. * http://wiki.open.qq.com/wiki/%E5%9B%9E%E8%B0%83%E5%8F%91%E8%B4%A7URL%E7%9A%84%E5%8D%8F%E8%AE%AE%E8%AF%B4%E6%98%8E_V3
  57. */
  58. static public function verifySig($method, $url_path, $params, $secret, $sig)
  59. {
  60. unset($params['sig']);
  61. // 先使用专用的编码规则对value编码
  62. foreach ($params as $k => $v)
  63. {
  64. $params[$k] = self::encodeValue($v);
  65. }
  66. // 再计算签名
  67. $sig_new = self::makeSig($method, $url_path, $params, $secret);
  68. return $sig_new == $sig;
  69. }
  70. /**
  71. * URL专用的编码算法
  72. * 编码规则为:除了 0~9 a~z A~Z !*()之外其他字符按其ASCII码的十六进制加%进行表示,例如"-"编码为"%2D"
  73. * @refer
  74. * http://wiki.open.qq.com/wiki/%E5%9B%9E%E8%B0%83%E5%8F%91%E8%B4%A7URL%E7%9A%84%E5%8D%8F%E8%AE%AE%E8%AF%B4%E6%98%8E_V3
  75. */
  76. static private function encodeValue($value)
  77. {
  78. $rst = '';
  79. $len = strlen($value);
  80. for ($i=0; $i<$len; $i++)
  81. {
  82. $c = $value[$i];
  83. if (preg_match ("/[a-zA-Z0-9!\(\)*]{1,1}/", $c))
  84. {
  85. $rst .= $c;
  86. }
  87. else
  88. {
  89. $rst .= ("%" . sprintf("%02X", ord($c)));
  90. }
  91. }
  92. return $rst;
  93. }
  94. }
  95. // end of script

原文: https://hudong.qq.com/docs/engine/server/thrid/api_doc/sig.html