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")