上传文件是当今开发中不可缺少的部分, 为了进行高效的上传, 所以要了解一些基础概念.
通常我们上传文件是通过接口直接上传到服务端, 如果做负载均衡或者动静分离的时候, 我们就会把文件服务器单独拆出来作为对象存储服务(OSS), 一般的改造是在服务端将上传的文件发送到对象存储到服务器中. 流程如下:
应用客户端(浏览器、APP、小程序等) --> 服务端(PHP、JAVA等) --> 对象存储服务端(aws s3、oss、cos、minIO等)
这样实际上是通过服务端进行了一次中转, 这样上传时间会很久,同时也牺牲了服务端的部分性能. 所以我们就会想如何直接上传到对象存储. 从而达到以下效果:
应用客户端(浏览器、APP、小程序等) --> 对象存储服务端(aws s3、oss、cos、minIO等)
其实在亚马逊AWS、阿里云、腾讯云本身就提供了jssdk以及相关的解决方案. 由于MinIO完全兼容aws s3 所以自然就有了和s3相同的办法, Pre-signed URLs
即预签名URL
.
为了安全不能任何人都可以上传, 所以通过服务端生成一个预签名URL
, 里面包含上传到AWS S3所需要的一些认证标识信息.
一般这种签名为了安全起见也是有过期时间的.
上传文件很简单, 分为两部, 第一步: 向服务端请求一个预签名URL
, 第二步: 使用这个预签名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));
表单代码
<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 中国大陆许可协议 进行许可。 可自由转载、引用,但需署名作者且注明文章出处。