Laravel安全地访问Amazon S3存储桶中的文件

5

我正在使用Amazon S3,但是我遇到了两个问题:

1.当我提交表单时,我无法直接上传文件到Amazon服务器。我的意思是,我必须将图像上传到PHP服务器上的一个上传文件夹,然后从那里检索并将它们上传到S3服务器。是否有一种方法可以在单击提交时直接上传图像到S3

2.如果我在S3 put object中传递'public',那么我可以访问或查看文件,但如果我使其公开,每个人都可以查看文件。但我需要保护所有文件,并仅向经过身份验证的用户显示。有人能建议我如何解决这个问题吗?

try {           
    $s3 = \Storage::disk('s3');
    $s3->put($strFileName, file_get_contents($img_path.$strFileName), 'public');
} catch (Aws\Exception\S3Exception $e) {
    echo "There was an error uploading the file.\n"+$e;
}

在提问之前,我读了许多来自stackoverflow的答案,但它们并没有帮助我解决问题。谢谢。
4个回答

5

对于你的第一个问题,可以直接将图片上传到AWS S3。

$s3 = \Storage::disk('s3')->getDriver();
$s3->put($filePath, file_get_contents($file), array('ContentDisposition' => 'attachment; filename=' . $file_name . '', 'ACL' => 'public-read'));

您需要指定文件路径和从表单获取的文件。

1
谢谢你的回答。你知道如何访问私有图片或保护图片不被公众访问吗? - Vision Coderz

3

1. 有没有一种方法可以在单击提交时直接上传图片

是的。

如何:

您需要使用JavaScript(带有AJAX)分两部分完成此操作;

a)当用户单击“提交”时,您捕获此事件,首先上传文件(请参见http://docs.aws.amazon.com/AWSJavaScriptSDK/guide/browser-examples.html以获取示例),然后

b)通过AJAX提交表单并处理响应。

但是:

这允许用户上传任何内容,并可能导致问题。有提示(就在示例下面)创建身份验证URL,有效期为15分钟(可变),但是如果用户花费的时间超过15分钟,或者在15分钟内尝试上传100个文件,或者上传其他类型的文件,则会发生什么情况?或格式不正确的图像文件等。

从服务器中拉取并验证它们是否为所需类型/大小的图像,然后上传到服务器更加安全。

当然,如果这是一个简单的管理工具,并且您正在控制访问代码的人,则可以尝试-希望您只会上传所期望的内容。

2. 我需要保护所有文件并仅向经过身份验证的用户显示

如果“经过身份验证的用户”是指“上传图像的用户”,则仅使用S3无法提供此功能,但CloudFront可以。您可以发出预授权的URL或已签名的cookie:http://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-choosing-signed-urls-cookies.html

