更新时间:2024-08-30 GMT+08:00

Key防盗链

为保障直播资源不被非法盗用,您可以使用直播的Key防盗链功能,在原始推流或播放地址末尾加上鉴权信息。在主播请求直播推流或观众请求播放时,CDN会对其URL带的加密信息进行合法性判断,仅校验通过的请求会予以响应,其它非法的访问将予以拒绝。

若您有其它自定义防盗链规则的需求,请您提交工单与华为云技术客服联系。

工作原理

图1 Key防盗链工作原理

流程说明如下所示:

  1. 租户在直播控制台开启Key防盗链功能,并配置鉴权方式、Key值和时长。
  2. 直播服务将租户配置的鉴权方式、Key值和时长下发到CDN节点中。
  3. 主播/观众通过租户提供的鉴权推流/播放URL向CDN请求推流或播放。
  4. CDN根据推流或播放URL中携带的鉴权信息校验请求的合法性,仅校验通过的请求会被允许。

注意事项

  • 该功能为可选项,默认不启用。启用该功能后,原始直播加速URL将无法使用,需要按规则生成合法的防盗链URL。
  • 建议推流与播放鉴权使用不同的Key值,以增强安全性。若防盗链URL过期,或者签名不能通过,直播流将播放失败,并返回“403 Forbidden”信息。
  • 针对RTMP、FLV这类长连接业务,只有服务端收到用户请求时,才进行防盗链参数校验,校验通过后可以持续播放。
  • 针对HLS这类业务,用户播放后会携带相同的防盗链参数,持续发起请求。一旦防盗链参数过期,服务端便会因校验不通过,而拒绝访问,导致播放中断。

    所以建议这类业务,适当调整鉴权过期时间,避免因时间过短,而中途就播放失败。示例:如果预估HLS播放时长每次都在1小时以内,可设置过期时间为3600秒。

前提条件

开启Key防盗链

  1. 登录视频直播控制台
  2. 在左侧导航栏中,选择域名管理,进入域名管理页面。
  3. 在需要配置鉴权信息的域名行单击“管理”
  4. 在左侧导航树中选择基础配置 > 鉴权配置
  5. 选择Key防盗链,弹出“Key防盗链”对话框。
  6. 单击“开关”,配置Key防盗链参数,如图2所示。

    图2 配置Key防盗链
    表1 Key防盗链参数说明

    参数名

    描述

    类型

    计算鉴权串的方式,可选为:方式A、方式B、方式C或方式D。

    AB鉴权方式:采用MD5信息摘要算法,具体实现方法请参见鉴权方式A鉴权方式B

    C鉴权方式:采用对称加密算法,具体实现方法请参见鉴权方式C

    D鉴权方式:采用HMAC-SHA256算法,具体实现方法请参见鉴权方式D

    说明:

    鉴权方式ABC存在安全风险,鉴权方式D拥有更高的安全性,建议您优先使用鉴权方式D。

    Key

    鉴权key值。

    • 支持自定义设置,由32位的字母和数字组成。
    • 支持自动生成。

    时长

    URL鉴权信息的超时时长,指的是鉴权信息中携带的请求时间与直播服务收到请求时的时间的最大差值,用于检查直播推流URL或者直播播放URL是否已过期,单位:秒,范围限制:1分钟-30天。

    说明:
    • 针对RTMP、FLV这类长连接业务,只有服务端收到用户请求时,才进行防盗链参数校验,校验通过后可以持续播放。
    • 针对HLS这类业务,用户播放后会携带相同的防盗链参数,持续发起请求。一旦防盗链参数过期,服务端便会因校验不通过,而拒绝访问,导致播放中断。

      所以建议这类业务,适当调整鉴权过期时间,避免因时间过短,而中途就播放失败。示例:如果预估HLS播放时长每次都在1小时以内,可设置过期时间为3600秒。

  7. 配置完成后,单击“确定”
  8. 通过以下方式获取鉴权地址。

    手动拼接:根据配置的鉴权类型拼接对应的鉴权地址,各鉴权类型对应的鉴权地址拼接方法请分别参见鉴权方式A鉴权方式B鉴权方式C鉴权方式D

  9. 验证防盗链功能。

    使用第三方直播推拉流工具,通过鉴权推流地址和播放地址进行验证,若原始推流地址和播放地址无法成功推流和播放,使用鉴权推流地址和播放地址能成功推流和播放,则表示Key防盗链生效。

