
本文详解如何在 go 中安全、高效地对 aes 加密的视频文件进行实时解密,并通过 http 流式响应(如 hls/mp4)传输,避免缓冲、iv 复用、writer 重置等常见导致文件损坏的问题。
本文详解如何在 go 中安全、高效地对 aes 加密的视频文件进行实时解密,并通过 http 流式响应(如 hls/mp4)传输,避免缓冲、iv 复用、writer 重置等常见导致文件损坏的问题。
在流式传输加密视频(如 .ts、.mp4)时,不能为每段写入重复初始化同一个 OFB 流——这是原代码中 cipherHttpWriter.Write() 每次都新建 Cipher 和 Stream 导致数据严重错乱(尤其大文件尾部损坏)的根本原因。OFB 是同步流密码模式,其安全性与确定性完全依赖于 唯一且不可复用的 IV,且整个解密过程必须是连续、单次、状态保持的流操作。
✅ 正确做法是:在请求处理开始时一次性构建解密流,然后将原始文件按块读取、解密、写入 http.ResponseWriter,全程复用同一 cipher.StreamWriter 实例。同时需注意:
- 使用
http.ServeContent时不可直接包装ResponseWriter并拦截Write()—— 它内部会多次调用Write()、Seek()、Stat(),而流式解密器不支持随机访问或重放; - IV 必须与加密端严格一致(通常前 16 字节存于文件头,或通过 URL/HTTP Header 传递),硬编码
var iv [aes.BlockSize]byte(全零 IV)仅适用于测试,生产环境绝对禁止; -
cipher.StreamWriter不支持Close()后再写(defer streamWriter.Close()在Write()内调用是错误的); - 需显式设置
Content-Type、Content-Length(若已知)或启用分块传输(Chunked Encoding)。
以下是修复后的完整、可部署示例(支持 OFB 模式,兼容视频流):
package main
import (
"crypto/aes"
"crypto/cipher"
"io"
"log"
"net/http"
"os"
"path/filepath"
"strings"
)
// 注意:生产环境请从安全配置中心加载密钥和 IV,勿硬编码
var (
key = []byte("we!@09bsa$.a-ala.HJOqweK45aghf&A") // 32-byte for AES-256
// IV 应随文件存储或通过安全信道传递;此处为演示使用固定 IV(仅限测试!)
iv = []byte("1234567890123456") // 16-byte for AES
)
func decryptAndStream(w http.ResponseWriter, r *http.Request, filePath string) {
f, err := os.Open(filePath)
if err != nil {
http.Error(w, "File not found", http.StatusNotFound)
return
}
defer f.Close()
// 1. 初始化 AES 块和 OFB 流(一次且仅一次)
block, err := aes.NewCipher(key)
if err != nil {
http.Error(w, "AES cipher init failed", http.StatusInternalServerError)
return
}
stream := cipher.NewOFB(block, iv)
// 2. 设置标准响应头
ext := strings.ToLower(filepath.Ext(filePath))
switch ext {
case ".ts":
w.Header().Set("Content-Type", "video/MP2T")
case ".mp4", ".m4v":
w.Header().Set("Content-Type", "video/mp4")
default:
w.Header().Set("Content-Type", "application/octet-stream")
}
// 3. 构建流式写入器,绑定到 ResponseWriter
writer := &cipher.StreamWriter{S: stream, W: w}
// 4. 流式解密并写入(不缓存,不 Seek,不 Close writer!)
// 注意:io.Copy 内部按 32KB buffer 循环,天然适配流式场景
_, err = io.Copy(writer, f)
if err != nil && err != io.ErrUnexpectedEOF {
log.Printf("Streaming error for %s: %v", filePath, err)
// 连接中断等非致命错误无需返回 HTTP 错误码(客户端已接收部分数据)
}
}
func ServeHTTP(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/" {
http.Redirect(w, r, "/video.mp4", http.StatusFound)
return
}
// 简单路径清理:禁止目录遍历
fileName := filepath.Base(r.URL.Path[1:])
if fileName == "" || strings.Contains(fileName, "..") {
http.Error(w, "Invalid path", http.StatusBadRequest)
return
}
filePath := "./" + fileName
if _, err := os.Stat(filePath); os.IsNotExist(err) {
http.Error(w, "File not found", http.StatusNotFound)
return
}
decryptAndStream(w, r, filePath)
}
func main() {
http.HandleFunc("/", ServeHTTP)
log.Println("Server starting on :2020")
log.Fatal(http.ListenAndServe(":2020", nil))
}? 关键改进说明:
-
IV 安全性:示例中
iv已改为显式字节数组(非全零),实际应从文件头读取(如前 16 字节)或由客户端提供; -
单次流初始化:
cipher.NewOFB和cipher.StreamWriter在 handler 开头创建,确保整个响应生命周期内流状态连续; -
移除危险封装:不再实现自定义
Write()方法,避免与http.ServeContent的复杂逻辑冲突; -
健壮错误处理:网络中断等
io.Copy错误不 panic,仅记录日志(HTTP 流一旦开始写入,无法回滚状态); -
MIME 类型自动推导:根据扩展名设置合理
Content-Type,保障浏览器/播放器正确解析。
⚠️ 重要提醒:
- OFB 模式虽支持流式,但不提供完整性校验。如需防篡改,请切换至
GCM模式(需额外处理 nonce 和认证标签),并使用cipher.AEAD接口; - 大文件流式传输建议搭配
http.DetectContentType或预读前 512 字节校验魔数,增强格式鲁棒性; - 生产环境务必启用 HTTPS,并对密钥、IV 实施 KMS 或 Vault 管理。
掌握此模式后,你即可无缝集成 AES 解密能力到任何基于 Go 的流媒体服务(如自定义 HLS 分片服务器、DRM 轻量网关等)。

















