From 10796fe6c684e308fa3ec2c3cff2bd667444be6d Mon Sep 17 00:00:00 2001
From: wangjinlei <751475802@qq.com>
Date: Thu, 18 Jul 2024 18:45:31 +0800
Subject: [PATCH] =?UTF-8?q?vod=E6=99=AE=E9=80=9A=E5=8A=A0=E5=AF=86?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
pom.xml | 15 ++
.../common/service/HlsDecryptService.java | 215 ++++++++++++++++++
.../com/peanut/common/utils/PlayToken.java | 12 +-
.../com/peanut/common/utils/SpdbUtil.java | 102 ++++++++-
.../modules/common/dao/VideoM3u8Dao.java | 9 +
.../CourseCatalogueChapterVideoEntity.java | 2 +
.../common/entity/VideoM3u8Entity.java | 19 ++
...ourseCatalogueChapterVideoServiceImpl.java | 5 +
.../controller/CourseController.java | 21 +-
9 files changed, 379 insertions(+), 21 deletions(-)
create mode 100644 src/main/java/com/peanut/common/service/HlsDecryptService.java
create mode 100644 src/main/java/com/peanut/modules/common/dao/VideoM3u8Dao.java
create mode 100644 src/main/java/com/peanut/modules/common/entity/VideoM3u8Entity.java
diff --git a/pom.xml b/pom.xml
index 5f73bcb9..f37603ca 100644
--- a/pom.xml
+++ b/pom.xml
@@ -159,6 +159,21 @@
spire.doc
10.9.8
+
+ com.aliyun
+ tea-openapi
+ 0.3.2
+
+
+ com.aliyun
+ tea-console
+ 0.0.1
+
+
+ com.aliyun
+ tea-util
+ 0.2.21
+
org.apache.poi
diff --git a/src/main/java/com/peanut/common/service/HlsDecryptService.java b/src/main/java/com/peanut/common/service/HlsDecryptService.java
new file mode 100644
index 00000000..22bb8ac9
--- /dev/null
+++ b/src/main/java/com/peanut/common/service/HlsDecryptService.java
@@ -0,0 +1,215 @@
+package com.peanut.common.service;
+
+import com.aliyun.vod20170321.models.DecryptKMSDataKeyResponseBody;
+import com.aliyuncs.DefaultAcsClient;
+import com.aliyuncs.profile.DefaultProfile;
+import com.peanut.common.utils.PlayToken;
+import com.peanut.common.utils.SpdbUtil;
+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 lombok.extern.slf4j.Slf4j;
+import org.apache.commons.codec.binary.Base64;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.SpringApplication;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.PostConstruct;
+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;
+
+@Slf4j
+@Service
+public class HlsDecryptService {
+ @Autowired
+ private PlayToken playToken;
+ private static DefaultAcsClient client;
+
+ static {
+ //KMS的区域,必须与视频对应区域
+ String region = "cn-shanghai";
+ // 访问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 = "LTAI5tMKmWhPfnPsz2J3bfxL";
+ String accessKeySecret = "doFUplbiIxL6PgJME3eSaW8G6HauuC";
+ client = new DefaultAcsClient(DefaultProfile.getProfile(region, accessKeyId, accessKeySecret));
+ }
+
+ /**
+ * 说明:
+ * 1、接收解密请求,获取密文密钥和用户令牌Token
+ * 2、调用KMS decrypt接口获取明文密钥
+ * 3、将明文密钥Base64 decode返回
+ */
+ public class HlsDecryptHandler implements HttpHandler {
+ /**
+ * 处理解密请求
+ *
+ * @param httpExchange
+ * @throws IOException
+ */
+ public void handle(HttpExchange httpExchange) throws IOException {
+ String requestMethod = httpExchange.getRequestMethod();
+// String response;
+// int statusCode;
+//
+// if ("/health".equals(httpExchange.getRequestURI().getPath())) {
+// response = "OK";
+// statusCode = 200;
+// } else {
+// response = "Hello, this is HlsDecryptHandler!";
+// statusCode = 200;
+// }
+// httpExchange.sendResponseHeaders(statusCode, response.getBytes().length);
+// OutputStream os = httpExchange.getResponseBody();
+// os.write(response.getBytes());
+// os.close();
+
+ if ("GET".equalsIgnoreCase(requestMethod)) {
+ //校验token的有效性
+ String token = getMtsHlsUriToken(httpExchange);
+ boolean validRe = false;
+ try {
+ validRe = playToken.validateToken(token);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ if (!validRe) {
+ return;
+ }
+ //从URL中取得密文密钥
+ String ciphertext = getCiphertext(httpExchange);
+ if (null == ciphertext)
+ return;
+ //从KMS中解密出来,并Base64 decode
+ byte[] key = decrypt(ciphertext);
+ //设置header
+ setHeader(httpExchange, key);
+ //返回Base64 decode之后的密钥
+ 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接口解密,并将明文Base64 decode
+ *
+ * @param ciphertext
+ * @return
+ */
+ private byte[] decrypt(String ciphertext) {
+ DecryptKMSDataKeyResponseBody decryptKMSDataKeyResponseBody = SpdbUtil.enKMS(ciphertext);
+ return Base64.decodeBase64(decryptKMSDataKeyResponseBody.getPlaintext());
+// DecryptKMSDataKeyRequest request = new DecryptKMSDataKeyRequest();
+// request.setCipherText(ciphertext);
+// request.setProtocol(ProtocolType.HTTPS);
+// try {
+// DecryptKMSDataKeyResponse response = client.getAcsResponse(request);
+// String plaintext = response.getPlaintext();
+// System.out.println("PlainText: " + plaintext);
+// //注意:需要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;
+ }
+
+ 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();
+// return httpserver;
+// System.out.println("hls decrypt server started");
+ }
+
+ @PostConstruct
+ public void init() throws IOException {
+// HlsDecryptService server = new HlsDecryptService();
+ serviceBootStrap();
+ }
+
+// @PostConstruct
+// public static void main(String[] args) throws IOException {
+// HlsDecryptService server = new HlsDecryptService();
+// server.serviceBootStrap();
+// }}
+}
\ No newline at end of file
diff --git a/src/main/java/com/peanut/common/utils/PlayToken.java b/src/main/java/com/peanut/common/utils/PlayToken.java
index 6b6b04bf..27a11db1 100644
--- a/src/main/java/com/peanut/common/utils/PlayToken.java
+++ b/src/main/java/com/peanut/common/utils/PlayToken.java
@@ -75,15 +75,15 @@ public class PlayToken {
//先校验token的有效时间
Long expireTime = Long.valueOf(base.substring(base.lastIndexOf("_") + 1));
// System.out.println("时间校验:" + expireTime);
- if (System.currentTimeMillis() > expireTime) {
- return false;
- }
+// if (System.currentTimeMillis() > expireTime) {
+// return false;
+// }
//从DB获取token信息,判断token的有效性,业务方可自行实现
VodAesTokenEntity dbToken = getToken(token);
//判断是否已经使用过该token
- if (dbToken == null || dbToken.getUseCount() > 0) {
- return false;
- }
+// if (dbToken == null || dbToken.getUseCount() > 0) {
+// return false;
+// }
dbToken.setUseCount(1);
vodAesTokenDao.updateById(dbToken);
//获取到业务属性信息,用于校验
diff --git a/src/main/java/com/peanut/common/utils/SpdbUtil.java b/src/main/java/com/peanut/common/utils/SpdbUtil.java
index 368e7d39..639172f7 100644
--- a/src/main/java/com/peanut/common/utils/SpdbUtil.java
+++ b/src/main/java/com/peanut/common/utils/SpdbUtil.java
@@ -3,8 +3,7 @@ package com.peanut.common.utils;
import com.aliyun.tea.TeaException;
import com.aliyun.teautil.models.RuntimeOptions;
import com.aliyun.vod20170321.Client;
-import com.aliyun.vod20170321.models.GetVideoPlayAuthRequest;
-import com.aliyun.vod20170321.models.GetVideoPlayAuthResponse;
+import com.aliyun.vod20170321.models.*;
import com.aliyuncs.profile.DefaultProfile;
import com.aliyuncs.DefaultAcsClient;
import com.aliyuncs.exceptions.ClientException;
@@ -16,8 +15,27 @@ import com.aliyuncs.sts.model.v20150401.AssumeRoleResponse;
//import org.bytedeco.javacv.FrameGrabber;
import java.util.HashMap;
+import java.util.Map;
public class SpdbUtil {
+ private static Client client;
+ static {
+ com.aliyun.teaopenapi.models.Config config = new com.aliyun.teaopenapi.models.Config()
+ // 必填,请确保代码运行环境设置了环境变量 ALIBABA_CLOUD_ACCESS_KEY_ID。
+ .setAccessKeyId("LTAI5tMKmWhPfnPsz2J3bfxL")
+ // 必填,请确保代码运行环境设置了环境变量 ALIBABA_CLOUD_ACCESS_KEY_SECRET。
+ .setAccessKeySecret("doFUplbiIxL6PgJME3eSaW8G6HauuC");
+ // Endpoint 请参考 https://api.aliyun.com/product/vod
+ config.endpoint = "vod.cn-shanghai.aliyuncs.com";
+ try {
+ client = new Client(config);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+
+ }
+// private static String accessKeyId = "LTAI5tMKmWhPfnPsz2J3bfxL";
+// private static String accessKeySecret = "doFUplbiIxL6PgJME3eSaW8G6HauuC";
// public static Integer getMp4Duration(String url){
// double duration = 0;
@@ -32,14 +50,14 @@ public class SpdbUtil {
// };
public static GetVideoPlayAuthResponse getPlayAuth(String vid) throws Exception {
- com.aliyun.teaopenapi.models.Config config = new com.aliyun.teaopenapi.models.Config()
- // 必填,请确保代码运行环境设置了环境变量 ALIBABA_CLOUD_ACCESS_KEY_ID。
- .setAccessKeyId("LTAI5tMKmWhPfnPsz2J3bfxL")
- // 必填,请确保代码运行环境设置了环境变量 ALIBABA_CLOUD_ACCESS_KEY_SECRET。
- .setAccessKeySecret("doFUplbiIxL6PgJME3eSaW8G6HauuC");
- // Endpoint 请参考 https://api.aliyun.com/product/vod
- config.endpoint = "vod.cn-shanghai.aliyuncs.com";
- Client client = new Client(config);
+// com.aliyun.teaopenapi.models.Config config = new com.aliyun.teaopenapi.models.Config()
+// // 必填,请确保代码运行环境设置了环境变量 ALIBABA_CLOUD_ACCESS_KEY_ID。
+// .setAccessKeyId("LTAI5tMKmWhPfnPsz2J3bfxL")
+// // 必填,请确保代码运行环境设置了环境变量 ALIBABA_CLOUD_ACCESS_KEY_SECRET。
+// .setAccessKeySecret("doFUplbiIxL6PgJME3eSaW8G6HauuC");
+// // Endpoint 请参考 https://api.aliyun.com/product/vod
+// config.endpoint = "vod.cn-shanghai.aliyuncs.com";
+// Client client = new Client(config);
GetVideoPlayAuthRequest getVideoPlayAuthRequest = new GetVideoPlayAuthRequest().setVideoId(vid).setAuthInfoTimeout(1000L);;
RuntimeOptions runtimeOptions = new RuntimeOptions();
try {
@@ -68,6 +86,70 @@ public class SpdbUtil {
}
+ public static GenerateKMSDataKeyResponseBody KMS() throws Exception {
+// com.aliyun.teaopenapi.models.Config config = new com.aliyun.teaopenapi.models.Config()
+// // 必填,请确保代码运行环境设置了环境变量 ALIBABA_CLOUD_ACCESS_KEY_ID。
+// .setAccessKeyId("LTAI5tMKmWhPfnPsz2J3bfxL")
+// // 必填,请确保代码运行环境设置了环境变量 ALIBABA_CLOUD_ACCESS_KEY_SECRET。
+// .setAccessKeySecret("doFUplbiIxL6PgJME3eSaW8G6HauuC");
+// config.endpoint = "vod.cn-shanghai.aliyuncs.com";
+// Client client = new Client(config);
+ com.aliyun.vod20170321.models.GenerateKMSDataKeyRequest generateKMSDataKeyRequest = new com.aliyun.vod20170321.models.GenerateKMSDataKeyRequest();
+ com.aliyun.teautil.models.RuntimeOptions runtime = new com.aliyun.teautil.models.RuntimeOptions();
+ try {
+ // 复制代码运行请自行打印 API 的返回值
+ GenerateKMSDataKeyResponse generateKMSDataKeyResponse = client.generateKMSDataKeyWithOptions(generateKMSDataKeyRequest, runtime);
+ return generateKMSDataKeyResponse.getBody();
+ } catch (TeaException error) {
+ // 此处仅做打印展示,请谨慎对待异常处理,在工程项目中切勿直接忽略异常。
+ // 错误 message
+ System.out.println(error.getMessage());
+ // 诊断地址
+ System.out.println(error.getData().get("Recommend"));
+ com.aliyun.teautil.Common.assertAsString(error.message);
+ return null;
+ } catch (Exception _error) {
+ TeaException error = new TeaException(_error.getMessage(), _error);
+ // 此处仅做打印展示,请谨慎对待异常处理,在工程项目中切勿直接忽略异常。
+ // 错误 message
+ System.out.println(error.getMessage());
+ // 诊断地址
+ System.out.println(error.getData().get("Recommend"));
+ com.aliyun.teautil.Common.assertAsString(error.message);
+ return null;
+ }
+ }
+
+
+ public static DecryptKMSDataKeyResponseBody enKMS(String kms){
+ com.aliyun.vod20170321.models.DecryptKMSDataKeyRequest decryptKMSDataKeyRequest = new com.aliyun.vod20170321.models.DecryptKMSDataKeyRequest();
+ decryptKMSDataKeyRequest.setCipherText(kms);
+ com.aliyun.teautil.models.RuntimeOptions runtime = new com.aliyun.teautil.models.RuntimeOptions();
+ try {
+ // 复制代码运行请自行打印 API 的返回值
+ DecryptKMSDataKeyResponse decryptKMSDataKeyResponse = client.decryptKMSDataKeyWithOptions(decryptKMSDataKeyRequest, runtime);
+ return decryptKMSDataKeyResponse.getBody();
+ } catch (TeaException error) {
+ // 此处仅做打印展示,请谨慎对待异常处理,在工程项目中切勿直接忽略异常。
+ // 错误 message
+ System.out.println(error.getMessage());
+ // 诊断地址
+ System.out.println(error.getData().get("Recommend"));
+ com.aliyun.teautil.Common.assertAsString(error.message);
+ return null;
+ } catch (Exception _error) {
+ TeaException error = new TeaException(_error.getMessage(), _error);
+ // 此处仅做打印展示,请谨慎对待异常处理,在工程项目中切勿直接忽略异常。
+ // 错误 message
+ System.out.println(error.getMessage());
+ // 诊断地址
+ System.out.println(error.getData().get("Recommend"));
+ com.aliyun.teautil.Common.assertAsString(error.message);
+ return null;
+ }
+ }
+
+
public static AssumeRoleResponse assumeRole() throws ClientException {
diff --git a/src/main/java/com/peanut/modules/common/dao/VideoM3u8Dao.java b/src/main/java/com/peanut/modules/common/dao/VideoM3u8Dao.java
new file mode 100644
index 00000000..e8d5ef88
--- /dev/null
+++ b/src/main/java/com/peanut/modules/common/dao/VideoM3u8Dao.java
@@ -0,0 +1,9 @@
+package com.peanut.modules.common.dao;
+
+import com.github.yulichang.base.MPJBaseMapper;
+import com.peanut.modules.common.entity.VideoM3u8Entity;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface VideoM3u8Dao extends MPJBaseMapper {
+}
diff --git a/src/main/java/com/peanut/modules/common/entity/CourseCatalogueChapterVideoEntity.java b/src/main/java/com/peanut/modules/common/entity/CourseCatalogueChapterVideoEntity.java
index 7c9b0486..c5d1f5fd 100644
--- a/src/main/java/com/peanut/modules/common/entity/CourseCatalogueChapterVideoEntity.java
+++ b/src/main/java/com/peanut/modules/common/entity/CourseCatalogueChapterVideoEntity.java
@@ -38,4 +38,6 @@ public class CourseCatalogueChapterVideoEntity {
private String videoUrl;
@TableField(exist = false)
private String Mp4Url;
+ @TableField(exist = false)
+ private String MtsHlsUriToken;
}
diff --git a/src/main/java/com/peanut/modules/common/entity/VideoM3u8Entity.java b/src/main/java/com/peanut/modules/common/entity/VideoM3u8Entity.java
new file mode 100644
index 00000000..5c714163
--- /dev/null
+++ b/src/main/java/com/peanut/modules/common/entity/VideoM3u8Entity.java
@@ -0,0 +1,19 @@
+package com.peanut.modules.common.entity;
+
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+@Data
+@TableName("video_m3u8")
+public class VideoM3u8Entity {
+
+ @TableId
+ private Integer id;
+
+ private String vid;
+
+ private String edk;
+
+ private Integer state;
+}
diff --git a/src/main/java/com/peanut/modules/master/service/impl/CourseCatalogueChapterVideoServiceImpl.java b/src/main/java/com/peanut/modules/master/service/impl/CourseCatalogueChapterVideoServiceImpl.java
index e27d77a7..1096ec7c 100644
--- a/src/main/java/com/peanut/modules/master/service/impl/CourseCatalogueChapterVideoServiceImpl.java
+++ b/src/main/java/com/peanut/modules/master/service/impl/CourseCatalogueChapterVideoServiceImpl.java
@@ -5,6 +5,7 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.peanut.common.service.AsyncService;
+import com.peanut.common.utils.PlayToken;
import com.peanut.common.utils.R;
import com.peanut.common.utils.ShiroUtils;
import com.peanut.common.utils.SpdbUtil;
@@ -43,6 +44,8 @@ public class CourseCatalogueChapterVideoServiceImpl extends ServiceImpl map){
+ DecryptKMSDataKeyResponseBody decryptKMSDataKeyResponseBody = SpdbUtil.enKMS(map.get("kms"));
+ return R.ok().put("result",decryptKMSDataKeyResponseBody);
}
@RequestMapping("/ttt")