全部产品
云市场

HLS标准加密安全播放最佳实践

更新时间:2019-07-09 10:52:58

简介

为了加强标准加密视频在解密播放时解密秘钥的安全性,业务方需要同时提供令牌服务和解密服务,其中令牌服务生成鉴权令牌,解密服务用于验证令牌和获取解密秘钥。

说明:

  • 令牌是被CDN改写到解密接口上,所以在使用标准加密前必须先提工单开启CDN域名的令牌改写功能。
  • 令牌改写功能与CDN域名鉴权互斥,因此开启令牌改写功能,暂不支持CDN域名鉴权功能。
  • 非CDN播放地址,不支持处理令牌参数。

基本原理

标准加密视频的解密播放时,令牌生成以及令牌校验需要业务方封装服务,其他逻辑过程都封装在阿里云播放器中,基本原理如下:

说明:

  • 如是非阿里云播放器,需要业务方自行封装解密播放处理过程。

解密逻辑

准备条件

标准加密视频在进行解密播放时,需要业务方提供令牌服务和解密服务,其中解密服务是用于获取视频的解密秘钥,而令牌服务是用于生成鉴权令牌。

令牌服务

生成令牌时,可以通过业务方用户ID、视频播放终端(web、ios、android)、视频ID等信息按照一定方式生成令牌。

说明:

  • 标准播放器在播放视频时只会请求一次解密秘钥,所以令牌最好只能使用一次且具备过期时间等特性。
  • 令牌生成校验可参考 令牌示例代码

解密服务

解密服务主要对调用方进行鉴权和返回解密秘钥

说明:
此处鉴权主要是指校验令牌的有效性,且令牌只能通过MtsHlsUriToken参数传递到解密服务。

  • 根据传递的令牌参数,判断令牌的有效性,如:是否是令牌服务生成、令牌是否过期、是否已经被使用过等。
  • 如果令牌校验通过,则根据密文秘钥获取到明文秘钥并将明文秘钥通过base64decode后返回。

说明:解密服务可参考 解密服务示例代码

实现过程

标准加密视频在解密播放时会经历一下几个处理阶段:

说明:

  • 此处主要解析 阿里云播放器 在标准加密视频解密播放的处理逻辑。

  • 非阿里云播放器需要自行封装请求播放服务过程,获取到加密视频地址后直接给播放器进行解密播放即可,详细请参考 GetPlayInfo

  1. 业务方需要先调用令牌服务颁发令牌Token。

  2. 令牌服务根据传递的信息生成令牌Token。

  3. 令牌服务将生成的令牌Token返回给调用方。

    说明:以上步骤不属于 阿里云播器 处理过程,需要业务方单独调用令牌服务生成令牌Token并传递给播放器,而使用的是非阿里云播放器,那么业务方可选择将该步集成到播放器播放处理逻辑中。

  4. 调用方通过播放器提供的PlayConfig参数,将令牌Token设置到MtsHlsUriToken上。

  5. 播放器获取设置的MtsHlsUriToken参数、播放的视频ID等信息,从播放服务获取播放地址。

    说明:

  6. 播放器获取到标准加密m3u8地址并请求m3u8。

  7. CDN改写m3u8内容,将MtsHlsUriToken改写到解密接口上。

    例如:m3u8内容

  8. 播放器解析m3u8内容并获取到解密接口并请求获取解密秘钥。

    例如:http://decrypt.com/decrypt?Ciphertext=NDYwMjA0NjEtODBjNC00NTVmLTlhNWItZGU5ZWEwODRlMzkyNmNDME9SM2lyWEVCVnVGOTVWcXdYSFhoNTU2NUFYYXlBQUFBQUFBQUFBRGhqQ3poaWc2KzJHMHBmWkRMK281Z2lUdysrTlFtZzNZeHJNOElFYk1Bb3lpa0luczhtS3hX&MtsHlsUriToken=NWItZGU5ZWEwODRlMzky

  9. 解密服务接收到请求,调用令牌接口校验令牌Token的有效性。

  10. 令牌Token校验通过,则将明文秘钥通过base64decode解码后再后返回给播放器。

  11. 播放器在获取到解密秘钥后,将对标准加密视频进行解密播放。

    说明:通用播放器在播放视频时,只会在解密播放前调用一次解密接口获取秘钥,后续解密播放过程不会再次请求解密接口。

