签名方法

最近更新时间:2024-06-04 03:04:13

天翼云视频监控API会对每个访问请求进行身份验证,所以无论使用 HTTP 还是 HTTPS 协议提交请求,都需要在公共请求参数中包含签名信息(Signature)以验证请求者身份。 签名信息由访问秘钥生成,访问秘钥包括access key ID和secret access key。

1. 申请访问秘钥

在第一次使用天翼云视频监控API时,请前往天翼云视频监控官网访问密钥页面申请访问秘钥。访问秘钥包括access key ID和secret access key。

  • access key ID用于标识API调用者的身份;
  • secret access key用于加密签名字符串和服务器端验证签名字符串的密钥;
  • 用户必须严格保管访问秘钥,避免泄露。

申请访问秘钥的具体步骤如下:

  1. 登录天翼云管理中心控制台;
  2. 前往视频监控的控制台页面;
  3. 在访问秘钥页面,单击【新建秘钥】,即可以创建一对access key ID/secret access key。

注意:每个账号最多可以拥有两对access key ID/secret access key。

2. 生成签名串

有了访问秘钥access key ID/secret access key后,就可以生成签名串了。以下是生成签名串的详细过程:
假设用户的access key ID和secret access key分别是:
access key ID: 8FR8VXACHFFQIT33****
secret access key: PwbZMn5wEqXVrjt3L6QSdxYyOvllrfLPzLcR****
注意:这里只是示例,请根据用户实际申请的access key ID/secret access key进行后续操作!
以查看流URL(DescribeStreamURL)请求为例,当用户调用这一接口时,其请求参数可能如下:

参数示例值描述
ActionDescribeStreamURL
Version2020-06-12API 版本号。格式:YYYY-MM-DD。
AccessKeyId8FR8VXACHFFQIT33****天翼云颁发给用户的访问服务所用的密钥 ID。
SignatureRxQApRehQz9PAkY9XnWrvMte5co=签名结果串。
关于签名的计算方法,参见签名方法。
SignatureMethodHMAC-SHA1签名方式,目前支持HMAC-SHA1。
Timestamp1598593304当前系统时间戳,可记录发起 API 请求的时间。例如1529223702,如果与当前时间相差过大(前后十分钟),会引起签名过期错误。
SignatureNonce11886唯一随机数,用于防止网络重放攻击。用户在不同请求间要使用不同的随机数值,即使请求失败下一次也需要更换该值。
SignatureVersion1.0签名算法版本。目前版本是 1.0。
DeviceId744925256942092288设备ID。
OutProtocolrtmp流播放协议。取值: rtmp,hls,flv
Typelive流类型,默认live。
取值:live(直播流)、vod(点播流,例如NVR上的历史流)

2.1 对参数排序

首先对所有请求参数按参数名的字典序( ASCII 码)升序排序。注意:

  1. 只按参数名进行排序,参数值保持对应即可,不参与比大小;
  2. 按 ASCII 码比大小,如 InstanceIds.2 要排在 InstanceIds.12 后面,不是按字母表,也不是按数值。用户可以借助编程语言中的相关排序函数来实现这一功能,如 PHP 中的 ksort 函数、go的sort.Strings()方法、java的TreeMap等。

上述示例参数的排序结果如下:

{
  'AccessKeyId': '8FR8VXACHFFQIT33****',
  'Action' : 'DescribeStreamURL',
  'DeviceId' : '744925256942092288',
  'OutProtocol' : 'rtmp',
  'SignatureMethod': 'HMAC-SHA1',
  'SignatureNonce' : 11886,
  'SignatureVersion' : '1.0',
  'Timestamp': 1598593304,
  'Type' : 'live',
  'Version': '2020-06-12',
}

使用其它程序设计语言开发时,可对上面示例中的参数进行排序,得到的结果一致即可。

2.2 拼接请求字符串

此步骤生成请求字符串。 将把上一步排序好的请求参数格式化成“参数名称=参数值”的形式,如对 Action 参数,其参数名称为 "Action" ,参数值为 "DescribeStreamURL" ,因此格式化后就为Action=DescribeStreamURL。注意:“参数值”为原始值而非 url 编码后的值。
然后将格式化后的各个参数用"&"拼接在一起,最终生成的请求字符串为:

