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;
    }
  }
}

注意事项

  • ANTI_KEY值必须与后台防盗链/图片加密设置中的HLS双重防盗链KEY完全一致
  • key地址key内容使用了不同的加密key规则,注意区分,不可混用
  • App端建议将ANTI_KEY混淆或加密存储在二进制中,避免直接明文暴露
  • 该功能必须配合TS加密一起使用,单独使用无效
  • 网页端请使用时间戳防盗链功能替代,兼容性更好 ⇒ 传送门
  • 可配合获取TS加密Key和IV接口使用 ⇒ 传送门,提前在本地获取解密后的Key内容,跳过第二重解密请求