签名方法
最近更新时间: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用于加密签名字符串和服务器端验证签名字符串的密钥;
- 用户必须严格保管访问秘钥,避免泄露。
申请访问秘钥的具体步骤如下:
- 登录天翼云管理中心控制台;
- 前往视频监控的控制台页面;
- 在访问秘钥页面,单击【新建秘钥】,即可以创建一对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)请求为例,当用户调用这一接口时,其请求参数可能如下:
参数 | 示例值 | 描述 |
---|---|---|
Action | DescribeStreamURL | |
Version | 2020-06-12 | API 版本号。格式:YYYY-MM-DD。 |
AccessKeyId | 8FR8VXACHFFQIT33**** | 天翼云颁发给用户的访问服务所用的密钥 ID。 |
Signature | RxQApRehQz9PAkY9XnWrvMte5co= | 签名结果串。 关于签名的计算方法,参见签名方法。 |
SignatureMethod | HMAC-SHA1 | 签名方式,目前支持HMAC-SHA1。 |
Timestamp | 1598593304 | 当前系统时间戳,可记录发起 API 请求的时间。例如1529223702,如果与当前时间相差过大(前后十分钟),会引起签名过期错误。 |
SignatureNonce | 11886 | 唯一随机数,用于防止网络重放攻击。用户在不同请求间要使用不同的随机数值,即使请求失败下一次也需要更换该值。 |
SignatureVersion | 1.0 | 签名算法版本。目前版本是 1.0。 |
DeviceId | 744925256942092288 | 设备ID。 |
OutProtocol | rtmp | 流播放协议。取值: rtmp,hls,flv |
Type | live | 流类型,默认live。 取值:live(直播流)、vod(点播流,例如NVR上的历史流) |
2.1 对参数排序
首先对所有请求参数按参数名的字典序( ASCII 码)升序排序。注意:
- 只按参数名进行排序,参数值保持对应即可,不参与比大小;
- 按 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 拼接签名原文字符串
此步骤生成签名原文字符串。 签名原文字符串由以下几个参数构成:
- 请求方法: 这里使用 GET 请求,注意方法为全大写。
- 请求主机: 查看流URL(DescribeStreamURL)的请求域名为: vssapi.ctyun.cn。
- 请求路径: 当前版本云API的请求路径固定为 / 。
- 请求字符串: 即上一步生成的请求字符串。
签名原文串的拼接规则为: 请求方法 + 请求主机 +请求路径 + ? + 请求字符串。
示例的拼接结果为:
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()