鉴权方式A

鉴权方式A主要通过Key、timestamp、rand(随机数)、uid(设置为0)和URL计算鉴权串。

鉴权URL格式
原始URL?auth_key={timestamp}-{rand}-{uid}-{md5hash}
md5hash的计算公式:
sstring = "{URI}-{Timestamp}-{rand}-{uid}-{Key}"
HashValue = md5sum(sstring)
表2 鉴权字段描述

字段

描述

timestamp

用户定义的有效访问时间起始点,值为1970年1月1日以来的当前时间秒数 。十进制或者十六进制整数。

示例:1592639100(即2020-06-20 15:45)

时长

鉴权URL有效的时间长度。

若设置的有效时间为1800s,则用户可在从timestamp开始的1800s内允许访问直播地址。超出该区间,鉴权失败。

示例:若设置的访问时间为2020-6-30 00:00:00,则链接真正失效时间为2020-6-30 00:30:00。

rand

随机数,建议使用UUID,不能包含中划线"-"。

示例:477b3bbc253f467b8def6711128c7bec

uid

userID。暂未使用,直接设置成0即可。

md5hash

通过md5算法计算出来的验证串,数字0-9和小写英文字母a-z混合组成,固定长度32。

sstring = "{URI}-{Timestamp}-{rand}-{uid}-{Key}"
HashValue = md5sum(sstring)

URI

指原始URL中从域名后开始到最后的路径。

示例:/livetest/huawei1.flv

Key

在控制台设置的防盗链Key值,具体请参见开启Key防盗链

鉴权URL示例

以生成播放鉴权地址为例,推流鉴权地址的生成与播放鉴权地址的生成方法相同。
原始URL:http://test-play.example.com/livetest/huawei1.flv
timestamp:1592639100
时长:1800s
Key:GCTbw44s6MPLh4GqgDpnfuFHgy25Enly
rand:477b3bbc253f467b8def6711128c7bec
uid:0
URI:/livetest/huawei1.flv
根据计算公式,得到md5hash
HashValue = md5sum("/livetest/huawei1.flv-1592639100-477b3bbc253f467b8def6711128c7bec-0-GCTbw44s6MPLh4GqgDpnfuFHgy25Enly") = dd1b5ffa00cf26acec0c169ae1cfabea

则鉴权播放地址为:

http://test-play.example.com/livetest/huawei1.flv?auth_key=1592639100-477b3bbc253f467b8def6711128c7bec-0-dd1b5ffa00cf26acec0c169ae1cfabea

鉴权方式B

鉴权方式B主要通过Key、timestamp和StreamName计算鉴权串。

鉴权URL格式
原始URL?txSecret=md5(Key + StreamName + txTime)&txTime=hex(timestamp)
表3 鉴权字段描述

字段

描述

txTime

播放URL的有效时间,为Unix时间戳的十六进制结果。

如果当前txTime的值大于当前请求的时间则可以正常播放,否则播放会被后台拒绝。

示例:5eed5888(即2020.06.20 08:30:00)

Key

在控制台设置的防盗链Key值,具体请参见开启Key防盗链

txSecret

URL中的加密参数。

通过将key,StreamName,txTime依次拼接的字符串进行MD5加密算法得出。

txSecret = md5(Key + StreamName + txTime)

时长

鉴权URL的有效时间长度。

txTime设置为当前时间,有效时间设置为1249s,则播放URL过期时间为当前时间+ 1249s。

鉴权URL示例

以生成播放鉴权地址为例,推流鉴权地址的生成与播放鉴权地址的生成同理。
原始URL:http://test-play.example.com/livetest/huawei1.flv
Key:GCTbw44s6MPLh4GqgDpnfuFHgy25Enly
StreamName:huawei1
txTime:5eed5888
时长:1249s
根据计算公式,得到txSecret
txSecret = md5(GCTbw44s6MPLh4GqgDpnfuFHgy25Enlyhuawei15eed5888) = 5cdc845362c332a4ec3e09ac5d5571d6

