HLS标准加密需要配合密钥管理服务和令牌服务使用,本文为您介绍HLS标准加密的相关概念、准备工作和接入流程。
HLS加密解密流程
上传加密流程图播放解密流程图
使用密钥管理服务会产生费用,计费详情请参见KMS计费说明。
令牌服务和解密服务需要自行搭建。
相关概念
准备工作
开通视频点播服务并登录视频点播控制台,开启对应服务区域的存储空间,具体操作,请参见开通存储管理。
在视频点播控制台上配置加速域名,并开启该域名视频相关中的HLS标准加密参数透传,开启后MtsHlsUriToken参数可以重写。具体操作,请参见添加加速域名、HLS标准加密参数透传。
登录RAM访问控制,获取并保存AccessKey ID和AccessKey Secret。
开通密钥管理服务并获取Service Key。
说明Service Key是密钥管理服务的一种加密主Key,接入标准加密的密钥必须要使用该Service Key生成。
Service Key与视频存储的源站地域必须一致,例如:视频存储在华东2,则Service Key必须是在华东2创建。
登录视频点播控制台,选择 ,在标准加密页面,创建Service Key。
创建成功后,需调用GenerateDataKey接口,请求参数
KeyId
传入别名alias/acs/vod,请求后的返回参数KeyId
将用于后续的转码处理。
已搭建服务端SDK,具体操作,请参见服务端SDK安装。
接入流程
添加加密模板和不转码模板。
HLS标准加密转码需要创建两个转码模板:加密模板和不转码模板。
不转码模板在开启对应服务区域的存储空间后会自动生成。
说明目前点播上传视频默认都会自动触发转码(自动触发暂不支持HLS标准加密)),因此对于标准加密为防止自动触发转码,需要先使用不转码模板上传视频(该类模板不会自动触发转码),然后再调用提交媒体转码作业接口发起标准加密转码。
添加加密模板并保存加密模板ID,操作流程如下:
RAM授权。
使用RAM服务给视频点播授权访问业务方密钥管理服务的权限,进入RAM授权页面,单击同意授权。
搭建密钥管理服务,封装阿里云密钥管理服务(KMS)。
调用GenerateDataKey接口生成一个AES_128密钥,该接口只需要传KeyId(Service Key)和KeySpec(固定为:AES_128)即可,其他参数不用传,否则可能加密失败。
调用成功后保存返回参数
CiphertextBlob
(密文密钥)的值。说明使用密钥会产生费用,具体费用说明,请参见API调用费用。
搭建令牌颁发服务,生成MtsHlsUriToken。
Java示例代码以及示例代码需要手动变更的地方如下所示:
ENCRYPT_KEY:加密字符串,长度为16,用户自行定义。
INIT_VECTOR:自定义字符串,长度为16,不能含有特殊字符。
playToken.generateToken(""):自定义字符串,长度为16。
最终代码所生成的Token即是MtsHlsUriToken。
import com.sun.deploy.util.StringUtils; import org.apache.commons.codec.binary.Base64; 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 = ""; //长度为16的自定义字符串,不能有特殊字符 private static String INIT_VECTOR = ""; public static void main(String[] args) throws Exception { PlayToken playToken = new PlayToken(); playToken.generateToken(""); } /** * 根据传递的参数生成令牌 * 说明: * 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的最终长度为16位字符(此例中,时间戳占13位,下划线(_)占1位,则还需传入2位字符。实际配置时也可按需全部更改,最终保证base为16位字符串即可。) base += "_" + expire; //生成token String token = encrypt(base, ENCRYPT_KEY); System.out.println(token); //保存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); //先校验token的有效时间 Long expireTime = Long.valueOf(base.substring(base.lastIndexOf("_") + 1)); if (System.currentTimeMillis() > expireTime) { return false; } //从DB获取token信息,判断token的有效性,业务方可自行实现 Token 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 Token getToken(String token) { //TODO 从DB 获取Token信息,用于校验有效性和合法性 return null; } /** * 校验业务信息的有效性,业务方可自行实现 * * @param infos * @return */ public boolean validateInfo(String... infos) { //TODO 校验信息的有效性,例如UID是否有效等 return true; } /** * AES加密生成Token * * @param key * @param value * @return * @throws Exception */ public String encrypt(String value, String key) throws Exception { IvParameterSpec e = new IvParameterSpec(INIT_VECTOR.getBytes("UTF-8")); SecretKeySpec skeySpec = new SecretKeySpec(key.getBytes("UTF-8"), "AES"); Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING"); cipher.init(Cipher.ENCRYPT_MODE, skeySpec, e); byte[] encrypted = cipher.doFinal(value.getBytes()); return Base64.encodeBase64String(encrypted); } /** * AES解密token * * @param key * @param encrypted * @return * @throws Exception */ public String decrypt(String encrypted, String key) throws Exception { IvParameterSpec e = new IvParameterSpec(INIT_VECTOR.getBytes("UTF-8")); SecretKeySpec skeySpec = new SecretKeySpec(key.getBytes("UTF-8"), "AES"); Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING"); cipher.init(Cipher.DECRYPT_MODE, skeySpec, e); byte[] original = cipher.doFinal(Base64.decodeBase64(encrypted)); return new String(original); } /** * Token信息,业务方可提供更多信息,这里仅仅给出示例 */ class Token { //Token的有效使用次数,分布式环境需要注意同步修改问题 int useCount; //token内容 String token; }}
搭建解密服务。
重要解密服务在播放视频前就需要启动,否则视频无法正常解密。
解密密钥EDK(密文密钥),调用Decrypt接口进行解密。如果业务方需要对解密接口进行安全验证,则需要提供令牌生成服务,生成的令牌能够在解密服务中被解析验证。
解密接口返回的数据,是GenerateDataKey生成的两种密钥中的明文密钥(PlainText)经过base64decode之后的数据。
Java示例代码以及示例代码需要手动变更的地方如下所示:
region:填写地域,例如华东2(上海),填写
cn-shanghai
。AccessKey:填写对应账号的AccessKey ID和AccessKey Secret。
httpserver:根据需求选择服务启动的端口号。
import com.aliyuncs.DefaultAcsClient; import com.aliyuncs.exceptions.ClientException; import com.aliyuncs.http.ProtocolType; import com.aliyuncs.kms.model.v20160120.DecryptRequest; import com.aliyuncs.kms.model.v20160120.DecryptResponse; import com.aliyuncs.profile.DefaultProfile; import com.sun.net.httpserver.Headers; import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpHandler; import com.sun.net.httpserver.HttpServer; import com.sun.net.httpserver.spi.HttpServerProvider; import org.apache.commons.codec.binary.Base64; import java.io.IOException; import java.io.OutputStream; import java.net.HttpURLConnection; import java.net.InetSocketAddress; import java.net.URI;import java.util.regex.Matcher; import java.util.regex.Pattern; public class HlsDecryptServer { private static DefaultAcsClient client; static { //KMS的区域,必须与视频对应区域 String region = ""; // 访问KMS的授权AccessKey信息 // 阿里云账号AccessKey拥有所有API的访问权限,建议您使用RAM用户进行API访问或日常运维。 // 强烈建议不要把AccessKey ID和AccessKey Secret保存到工程代码里,否则可能导致AccessKey泄露,威胁您账号下所有资源的安全。 // 本示例通过从环境变量中读取AccessKey,来实现API访问的身份验证。运行代码示例前,请配置环境变量ALIBABA_CLOUD_ACCESS_KEY_ID和ALIBABA_CLOUD_ACCESS_KEY_SECRET。 String accessKeyId = System.getenv("ALIBABA_CLOUD_ACCESS_KEY_ID"); String accessKeySecret = System.getenv("ALIBABA_CLOUD_ACCESS_KEY_SECRET"); client = new DefaultAcsClient(DefaultProfile.getProfile(region, accessKeyId, accessKeySecret)); } /** * 说明: * 1、接收解密请求,获取密文密钥和令牌Token * 2、调用KMS decrypt接口获取明文密钥 * 3、将明文密钥base64decode返回 */ public class HlsDecryptHandler implements HttpHandler { /** * 处理解密请求 * @param httpExchange * @throws IOException */ public void handle(HttpExchange httpExchange) throws IOException { String requestMethod = httpExchange.getRequestMethod(); if ("GET".equalsIgnoreCase(requestMethod)) { //校验token的有效性 String token = getMtsHlsUriToken(httpExchange); boolean validRe = validateToken(token); if (!validRe) { return; } //从URL中取得密文密钥 String ciphertext = getCiphertext(httpExchange); if (null == ciphertext) return; //从KMS中解密出来,并Base64 decode byte[] key = decrypt(ciphertext); //设置header setHeader(httpExchange, key); //返回base64decode之后的密钥 OutputStream responseBody = httpExchange.getResponseBody(); responseBody.write(key); responseBody.close(); } } private void setHeader(HttpExchange httpExchange, byte[] key) throws IOException { Headers responseHeaders = httpExchange.getResponseHeaders(); responseHeaders.set("Access-Control-Allow-Origin", "*"); httpExchange.sendResponseHeaders(HttpURLConnection.HTTP_OK, key.length); } /** * 调用KMS decrypt接口解密,并将明文base64decode * @param ciphertext * @return */ private byte[] decrypt(String ciphertext) { DecryptRequest request = new DecryptRequest(); request.setCiphertextBlob(ciphertext); request.setProtocol(ProtocolType.HTTPS); try { DecryptResponse response = client.getAcsResponse(request); String plaintext = response.getPlaintext(); //注意:需要base64 decode return Base64.decodeBase64(plaintext); } catch (ClientException e) { e.printStackTrace(); return null; } } /** * 校验令牌有效性 * @param token * @return */ private boolean validateToken(String token) { if (null == token || "".equals(token)) { return false; } //TODO 业务方实现令牌有效性校验 return true; } /** * 从URL中获取密文密钥参数 * @param httpExchange * @return */ private String getCiphertext(HttpExchange httpExchange) { URI uri = httpExchange.getRequestURI(); String queryString = uri.getQuery(); String pattern = "CipherText=(\\w*)"; Pattern r = Pattern.compile(pattern); Matcher m = r.matcher(queryString); if (m.find()) return m.group(1); else { System.out.println("Not Found CipherText Param"); return null; } } /** * 获取Token参数 * * @param httpExchange * @return */ private String getMtsHlsUriToken(HttpExchange httpExchange) { URI uri = httpExchange.getRequestURI(); String queryString = uri.getQuery(); String pattern = "MtsHlsUriToken=(\\w*)"; Pattern r = Pattern.compile(pattern); Matcher m = r.matcher(queryString); if (m.find()) return m.group(1); else { System.out.println("Not Found MtsHlsUriToken Param"); return null; } } } /** * 服务启动 * * @throws IOException */ private void serviceBootStrap() throws IOException { HttpServerProvider provider = HttpServerProvider.provider(); //监听端口可以自定义,能同时接受最多30个请求 HttpServer httpserver = provider.createHttpServer(new InetSocketAddress(8099), 30); httpserver.createContext("/", new HlsDecryptHandler()); httpserver.start(); System.out.println("hls decrypt server started"); } public static void main(String[] args) throws IOException { HlsDecryptServer server = new HlsDecryptServer(); server.serviceBootStrap(); }}
上传视频。
使用不转码模板创建视频上传凭证和地址。控制台具体操作,请参见控制台上传;服务端接口上传,请参见获取音视频上传地址和凭证。
接收上传完成回调消息。
发起标准加密转码。
调用提交媒体转码作业接口发起标准加密转码。
Java示例代码以及示例代码需要手动变更的地方如下所示:
request.setTemplateGroupId(""):传入加密模板ID。
request.setVideoId(""):传入视频ID。
encryptConfig.put("CipherText",""):传入步骤三获取的CiphertextBlob值。
encryptConfig.put("DecryptKeyUri",""):传入播放地址、
CiphertextBlob
值以及MtsHlsUriToken
。以在本地的8099端口为例,播放地址为:http://172.16.0.1:8099?CipherText=CiphertextBlob值&MtsHlsUriToken=MtsHlsUriToken值
。
import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import com.aliyuncs.DefaultAcsClient; import com.aliyuncs.exceptions.ClientException; import com.aliyuncs.profile.DefaultProfile; import com.aliyuncs.vod.model.v20170321.SubmitTranscodeJobsRequest; import com.aliyuncs.vod.model.v20170321.SubmitTranscodeJobsResponse; public class SubmitTranscodeJobs { // 阿里云账号AccessKey拥有所有API的访问权限,建议您使用RAM用户进行API访问或日常运维。 // 强烈建议不要把AccessKey ID和AccessKey Secret保存到工程代码里,否则可能导致AccessKey泄露,威胁您账号下所有资源的安全。 // 本示例通过从环境变量中读取AccessKey,来实现API访问的身份验证。运行代码示例前,请配置环境变量ALIBABA_CLOUD_ACCESS_KEY_ID和ALIBABA_CLOUD_ACCESS_KEY_SECRET。 private static String accessKeyId = System.getenv("ALIBABA_CLOUD_ACCESS_KEY_ID"); private static String accessKeySecret = System.getenv("ALIBABA_CLOUD_ACCESS_KEY_SECRET"); public static SubmitTranscodeJobsResponse submitTranscodeJobs(DefaultAcsClient client) throws Exception{ SubmitTranscodeJobsRequest request = new SubmitTranscodeJobsRequest(); request.setTemplateGroupId(""); request.setVideoId(""); JSONObject encryptConfig = new JSONObject(); encryptConfig.put("CipherText",""); encryptConfig.put("DecryptKeyUri",""); encryptConfig.put("KeyServiceType","KMS"); request.setEncryptConfig(encryptConfig.toJSONString()); return client.getAcsResponse(request); } public static void main(String[] args) throws ClientException { // 点播服务接入区域 String regionId = "cn-shanghai"; DefaultProfile profile = DefaultProfile.getProfile(regionId, accessKeyId, accessKeySecret); DefaultAcsClient client = new DefaultAcsClient(profile); SubmitTranscodeJobsResponse response; try { response = submitTranscodeJobs(client); System.out.println("RequestId is:"+response.getRequestId()); System.out.println("TranscodeTaskId is:"+response.getTranscodeTaskId()); System.out.println("TranscodeJobs is:"+ JSON.toJSON(response.getTranscodeJobs())); } catch (Exception e) { e.printStackTrace(); } } }
验证加密转码是否成功。
您可以登录视频点播控制台查看该视频的视频地址,通过以下三种方式来逐步判断标准加密是否成功。
当视频加密转码后,如果该视频的视频地址只有一个M3U8格式的视频地址,那么该视频状态为转码失败。
当视频加密转码后,如果视频不只有M3U8格式的输出(例如还存在格式为MP4的原始文件),只需查看M3U8格式后是否带有标准加密,一般情况下,如果存在则表明标准加密已成功。
如果以上两种方式都不能判断,那么可以将带有加密标志的M3U8文件的地址拷贝出来,使用
curl -v "M3U8文件地址"
,查看获取到的M3U8内容是否存在URI="<业务方在发起标准加密时传递的解密地址,即加密配置 EncryptConfig中的DecryptKeyUri参数值>"
关键信息,有则表明为标准加密且加密成功。
播放流程
获取视频的播放地址和凭证。
传入认证信息。
获取M3U8文件地址后,播放器会解析M3U8文件中的EXT-X-KEY标签中的URI并访问,从而获取到带密文密钥的解密接口URI,此URI为您发起标准加密时传递的加密配置 EncryptConfig中的
DecryptKeyUri
参数值。若只允许合法用户才可以访问,那么需要播放器在获取解密密钥时携带您承认的认证信息,认证信息可以通过MtsHlsUriToken参数传入。
示例:
视频的播放地址为:
https://demo.aliyundoc.com/encrypt-stream****-hd.m3u8
,则请求时需要携带MtsHlsUriToken
参数传入。最终请求地址为:
https://demo.aliyundoc.com/encrypt-stream****-hd.m3u8?MtsHlsUriToken=<令牌>
解密地址为:
https://demo.aliyundoc.com?Ciphertext=ZjJmZGViNzUtZWY1Mi00Y2RlLTk3MTMtOT****
最终解密请求地址为:
https://demo.aliyundoc.com?Ciphertext=ZjJmZGViNzUtZWY1Mi00Y2RlLTk3MTMtOT****&MtsHlsUriToken=<颁发的令牌>
。
播放。
播放器在解析到解密地址URI时会自动请求解密接口获取解密密钥,拿到解密密钥去解密加密过的ts文件进行播放。