开启M3U8标准加密改写功能后,可以改写HLS(HTTP Live Streaming)协议的M3U8文件(Media Playlist,媒体播放列表)。改写成功后会在M3U8文件内#EXT-X-KEY
标签后面增加加密参数(包括加密算法、密钥URI地址和鉴权参数),客户端收到被改写的M3U8文件以后,将会使用带鉴权参数的密钥URI来发起请求,从CDN节点获取到密钥以后将会使用对应的加密算法和密钥来解密TS文件。即通过配置M3U8标准加密改写功能,可以实现对HLS数据访问过程的加密保护。
背景信息
HLS(HTTP Live Streaming的缩写)是一个由苹果公司提出的基于HTTP的流媒体网络传输协议。HLS协议基于HTTP协议,客户端按照顺序使用HTTP协议下载存储在服务器上的文件。HLS协议规定,视频的封装格式是TS(Transport Stream),除了TS视频文件本身,还定义了用来控制播放的M3U8文件(文本文件)。HLS协议的工作原理是把整个视频流分割成一个个小的TS格式视频文件来传输,在开始一个流媒体会话时,客户端会先下载一个包含TS文件URL地址的M3U8文件(相当于一个播放列表),给客户端用于下载TS文件。
#EXTM3U
:M3U8文件头,必须放在第一行。EXT-X-MEDIA-SEQUENCE
:第一个TS分片的序列号,一般情况下是0,但是在直播场景下,这个序列号标识直播段的起始位置;#EXT-X-MEDIA-SEQUENCE:0
。#EXT-X-TARGETDURATION
:每个分片TS的最大的时长;#EXT-X-TARGETDURATION:10
,表示每个分片的最大时长是10秒。#EXT-X-ALLOW-CACHE
:是否允许cache,#EXT-X-ALLOW-CACHE:YES
、#EXT-X-ALLOW-CACHE:NO
,默认情况下是YES。#EXT-X-ENDLIST
:M3U8文件结束符。#EXTINF
:extra info,分片TS的信息,如时长,带宽等;一般情况下是#EXTINF:<duration>,[<title>]
后面可以跟其他的信息,逗号之前是当前分片的TS时长。分片时长要小于#EXT-X-TARGETDURATION
定义的值。#EXT-X-VERSION
:M3U8版本号。#EXT-X-DISCONTINUITY
:该标签表明其前一个切片与下一个切片之间存在中断。#EXT-X-PLAYLIST-TYPE
:表明流媒体类型。#EXT-X-KEY
:是否加密解析。例如:#EXT-X-KEY:METHOD=AES-128,URI="https://example.com/video.key?token=xxx"
加密算法是AES-128,密钥通过请求https://example.com/video.key?token=xxx
来获取,密钥请求回来以后存储在本地,并用于解密后续下载的TS视频文件。
技术原理
- 客户端向CDN节点发起对M3U8文件的访问请求,例如:
http://example.com/media/index.m3u8?MtsHlsUriToken=xxx
。 - CDN节点对客户端的访问请求进行校验,校验通过。
- CDN节点从源站下载原始M3U8文件,并缓存原始M3U8文件。
- CDN节点对原始M3U8文件的
#EXT-X-KEY
标签进行改写,增加加密方式、密钥URI和鉴权参数,例如:#EXT-X-KEY:METHOD=AES-128,URI="https://example.com/video.key?MtsHlsUriToken=xxx"
。 - CDN节点将改写后的M3U8文件返回给客户端。
- 客户端解析改写后的M3U8文件,拿到密钥URI地址
https://example.com/video.key?MtsHlsUriToken=xxx
,并发起访问请求。 - CDN节点收到客户端请求,鉴权通过之后,将key文件返回给客户端。
- 客户端继续解析改写后的M3U8文件,从CDN节点下载其中的TS视频文件。
- 客户端使用key文件内的密钥和前面
#EXT-X-KEY
标签内定义的加密算法来解密TS视频文件。
适用场景
HLS协议采用M3U8文件来告知客户端视频文件播放列表,客户端拿到M3U8文件以后就可以直接播放视频,为了避免源站的视频文件被非授权客户端访问,需要对HLS协议使用的TS视频文件做加密,对TS视频文件做了加密以后,还需要告知客户端解密方法,这里就可以通过配置M3U8标准加密改写功能,通过#EXT-X-KEY
标签来告知客户端加密算法、密钥URI和鉴权key。
使用方法
- 在视频点播控制台开启HLS标准加密参数透传。详细步骤请参见HLS标准加密参数透传。
- 客户端携带MtsHlsUriToken参数向CDN节点发起对M3U8文件的访问请求。其中,MtsHlsUriToken需要您自行搭建令牌服务,颁发用户令牌(即生成MtsHlsUriToken)。下述代码所生成的Token即是MtsHlsUriToken。下述Java示例代码中,您需要按实际情况变更的参数如下:
参数 传入值 ENCRYPT_KE 加密Key,为用户自定义字符串,长度为16、24或32位。 INIT_VECTOR 加密偏移量,为用户自定义字符串,长度为16位,不能含有特殊字符。 import org.apache.commons.codec.binary.Base64; import org.apache.commons.lang3.StringUtils; import javax.crypto.Cipher; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; import java.util.Arrays; public class PlayToken { //非AES生成方式,无需以下参数 private static String ENCRYPT_KEY = ""; //加密Key,为用户自定义的字符串,长度为16、24或32位 private static String INIT_VECTOR = ""; //加密偏移量,为用户自定义字符串,长度为16位,不能含有特殊字符 public static void main(String[] args) throws Exception { String serviceId = "12"; PlayToken playToken = new PlayToken(); String aesToken = playToken.generateToken(serviceId); //System.out.println("aesToken " + aesToken); //System.out.println(playToken.validateToken(aesToken)); //验证解密部分 } /** * 根据传递的参数生成令牌 * 说明: * 1、参数可以是业务方的用户ID、播放终端类型等信息 * 2、调用令牌接口时生成令牌Token * @param args * @return */ public String generateToken(String... args) throws Exception { if (null == args || args.length <= 0) { return null; } String base = StringUtils.join(Arrays.asList(args), "_"); //设置30S后,该token过期,过期时间可以自行调整 long expire = System.currentTimeMillis() + 30000L; base += "_" + expire; //自定义字符串,base的长度为16位字符(此例中,时间戳占13位,下划线(_)占1位,则还需传入2位字符。实际配置时也可按需全部更改,最终保证base为16、24或32位字符串即可。) //生成token String token = encrypt(base, ENCRYPT_KEY); //arg1为要加密的自定义字符串,arg2为加密Key //保存token,用于解密时校验token的有效性,例如:过期时间、token的使用次数 saveToken(token); return token; } /** * 验证token的有效性 * 说明: * 1、解密接口在返回播放密钥前,需要先校验Token的合法性和有效性 * 2、强烈建议同时校验Token的过期时间以及Token的有效使用次数 * @param token * @return * @throws Exception */ public boolean validateToken(String token) throws Exception { if (null == token || "".equals(token)) { return false; } String base = decrypt(token,ENCRYPT_KEY); //arg1为解密字符串,arg2为解密Key //先校验token的有效时间 Long expireTime = Long.valueOf(base.substring(base.lastIndexOf("_") + 1)); System.out.println("时间校验:" + expireTime); if (System.currentTimeMillis() > expireTime) { return false; } //从DB获取token信息,判断token的有效性,业务方可自行实现 TokenInfo dbToken = getToken(token); //判断是否已经使用过该token if (dbToken == null || dbToken.useCount > 0) { return false; } //获取到业务属性信息,用于校验 String businessInfo = base.substring(0, base.lastIndexOf("_")); String[] items = businessInfo.split("_"); //校验业务信息的合法性,业务方实现 return validateInfo(items); } /** * 保存Token到DB * 业务方自行实现 * * @param token */ public void saveToken(String token) { //TODO 存储Token } /** * 查询Token * 业务方自行实现 * * @param token */ public TokenInfo getToken(String token) { //TODO 从DB获取Token信息,用于校验有效性和合法性 return null; } /** * 校验业务信息的有效性,业务方可自行实现 * * @param infos * @return */ public boolean validateInfo(String... infos) { //TODO 校验信息的有效性,例如UID是否有效等 return true; } /** * AES加密生成Token * * @param encryptStr 要加密的字符串 * @param encryptKey 加密Key * @return * @throws Exception */ public String encrypt(String encryptStr, String encryptKey) throws Exception { IvParameterSpec e = new IvParameterSpec(INIT_VECTOR.getBytes("UTF-8")); SecretKeySpec skeySpec = new SecretKeySpec(encryptKey.getBytes("UTF-8"), "AES"); Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING"); cipher.init(Cipher.ENCRYPT_MODE, skeySpec, e); byte[] encrypted = cipher.doFinal(encryptStr.getBytes()); return Base64.encodeBase64String(encrypted); } /** * AES解密token * * @param encryptStr 解密字符串 * @param decryptKey 解密Key * @return * @throws Exception */ public String decrypt(String encryptStr, String decryptKey) throws Exception { IvParameterSpec e = new IvParameterSpec(INIT_VECTOR.getBytes("UTF-8")); SecretKeySpec skeySpec = new SecretKeySpec(decryptKey.getBytes("UTF-8"), "AES"); Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING"); cipher.init(Cipher.DECRYPT_MODE, skeySpec, e); byte[] encryptByte = Base64.decodeBase64(encryptStr); byte[] decryptByte = cipher.doFinal(encryptByte); return new String(decryptByte); } /** * Token信息,业务方可提供更多信息,这里仅给出示例供参考 */ class TokenInfo { //Token的有效使用次数,分布式环境需要注意同步修改问题 int useCount; //token内容 String token; }}
- CDN节点收到客户端请求后,鉴权通过则解密播放文件。若上述步骤二中生成的
MtsHlsUriToken
参数值为test
,则当CDN解密播放时,会将MtsHlsUriToken=test
追加到M3U8文件中#EXT-X-KEY
标签的URI末尾。具体的鉴权校验逻辑您需要自行实现,可以参考播放HLS标准加密视频中开启M3U8标准加密改写方式的解密服务的示例代码。