Laravel如何分块加密解密大文件?解决解密无效负载报错
大文件分块加密解密(Laravel Encrypter)问题解决
问题背景
直接加密大文件时触发内存溢出错误:
Allowed memory size of 134217728 bytes exhausted
原实现方式会把整个文件和加密内容加载到内存,仅适用于小文件:
Storage::put($filePath, $encrypted->encrypt(file_get_contents($file)));
尝试分块加密解密时,解密阶段出现:
Illuminate\Contracts\Encryption\DecryptException: The payload is invalid.
错误原因
Laravel的Encrypter::encrypt()方法会给每个加密块生成独立的**IV(初始化向量)**和认证信息,加密后的每个块是完整的加密 payload(包含IV、加密内容、哈希校验)。但解密时按固定1MB读取,会把一个完整的加密 payload 截断,或者把多个payload混读,导致解密时无法识别有效payload,触发校验错误。
正确实现方案
要实现分块加密解密,需要:
- 加密时,给每个加密块添加长度标记,方便解密时准确读取完整的加密payload
- 解密时,先读取块的长度,再读取对应长度的加密内容,确保每次解密的是完整的加密块
修改后的加密代码
use Illuminate\Encryption\Encrypter; use Illuminate\Http\Request; use Illuminate\Support\Facades\Config; use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Storage; Route::post('/upload', function (Request $request) { $file = $request->file('file'); if (!$file) { return response()->json(['success' => false, 'message' => 'File not uploaded']); } $key = Config::get('app.file_key'); $key = str_replace('base64:', '', $key); $key = base64_decode($key); $encrypted = new Encrypter($key, Config::get('app.cipher')); $encryptedFilePath = storage_path('app/public/my-file.enc'); $chunkSize = 1024 * 1024; // 1MB 明文块大小 $fpSource = fopen($file->path(), 'rb'); $fpDest = fopen($encryptedFilePath, 'wb'); while (!feof($fpSource)) { $plaintext = fread($fpSource, $chunkSize); // 加密当前明文块 $encryptedChunk = $encrypted->encrypt($plaintext); // 先写入加密块的长度(用4字节无符号大端存储,最大支持4GB单个块) fwrite($fpDest, pack('N', strlen($encryptedChunk))); // 写入加密块内容 fwrite($fpDest, $encryptedChunk); } fclose($fpSource); fclose($fpDest); return response()->json(['success' => true, 'message' => 'File uploaded and encrypted successfully']); })->name('upload');
修改后的解密代码
Route::get('/decrypt/{file_name}', function ($file_name) { $key = Config::get('app.file_key'); $key = str_replace('base64:', '', $key); $key = base64_decode($key); $encrypted = new Encrypter($key, Config::get('app.cipher')); $sourceFilePath = storage_path("app/public/$file_name"); $destinationFilePath = storage_path("app/public/decrypted-$file_name"); if (!file_exists($sourceFilePath)) { return response()->json(['success' => false, 'message' => 'Encrypted file not found']); } $fpEncrypted = fopen($sourceFilePath, 'rb'); $fpDest = fopen($destinationFilePath, 'wb'); while (!feof($fpEncrypted)) { // 先读取4字节的块长度 $lengthBytes = fread($fpEncrypted, 4); if (strlen($lengthBytes) < 4) { break; // 文件结束或损坏 } // 解析出加密块的长度 $chunkLength = unpack('N', $lengthBytes)[1]; // 读取对应长度的加密块内容 $encryptedChunk = fread($fpEncrypted, $chunkLength); // 解密并写入明文 $decryptedChunk = $encrypted->decrypt($encryptedChunk); fwrite($fpDest, $decryptedChunk); } fclose($fpEncrypted); fclose($fpDest); return response()->download($destinationFilePath, 'decrypted-' . $file_name); })->name('decrypt');
关键说明
- 块长度标记:用
pack('N', $length)将加密块长度转为4字节无符号大端整数,确保跨平台兼容性,解密时用unpack解析出长度,保证每次读取完整的加密payload。 - 内存控制:加密和解密时每次仅处理1MB明文块(加密后块会更大,但读取时按实际加密块长度读取,不会加载整个文件到内存)。
- 错误处理:解密时增加了文件存在检查和长度读取完整性判断,避免文件损坏导致的异常。
内容的提问来源于stack exchange,提问作者9uifranco
相关产品推荐
相关产品推荐

