使用Laravel从Web服务器流式传输Amazon S3对象

7
在我使用laravel 5.1构建的Web应用程序中,用户可以上传一些敏感文件,我将这些文件存储在Amazon S3中。稍后,我希望有权限的用户可以下载此文件。由于我想要进行身份验证检查,因此我不能使用传统方法通过向他们提供S3文件的直接链接来下载文件。
我的方法:
  1. 当用户请求下载时,我的服务器会将文件下载到本地,然后流式传输给用户。问题:有时需要很长时间,因为文件太大。
  2. 给用户一个预签名URL以直接从S3下载。 URL仅在5分钟内有效。问题:如果该URL被共享,则任何人都可以在5分钟内下载它。
  3. 根据此文章,直接从S3向客户端流式传输数据。这看起来很有前途,但我不知道如何实现这一点。
根据这篇文章,我需要:
  1. 注册流包装器 - 这是我的第一个问题,因为我不知道如何获取S3Client对象,因为laravel使用flysystem,并且我不知道调用哪些方法才能获取此对象。也许我需要在composer.json中单独包含S3包?
  2. 禁用输出缓冲 - 我需要在laravel中执行此操作还是laravel已经处理好了?
我相信其他开发人员以前也看到过这样的问题,并希望得到一些帮助指针。如果有人已经使用laravel Response::download($pathToFile, $name, $headers)直接从S3流式传输到客户端,则我很乐意听取您的方法。

1
这并不是直接从S3到用户的流媒体,而是通过S3->Laravel->用户进行流媒体传输。您的服务器仍处于循环中,因此您正在承担带宽成本,并错失了直接从S3提供服务的大部分好处。 - ceejayoz
你可以尝试使用临时IAM凭证来拼凑一些东西,以限制签名URL的使用仅限于特定IP。 - ceejayoz
我的理解是,您将拥有一个名为“temporary”的IAM用户,其权限非常有限。您将向其分配临时访问令牌,供用户使用,他们只能使用这些令牌来访问特定IP的特定签名URL。 - ceejayoz
2
只要请求开始,就会被允许完成。顺便说一句,如果您将CloudFront放在S3之前,您的签名URL可以限制为IP地址。请参见http://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-creating-signed-url-custom-policy.html - "IpAddress":{"AWS:SourceIp":"optional IP address"} - 这将为您的方案增加一些额外的安全性。 - ceejayoz
@ceejayoz 这似乎是一个可行的解决方案,因为“已经开始的请求将被允许完成”。谢谢。 :) - Rash
显示剩余4条评论
1个回答

9

通过评论讨论,我总结了一些关键要点想要分享。

预签名URL

正如@ceejayoz指出的那样,预签名 URL并不是一个坏主意,因为:

  1. 我可以将时间保持在10秒钟左右,这对于任何重定向和开始下载都非常完美,但对于链接共享来说不够。
  2. 我之前的理解是下载必须在给定时间内完成。因此,如果链接在10秒钟后到期,则下载必须在此之前完成。但是@ceejayoz指出这不是这种情况。已经开始的下载被允许完成。
  3. 使用 CloudFront ,我还可以限制IP地址以增加安全性。


IAM角色

他还指出了另一种不太好的方法 - 创建临时IAM用户。如果没有正确执行,则这将是维护噩梦,因此只有在您知道自己在做什么时才能执行。


S3流式传输

这是我现在选择的方法。也许以后我会转移到第一种方法。

警告:如果您进行流式传输,则您的服务器仍然是中间人,并且所有数据都将通过您的服务器传输。因此,如果服务器失败或速度慢,您的下载速度将变慢。

我的第一个问题是如何注册流包装器:

由于我正在使用Laravel和Laravel使用flysystem进行S3管理,因此没有简单的方法可以获取S3Client。因此,我在我的composer.json 中添加了另一个软件包AWS SDK for Laravel

"aws/aws-sdk-php-laravel" : "~3.0"

然后我按照以下方式编写了我的代码:

class FileDelivery extends Command implements SelfHandling
{
    private $client;
    private $remoteFile;
    private $bucket;

    public function __construct($remoteFile)
    {
        $this->client = AWS::createClient('s3');
        $this->client->registerStreamWrapper();
        $this->bucket = 'mybucket';
        $this->remoteFile = $remoteFile;
    }

    public function handle()
    {
        try
        {
            // First get the meta-data of the object.
            $headers = $this->client->headObject(array(
                'Bucket' => $this->bucket,
                'Key' => $this->remoteFile
            ));

            $headers = $headers['@metadata'];
            if($headers['statusCode'] !== 200)
            {
                throw new S3Exception();
            }
        }
        catch(S3Exception $e)
        {
            return 404;
        }

        // return appropriate headers before the stream starts.
        http_response_code($headers['statusCode']);
        header("Last-Modified: {$headers['headers']['last-modified']}");
        header("ETag: {$headers['headers']['etag']}");
        header("Content-Type: {$headers['headers']['content-type']}");
        header("Content-Length: {$headers['headers']['content-length']}");
        header("Content-Disposition: attachment; filename=\"{$this->filename}\"");

        // Since file sizes can be too large,
        // buffers can suffer because they cannot store huge amounts of data.
        // Thus we disable buffering before stream starts.
        // We also flush anything pending in buffer.
        if(ob_get_level())
        {
            ob_end_flush();
        }
        flush();

        // Start the stream.
        readfile("s3://{$this->bucket}/{$this->remoteFile}");
    }
}

我的第二个问题是在laravel中是否需要禁用输出缓冲?

我认为答案是肯定的。缓冲可以让数据立即从缓冲区刷新,从而降低内存消耗。由于我们没有使用任何laravel函数将数据卸载到客户端,因此这不是laravel完成的,需要我们自己完成。


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