MinIO 中使用 Pre-signed URLs 前端直接上传文件

MinIO php PHP · ez · 于 2年前 发布 · 5771 次阅读

概述

上传文件是当今开发中不可缺少的部分, 为了进行高效的上传, 所以要了解一些基础概念.

什么是前端直接上传文件

通常我们上传文件是通过接口直接上传到服务端, 如果做负载均衡或者动静分离的时候, 我们就会把文件服务器单独拆出来作为对象存储服务(OSS), 一般的改造是在服务端将上传的文件发送到对象存储到服务器中. 流程如下:

应用客户端(浏览器、APP、小程序等) --> 服务端(PHP、JAVA等) --> 对象存储服务端(aws s3、oss、cos、minIO等)

这样实际上是通过服务端进行了一次中转, 这样上传时间会很久,同时也牺牲了服务端的部分性能. 所以我们就会想如何直接上传到对象存储. 从而达到以下效果:

应用客户端(浏览器、APP、小程序等) --> 对象存储服务端(aws s3、oss、cos、minIO等)

什么 Pre-signed URLs

其实在亚马逊AWS、阿里云、腾讯云本身就提供了jssdk以及相关的解决方案. 由于MinIO完全兼容aws s3 所以自然就有了和s3相同的办法, Pre-signed URLs预签名URL.

为了安全不能任何人都可以上传, 所以通过服务端生成一个预签名URL, 里面包含上传到AWS S3所需要的一些认证标识信息.

一般这种签名为了安全起见也是有过期时间的.

上传文件

上传文件很简单, 分为两部, 第一步: 向服务端请求一个预签名URL, 第二步: 使用这个预签名URL把文件推上去.

1. 服务端预签名URL接口

官网介绍的案例是js版本的服务端通过Express Node.js server 来实现. 可偏偏就没有PHP版本, 既然兼容了AWS S3 想使用S3的PHP SDK 测试后发现, 生成的签名校验不过去. 无奈之下, 自己按照生成规则编写了一个php版本的签名生成方法, 具体代码如下:

  // OSS config for edu module
    define("OSS_ACCESS_KEY", "abcdefgh");
    define("OSS_ACCESS_SECRET", "xxxx1234");
    define("OSS_ACCESS_BUCKET", "edu");
    define("OSS_ACCESS_REGION", "beijing");
    define("OSS_ACCESS_ENDPOINT", "oss.minio.bestyii.com");

/**
 * Returns a pre-signed URL to access a restricted AWS S3 object.
 *
 * @param string $access_key the AWS access key
 * @param string $secret_key the AWS secret key associated with the access key
 * @param string $bucket the S3 bucket
 * @param string $canonical_uri the object in the S3 bucket expressed as a canonical URI.
 * This should begin with the / character, and should not be URL-encoded
 * @param int $expires the time that the pre-signed URL will expire, in seconds
 * @param string $region the AWS region
 * @param array $extra_headers any extra headers to be signed
 * @return string a HTTPS pre-signed URL for the AWS S3 object
 */