AccessKeyId=8FR8VXACHFFQIT33****&Action=DescribeStreamURL&DeviceId=744925256942092288&OutProtocol=rtmp&SignatureMethod=HMAC-SHA1&SignatureNonce=11886&SignatureVersion=1.0&Timestamp=1598593304&Type=live&Version=2020-06-12

2.3 拼接签名原文字符串

此步骤生成签名原文字符串。 签名原文字符串由以下几个参数构成:

  1. 请求方法: 这里使用 GET 请求,注意方法为全大写。
  2. 请求主机: 查看流URL(DescribeStreamURL)的请求域名为: vssapi.ctyun.cn
  3. 请求路径: 当前版本云API的请求路径固定为 / 。
  4. 请求字符串: 即上一步生成的请求字符串。

签名原文串的拼接规则为: 请求方法 + 请求主机 +请求路径 + ? + 请求字符串。
示例的拼接结果为:

GETvssapi.ctyun.cn/?AccessKeyId=8FR8VXACHFFQIT33****&Action=DescribeStreamURL&DeviceId=744925256942092288&OutProtocol=rtmp&SignatureMethod=HMAC-SHA1&SignatureNonce=11886&SignatureVersion=1.0&Timestamp=1598593304&Type=live&Version=2020-06-12

2.4 生成签名串

此步骤生成签名串。 首先使用HMAC-SHA1算法对上一步中获得的签名原文字符串进行签名,然后将生成的签名串使用 Base64 进行编码(普通Base算法,作为http参数发送时再对特殊字符做编码),即可获得最终的签名串。
具体代码如下,以 PHP 语言为例:

$secretAccessKey = 'PwbZMn5wEqXVrjt3L6QSdxYyOvllrfLPzLcR****';
$srcStr = 'GETvssapi.ctyun.cn/?AccessKeyId=8FR8VXACHFFQIT33****&Action=DescribeStreamURL&DeviceId=744925256942092288&OutProtocol=rtmp&SignatureMethod=HMAC-SHA1&SignatureNonce=11886&SignatureVersion=1.0&Timestamp=1598593304&Type=live&Version=2020-06-12';
$signStr = base64_encode(hash_hmac('sha1', $srcStr, $secretAccessKey, true));
echo $signStr;

最终得到的签名串为:

RxQApRehQz9PAkY9XnWrvMte****

使用其它程序设计语言开发时,可用上面示例中的原文进行签名验证,得到的签名串与例子中的一致即可。

3. 签名串编码

生成的签名串并不能直接作为请求参数,需要对其进行 URL 编码。
如上一步生成的签名串为 5YR2gw7Sdk4YTebkP8lqI0ctRZY= ,最终得到的签名串请求参数(Signature)为:5YR2gw7Sdk4YTebkP8lqI0ctRZY%3d,它将用于生成最终的请求 URL。

  • 如果用户的请求方法是 GET,或者请求方法为 POST 同时 Content-Type 为 application/x-www-form-urlencoded,则发送请求时所有请求参数的值均需要做 URL 编码,参数键和=符号不需要编码。非 ASCII 字符在 URL 编码前需要先以 UTF-8 进行编码。
  • 有些编程语言的网络库会自动为所有参数进行 urlencode,在这种情况下,就不需要对签名串进行 URL 编码了,否则两次 URL 编码会导致签名失败。
  • 其他参数值也需要进行编码,编码采用 RFC 3986。使用 %XY 对特殊字符例如汉字进行百分比编码,其中“X”和“Y”为十六进制字符(0-9 和大写字母 A-F),使用小写将引发错误。

4. 签名演示

在实际调用天翼云视频监控API时,推荐使用配套的天翼云视频监控SDK,SDK 封装了签名的过程,开发时只关注产品提供的具体接口即可。当前支持的编程语言有:

  • Java
  • Python
  • Go
  • Node.js

为了更清楚的解释签名过程,下面以实际编程语言为例,将上述的签名过程具体实现。请求的域名、调用的接口和参数的取值都以上述签名过程为准,代码只为解释签名过程,并不具备通用性,实际开发请尽量使用SDK。
最终输出的 url 可能为: https://vssapi.ctyun.cn/?AccessKeyId=8FR8VXACHFFQIT33****&Action=DescribeStreamURL&DeviceId=744925256942092288&OutProtocol=rtmp&SignatureMethod=HMAC-SHA1&SignatureNonce=11886&SignatureVersion=1.0&Timestamp=1598593304&Type=live&Version=2020-06-12&Signature=5YR2gw7Sdk4YTebkP8lqI0ctRZY=

  • 由于示例中的密钥是虚构的,时间戳也不是系统当前时间,因此如果将此 url 在浏览器中打开或者用 curl 等命令调用时会返回鉴权错误:签名过期。为了得到一个可以正常返回的 url ,需要修改示例中的access key ID和secret access key为真实的密钥,并使用系统当前时间戳作为 Timestamp 。
  • 在下面的示例中,不同编程语言,甚至同一语言每次执行得到的 url 可能都有所不同,表现为参数的顺序不同,但这并不影响正确性。只要所有参数都在,且签名计算正确即可。