则鉴权播放地址为:

http://test-play.example.com/livetest/huawei1.flv?txSecret=5cdc845362c332a4ec3e09ac5d5571d6&txTime=5eed5888

鉴权方式C

鉴权方式C主要通过Key、Timestamp、AppName、StreamName和CheckLevel计算鉴权串。

鉴权URL格式
原始URL?auth_info=加密串.EncodedIV
鉴权字段的生成算法如下所示,具体代码示例请参考代码示例
  • LiveID = <AppName>+"/"+<StreamName>
  • 加密串 = UrlEncode(Base64(AES128(<Key>,"$"+<Timestamp>+"$"+<LiveID>+"$"+<CheckLevel>)))
  • EncodedIV = Hex(加密使用的IV)

算法中各加密参数说明如表4所示。

表4 加密参数说明

字段

描述

AppName

应用名称,与推流或播放地址中的AppName一致。

StreamName

流名称,与推流或播放地址中的StreamName一致。

Key

在控制台设置的防盗链Key值,具体请参见开启Key防盗链

LiveID

直播流ID,用于标识唯一的直播流,由AppName和StreamName组成。

LiveID = <AppName>+"/"+<StreamName>

Timestamp

鉴权参数生成的UTC时间,格式为“yyyyMMddHHmmss”,用于检查鉴权参数是否已过期,即Timestamp和当前时间差值的绝对值是否大于配置的超时时长。

CheckLevel

检查级别。取值为3或者5。

  • CheckLevel=3,只检查LiveID是否匹配,不检查鉴权URL是否过期。
  • CheckLevel=5,检查LiveID是否匹配,Timestamp是否超时。

IV

CBC对称加密算法依赖IV向量,随机生成的16位数字和字母组合,IV值长度为128位;CBC模式,PKCS7填充。

鉴权URL示例

以生成播放鉴权地址为例,推流鉴权地址的生成与播放鉴权地址的生成同理。

原始URL:http://test-play.example.com/livetest/huawei1.flv
AppName:livetest
StreamName:huawei1
Key:GCTbw44s6MPLh4GqgDpnfuFHgy25Enly
LiveID:livetest/huawei1
Timestamp:20190428110000
CheckLevel:3
IV:yCmE666N3YAq30SN
根据计算公式,得到“加密串”“EncodedIV”
加密串 = I90KW7GhxOMwoy5yaeKMStZsOC%2B6WIyqU2kLBYAvcso%3D
EncodIV = 79436d453636364e335941713330534e
则鉴权播放地址为:
http://test-play.example.com/livetest/huawei1.flv?auth_info=I90KW7GhxOMwoy5yaeKMStZsOC%2B6WIyqU2kLBYAvcso%3D.79436d453636364e335941713330534e

鉴权方式D

鉴权方式D主要通过Key、timestamp和StreamName计算鉴权串。

鉴权URL格式
原始URL?hwSecret=hmac_sha256(Key, StreamName + hwTime)&hwTime=hex(timestamp)
表5 鉴权字段描述

字段

描述

hwTime

播放URL的有效时间,为Unix时间戳的十六进制结果。

如果当前hwTime+时长的值大于当前请求的时间则可以正常播放,否则播放会被后台拒绝。

示例:5eed5888(即2020.06.20 08:30:00)

Key

在控制台设置的防盗链Key值,具体请参见开启Key防盗链

hwSecret

URL中的加密参数。

以Key和StreamName+hwTime为参数进行HMAC-SHA256加密算法得出。

hwSecret = hmac_sha256(Key, StreamName + hwTime)

时长

鉴权URL的有效时间长度。

hwTime设置为当前时间,有效时间设置为1249s,则播放URL过期时间为当前时间+ 1249s。

鉴权URL示例

以生成播放鉴权地址为例,推流鉴权地址的生成与播放鉴权地址的生成同理。
原始URL:http://test-play.example.com/livetest/huawei1.flv
Key:GCTbw44s6MPLh4GqgDpnfuFHgy25Enly
StreamName:huawei1
hwTime:5eed5888
时长:1249s
根据计算公式,得到hwSecret
hwSecret = hmac_sha256(GCTbw44s6MPLh4GqgDpnfuFHgy25Enly, huawei15eed5888) = ce201856a0957413319e883c8ccae13602f01d3d91e21daf5161964cf708a6a8

