HLS双重防盗链App端对接教程
提供App端对接HLS双重防盗链的解密示例,用于自定义播放器场景。
提示:该功能仅对开启TS加密功能后转码的视频生效。
对接之前请先了解下HLS双重防盗链的原理及设置 ⇒ 传送门。
该功能仅推荐App端使用,网页端由于国产浏览器内核对自定义HLS解析器支持较差,不推荐网页端使用。
双重加密原理
该功能对key地址和key内容分别进行了加密,App端需要依次解密才能正常播放。
第一重:key地址加密
后端将m3u8中原始的key地址替换成DES-ECB加密后的token参数:
# 原始key地址
#EXT-X-KEY:METHOD=AES-128,URI="/videos/202603/21/xxx/ts.key"
# 替换后的key地址
#EXT-X-KEY:METHOD=AES-128,URI="https://你的域名?token=xxxxxx加密字符串"
# 地址加密所用desKey规则(取前4位拼接完整KEY)
desKey = "efvt" + "efvtoken" = "efvtefvtoken"
第二重:key内容加密
App还原真实key地址后,请求该地址返回的不是原始key内容,而是经过DES-ECB加密后的内容,需要再次解密才能得到真实key:
# key内容加密所用desKey规则(直接用完整KEY,不拼接)
desKey = "efvtoken"
完整解密流程:
解析m3u8拿到加密的token URI
↓
用「前4位+完整KEY」解密token → 还原真实key地址
↓
请求真实 key 地址 → 返回加密后的key内容
↓
用「完整KEY」解密key内容 → 得到真实key
↓
用真实key解密TS切片正常播放
Android (Kotlin) 示例
import javax.crypto.Cipher
import javax.crypto.spec.SecretKeySpec
import android.util.Base64
object AntiKeyDecryptor {
private const val ANTI_KEY = "efvtoken" // 与后台防盗链KEY保持一致
// 通用DES-ECB解密
private fun desDecrypt(ciphertext: String, key: String): String {
val keySpec = SecretKeySpec(key.toByteArray(), "DES")
val cipher = Cipher.getInstance("DES/ECB/PKCS5Padding")
cipher.init(Cipher.DECRYPT_MODE, keySpec)
val decoded = Base64.decode(ciphertext, Base64.DEFAULT)
return String(cipher.doFinal(decoded))
}
// 第一重:解密key地址(前4位+完整KEY)
fun decryptKeyUrl(token: String): String {
val desKey = ANTI_KEY.substring(0, 4) + ANTI_KEY
return desDecrypt(token, desKey)
}
// 第二重:解密key内容(直接用完整KEY)
fun decryptKeyContent(encryptedContent: String): String {
return desDecrypt(encryptedContent, ANTI_KEY)
}
// 从key URI提取token并还原真实地址
fun resolveKeyUrl(keyUri: String): String {
return try {
val token = Uri.parse(keyUri).getQueryParameter("token") ?: return keyUri
decryptKeyUrl(token)
} catch (e: Exception) {
keyUri
}
}
}
// 播放器拦截key请求的使用示例(以ExoPlayer为例)
// 在DefaultHttpDataSource中拦截:
// 1. 拦截到key请求URL,提取token,解密还原真实key地址
// 2. 请求真实key地址,拿到加密后的key内容
// 3. 用decryptKeyContent() 解密内容,得到真实key
// 4. 将真实key返回给播放器
iOS (Swift) 示例
import CommonCrypto
import Foundation
struct AntiKeyDecryptor {
static let antiKey = "efvtoken" // 与后台防盗链KEY保持一致
// 通用DES-ECB解密
static func desDecrypt(ciphertext: String, key: String) -> String? {
guard let data = Data(base64Encoded: ciphertext) else { return nil }
let keyBytes = Array(key.utf8)
var outBuf = [UInt8](repeating: 0, count: data.count + kCCBlockSizeDES)
var outLen = 0
let status = CCCrypt(
CCOperation(kCCDecrypt), CCAlgorithm(kCCAlgorithmDES),
CCOptions(kCCOptionECBMode | kCCOptionPKCS7Padding),
keyBytes, kCCKeySizeDES,
nil,
Array(data), data.count,
&outBuf, outBuf.count, &outLen
)
guard status == kCCSuccess else { return nil }
return String(bytes: outBuf[..<outLen], encoding: .utf8)
}
// 第一重:解密key地址(前4位+完整KEY)
static func decryptKeyUrl(token: String) -> String? {
let desKey = String(antiKey.prefix(4)) + antiKey
return desDecrypt(ciphertext: token, key: desKey)
}
// 第二重:解密key内容(直接用完整KEY)
static func decryptKeyContent(encrypted: String) -> String? {
return desDecrypt(ciphertext: encrypted, key: antiKey)
}
// 从key URI提取token并还原真实地址
static func resolveKeyUrl(_ keyUri: String) -> String {
guard let url = URLComponents(string: keyUri),
let token = url.queryItems?.first(where: { $0.name == "token" })?.value,
let real = decryptKeyUrl(token: token) else { return keyUri }
return real
}
}
Flutter (Dart) 示例
import 'package:encrypt/encrypt.dart';
class AntiKeyDecryptor {
static const antiKey = 'efvtoken'; // 与后台防盗链KEY保持一致
// 通用 DES-ECB 解密
static String desDecrypt(String ciphertext, String keyStr) {
final key = Key.fromUtf8(keyStr);
final encrypter = Encrypter(DES(mode: DESMode.ecb, padding: 'PKCS7'));
final encrypted = Encrypted.fromBase64(ciphertext);
return encrypter.decrypt(encrypted, iv: IV(Uint8List(8)));
}
// 第一重:解密key地址(前4位+完整KEY)
static String decryptKeyUrl(String token) {
final desKey = antiKey.substring(0, 4) + antiKey;
return desDecrypt(token, desKey);
}
// 第二重:解密key内容(直接用完整KEY)
static String decryptKeyContent(String encrypted) {
return desDecrypt(encrypted, antiKey);
}
// 从key URI提取token并还原真实地址
static String resolveKeyUrl(String keyUri) {
try {
final uri = Uri.parse(keyUri);
final token = uri.queryParameters['token'];
if (token == null) return keyUri;
return decryptKeyUrl(token);
} catch (e) {
return keyUri;
}
}
}