知识付费——移动端音视频加密、防盗播实现方案

近几年移动端知识付费App,越来越多越来越火爆。例如:网易公开课、网易云课堂、樊登读书、逻辑思维、i春秋,甚至于知乎App都上架付费课程了。
移动端付费课程,基本是以视频+音频形式呈现给大家的。那么对于这些付费课程:如何在保证用户体验的前提下,防止媒体资源被盗播,维护内容生产者的利益,就成为一个急需解决的问题

注意:保证用户体验是前提。否则再好的课,用户体验垃圾,也卖不出去。

如今市面上,移动端加密、防盗播的方式很多。这里只是讨论一种:我认为的用户体验较好,技术实现成熟,又有效防盗播的方式

注意:防止盗播,并不能100%杜绝盗播。只能不断增加App的破解成本,完全无法破解的App是不存在的。所以,想100%防止盗播也是不可能实现的。

一、实现方案

这里采用的方案是:客户端播放AES-128加密的m3u8媒体资源

为什么是m3u8 ?

  • m3u8采用AES-128对称加密算法加密,技术成熟稳定
  • 前边说了,保证用户体验为前提

音视频播放过程中,用户进入播放页后,音视频的秒开率(1秒内成功加载的播放数/播放总数)是影响用户体验的重要指标;m3u8媒体资源是一个文本文件,其由一个个ts视频片段的播放地址构成,选择合适的ts切片大小,能有效提高音视频的秒开率,保证用户的观看体验。

用户体验的大前提满足了,那如何实现呢?

  • 客户端播放的视频源为 AES-128对称加密的 m3u8 媒体资源;

播放地址最终组织形式如下:

https://domain/course101.m3u8?playKey=i_am_decrypting_key101
  • 播放地址 https://domain/course101.m3u8playKey 需从服务端获取;

加密m3u8的解密关键就在于playKey,因此,防止playKey被破解是防盗播的一个关键点

  • 将最终播放地址交于播放器播放

二、实现举例

对于m3u8媒体资源,无论是 视频切片加密ts视频片段解密播放 在技术实现上已经非常成熟,不存在技术壁垒,实现已经不是问题。
那么摆在移动端最主要的问题就是:防止媒体资源被盗播,维护内容生产者的利益

注:虽然技术实现已经不是问题,但这里还是会把 移动端技术实现的细节防破解(防盗播)的实现细节进行呈现。

技术实现上,大概分为以下几个步骤:

  • 获取媒体资源播放地址
  • 获取媒体资源的解密token (playKey)
  • 拼接最终播放地址,播放m3u8媒体资源

下面我们从移动端请求课程内容列表,到成功播放的全过程来举一个例子:

1、获取ID为101课程的内容列表

首先通过课程ID 101向服务端发起请求,获取该课程的内容列表。请求方式和返回结果如下:

a、发起请求:

https://domain/xxx/getCourseList.do?courseId=101

为了简单易懂,这里简单模拟一个get请求:

  • 请求参数 courseId101
  • 请求地址为 https://domain/xxx/getCourseList.do
  • 获取id为101的课程,其对应的内容列表。

b、返回结果:

课程101的课程内容列表如下:

[
    {
      "mediaId": "101", //媒体资源id
      "mediaUrl": "n3GuNo5Rc44anmLGrRU8Rne/JU9cHzc1vXZWiYEwcD0=", // 媒体资源的m3u8播放地址
      "encryptId": "LLVahEH+HjZEOJk6RfJtww==" // 获取媒体资源playKey的encryptId
    },
    {
      "mediaId": "102",
      "mediaUrl": "n3GuNo5Rc44anmLGrRU8RlKQGcM3X5R3oVqpuCrTpDk=",
      "encryptId": "Ez3wHHWx5ddq+D2miV2dFg=="
    },
    {
      "mediaId": "103",
      "mediaUrl": "n3GuNo5Rc44anmLGrRU8RqLaIqPDWyZQ5VotuUSyNI8=",
      "encryptId": "R5a31ZJIy9Z5+tNPplxHLQ=="
    }
  ]

正如注释中写的:

  • mediaId媒体资源id
  • mediaUrl媒体资源的m3u8播放地址

可能会疑问: n3GuNo5Rc44anmLGrRU8Rne/JU9cHzc1vXZWiYEwcD0= 是什么鬼,为什么不像播放地址?
这里正是防止媒体资源被盗播,维护内容生产者的利益的 第二步
这里将资源地址https://domain/course101.m3u8进行了一个简单的AES加密
对于其资源地址的加解密,会在下文进行详细说明。

  • encryptId 则是 用于获取媒体资源的解密playKey

encryptId的具体作用,会在下文中详细说明

注:这里服务器要做一个用户权限的校验:未购买用户不返回其 mediaUrl 和 encryptId

c、未购买用户的返回结果:

未购买用户不返回其 mediaUrl 和 encryptId
正是预防移动端被不良用心的人员,恶意采用Http抓包进行破解。是防止媒体资源被盗播,维护内容生产者的利益的 第一步,也是很重要的一步。 因为:

  • 移动端无论如何防破解,总不能100%保证无法破解;
  • 想要获取播放地址,必须购买课程,对于用心不了的人员,增加了其破解成本;

未购买用户,通过课程ID 101向服务端发起请求,返回数据如下:

[
    {
      "mediaId": "101",
      "mediaUrl": "",
      "encryptId": ""
    },
    {
      "mediaId": "102",
      "mediaUrl": "",
      "encryptId": ""
    },
    {
      "mediaId": "103",
      "mediaUrl": "",
      "encryptId": ""
    }
  ]

注:未购买用户,请求某课程列表接口时,不要返回 媒体资源的m3u8播放地址获取媒体资源playKey的encryptId。这很重要

d、mediaUrl与encryptId解密:

为防止移动端App被Http抓包,mediaUrlencryptId 都不是明文传输的。因此移动端拿到课程的内容列表后,首先需要对mediaUrlencryptId进行解密。

关于解密:

  • 对称加密算法,大家可以根据实际需求,自行进行选择;
  • 解密相关代码,建议由C语言实现,打成SO添加到移动客户端中

原因是增加移动端破解难道,提高破解成本

这里只是采用了简单的AES对称加密:

完成解密后的数据格式如下:

[
    {
      "mediaId": "101", //媒体资源id
      "mediaUrl": "https://domain/course101.m3u8", // 媒体资源的m3u8播放地址
      "encryptId": "qazwsx101" // 获取媒体资源playKey的encryptId
    },
    {
      "mediaId": "102",
      "mediaUrl": "https://domain/course102.m3u8",
      "encryptId": "qazwsx102"
    },
    {
      "mediaId": "103",
      "mediaUrl": "https://domain/course103.m3u8",
      "encryptId": "qazwsx103"
    }
  ]

2、获取 playKey

在第1步中,已成功获取到媒体资源的播放地址 https://domain/course101.m3u8encryptId。为了播放媒体资源,下面来获取到加密m3u8视频的解密playKey

这里 playKey 仍然需要从服务端进行获取,获取方式如下:

模拟请求

发起请求:

https://domain/xxx/getMediaToken.do?encryptId=C0AFC0A81&mediaId=101

返回结果:

{
  "playKey": "I_am_playkey_001"
}

这里仍然模拟一个简单get请求:

  • 请求参数 encryptIdqazwsx101
  • 请求参数 mediaId101
  • 请求地址为 https://domain/xxx/getMediaToken.do

注:playKey是加密m3u8解密的关键,是防止媒体资源被破解的重中之重。

关于 playKey 加密

这里是防止媒体资源被盗播,维护内容生产者的利益的 第三步和第四步

  • 1、playKey 服务端需加密传输
  • 2、请求getMediaToken.do接口时,服务端需对该用户的购买状态进行验证,未购用户不返回其对应的playKey;
  • 3、移动端获取到playKey后,在SO(C层代码达成的)中对playKey进行解密;
  • 4、解密后的playKey存在一定的过期时间;或者使用次数限制(建议服务端限定只能使用一次)

加密算法要与mediaUrl 和encryptId 的解密算法区分开,增加破解成本

3、拼接播放地址

经过以上步骤,购买用户成功获取到 播放地址 https://domain/xxx/getMediaToken.do解密key I_am_playkey_001
将mediaUrl 与playKey 拼接起来,可以交给播放器播放了:

https://domain/course101.m3u8?playKey=i_am_decrypting_key101

到此,移动端的工作完成。
OK. 移动端完事大吉

到这里播放器服务端的交互刚刚开始。

这里有必要先介绍一下,加密m3u8视频的文件格式:

加密m3u8视频的文件格式

#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:19
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-KEY:METHOD=AES-128,URI="http://domain//hls.key"
#EXT-CUSTOM-YUNXIN:ntsversion=0,ntsprivatedata=d68edb4f4ee3c64233b1ece785758291727d053d
#EXTINF:11.960000,
http://domain/encrypt0.ts
#EXTINF:9.200000,
http://domain/encrypt1.ts
#EXTINF:10.000000,
http://domain/encrypt2.ts
#EXT-X-ENDLIST

以上为加密m3u8视频的文件格式:

  • 播放器在播放前,会用该地址 http://domain//hls.key向业务服务器发起请求,请求加密m3u8视频的解密秘钥

解密秘钥如下图所示:

加密m3u8视频的解密秘钥并非playKey=i_am_decrypting_key101,而是一个16字节的文件。

m3u8的解密秘钥实际是一个16字节的文件。
这里肯定会有很多疑问,别着急,我们继续前边的话题播放器服务端交互。