令牌示例代码

本示例代码主要是对 令牌生成令牌校验 相关实现的模板代码,不作为实际部署代码。

说明:

  • 以下代码仅仅提供令牌Token生成和校验逻辑的一个范例模板,不作为实际部署代码用
  • 该示例代码的令牌生成采用简单的AES加密生成,业务方也可自行实现其他生成方式
  • 关于生成的令牌Token的存储、获取由业务方自行实现。
  1. public class PlayToken {
  2. //非AES生成方式,无需以下参数
  3. private static String ENCRYPT_KEY = "";
  4. private static String INIT_VECTOR = "";
  5. /**
  6. * 根据传递的参数生成令牌
  7. * 说明:
  8. * 1、参数可以是业务方的用户ID、播放终端类型等信息
  9. * 2、调用令牌接口时生成令牌Token
  10. * @param args
  11. * @return
  12. */
  13. public String generateToken(String... args) throws Exception {
  14. if (null == args || args.length <= 0) {
  15. return null;
  16. }
  17. String base = StringUtils.join(Arrays.asList(args), "_");
  18. //设置30S后,该token过期,过期时间可以自行调整
  19. long expire = System.currentTimeMillis() + 30000L;
  20. base += "_" + expire;
  21. //生成token
  22. String token = encrypt(base, ENCRYPT_KEY);
  23. //保存token,用于解密时校验token的有效性,例如:过期时间、token的使用次数
  24. saveToken(token);
  25. return token;
  26. }
  27. /**
  28. * 验证token的有效性
  29. * 说明:
  30. * 1、解密接口在返回播放秘钥前,需要先校验Token的合法性和有效性
  31. * 2、强烈建议同时校验Token的过期时间以及Token的有效使用次数
  32. * @param token
  33. * @return
  34. * @throws Exception
  35. */
  36. public boolean validateToken(String token) throws Exception {
  37. if (null == token || "".equals(token)) {
  38. return false;
  39. }
  40. String base = decrypt(token, ENCRYPT_KEY);
  41. //先校验token的有效时间
  42. Long expireTime = Long.valueOf(base.substring(base.lastIndexOf("_") + 1));
  43. if (System.currentTimeMillis() > expireTime) {
  44. return false;
  45. }
  46. //从DB获取token信息,判断token的有效性,业务方可自行实现
  47. TokenInfo dbToken = getToken(token);
  48. //判断是否已经使用过该token
  49. if (dbToken == null || dbToken.useCount > 0) {
  50. return false;
  51. }
  52. //获取到业务属性信息,用于校验
  53. String businessInfo = base.substring(0, base.lastIndexOf("_"));
  54. String[] items = businessInfo.split("_");
  55. //校验业务信息的合法性,业务方实现
  56. return validateInfo(items);
  57. }
  58. /**
  59. * 保存Token到DB
  60. * 业务方自行实现
  61. *
  62. * @param token
  63. */
  64. public void saveToken(String token) {
  65. //TODO 存储Token
  66. }
  67. /**
  68. * 查询Token
  69. * 业务方自行实现
  70. *
  71. * @param token
  72. */
  73. public TokenInfo getToken(String token) {
  74. //TODO 从DB 获取Token信息,用于校验有效性和合法性
  75. return null;
  76. }
  77. /**
  78. * 校验业务信息的有效性,业务方可自行实现
  79. *
  80. * @param infos
  81. * @return
  82. */
  83. public boolean validateInfo(String... infos) {
  84. //TODO 校验信息的有效性,例如UID是否有效等
  85. return true;
  86. }
  87. /**
  88. * AES加密生成Token
  89. *
  90. * @param key
  91. * @param value
  92. * @return
  93. * @throws Exception
  94. */
  95. public String encrypt(String key, String value) throws Exception {
  96. IvParameterSpec e = new IvParameterSpec(INIT_VECTOR.getBytes("UTF-8"));
  97. SecretKeySpec skeySpec = new SecretKeySpec(key.getBytes("UTF-8"), "AES");
  98. Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING");
  99. cipher.init(Cipher.ENCRYPT_MODE, skeySpec, e);
  100. byte[] encrypted = cipher.doFinal(value.getBytes());
  101. return Base64.encodeBase64String(encrypted);
  102. }
  103. /**
  104. * AES解密token
  105. *
  106. * @param key
  107. * @param encrypted
  108. * @return
  109. * @throws Exception
  110. */
  111. public String decrypt(String key, String encrypted) throws Exception {
  112. IvParameterSpec e = new IvParameterSpec(INIT_VECTOR.getBytes("UTF-8"));
  113. SecretKeySpec skeySpec = new SecretKeySpec(key.getBytes("UTF-8"), "AES");
  114. Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING");
  115. cipher.init(Cipher.DECRYPT_MODE, skeySpec, e);
  116. byte[] original = cipher.doFinal(Base64.decodeBase64(encrypted));
  117. return new String(original);
  118. }
  119. /**
  120. * Token信息,业务方可提供更多信息,这里仅仅给出示例
  121. */
  122. class Token {
  123. //Token的有效使用次数,分布式环境需要注意同步修改问题
  124. int useCount;
  125. //token内容
  126. String token;
  127. }
  128. }

