通过HLS加密防止视频泄露
环境准备
已下载SDK。
场景说明
使用防盗链机制可以控制播放行为,避免非授权用户通过播放URL下载或播放点播视频,但无法阻止恶意的付费用户将视频下载到本地后进行二次分发。
为了有效防止视频泄露和盗链问题,华为云视频点播提供了对HLS视频内容进行加密的能力。加密后的视频,即使恶意用户下载也无法分发给其他人观看。HLS加密涉及到业务侧的密钥服务和Token生成服务的搭建,所以本方案主要适用于能自行搭建一套完整的鉴权及密钥管理服务的业务侧。
实现原理
华为云视频点播提供的HLS加密使用的HLS规范中的通用加密方案,通过指定的AES-128加密算法来加密每一个TS,并在生成的m3u8文件中描述播放器如何解密TS文件的方法,支持所有的HLS播放器。
本方案中,点播服务集成了华为云的KMS,向HLS加密提供密钥。
- 加密流程
- 业务侧将视频上传到点播服务(VOD)后,请求HLS加密。
- 点播服务收到加密请求后,向KMS请求加密密钥,并将获取的密钥ID和密钥密文存储在点播服务中。
- 点播服务向媒体处理服务请求HLS加密,媒体处理服务通过转码功能将对应的视频进行加密。
转码加密后生成的m3u8文件带有“#EXT-X-KEY”标签,该标签包含了“METHOD”和“URI”属性,其中“URI”即为业务侧搭建的密钥管理服务的地址,示例如下所示。
若加密后使用的播放地址是https,则密钥管理服务的地址也需要配置为https协议,否则无法在点播控制台预览播放。
#EXTM3U #EXT-X-VERSION:3 #EXT-X-TARGETDURATION:6 #EXT-X-MEDIA-SEQUENCE:0 #EXT-X-KEY:METHOD=AES-128,URI="https://domain-sample/encrypt/get-key?asset_id=6aee80009c4ca6970f508d6334194794",IV=0x80a3ff24ccd788042ca7f2237e74c59d #EXTINF:5.000000, 6aee80009c4ca6970f508d6334194794_1_1920X1080_3000_0_0.ts #EXTINF:5.000000, 6aee80009c4ca6970f508d6334194794_1_1920X1080_3000_0_1.ts #EXT-X-ENDLIST
- 加密后,点播服务通过CDN将加密的HLS视频文件进行加速分发。
- 解密流程
- 终端用户登录播放器终端,业务侧会对终端用户进行身份校验,校验通过后,会为播放终端分配一个Token,并将带Token的播放地址返回给播放器端。
示例:若转码加密后的HLS视频播放地址为:https://1280.cdn-vod.huaweicloud.com/input/test.m3u8,则播放器终端获取的播放地址为:https://1280.cdn-vod.huaweicloud.com/input/test.m3u8?token={token}
- 播放器终端通过带Token的播放URL向CDN请求播放。由于Token是动态的,所以CDN收到请求后,会直接回源到点播服务。点播服务会将请求URL中的Token写入请求的m3u8文件的“URI”中。
点播服务返回给CDN的m3u8文件中会携带播放终端的Token值,示例如下所示。
#EXTM3U #EXT-X-VERSION:3 #EXT-X-TARGETDURATION:6 #EXT-X-MEDIA-SEQUENCE:0 #EXT-X-KEY:METHOD=AES-128,URI="https://domain-sample/encrypt/get-key?asset_id=6aee80009c4ca6970f508d6334194794&token={token}",IV=0x80a3ff24ccd788042ca7f2237e74c59d #EXTINF:5.000000, 6aee80009c4ca6970f508d6334194794_1_1920X1080_3000_0_0.ts #EXTINF:5.000000, 6aee80009c4ca6970f508d6334194794_1_1920X1080_3000_0_1.ts #EXT-X-ENDLIST
- 播放终端解析返回的m3u8文件,得到EXT-X-KEY标签中的“URI”内容,向“URI”请求密钥。
- 业务侧的密钥管理服务收到请求后,先验证Token的合法性,若Token合法,则通过调用点播服务的API查询密钥。
密钥管理服务可以选择将查询到的密钥缓存在本地,当下次有其它播放终端请求时,可以直接返回,无需每次都向点播服务获取。
- 密钥管理服务将点播服务返回的密钥返回给播放终端。播放终端通过获取的密钥解密播放m3u8文件。
- 终端用户登录播放器终端,业务侧会对终端用户进行身份校验,校验通过后,会为播放终端分配一个Token,并将带Token的播放地址返回给播放器端。
搭建相关服务
若需要使用HLS加密功能,并实现解密播放功能,您需要在您的业务端服务器搭建密钥管理服务和Token生成服务。
- 密钥管理服务,搭建的密钥管理服务需要具备如下功能,密钥管理服务示例代码请参见示例代码。
- 支持身份鉴定:如实现原理中描述,密钥管理服务收到密钥请求时,需要验证请求的Token是否合法。
- 支持向点播服务获取密钥:HLS加密的原始密钥是存储在点播服务中的,因此,密钥管理服务需要调用点播服务API,获取对应媒资的密钥。
- 支持缓存获取的密钥:为避免每次都从点播服务获取密钥,密钥管理服务应该具备缓存功能,将获取的密钥进行缓存。
- Token生成服务:当终端用户登录您的播放终端时,您的业务侧服务应该对终端用户的合法性进行校验,生成对应的Token,将带Token的播放地址返回播放器端。Token生成示例代码请参见示例代码。
生成的Token需要包含大写字母、小写字母和数字,长度可自行限制。每一次登录分配一个唯一的Token,且具备时效性。遵循权限最小化原则,建议仅将该Token用于HLS加密视频的场景。
视频加密
- 上传待加密的视频文件。
若待加密的视频还未上传,可以通过控制台上传等方式上传到点播服务中。
- 配置获取解密密钥URL。
加密前,需要将搭建相关服务中搭建的密钥管理服务的地址配置到点播服务中,加密时,将把该地址写入转码生成的m3u8文件中。
- 创建转码模板。
HLS加密是通过转码来实现的,所以在转码前,需要创建开启加密的转码模板。
- 在左侧导航树中选择“全局设置 > 转码设置”,进入转码设置页面。
- 单击“自定义转码模板组”,在新建议转码模板页面配置相关参数。
图1 设置基本信息
基本信息中“输出格式”选择“HLS”,打开“加密”开关,其它参数可以根据实际需求配置,具体可参考转码设置。
- 单击“确定”,完成HLS转码模板的配置。
- 视频加密。
- 在左侧导航树中,选择“音视频管理”,进入音视频管理页面。
- 勾选需要HLS加密的视频,单击“转码”。
- 在弹出框中选择步骤3中创建的转码模板,单击“确定”。
视频开始转码,当转码状态为“转码成功”时表示转码完成,即HLS加密完成。
视频播放
由于本方案采用的HLS标准加密,对于支持HLS协议播放的播放器都可以进行解密播放。
- 登录视频点播控制台,在左侧导航树中选择“音视频管理”,进入音视频管理页面。
- 在已经加密的视频行单击“管理”,选择“播放地址”页签。
- 在对应的HLS格式行单击,播放HLS视频。
图2 播放地址
- 打开浏览器的开发者模式,可以看到控制台在预览播放时有自动通过配置的获取密钥URL去请求密钥,并解密播放。
图3 浏览器开发者模式
示例代码
- 密钥管理服务示例代码
示例中采用UUID方式生成Token,您也可以自行选择生成方式。此外,示例代码中未包含登录终端用户的合法性校验,若有需要,您也可以自行实现。
密钥管理服务收到密钥请求时,会先查看缓存中是否保存该媒资的解密密钥,若没有,则调用点播服务端SDK查询密钥。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 java.util.Base64; import java.util.UUID; import javax.servlet.http.HttpServletResponse; import org.apache.commons.lang3.StringUtils; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import com.huawei.kms.initvodclient.VodClientFactory; import com.huawei.kms.util.CacheUtils; import com.huaweicloud.sdk.vod.v1.model.ShowAssetCipherRequest; import retrofit2.http.Header; @RestController public class KeyManagerController { /** * 给合法客户端分配token并返回带token的播放url * * @param accessToken 终端携带的鉴权信息,这里需要校验身份信息是否合法 * @param playUrl 播放url * @return 返回带token的播放url */ @GetMapping("/get-url") public String getTokenPlayUrl(@Header("access-token") String accessToken, @RequestParam(value = "play_url", required = true) String playUrl) { // 为合法终端分配token, 这里"*****"需要客户侧代码生成 String token = "*****"; // 构造带token的play_url并返回, http://{domain}/asset/{asset_id}/play_video/index.m3u8?token={token} return playUrl.substring(0, playUrl.lastIndexOf("/") + 1) + playUrl.substring(playUrl.lastIndexOf("/") + 1) + "?token=" + token; } /** * @param asset_id 媒资id * @param token 给终端分配的token,这里需要校验token是否合法,只给校验通过的终端返回密钥 * @param response * @return 返回字节数组类型的密钥 */ @GetMapping(value = "/get-key",headers = "Accept=application/octet-stream") public byte[] getKey(@RequestParam(value = "asset_id", required = true) String asset_id, @RequestParam(value = "token", required = true) String token, HttpServletResponse response) { // 获取密钥,先从缓存中获取,假如不存在,再从点播服务获取。这里以本地缓存为例,用户可以自行选择缓存方式,比如存在缓存数据库 String key = CacheUtils.getCipherFromCache(asset_id); if (StringUtils.isEmpty(key)) { ShowAssetCipherRequest request = new ShowAssetCipherRequest(); request.withAssetId(asset_id); key = VodClientFactory.getClient().showAssetCipher(request).getDk(); // 跨域放通,填写实际的站点或填写“*” response.setHeader("Access-Control-Allow-Origin", "*"); response.setHeader("Content-Length", "16"); // 设置返回密钥的数据类型 response.setHeader("Content-Type", "application/octet-stream"); // 更新缓存 CacheUtils.updateCipherFromCache(asset_id, key); } return Base64.getDecoder().decode(key); } }
- 获取VodClient示例代码
import com.huaweicloud.sdk.core.auth.BasicCredentials; import com.huaweicloud.sdk.core.auth.ICredential; import com.huaweicloud.sdk.vod.v1.VodClient; import com.huaweicloud.sdk.vod.v1.region.VodRegion; public class VodClientFactory { private final static String AK = System.getenv("CLOUD_SDK_AK"); private final static String SK = System.getenv("CLOUD_SDK_SK"); private final static String REGION="cn-north-4"; // 服务实际节点,如cn-north-1,cn-east-2 private static volatile VodClient vodClient = null; public static VodClient getClient() { if (vodClient == null) { synchronized (VodClient.class) { if (vodClient == null) { ICredential auth = new BasicCredentials() .withAk(AK) .withSk(SK); vodClient = VodClient.newBuilder().withCredential(auth) .withRegion(VodRegion.valueOf(REGION)) .build(); } } } return vodClient; } }
- 缓存示例代码
密钥管理服务从VOD中获取到解密密钥后,需要将密钥缓存下来,避免同一媒资重复请求VOD获取。示例中采用本地缓存方式,您也可以选择数据库方式缓存。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import java.util.concurrent.TimeUnit; public class CacheUtils { private static Cache<String, String> cipherCache = CacheBuilder.newBuilder() .maximumSize(100) // 设置缓存的最大容量 .expireAfterWrite(10, TimeUnit.MINUTES) // 设置缓存在写入一分钟后失效 .concurrencyLevel(10) // 设置并发级别为10 .recordStats() // 开启缓存统计 .build(); public static String getCipherFromCache(String key) { return cipherCache.getIfPresent(key); } public static void updateCipherFromCache(String key, String value) { cipherCache.put(key, value); } }
- 上述示例代码所需的Maven依赖,如下所示:
注:以下使用的jar包版本非固定,以JAVA项目以及jar包的实际情况为准。
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.3.12.RELEASE</version> <relativePath/> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.7</version> </dependency> <dependency> <groupId>com.huaweicloud.sdk</groupId> <artifactId>huaweicloud-sdk-vod</artifactId> <version>3.1.72</version> </dependency> <dependency> <groupId>com.squareup.retrofit2</groupId> <artifactId>retrofit</artifactId> <version>2.5.0</version> </dependency> <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>27.0.1-jre</version> </dependency> </dependencies>