如果“经过身份验证的用户”是指上传文件的人,则根据文档,在不获取对底层客户端的访问权限的情况下,Laravel类中不可能实现此目标。publicprivate是默认的可见性选项,它们被转换为public-read,但您需要authenticated-readbucket-owner-read或其他一些预定义授予(参考:http://docs.aws.amazon.com/AmazonS3/latest/dev/acl-overview.html#canned-acl)如果authenticated-read或其他预定义的ACL授予不提供所需的权限配置文件,则可以创建自己的(有关详细信息,请参见同一页上面的详细信息)。

解决方案是抓取底层客户端,然后直接执行put-object操作。 (然后如果您走得那么远,最好放弃Laravel库并拉取s3并自己完成所有操作-然后您可以完全控制一切)。

$client = $disk->getDriver()->getAdapter()->getClient();

$client->putObject([
    'Bucket' => $bucket,
    'Key'    => $fileName,
    'Body'   => $fileBody,
    'ACL'    => 'authenticated-read'  /* Or which ACL suits */
]);

@Robbie,感谢您详细的回答。'ACL' => 'authenticated-read' 好的,我该如何检索它呢?因为我是亚马逊S3服务器的新手。再次感谢。 - Vision Coderz
你需要哪个位的编程示例呢?在 Laravel 库之外进行操作的最佳参考是 AWS SDK for PHP(这里:http://docs.aws.amazon.com/aws-sdk-php/v3/api/api-s3-2006-03-01.html#putobject)。 - Robbie

2

我最近解决了这个问题。首先,你可以直接上传到S3,这是我用于此的一些信息: http://docs.aws.amazon.com/AmazonS3/latest/dev/HTTPPOSTExamples.html

首先,您需要在服务器端创建策略和签名,以添加到HTML表单中以上传文件。

$policy = base64_encode(json_encode([
            "expiration" => "2100-01-01T00:00:00Z",
            "conditions" => [
                ["bucket"=> "bucketname"],
                ["starts-with", '$key', "foldername"],
                ["acl" => "public-read"],
                ["starts-with", '$Content-Type', "image/"],
                ["success_action_status" => '201'],
            ]
        ]));
$signature = base64_encode(hash_hmac('sha1',$policy,getenv('S3_SECRET_KEY'),true));

现在在前端我的表单中,我不使用提交按钮,您可以使用提交按钮,但需要捕获提交并防止表单在上传完成之前实际提交。
当我们点击保存时,它会生成一个md5(使用npm安装)文件名,以便文件名不能随机猜测,然后使用ajax将文件上传到S3。完成后,它将文件数据和返回的aws数据放入隐藏输入框并提交表单。它应该看起来像这样:
<form action="/post/url" method="POST" id="form">
    <input type="text" name="other_field" />
    <input type="file" class="form-control" id="image_uploader" name="file" accept="image/*" />
    <input type="hidden" id="hidden_medias" name="medias" value="[]" />
</form>
<input type="button" value="save" id="save" />
<script>
$(document).ready(function(){
    $('#save').click(function(){
            uploadImage(function () {
                $('#form').submit();
            });
    });
});
var uploadImage = function(callback) {
    var file = $('#image_uploader')[0].files[0];
    if(file !== undefined) {
        var data = new FormData();
        var filename = md5(file.name + Math.floor(Date.now() / 1000));
        var filenamePieces = file.name.split('.');
        var extension = filenamePieces[filenamePieces.length - 1];
        data.append('acl',"public-read");
        data.append('policy',"{!! $policy !!}");
        data.append('signature',"{!! $signature !!}");
        data.append('Content-type',"image/");
        data.append('success_action_status',"201");
        data.append('AWSAccessKeyId',"{!! getenv('S3_KEY_ID') !!}");
        data.append('key',filename + '.' + extension);
        data.append('file', file);

        var fileData = {
            type: file.type,
            name: file.name,
            size: file.size
        };

        $.ajax({
            url: 'https://{bucket_name}.s3.amazonaws.com/',
            type: 'POST',
            data: data,
            processData: false,
            contentType: false,

            success: function (awsData) {
                var xmlData = new XMLSerializer().serializeToString(awsData);
                var currentImages = JSON.parse($('#hidden_medias').val());
                currentImages.push({
                    awsData: xmlData,
                    fileData: fileData
                });
                $('#hidden_medias').val(JSON.stringify(currentImages));
                callback();
            },
            error: function (errorData) {
                console.log(errorData);
            }
        });
    }
};
</script>

控制器监听提交,然后解析该输入字段中的JSON并创建一个Media实例(我创建的模型),它为每个图像存储awsData和fileData。
然后,不是像这样将html图像标签指向s3文件:
<img src="https://{bucketname}.s3.amazonaws.com/filename.jpg" />

我会像这样做:

我做了这样的事情:

<img src="/medias/{id}" />

然后路由可以通过正常的auth中间件,你只需要在Laravel中做以下操作。最后,该路由指向一个控制器,执行以下操作:

public function getResponse($id)
{
    $media = Media::find($id);
    return (new Response('',301,['Location' => $media->info['aws']['Location']]));
}

这个功能会使用301重定向,将头部位置设置为实际的aws文件。由于我们在上传文件到aws时生成一个md5文件名,因此每个文件名都是一个md5,所以人们无法随意搜索存储桶中的aws文件。


@Pitchinnate,感谢您提供更详细的信息。我一定会尝试并告诉您结果。非常感谢您抽出时间来解释。 - Vision Coderz
@Pitchinnate,现在对我来说最重要的是保护图像不被未经授权的人访问。您已经解释得很清楚了,但我仍然有些困惑如何安全地从S3检索图像。如果您不介意,能否进一步解释一下? - Vision Coderz
从技术上讲,某些图像并没有完全受到保护,但这使得试图获取它们的人变得困难。请求图像首先传递给 Laravel,Laravel 能够验证用户是否已登录并具有查看图像的权限(可以使用中间件完成)。如果用户具有权限,则会发送一个 301 到实际文件的位置。 - Pitchinnate
1
我还有一篇关于这个的博客文章,详细介绍了一些内容:http://natedenlinger.com/uploading-files-directly-to-aws-s3-in-laravel-5-3/ - Pitchinnate
@Pitchinnate。非常感谢。我会尝试一次。 - Vision Coderz
显示剩余3条评论

2

这是一个关于编程的内容,以下是翻译:

这是我也在使用但没有使用 Laravel 的问题解决方案。

1. 如果你想要将任何文件直接上传到 Amazon AWS S3 Bucket 中的特定文件夹,可以按照以下方式进行操作。

HTML

<form action="upload.php" enctype="multipart/form-data">
    <input type="file" name="file" id="file" />
    <button type="submit">Upload file</button>
</form>

PHP - upload.php

包含 AWS PHP SDK

require "vendor/autoload.php";

初始化 S3 客户端

$credentials = new Aws\Credentials\Credentials(
    '<AWS ACCESS KEY>', 
    '<AWS ACCESS SECRET>'
);

$s3Client = Aws\S3\S3Client::factory(
    [
        'credentials' => credentials
        'region' => 'us-east-1',
        'version' => 'latest'
    ]
);

创建文件上传实体

$uploadEntity = array(
    'Bucket' => <S3 Bucket Name>,
    'Key'    => '<Upload_Folder_If_Any>/<FileName>',
    'Body'   => fopen($_FILES['file']['tmp_name'], 'r+'),
    //'ContentDisposition' => 'attachment; filename="<FileName>"' <-- If need to allow downloading
);

上传至 S3 存储桶

try {
    // $result will be Aws\Result object
    $result = $s3Client->putObject($uploadEntity);  
} catch (Aws\S3\Exception\S3Exception $exception) {
    // S3 Exception
}

2. 现在为了只向经过身份验证的用户提供上传的文件

首先,您需要为s3 Bucket创建私有存储桶策略。

存储桶策略 - 要生成存储桶策略,您可以使用策略生成器。使用此工具,您可以生成如下策略。从Improve.dk网站复制

{
  "Id": "Policy1319566876317",
  "Statement": [
    {
      "Sid": "Stmt1319566860498",
      "Action": [
        "s3:GetObject"
      ],
      "Effect": "Allow",
      "Resource": "arn:aws:s3:::<BucketName>/*",
      "Principal": {
        "CanonicalUser": [
          "<CannoicalUserId_of_OAI>"
        ]
      }
    }
  ]
}

其次,您需要设置CloudFront私有Web分发S3 Bucket。只有这样,您才能通过AWS签名URL或签名Cookie将内容仅提供给经过身份验证的用户。
第三,要生成签名URL,您需要从AWS控制台获取pem文件。 生成签名URL
$cdnName = '<AWS CLOUDFRONT WEB DISTRIBUTION CDN>';
$assetName = '<UPLOAD_FOLDER_IF_ANY>/<FILENAME>';
$expiry = time() + 300;     // 5 mins expiry time, ie. the signed url will be valid only for 5 mins

$cloudfront = CloudFrontClient::factory(array(
    'credentials' => $credentials,
    'region' => 'us-east-1',
    'version' => 'latest'
));

// Use this for creating signed url
$signedUrl = $cloudFront->getSignedUrl([
    'url'         => $cdnName . '/' . $assetName,
    'expires'     => $expiry,
    'private_key' => '/path/to/your/cloudfront-private-key.pem',
    'key_pair_id' => '<cloudfront key pair id>'
]);

// Use this for signed cookie    
$signedCookie = $cloudFront->getSignedCookie([
    'url'         => $cdnName . '/' . $assetName,
    'expires'     => $expiry,
    'private_key' => '/path/to/your/cloudfront-private-key.pem',
    'key_pair_id' => '<cloudfront key pair id>'
]);

@Haridarshan,感谢您的回答,我稍后会尝试。 - Vision Coderz
@iCoders 将添加一些设置 S3 存储桶策略和私有网络分发所需的基本事项。 - Haridarshan

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接