播放器 与 服务端 交互

  • 播放器服务端 发起https://domain/course101.m3u8?playKey=i_am_decrypting_key101 请求后;
  • 服务端 处理相对复杂

a、首先校验playKey=i_am_decrypting_key101是否有效;
b、若有效则将加密m3u8视频中的http://domain//hls.key更换为http://domain//hls.key?playKey=i_am_decrypting_key101
c、下发m3u8文件给播放器;

  • 播放器收到服务端下发的加密m3u8文件后:
    a、播放器读取加密m3u8文件,获取解密秘钥的请求地址http://domain//hls.key?playKey=i_am_decrypting_key101
    b、播放器服务端发起 http://domain//hls.key?playKey=i_am_decrypting_key101请求,请求解密秘钥;
  • 服务端收到播放器的请求后,需对playKey=i_am_decrypting_key101进行校验,查看是否过期或者被使用过,若存在异常则不下发秘钥;若正常,则下发秘钥
  • 播放器收到秘钥文件后,正常播放;

OK. 这次是真的 完事大吉

4、移动端加固

如果仅仅做到上一步就结束了,移动端被破解的概率还是很高。因为SO任然可以被反编译破解,而且SO的反编译技术也已经相当成熟。
所以,无论在SO中采用了怎样复杂的加密算法,SO被反编译后,还是有被破解的可能

因此,以上步骤都完成后,我们仍然要对我们最终生成的APP进行一次加密。Android端推荐采用APP加固处理

5、SO动态下发,定期更新加密算法

以上步骤都做完,如果APP仍然被破解了。我们可以再加一层防破解处理:动态下发SO,定期更新加密算法
这样就算线上APP被破解了,也可以在不发版的情况下从容应对,更新一下加密算法就可以了。

三、总结一下

这里来简单总结一下,上边介绍的实现步骤

  • a、移动端拿 courseId 向服务端发起Http请求,获取课程的内容列表
  • b、服务端对用户进行权限验证,未购买用户不返回其对应的播放地址
  • c、移动端在SO层中对 mediaUrl与 encryptId 进行解密
  • d、移动端 用解密后的encryptId,向服务端请求对应媒体资源的 playKey
  • e、服务端对用户进行权限验证,未购买用户不返回其对应的 playKey
  • f、移动端 在SO层中对 playKey 进行解密

注意,一定要与第三步的解密算法区分开

  • g、拼接mediaUrl 与 playKey ,交给播放器播放;

到此,移动端工作完成

  • h、播放器服务端发起下发m3u8请求
  • i、服务端校验playKey;并动态替换m3u8中的URI字段
  • j、播放器从m3u8文件中取出URI字段后,向服务端请求解密秘钥;
  • k、服务端校验playKey;并返回解密秘钥文件;
  • l、播放器播放

到此,加密m3u8 终于播放出来了

  • m、移动端App加固处理;
  • n、为增强破解难度,这里解密SO可由动态下发,定期更新加密算法
  • m、如果仍然担心App被破解,服务端下发的解密秘钥文件可以设置为非明文状态;客户端拿到解密秘钥后,以本地代理的方式设置给播放器进行播放。

注:这里有三个关键步骤:1、服务端对用户进行权限验证,未购买用户不返回其对应的播放地址;2、移动端 在SO层中对 playKey 进行解密;3、移动端App加固处理。

这三个关键步骤缺一不可,缺少任何一个移动端的破解难度和成本都将大大降低。

  • 未购买用户不返回其对应的播放地址

可以极大的降低付费课程被大面积破解的情况出现。因为购买课程亦需要成本。破解的课程越多,那需要购买的课程就越多,破解成本也就越高。

  • 在SO层中对 playKey 进行解密

对于加密m3u8媒体资源,破解的关键就在于playKey,因此playKey的加密算法应足够的复杂

  • 移动端App加固处理

移动端App加固,是移动端代码反编译非常重要的一环。如果没有这一环在SO层中对 playKey 进行解密的作用不是很大,因为无论SO中加密算法如何复杂,只要SO被翻遍后,再复杂的加密算法也没有意义了。

以上为全部处理流程

如果以上步骤都做了,付费音视频被盗播的可能性应该不大;但仍然存在Http抓包的可能性:

  • http抓包时,会抓到https://domain/course101.m3u8?playKey=i_am_decrypting_key101请求

由于playKey只有一次使用有效,或者几个小时的超时时间。因此该内容无法被大规模传播;

  • http抓包时,会抓到播放器的秘钥下发请求 http://domain//hls.key?playKey=i_am_decrypting_key101

由于服务端存在playKey有效性的校验,下次使用失效,因此也无法大规模传播;
如果对服务端明文秘钥文件下发存在疑虑,可以做以上流程图的“防盗播9”;

  • 防盗播9 加上APP加固,在实现上应该是比较安全了

如果这样仍被破解,那我也只能表示佩服 佩服

03-05 16:17