4.1 Java

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.xml.bind.DatatypeConverter;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.Random;
import java.util.TreeMap;

public class VssSignTest {
    private final static String CHARSET = "UTF-8";

    /**
     * 生成签名
     *
     * @param s   待签名的字符串
     * @param key SecretKey
     * @return
     * @throws Exception
     */
    public static String sign(String s, String key) throws Exception {
        Mac mac = Mac.getInstance("HmacSHA1");
        SecretKeySpec secretKeySpec = new SecretKeySpec(key.getBytes(CHARSET), mac.getAlgorithm());
        mac.init(secretKeySpec);
        byte[] hash = mac.doFinal(s.getBytes(CHARSET));
        return DatatypeConverter.printBase64Binary(hash);
    }

    /**
     * 拼装待签名的字符串
     *
     * @param params 请求参数
     * @return
     */
    public static String getStringToSign(TreeMap<String, Object> params) {
        StringBuilder s2s = new StringBuilder("GETvssapi.ctyun.cn/?");
        // 签名时要求对参数进行字典排序,此处用TreeMap保证顺序
        for (String k : params.keySet()) {
            s2s.append(k).append("=").append(params.get(k).toString()).append("&");
        }
        return s2s.substring(0, s2s.length() - 1);
    }

    /**
     * 接口请求URL
     *
     * @param params 请求参数
     * @return
     * @throws UnsupportedEncodingException
     */
    public static String getUrl(TreeMap<String, Object> params) throws UnsupportedEncodingException {
        StringBuilder url = new StringBuilder("https://vssapi.ctyun.cn/?");
        // 实际请求的url中对参数顺序没有要求
        for (String k : params.keySet()) {
            // 需要对请求串进行urlencode,由于key都是英文字母,故此处仅对其value进行urlencode
            url.append(k).append("=").append(URLEncoder.encode(params.get(k).toString(), CHARSET)).append("&");
        }
        return url.substring(0, url.length() - 1);
    }

    public static void main(String[] args) throws Exception {
        String secretId = "8FR8VXACHFFQIT33****"; // 请前往天翼云视频监控官网访问密钥页面申请访问秘钥
        String secretKey = "PwbZMn5wEqXVrjt3L6QSdxYyOvllrfLPzLcR****"; // 请前往天翼云视频监控官网访问密钥页面申请访问秘钥
        TreeMap<String, Object> params = new TreeMap<String, Object>(); // TreeMap可以自动排序
        // 使用随机数
        params.put("SignatureNonce", new Random().nextInt(java.lang.Integer.MAX_VALUE)); // 公共参数
        // 使用系统当前时间
        params.put("Timestamp", String.valueOf(System.currentTimeMillis() / 1000)); // 公共参数
        params.put("AccessKeyId", secretId);                                        // 公共参数
        params.put("Action", "CreateVSSGroup");                                     // 公共参数
        params.put("Version", "2020-06-12");                                        // 公共参数
        params.put("SignatureVersion", "1.0");                                      // 公共参数
        params.put("SignatureMethod", "HMAC-SHA1");

        params.put("AppName", "live");                                              // 业务参数
        params.put("Description", "testcreategroupzzz33");                                 // 业务参数
        params.put("GroupName", "testby123");                                   // 业务参数
        params.put("InNetworkType", "public");                                      // 业务参数
        params.put("OutNetworkType", "public");                                     // 业务参数
        params.put("InProtocol", "ehome");                                          // 业务参数
        params.put("OutProtocol", "webrtc");                                        // 业务参数
        params.put("PullType", "1");                                                // 业务参数
        params.put("PushType", "1");                                                // 业务参数
        params.put("Region", "0851002");                                            // 业务参数


        params.put("Signature", sign(getStringToSign(params), secretKey));          // 公共参数
        System.out.println("开放接口请求URL为:" + getUrl(params));
    }
}

