直播推拉流时间戳防盗链
七牛直播推流/拉流时间戳防盗链
本文覆盖两部分:
- 推流时间戳防盗链:RTMP 推流
- 拉流时间戳防盗链:HLS(.m3u8)、FLV(.flv)、RTMP 播放等
术语
- publishDomain:推流域名
- playDomain:播放域名
- bucket:直播空间名称
- stream:直播流名称
- publishKey:推流鉴权密钥(配置在推流域名/空间)
- playKey:播放防盗链密钥(配置在播放域名)
- t(expireAt):过期时间的 UNIX 时间戳(秒)
- path:URL 的路径部分,必须以
/开头;如rtmp://test.miku.com/sdk-live/test的 path 为/sdk-live/test
签名串中涉及 URL 编码时,需保留斜杠
/(参考下文示例代码的处理方式)。
一、推流时间戳防盗链
在推流域名上设置 publishKey,并在推流地址后追加 ?sign=...&t=...。
签名公式
sign = md5(publishKey + path + t).to_lower()
path:如/sdk-live/testt:过期时间 UNIX 时间戳(秒)
示例
publishDomain: test.miku.com
bucket: sdk-live
stream: test
publishKey: test
t (expireAt): 1756110618
path: /sdk-live/test
SignStr = "test/sdk-live/test1756110618"
sign = md5(SignStr).to_lower() = 6a1b665f529c8b57d6408b72e4d21350
最终推流地址:
rtmp://test.miku.com/sdk-live/test?sign=6a1b665f529c8b57d6408b72e4d21350&t=1756110618
Portal 中主/副密钥(均可以进行鉴权)均可使用;建议使用副密钥进行轮换,降低主密钥更换风险。
二、拉流时间戳防盗链
播放防盗链可以有效避免直播资源被非法盗用的问题。
在播放域名上开启“时间戳防盗链”并设置 playKey。访问播放地址时,边缘节点按相同规则校验,非法则拒绝(常见表现:HLS/FLV 403;RTMP 播放连接失败/服务返回错误)。
推流地址格式
[rtmp/http]://<playDomain>/<bucket>/<stream>.[flv/m3u8]
签名公式
sign = md5(playKey + path + t).to_lower()
path 取值示例
| 协议 | 地址示例 | 用于签名的 path |
|---|---|---|
| HLS | http://play.example.com/bucket/stream.m3u8 |
/bucket/stream.m3u8 |
| FLV | http://play.example.com/bucket/stream.flv |
/bucket/stream.flv |
| RTMP播放 | rtmp://play.example.com/bucket/stream |
/bucket/stream |
注意:不同协议的
path不同
生成步骤
- 开启时间戳防盗链并获取 playKey
控制台 → 直播空间 → 播放域名 → 开启时间戳防盗链 → 配置主/副密钥 - 确定过期时间
t(秒)# macOS: t=$(date -j -f "%Y-%m-%d %H:%M:%S" '2025-10-29 20:00:00' +%s) # 结果示例:1761739200 - 计算
signsign=md5(playKey + path + t).to_lower()
HLS 示例
未鉴权地址:
http://pili-hls.pilitest.com/bucket/stream.m3u8
开启后:
playDomain: pili-hls.pilitest.com
playKey = test
path = /bucket/stream.m3u8
t = 1761739200
sign = md5("test/bucket/stream.m3u81761739200").to_lower()
= 3acc8aa865f23adfdbceba694e7dc4b9
最终播放地址:
http://pili-hls.pilitest.com/bucket/stream.m3u8?sign=3acc8aa865f23adfdbceba694e7dc4b9&t=1761739200
三、参考代码(兼容推流/拉流)
同一签名方法适用于推流与拉流。区别仅在于:使用的 Key(publishKey/playKey) 与 path(推拉流 RTMP
/bucket/stream;拉流 HLS/bucket/stream.m3u8;拉流 FLV/bucket/stream.flv)。
Java
package com.qiniu.hugo.miku;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.security.MessageDigest;
public class TimestampAntiLeechUrl {
private static final char[] DIGITS_LOWER = {
'0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f'
};
public static void main(String[] args) {
// 推流(Publish)
String publishUrl = "rtmp://test.miku.com/sdk-live/test";
long t1 = System.currentTimeMillis()/1000 + 20*60;
String publishKey = "test";
System.out.println(signUrl(publishUrl, publishKey, t1));
// HLS 播放(Play)
String hlsUrl = "http://test.miku.com/sdk-live/test.m3u8";
long t2 = System.currentTimeMillis()/1000 + 20*60;
String playKey = "test";
System.out.println(signUrl(hlsUrl, playKey, t2));
}
public static String signUrl(String url, String key, long expireTs) {
try {
String path = url.substring(url.indexOf("/", url.indexOf("//") + 2));
// 保留斜杠,仅编码其他字符
String encodedPath = URLEncoder.encode(path, "UTF-8").replace("%2F", "/");
String t = String.valueOf(expireTs);
String toSign = key + encodedPath + t;
String sign = md5Lower(toSign);
return String.format("%s?sign=%s&t=%s", url, sign, t);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
private static String md5Lower(String src) throws Exception {
MessageDigest md = MessageDigest.getInstance("MD5");
md.update(src.getBytes(Charset.forName("UTF-8")));
byte[] bytes = md.digest();
return encodeHex(bytes);
}
private static String encodeHex(byte[] data) {
char[] out = new char[data.length << 1];
for (int i = 0, j = 0; i < data.length; i++) {
out[j++] = DIGITS_LOWER[(0xF0 & data[i]) >>> 4];
out[j++] = DIGITS_LOWER[0x0F & data[i]];
}
return new String(out);
}
}
Go
package main
import (
"crypto/md5"
"encoding/hex"
"fmt"
"net/url"
"strings"
"time"
)
func main() {
// 推流(Publish)
pub := "rtmp://test.miku.com/sdk-live/test"
fmt.Println(SignURL(pub, "test", time.Now().Unix()+20*60))
// HLS 播放(Play)
hls := "http://test.miku.com/sdk-live/test.m3u8"
fmt.Println(SignURL(hls, "test", time.Now().Unix()+20*60))
}
func SignURL(baseURL, key string, expire int64) (string, error) {
u, err := url.Parse(baseURL)
if err != nil {
return "", err
}
// path 必须以 / 开头
path := u.Path
// 保留斜杠,仅编码其他字符
encodedPath := strings.ReplaceAll(url.QueryEscape(path), "%2F", "/")
t := fmt.Sprintf("%d", expire)
toSign := key + encodedPath + t
sign := md5Lower(toSign)
return fmt.Sprintf("%s?sign=%s&t=%s", baseURL, sign, t), nil
}
func md5Lower(s string) string {
sum := md5.Sum([]byte(s))
return hex.EncodeToString(sum[:])
}
四、易错点与最佳实践
-
密钥配置位置
- 推流鉴权用 publishKey(配置在推流域名/空间)
- 播放防盗链用 playKey(配置在播放域名)
-
path 必须准确
- 推拉流 RTMP:
/bucket/stream - 拉流 HLS:
/bucket/stream.m3u8 - 拉流 FLV:
/bucket/stream.flv
- 推拉流 RTMP:
-
URL 编码
- 仅编码必要字符并保留
/,参考示例代码
- 仅编码必要字符并保留
-
时间戳单位
t为秒级 UNIX 时间戳
-
主/副密钥轮换
- 支持主/副密钥;使用副密钥灰度切换更稳妥
-
排查清单
- 403/连接失败:检查
path是否正确、t是否过期、签名大小写、是否双重编码 - 跨协议失败:核对协议对应的
path后缀(.m3u8/.flv) - 同一域名多密钥:确认实际启用的是哪一把(主/副)
- 403/连接失败:检查
文档反馈
(如有产品使用问题,请 提交工单)