AWS S3预签名URL上传验证:如何确保文件完整且一致?
验证S3预签名URL上传文件的完整性与一致性
背景与现有流程
我需要把文件托管在AWS S3,由客户端浏览器直接上传——之前通过后端流式传输大文件时问题频发,所以改用预签名URL方案,流程是:
- 前端先把文件信息发给后端,后端将信息存入数据库,同时生成预签名上传URL返回给前端
- 前端通过该URL直接上传文件到S3,完成后通知后端做验证并更新数据库
现有核心代码如下:
后端生成预签名URL的代码
$request->validate([ 'attachment'=>'required|string' , ]); $original_name = $request->input('attachment'); $newName = 'some-versioning-info-'.$original_name ; $s3Upload = // 将文件数据存入数据库的逻辑 list($client , $bucket ) = $this->getS3Client(); $cmd = $client->getCommand('PutObject', [ 'Bucket' => $bucket, 'Key' => $newName ]); $request = $client->createPresignedRequest($cmd, '+20 minutes')->withMethod('PUT'); $url = (string)$request->getUri(); return response( ['url'=>$url , 'token' => $s3Upload->token ]);
后端验证上传的代码
function verifyUpload($token){ $s3Upload = S3UploadUrl::where('token' , $token )->firstOrFail(); if(!Storage::disk('s3')->exists($s3Upload->file_name)) throw new ApiException("file not found!"); $size = Storage::disk('s3')->size($s3Upload->file_name); if($size < 1 ) throw new ApiException("something is wrong with the file"); // 验证通过,更新数据库 }
当前问题
现在的问题是:S3对PUT请求的成功返回太宽松——哪怕前端只发空PUT请求,S3也会返回成功,导致可能存储空文件。目前只能通过检查文件大小判断,但这无法确认文件内容和客户端上传的完全一致。
优化验证方案
1. 前端提前计算文件哈希,后端对比校验
这是最直接的内容一致性验证方式:
- 前端在上传前,用JavaScript计算文件的MD5或SHA-256哈希,把哈希值和文件名、文件大小一起传给后端
- 后端把哈希值存入数据库的
s3Upload记录中 - 后端验证时,从S3获取文件内容并计算哈希,和数据库中存储的哈希值对比
前端示例(计算MD5):
async function calculateFileHash(file) { const arrayBuffer = await file.arrayBuffer(); const hashBuffer = await crypto.subtle.digest('MD5', arrayBuffer); const hashArray = Array.from(new Uint8Array(hashBuffer)); return hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); } // 调用:获取文件后计算哈希,和文件信息一起发后端 const fileHash = await calculateFileHash(selectedFile);
后端修改(生成URL时接收哈希):
// 新增验证规则 $request->validate([ 'attachment'=>'required|string', 'file_size' => 'required|integer|min:1', 'file_hash' => 'required|string' ]); $original_name = $request->input('attachment'); $fileSize = $request->input('file_size'); $fileHash = $request->input('file_hash'); $newName = 'some-versioning-info-'.$original_name ; // 存入数据库时带上file_size和file_hash $s3Upload = S3UploadUrl::create([ 'file_name' => $newName, 'original_name' => $original_name, 'file_size' => $fileSize, 'file_hash' => $fileHash, // 其他字段 ]);
后端验证时对比哈希:
function verifyUpload($token){ $s3Upload = S3UploadUrl::where('token' , $token )->firstOrFail(); if(!Storage::disk('s3')->exists($s3Upload->file_name)) throw new ApiException("file not found!"); // 先验证大小是否匹配 $size = Storage::disk('s3')->size($s3Upload->file_name); if($size !== $s3Upload->file_size) throw new ApiException("file size mismatch!"); // 获取S3文件内容并计算MD5哈希 $fileContent = Storage::disk('s3')->get($s3Upload->file_name); $calculatedHash = md5($fileContent); if($calculatedHash !== $s3Upload->file_hash) throw new ApiException("file content mismatch!"); // 验证通过,更新数据库 }
2. 生成预签名URL时强制指定Content-Length
可以在生成预签名URL时,绑定文件的Content-Length,这样前端上传的文件大小必须和指定值完全一致,否则S3会直接拒绝请求,从源头避免空文件或大小不符的情况:
后端修改生成URL的逻辑:
// 先从请求获取前端传来的file_size $fileSize = $request->input('file_size'); $cmd = $client->getCommand('PutObject', [ 'Bucket' => $bucket, 'Key' => $newName, 'ContentLength' => $fileSize // 强制指定文件大小 ]); $request = $client->createPresignedRequest($cmd, '+20 minutes')->withMethod('PUT'); $url = (string)$request->getUri();
这样一来,前端如果上传空文件或者大小不符的文件,S3会直接返回400错误,根本不会存储无效文件,后端验证时只需要确认文件存在即可。
3. 利用S3的ETag做验证(注意分段上传场景)
S3的ETag是对象的哈希标识,普通单块上传的ETag就是文件的MD5哈希值;如果是分段上传,ETag格式是{MD5拼接值}-{分段数}。可以利用这个特性做验证:
- 前端计算文件MD5,传给后端存到数据库
- 后端验证时,获取S3文件的ETag,根据上传方式(单块/分段)做对比
后端获取ETag并验证:
function verifyUpload($token){ $s3Upload = S3UploadUrl::where('token' , $token )->firstOrFail(); $metadata = Storage::disk('s3')->getMetadata($s3Upload->file_name); if(!$metadata) throw new ApiException("file not found!"); $etag = trim($metadata['etag'], '"'); // 去掉ETag的引号 // 单块上传的情况:ETag直接等于MD5 if(strpos($etag, '-') === false){ if($etag !== $s3Upload->file_hash) throw new ApiException("file content mismatch!"); } else { // 分段上传的情况:需要前端计算分段哈希并传给后端,这里简化处理,或者跳过(如果前端不用分段上传可以忽略) throw new ApiException("resumable uploads need additional hash verification"); } // 验证通过,更新数据库 }
4. 强制前端上传时携带Content-MD5请求头
可以让前端在PUT请求时携带Content-MD5请求头,S3会自动校验这个MD5和文件内容是否一致,不一致则返回400错误。后端生成预签名URL时,需要允许这个请求头:
后端生成URL时允许Content-MD5头:
$cmd = $client->getCommand('PutObject', [ 'Bucket' => $bucket, 'Key' => $newName ]); // 允许前端发送Content-MD5头 $request = $client->createPresignedRequest($cmd, '+20 minutes') ->withMethod('PUT') ->withHeader('Content-MD5', '{{ContentMD5}}'); // 占位符表示允许该头 $url = (string)$request->getUri();
前端上传时携带头:
// 计算文件MD5的base64编码(S3要求Content-MD5是base64格式的MD5哈希) async function calculateContentMD5(file) { const arrayBuffer = await file.arrayBuffer(); const hashBuffer = await crypto.subtle.digest('MD5', arrayBuffer); return btoa(String.fromCharCode(...new Uint8Array(hashBuffer))); } // 上传时设置请求头 const contentMD5 = await calculateContentMD5(selectedFile); await fetch(presignedUrl, { method: 'PUT', body: selectedFile, headers: { 'Content-MD5': contentMD5 } });
这样S3会自动校验,只有MD5匹配才会存储文件,后端验证时只需要确认文件存在且大小正确即可。
内容的提问来源于stack exchange,提问作者hretic
相关产品推荐
相关产品推荐