4.2 Go

package main

import (
    "bytes"
    "crypto/hmac"
    "crypto/sha1"
    "encoding/base64"
    "fmt"
    "net/url"
    "sort"
    "strconv"
    "strings"
    "time"
)

func getStringToSign(params map[string]string) string {
    s2s := bytes.Buffer{}
    s2s.WriteString("GET")
    s2s.WriteString("vssapi.ctyun.cn")
    s2s.WriteString("/?")

    // 签名时要求对参数进行字典排序
    keys := make([]string, 0, len(params))
    for k := range params {
        keys = append(keys, k)
    }
    sort.Strings(keys)
    for i, k := range keys {
        s2s.WriteString(k)
        s2s.WriteString("=")
        s2s.WriteString(params[k])
        if i < len(keys)-1 {
            s2s.WriteString("&")
        }
    }
    return s2s.String()

}

func sign(s, k string) string {
    key := []byte(k)
    hmac := hmac.New(sha1.New, key)
    hmac.Write([]byte(s))
    signedBytes := hmac.Sum(nil)
    return base64.StdEncoding.EncodeToString(signedBytes)
}

func getUrl(params map[string]string) string {
    urlBuffer := bytes.Buffer{}
    urlBuffer.WriteString("https://vssapi.ctyun.cn/?")
    // 实际请求的url中对参数顺序没有要求
    for k, v := range params {
        urlBuffer.WriteString(k)
        urlBuffer.WriteString("=")
        urlBuffer.WriteString(url.QueryEscape(v))
        urlBuffer.WriteString("&")
    }
    urlStr := urlBuffer.String()
    if strings.HasSuffix(urlStr, "&") {
        urlStr = urlStr[:len(urlStr)-1]
    }
    return urlStr
}

func main() {
    secretId := "8FR8VXACHFFQIT33****"                      // 请前往天翼云视频监控官网访问密钥页面申请访问秘钥
    secretKey := "PwbZMn5wEqXVrjt3L6QSdxYyOvllrfLPzLcR****" // 请前往天翼云视频监控官网访问密钥页面申请访问秘钥
    params := make(map[string]string)
    params["SignatureNonce"] = strconv.FormatInt(time.Now().UnixNano(), 10) // 公共参数 使用随机数
    params["Timestamp"] = strconv.FormatInt(time.Now().Unix(), 10)          // 公共参数 使用当前时间
    params["AccessKeyId"] = secretId                                        // 公共参数 SecretId
    params["Action"] = "DescribeStreamURL"                                  // 公共参数
    params["Version"] = "2020-06-12"                                        // 公共参数
    params["SignatureMethod"] = "HMAC-SHA1"                                 // 公共参数
    params["SignatureVersion"] = "1.0"                                      // 公共参数

    params["Type"] = "live"                  // 业务参数
    params["DeviceId"] = "29941936081090619" // 业务参数
    params["OutProtocol"] = "rtmp"           // 业务参数

    params["Signature"] = sign(getStringToSign(params), secretKey) // 公共参数 签名
    fmt.Println("VSS OPEN API URL: " + getUrl(params))
}

4.3 Python

import time
import hashlib
import hmac
import base64
from urllib import parse


def sign(s, k):
    """
    生成签名
    :param s:   待签名的字符串
    :param k:   SecretKey
    :return:
    """
    key = bytes(k, 'UTF-8')
    message = bytes(s, 'UTF-8')

    digester = hmac.new(key, message, hashlib.sha1)
    signature1 = digester.digest()

    signature2 = base64.b64encode(signature1)

    return str(signature2, 'UTF-8')


def get_string_to_sign(payload):
    """
    拼装待签名的字符串
    :param payload: 请求参数
    :return:
    """
    sorted_payload = sorted(payload.items(), key=lambda d: d[0])  # 签名时要求对参数进行字典排序
    sign = "GET" + "vssapi.ctyun.cn/?" + parse.unquote_plus(parse.urlencode(sorted_payload, encoding="utf-8"))
    return sign


def get_url(payload):
    """
    接口请求URL
    :param payload: 请求参数
    :return:
    """
    url_str = "https://vssapi.ctyun.cn/?"
    i = 0
    for k in payload:
        if i > 0:
            url_str += "&"
        url_str += k
        url_str += "="
        url_str += parse.quote_plus(payload[k], encoding="utf-8")
        i += 1
    return url_str