function aws_s3_link($access_key, $secret_key, $bucket, $canonical_uri, $expires = 0, $region = 'us-east-1', $extra_headers = array())
{
    $encoded_uri = '/' . $bucket . '/' . str_replace('%2F', '/', rawurlencode($canonical_uri));
    $signed_headers = array();
    foreach ($extra_headers as $key => $value) {
        $signed_headers[strtolower($key)] = $value;
    }
    ksort($signed_headers);
    $header_string = '';
    foreach ($signed_headers as $key => $value) {
        $header_string .= $key . ':' . trim($value) . "\n";
    }
    $signed_headers_string = implode(';', array_keys($signed_headers));
    $signed_headers_string = strtolower($signed_headers_string);
    $timestamp = time();
    $date_text = gmdate('Ymd', $timestamp);
    $time_text = gmdate('Ymd\THis\Z', $timestamp);
    $algorithm = 'AWS4-HMAC-SHA256';
    $scope = "$date_text/$region/s3/aws4_request";
    $x_amz_params = array(
        'X-Amz-Algorithm' => $algorithm,
        'X-Amz-Credential' => $access_key . '/' . $scope,
        'X-Amz-Date' => $time_text,
        'X-Amz-SignedHeaders' => $signed_headers_string
    );
    if ($expires > 0) $x_amz_params['X-Amz-Expires'] = $expires;
    ksort($x_amz_params);
    $query_string_items = array();
    foreach ($x_amz_params as $key => $value) {
        $query_string_items[] = rawurlencode($key) . '=' . rawurlencode($value);
    }
    $query_string = implode('&', $query_string_items);
    $canonical_request = "PUT\n$encoded_uri\n$query_string\n$header_string\n$signed_headers_string\nUNSIGNED-PAYLOAD";
    $string_to_sign = "$algorithm\n$time_text\n$scope\n" . hash('sha256', $canonical_request, false);
    $signing_key = hash_hmac('sha256', 'aws4_request', hash_hmac('sha256', 's3', hash_hmac('sha256', $region, hash_hmac('sha256', $date_text, 'AWS4' . $secret_key, true), true), true), true);
    $signature = hash_hmac('sha256', $string_to_sign, $signing_key);
    $url = 'https://' . $signed_headers['host'] . $encoded_uri . '?' . $query_string . '&X-Amz-Signature=' . $signature;
    return $url;
}

$key = isset($_GET['key']) ? trim($_GET['key']) : null;
if (empty($key)) {
    echo 'key 参数必填';
    exit;
}
$key = urldecode($key);

echo aws_s3_link(OSS_ACCESS_KEY, OSS_ACCESS_SECRET, OSS_ACCESS_BUCKET, $key, 600, OSS_ACCESS_REGION, array('host' => OSS_ACCESS_ENDPOINT));

2. 应用客户端上传代码

表单代码

<td class="even"><input id="edu-video-input" type="file" accept="audio/mp4, video/mp4">
<input type="hidden" name="edu-video" id="edu-video" value="">
<div id="upload-video-btn" class="uploadBtn" style="padding: 8px;">上传视频</div>
<div id="video-upload—result" style="padding: 8px;"></div>
</td>

上传代码


    $(document).ready(function () {
        console.log('init');
        var fileChooser = $("#workstudy-video-input");
        var video = $("#workstudy-video");
        var button = $("#upload-video-btn");
        var results = $("#video-upload—result");
        button.click(function () {
            var files = fileChooser.attr('files');
            var file = files[0];
            if (file) {
                retrieveNewURL(file, (file, url) => {
                    // Upload the file to the server.
                    uploadFile(file, url);
                });
            } else {
                results.html('<p style="color: #ff0000">'+'请选择视频.'+'</p>');
            }
        });

        // `retrieveNewURL` accepts the name of the current file and invokes the `/presignedUrl` endpoint to
        // generate a pre-signed URL for use in uploading that file:
        function retrieveNewURL(file, cb) {
            console.log(file);
            fetch(`/minio.php?key=${Date.now()}.mp4`).then((response) => {
                response.text().then((url) => {
                    cb(file, url);
                });
            }).catch((e) => {
                console.error(e);
            });
        }

        // ``uploadFile` accepts the current filename and the pre-signed URL. It then uses `Fetch API`
        function uploadFile(file, url) {
            results.html('<img src="/modules/system/images/loader.gif">');

            fetch(url, {
                method: 'PUT',
                body: file
            }).then(() => {
                // If multiple files are uploaded, append upload status on the next line.
                results.html('<p style="color: #6ce26c">'+file.name + '上传成功'+'</p>');
                fileChooser.val('');
                video.val(getUrlNoQueryParams(url));
            }).catch((e) => {
                console.error(e);
            });
        }

        function getUrlNoQueryParams(url) {
            if (url.indexOf('?') > 0) {
                return url.substring(0, url.indexOf('?'))
            }
            return url
        }
    });

抛砖引玉的参考一下吧

本文由 ez 创作,采用 知识共享署名 3.0 中国大陆许可协议 进行许可。 可自由转载、引用,但需署名作者且注明文章出处。

共收到 0 条回复
没有找到数据。
添加回复 (需要登录)
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册