解密服务示例代码

说明:

  • 以下代码可直接运行启动,但仅仅作为测试使用,不可作为线上正式部署
  1. import com.aliyuncs.DefaultAcsClient;
  2. import com.aliyuncs.exceptions.ClientException;
  3. import com.aliyuncs.http.ProtocolType;
  4. import com.aliyuncs.kms.model.v20160120.DecryptRequest;
  5. import com.aliyuncs.kms.model.v20160120.DecryptResponse;
  6. import com.aliyuncs.profile.DefaultProfile;
  7. import com.sun.net.httpserver.Headers;
  8. import com.sun.net.httpserver.HttpExchange;
  9. import com.sun.net.httpserver.HttpHandler;
  10. import com.sun.net.httpserver.HttpServer;
  11. import com.sun.net.httpserver.spi.HttpServerProvider;
  12. import org.apache.commons.codec.binary.Base64;
  13. import java.io.IOException;
  14. import java.io.OutputStream;
  15. import java.net.HttpURLConnection;
  16. import java.net.InetSocketAddress;
  17. import java.net.URI;
  18. import java.util.regex.Matcher;
  19. import java.util.regex.Pattern;
  20. public class HlsDecryptServer {
  21. private static DefaultAcsClient client;
  22. static {
  23. //KMS的区域,必须与视频对应区域
  24. String region = "<视频对应区域>";
  25. //访问KMS的授权AK信息
  26. String accessKeyId = "<您使用的AccessKeyId>";
  27. String accessKeySecret = "<您使用的AccessKeySecrect>";
  28. client = new DefaultAcsClient(DefaultProfile.getProfile(region, accessKeyId, accessKeySecret));
  29. }
  30. /**
  31. * 说明:
  32. * 1、接收解密请求,获取密文秘钥和令牌Token
  33. * 2、调用KMS decrypt接口获取明文秘钥
  34. * 3、将明文秘钥base64decode返回
  35. */
  36. public class HlsDecryptHandler implements HttpHandler {
  37. /**
  38. * 处理解密请求
  39. * @param httpExchange
  40. * @throws IOException
  41. */
  42. public void handle(HttpExchange httpExchange) throws IOException {
  43. String requestMethod = httpExchange.getRequestMethod();
  44. if ("GET".equalsIgnoreCase(requestMethod)) {
  45. //校验token的有效性
  46. String token = getMtsHlsUriToken(httpExchange);
  47. boolean validRe = validateToken(token);
  48. if (!validRe) {
  49. return;
  50. }
  51. //从URL中取得密文密钥
  52. String ciphertext = getCiphertext(httpExchange);
  53. if (null == ciphertext)
  54. return;
  55. //从KMS中解密出来,并Base64 decode
  56. byte[] key = decrypt(ciphertext);
  57. //设置header
  58. setHeader(httpExchange, key);
  59. //返回base64decode之后的密钥
  60. OutputStream responseBody = httpExchange.getResponseBody();
  61. responseBody.write(key);
  62. responseBody.close();
  63. }
  64. }
  65. private void setHeader(HttpExchange httpExchange, byte[] key) throws IOException {
  66. Headers responseHeaders = httpExchange.getResponseHeaders();
  67. responseHeaders.set("Access-Control-Allow-Origin", "*");
  68. httpExchange.sendResponseHeaders(HttpURLConnection.HTTP_OK, key.length);
  69. }
  70. /**
  71. * 调用KMS decrypt接口解密,并将明文base64decode
  72. * @param ciphertext
  73. * @return
  74. */
  75. private byte[] decrypt(String ciphertext) {
  76. DecryptRequest request = new DecryptRequest();
  77. request.setCiphertextBlob(ciphertext);
  78. request.setProtocol(ProtocolType.HTTPS);
  79. try {
  80. DecryptResponse response = client.getAcsResponse(request);
  81. String plaintext = response.getPlaintext();
  82. //注意:需要base64 decode
  83. return Base64.decodeBase64(plaintext);
  84. } catch (ClientException e) {
  85. e.printStackTrace();
  86. return null;
  87. }
  88. }
  89. /**
  90. * 校验令牌有效性
  91. * @param token
  92. * @return
  93. */
  94. private boolean validateToken(String token) {
  95. if (null == token || "".equals(token)) {
  96. return false;
  97. }
  98. //TODO 业务方实现令牌有效性校验
  99. return true;
  100. }
  101. /**
  102. * 从URL中获取密文秘钥参数
  103. * @param httpExchange
  104. * @return
  105. */
  106. private String getCiphertext(HttpExchange httpExchange) {
  107. URI uri = httpExchange.getRequestURI();
  108. String queryString = uri.getQuery();
  109. String pattern = "Ciphertext=(\\w*)";
  110. Pattern r = Pattern.compile(pattern);
  111. Matcher m = r.matcher(queryString);
  112. if (m.find())
  113. return m.group(1);
  114. else {
  115. System.out.println("Not Found Ciphertext Param");
  116. return null;
  117. }
  118. }
  119. /**
  120. * 获取Token参数
  121. *
  122. * @param httpExchange
  123. * @return
  124. */
  125. private String getMtsHlsUriToken(HttpExchange httpExchange) {
  126. URI uri = httpExchange.getRequestURI();
  127. String queryString = uri.getQuery();
  128. String pattern = "MtsHlsUriToken=(\\w*)";
  129. Pattern r = Pattern.compile(pattern);
  130. Matcher m = r.matcher(queryString);
  131. if (m.find())
  132. return m.group(1);
  133. else {
  134. System.out.println("Not Found MtsHlsUriToken Param");
  135. return null;
  136. }
  137. }
  138. }
  139. /**
  140. * 服务启动
  141. *
  142. * @throws IOException
  143. */
  144. private void serviceBootStrap() throws IOException {
  145. HttpServerProvider provider = HttpServerProvider.provider();
  146. //监听端口9999,能同时接受30个请求
  147. HttpServer httpserver = provider.createHttpServer(new InetSocketAddress(9999), 30);
  148. httpserver.createContext("/", new HlsDecryptHandler());
  149. httpserver.start();
  150. System.out.println("hls decrypt server started");
  151. }
  152. public static void main(String[] args) throws IOException {
  153. HlsDecryptServer server = new HlsDecryptServer();
  154. server.serviceBootStrap();
  155. }
  156. }