Laravel 使用预签名 URL 上传文件
前言⌗
普通的文件上传流程是客户端将文件发送到后端服务,然后后端服务器再上传到 S3,从中不能看出,一个文件被流转了两次。如果是小文件还可以,如果是大文件,则导致后端服务器流量被占用!
另一个问题就是如果后端服务在国内,经常会出现上传到 S3 超时。
为了避免以上两个问题,最好的方案是由客户端直接进行上传,即避免了多次流转,也便于解决国内服务器访问 S3 超时问题(客户端自行使用代理访问)。
认证问题⌗
上传到 S3 需要 Key 和 Secret,而这些信息又不能直接给到客户端,包括浏览器和 Native App,因为一旦泄露,对于 S3 来说将是非常危险的!
为了避免认证问题,S3 提供与签名请求来上传文件,后端根据上传的文件生成预签名 URL,客户端根据这个 URL 再上传文件到 S3。
这样几不用暴露 Key 和 Secret 给前端,同时可以限制客户端只能将文件上传到指定 Bucket 下的指定路径!
即使,恶意用户通过获取这个 URL 来访问 S3 也只能上传文件,对整个桶造成不了什么影响!
生 PreSigned URL⌗
composer require league/flysystem-aws-s3-v3 "~1.0"
Laravel 9.* 使用 league/flysystem-aws-s3-v3 “^3.0”。
直接使用 AWS SDK⌗
$s3 = Storage::disk('s3');
$client = $s3->getDriver()->getAdapter()->getClient();
$expiry = now()->addSeconds(60);
$command = $client->getCommand('PutObject', [
'Bucket' => config('filesystems.disks.s3.bucket'),
'Key' => 'path/to/file/' . Str::random(64),
'ACL' => 'public-read',
]);
$request = $client->createPresignedRequest($command, $expiry);
$presignedUrl = (string)$request->getUri();
扩展 league/flysystem⌗
Laravel 8.* 需要再 AppServiceProvider.php
中的 boot
方法中定义如下代码:
use Illuminate\Filesystem\FilesystemAdapter;
FilesystemAdapter::macro('temporaryUploadUrl', function ($path, $expiration, array $options = [])
{
$adapter = $this->driver->getAdapter();
if ($adapter instanceof AwsS3Adapter) {
$client = $adapter->getClient();
$command = $client->getCommand('PutObject', array_merge([
'Bucket' => $adapter->getBucket(),
'Key' => $adapter->getPathPrefix().$path
], $options));
$uri = $client->createPresignedRequest(
$command, $expiration
)->getUri();
// If an explicit base URL has been set on the disk configuration then we will use
// it as the base URL instead of the default path. This allows the developer to
// have full control over the base path for this filesystem's generated URLs.
/** @phpstan-ignore-next-line */
if (! is_null($url = $this->driver->getConfig()->get('temporary_url'))) {
/** @phpstan-ignore-next-line */
$uri = $this->replaceBaseUrl($uri, $url);
}
return (string) $uri;
}
throw new RuntimeException('This driver does not support creating temporary upload URLs.');
});
Laravel 9.* 需要再 AppServiceProvider.php
中的 boot
方法中定义如下代码:
use Illuminate\Filesystem\AwsS3V3Adapter;
AwsS3V3Adapter::macro('temporaryUploadUrl', function ($path, $expiration, array $options = []) {
/** @phpstan-ignore-next-line */
$command = $this->getClient()->getCommand('PutObject', array_merge([
'Bucket' => Arr::get($this->getConfig(), 'bucket'),
'Key' => $this->path($path),
], $options));
/** @phpstan-ignore-next-line */
$uri = $this->getClient()->createPresignedRequest(
$command, $expiration, $options
)->getUri();
// If an explicit base URL has been set on the disk configuration then we will use
// it as the base URL instead of the default path. This allows the developer to
// have full control over the base path for this filesystem's generated URLs.
$temporaryUrl = Arr::get($this->getConfig(), 'temporary_url');
if ($temporaryUrl) {
/** @phpstan-ignore-next-line */
$uri = $this->replaceBaseUrl($uri, $temporaryUrl);
}
return (string) $uri;
});
使用⌗
$prefix = 'path/to/file';
$name = Str::random(64);
$path = sprintf('%s/%s', $prefix, $name);
Storage::disk('s3')->temporaryUploadUrl($path, now()->addMinutes(10), ['ACL' => 'public-read']);
前端拿到这个 URL 就可以上传文件了,另外 MinIO 也支持,具体如何实施可以参考官方文档
I hope this is helpful, Happy hacking…