if __name__ == '__main__':
    secret_id = "8FR8VXACHFFQIT33****"  # 请前往天翼云视频监控官网访问密钥页面申请访问秘钥
    secret_key = "PwbZMn5wEqXVrjt3L6QSdxYyOvllrfLPzLcR****"  # 请前往天翼云视频监控官网访问密钥页面申请访问秘钥
    params = {
        "SignatureNonce": str(int(time.time()*1000000000)),  # 使用随机数
        "Timestamp": str(int(time.time())),     # 使用系统当前时间
        "AccessKeyId": secret_id,               # 公共参数
        "Action": "DescribeStreamURL",          # 公共参数
        "Version": "2020-06-12",                # 公共参数
        "SignatureMethod": "HMAC-SHA1",         # 公共参数
        "SignatureVersion": "1.0",              # 公共参数

        "Type": "live",                         # 业务参数
        "DeviceId": "29941936081090619",        # 业务参数
        "OutProtocol": "rtmp",                  # 业务参数
    }
    params["Signature"] = sign(get_string_to_sign(params), secret_key)
    print("开放接口请求URL为:" + get_url(params))

4.4 Node.js

'use strict'

const url = require('url')
const axios = require('axios')
const CryptoJS = require('crypto-js')

class ROAClient {
  constructor(config) {
    this.endpoint = config.endpoint
    this.apiVersion = config.apiVersion
    this.accessKeyId = config.accessKeyId
    this.secretAccessKey = config.secretAccessKey
    this.host = url.parse(this.endpoint).hostname
  }

  getSignature(url) {
    const hash = CryptoJS.HmacSHA1(url, this.secretAccessKey)
    const signature = hash.toString(CryptoJS.enc.Base64)
    return signature
  }

  getCanonicalizedResource(uriPattern, query, flag) {
    const keys = Object.keys(query).sort()

    if (keys.length === 0) {
      return uriPattern
    }

    var result = []
    for (var i = 0; i < keys.length; i++) {
      const key = keys[i]
      if (Object.prototype.toString.call(query[key]) === '[object Object]') {
        for (const o in query[key]) {
          if (query[key][o] !== undefined) {
            if (flag) {
              result.push(`${key}.${o}=${encodeURI(query[key][o])}`)
            } else {
              result.push(`${key}.${o}=${query[key][o]}`)
            }
          }
        }
      } else {
        if (flag && key.indexOf('Signature') === -1) {
          result.push(`${key}=${encodeURI(query[key])}`)
        } else {
          result.push(`${key}=${query[key]}`)
        }
      }
    }
    return `${uriPattern}?${result.join('&')}`
  }

  request(method, uriPattern, query = {}, body = '', headers = {}) {
    if (Object.keys(query).length) {
      query = Object.assign(query, {
        AccessKeyId: this.accessKeyId,
        Version: this.apiVersion,
        SignatureMethod: 'HMAC-SHA1',
        Timestamp: parseInt(new Date().getTime() / 1000),
        SignatureVersion: '1.0'
      })

      const urlResource = this.getCanonicalizedResource(uriPattern, query, true)

      const tempUrl = `${method}${this.host}${urlResource}`

      query.Signature = this.getSignature(tempUrl)

      var url = `${this.endpoint}${urlResource}`
    }

    return axios({
      method,
      url,
      data: body,
      headers: headers
    })
  }

  get(path, query, headers) {
    return this.request('GET', path, query, '', headers)
  }
}

// 使用
const client = new ROAClient({
  endpoint: 'https://vssapi.ctyun.cn',
  accessKeyId: '8FR8VXACHFFQIT33****', // 请前往天翼云视频监控官网访问密钥页面申请访问秘钥
  secretAccessKey: 'PwbZMn5wEqXVrjt3L6QSdxYyOvllrfLPzLcR****' // 请前往天翼云视频监控官网访问密钥页面申请访问秘钥
})

async function init() {
  try {
    const GETDescribeVSSGroups = await client.describeVSSGroups({
      SignatureMethod: 'HMAC-SHA1',
      SignatureVersion: '1.0',
      Action: 'DescribeVSSGroups',
      Version: '2020-06-12',
      SortBy: 'CreatedTime',
      SortDirection: 'asc',
      PageNum: '1',
      PageSize: '20',
      IncludeGroupStats: '0'
    })
    console.log(GETDescribeVSSGroups.data)
  } catch (err) {
    console.log(err)
  }
}

init()