[实战] 从零打造防盗链视频平台:Spring Boot + Flutter + 阿里云OSS 全链路安全方案
在构建付费课程或私有视频平台时,开发者最头疼的问题往往不是“怎么播放”,而是**“怎么防止被白嫖”**。
如果不做防护,只需一个简单的抓包工具(如 Charles),甚至直接查看浏览器 Network,攻击者就能拿到 .mp4 地址,脚本一跑,全站资源瞬间被爬空。
本文将基于 Spring Boot (后端) + Flutter (移动端) + 阿里云 OSS (存储) 的技术栈,分享一套工业级的视频防盗、防抓包、防录屏方案。
核心思路:把视频“切碎”并“上锁”
传统的 .mp4 直链播放就像把钱直接放在桌子上。我们需要改变存储和分发方式:
- 存储层:抛弃 MP4,采用 HLS (m3u8) 切片技术。
- 加密层:使用 AES-128 对每一个视频切片进行加密。
- 分发层:阿里云 OSS 开启私有权限,配合 STS 临时签名。
- 鉴权层:解密密钥(Key)由后端动态分发,验证用户身份(Token)。
- 客户端:Flutter 端硬件级防录屏 + 动态盲水印。
架构全览
第一步:内容生产(转码与加密)
视频上传后,不能直接存储,必须经过 FFmpeg 处理。
我们需要准备一个 key_info 文件,告诉 FFmpeg 加密用的密钥在哪里,以及未来播放器该去哪里找这个密钥。
key_info.txt 内容示例:
http://api.your-domain.com/video/key?id=VIDEO_ID <-- 播放器会向这个地址请求解密Key
/path/to/enc.key <-- 实际加密用的二进制文件路径
FFmpeg 转码指令:
ffmpeg -y -i input.mp4 \
-c:v libx264 -c:a aac \
-hls_time 10 \
-hls_key_info_file key_info.txt \
-hls_playlist_type vod \
-hls_segment_filename "output_%03d.ts" \
playlist.m3u8
关键点:
- 生成后的
.m3u8文件内部会包含一行#EXT-X-KEY:METHOD=AES-128,URI="..."。 - 这个 URI 必须指向你的 Spring Boot 后端,而不是 OSS。这是防抓包的核心——没有业务 Token,谁也拿不到解密钥匙。
第二步:云存储配置(阿里云 OSS)
即便视频被加密了,我们也不希望闲杂人等随意下载切片浪费流量。
- Bucket 权限:设置为 私有(Private)。
- STS 临时授权:
Flutter 客户端不应该持有 OSS 的永久 AccessKey。后端应集成阿里云 STS SDK。
当用户请求播放时,后端签发一个有效期仅 1 小时的 URL 给前端。即使这个 URL 被抓包,过一会儿也就失效了。
第三步:后端门神(Spring Boot 密钥分发)
这是整个安全体系的咽喉。当播放器解析到 .m3u8 中的 Key URI 时,会发起请求。我们需要在这里拦截。
Controller 实现逻辑:
@RestController
@RequestMapping("/video")
public class VideoKeyController {
@GetMapping("/key")
public void getKey(@RequestParam("id") String videoId,
@RequestHeader(value = "Authorization", required = false) String token,
HttpServletResponse response) throws IOException {
// 1. 严格鉴权
if (!authService.isValidUser(token) || !authService.hasPermission(token, videoId)) {
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
return;
}
// 2. (可选) 一次性令牌校验
// 为了防止 Token 泄露,可以在 m3u8 url 中拼接入一次性 ticket,用完即焚
// 3. 获取密钥 (通常存储在数据库或 Redis,或者 OSS 的受保护目录)
byte[] keyBytes = encryptionService.getSecretKey(videoId);
// 4. 返回二进制流
response.setContentType("application/octet-stream");
response.getOutputStream().write(keyBytes);
}
}
第四步:客户端实现(Flutter)
Flutter 端面临两个挑战:Header 注入 和 防录屏。
1. 播放器选型与 Header 注入
原生 video_player 比较简陋,难以定制 Header。推荐使用 Better Player 或 FijkPlayer (基于 ijkplayer)。
我们需要在请求 m3u8 时,把用户的 Token 塞入 Header,以便后端在请求 Key 时能获取到。
// 使用 Better Player 示例
var dataSource = BetterPlayerDataSource(
BetterPlayerDataSourceType.network,
"https://oss.your-domain.com/video/playlist.m3u8?Signature=...", // 阿里云 STS 签名后的 URL
headers: {
"Authorization": "Bearer $userToken", // 关键:注入 Token
"User-Agent": "MySecureApp/1.0"
},
);
var controller = BetterPlayerController(configuration);
controller.setupDataSource(dataSource);
2. 禁止录屏与截屏
这是防止“物理盗版”的第一道防线。Android 和 iOS 都提供了系统级 API 来阻止录屏(录制结果为黑屏)。
使用 flutter_windowmanager 插件:
import 'package:flutter_windowmanager/flutter_windowmanager.dart';
class VideoPage extends StatefulWidget {
@override
_VideoPageState createState() => _VideoPageState();
}
class _VideoPageState extends State<VideoPage> {
@override
void initState() {
super.initState();
// 开启“安全模式”
secureScreen();
}
Future<void> secureScreen() async {
await FlutterWindowManager.addFlags(FlutterWindowManager.FLAG_SECURE);
}
@override
void dispose() {
// 页面销毁时清除 flag (可选)
FlutterWindowManager.clearFlags(FlutterWindowManager.FLAG_SECURE);
super.dispose();
}
}
第五步:终极手段(动态盲水印)
如果用户用另一台手机对着屏幕拍,技术手段是无法阻断的。但我们可以增加他的犯罪成本。
在视频层之上,覆盖一层半透明的、肉眼几乎不可见的水印,内容是当前用户的 ID。一旦视频泄露,可以通过技术手段提取水印,封禁该账号。
Flutter 实现思路:
Stack(
children: [
VideoPlayerWidget(), // 视频层
IgnorePointer( // 穿透层,确保不影响点击视频控制栏
child: Container(
color: Colors.transparent,
child: GridView.count(
crossAxisCount: 3,
children: List.generate(20, (index) {
return Center(
child: Transform.rotate(
angle: -0.5,
child: Text(
"UID: 8848",
style: TextStyle(
color: Colors.white.withOpacity(0.05), // 极低透明度
fontSize: 16,
),
),
),
);
}),
),
),
),
],
)
总结
通过这一套组合拳,我们构建了多重防御体系:
- 文件层面:TS 切片 + AES 加密,下载下来也看不了。
- 网络层面:OSS 私有化 + STS 签名,链接过期即失效。
- 鉴权层面:解密 Key 需 Token 换取,后端严格控制权限。
- 物理层面:App 禁止录屏,录制即黑屏。
- 溯源层面:动态水印,泄露必被抓。
虽然“绝对的安全”不存在,但这一套方案足以将 99% 的脚本小子和普通盗录者拒之门外,是目前性价比最高的商业级视频保护方案。