则鉴权播放地址为:

http://test-play.example.com/livetest/huawei1.flv?hwSecret=ce201856a0957413319e883c8ccae13602f01d3d91e21daf5161964cf708a6a8&hwTime=5eed5888

代码示例

以下为鉴权方式C的鉴权串生成代码示例。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;

public class Main {

        public static void main(String[] args) {

		// data="$"+<Timestamp>+"$"+<LiveID>+"$"+<CheckLevel>,具体请参见“鉴权方式C”
                String data = "$20190428110000$live/stream01$3";

                // 随机生成的16位数字和字母组合
		byte[] ivBytes = "yCmE666N3YAq30SN".getBytes();

                //在直播控制台配置的Key值
		byte[] key = "GCTbw44s6MPLh4GqgDpnfuFHgy25Enly".getBytes();

                String msg = aesCbcEncrypt(data, ivBytes, key);
		try {
			System.out.println(URLEncoder.encode(msg, "UTF-8") + "." + bytesToHexString(ivBytes));
		} catch (UnsupportedEncodingException e) {
			e.printStackTrace();
		}
	}

        private static String aesCbcEncrypt(String data, byte[] ivBytes, byte[] key) {
		try {
			SecretKeySpec sk = new SecretKeySpec(key, "AES");
			Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");

                        if (ivBytes != null) {
				cipher.init(Cipher.ENCRYPT_MODE, sk, new IvParameterSpec(ivBytes));
			} else {
				cipher.init(Cipher.ENCRYPT_MODE, sk);
			}

                        return Base64.encode(cipher.doFinal(data.getBytes("UTF-8")));
		} catch (Exception e) {
			return null;
		}
	}

        public static String bytesToHexString(byte[] src) {
		StringBuilder stringBuilder = new StringBuilder("");
		if ((src == null) || (src.length <= 0)) {
			return null;
		}

                for (int i = 0; i < src.length; i++) {
			int v = src[i] & 0xFF;
			String hv = Integer.toHexString(v);
			if (hv.length() < 2) {
				stringBuilder.append(0);
			}
			stringBuilder.append(hv);
		}
		return stringBuilder.toString();
	}
}

以下是Base64类,用于将加密串进行编码。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
public class Base64
{

    /** Base64编码表。*/
    private static char base64Code[] =
    {
        'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R',
        'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j',
        'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1',
        '2', '3', '4', '5', '6', '7', '8', '9', '+', '/',};

    /**
     * 构造方法私有化,防止实例化。
     */
    private Base64()
    {
        super();
    }

    /**
     * Base64编码。将字节数组中字节3个一组编码成4个可见字符。
     * @param bytes 需要被编码的字节数据。
     * @return 编码后的Base64字符串。
     */
    public static String encode(byte[] bytes)
    {
        int a = 0;

        // 按实际编码后长度开辟内存,加快速度
        StringBuffer buffer = new StringBuffer(((bytes.length - 1) / 3) << 2 + 4);

        // 进行编码
        for (int i = 0; i < bytes.length; i++)
        {
            a |= (bytes[i] << (16 - i % 3 * 8)) & (0xff << (16 - i % 3 * 8));
            if (i % 3 == 2 || i == bytes.length - 1)
            {
                buffer.append(Base64.base64Code[(a & 0xfc0000) >>> 18]);
                buffer.append(Base64.base64Code[(a & 0x3f000) >>> 12]);
                buffer.append(Base64.base64Code[(a & 0xfc0) >>> 6]);
                buffer.append(Base64.base64Code[a & 0x3f]);
                a = 0;
            }
        }

        // 对于长度非3的整数倍的字节数组,编码前先补0,编码后结尾处编码用=代替,
        // =的个数和短缺的长度一致,以此来标识出数据实际长度
        if (bytes.length % 3 > 0)
        {
            buffer.setCharAt(buffer.length() - 1, '=');
        }
        if (bytes.length % 3 == 1)
        {
            buffer.setCharAt(buffer.length() - 2, '=');
        }
        return buffer.toString();
    }

}