You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.18 20:42:04