前后端分离下Laravel上传大文件到oss
思路分析
因为业务调整,原本的系统需要支持上传视频这个需求,视频假设是300M-2G区间大小。
最初的时候,我们的文件都是放到服务器的某个文件夹的,加入了视频,不难发现这样做得不偿失,太占用服务器空间了,于是就像把文件上传到oss;
第一版思路,在不变原来的上传思路情况下,由前端上传文件到服务器,服务器再上传到oss,然后删除服务器上的临时文件,毫无疑问,这真是一个要命的方案,这样想我一定是脑子有问题。
很快我调整了思路,上一版的缺点很多,我主要在意的是上传太慢,于是我想到了分片上传,参考了其他系统的方案,我定下了第二版方案,开放两个接口,第一个接口对上传上来的大文件进行切片,然后切片上传至oss。这个时候我开始尝试了,毫无疑问,不太可行,首先这样做的问题是php.ini配置中设置太大,以及nginx中也有限制,改参数也很愚蠢,因为这样做依旧是需要先把大文件上传上来,这样一来,改参数并不是被赞同的,同时也很容易遇到等待时间过长的问题。
为了解决第二版的问题,我想到了第三版的方案,那就是由前端进行切片,然后在轮询请求上传文件,这样做也遇到了个问题,那就是容易遇到“RequestCoreException: cURL error: Empty reply from server (52)”这个报错,经过查询,这个是因为请求头的大小与实际上传大小有偏差,因为oss是通过php的内置函数filesize()这个函数来获取文件大小的,这个时候因为php缓存问题,会出现大小不一致等问题,当然还有一个大问题就是oss的分片上传我理解错了,导致这一方案被pass了。
因为项目催的紧,我萌生了让前端直接传到oss的念头,这样就解决了很多问题,但是如果把id和key都给前端,这样会相当不安全,很容易出问题,这个方案也被pass了。
就在我一筹莫展的时候,一位好友给我提供了方案,同样是让前端直传到oss,但是是通过签名+回调的方式来实现,这个方案解决了我目前遇到的问题,接下来会详细贴出这个方案的源代码。
源码+说明
这个方案出自阿里云官方给的另外一个示例,地址在这里:https://help.aliyun.com/document_detail/91771.html?spm=a2c4g.11186623.0.0.2fd13967WOYZ0F
他提供了一个示例文件,通过追读源码,理解思路,其实很容易明白其核心,总共分为三步:
第一步:获取签名
第二步:通过将签名返回的内容及文件,直接上传至阿里云的host(地址)
第三步:回调
那么你大概理解了,也许需要看下实际操作,一共有两个接口:
获取签名:
public function get_signature(Request $request){
$id = "你的AccessKey ID";
$key = "你的AccessKey Secret";
// $host的格式为 bucketname.endpoint,请替换为您的真实信息。
$host = 'https://test.oss-cn-hangzhou.aliyuncs.com';
// $callbackUrl为上传回调服务器的URL,请将下面的IP和Port配置为您自己的真实URL信息。
$callbackUrl = "https://test.com/api/v1/callback";//这是回调函数的路由地址
// 用户上传文件时指定的前缀,这样文件就只能上传到 test 文件夹下面
$dir = 'test/';
$callback_param = [
'callbackUrl' => $callbackUrl,
/**
* 回调的 body,里面可以配置一些信息来确定用户身份唯一性
* 如下设置,上传图片后返回的信息如下:{"uid":"666","name":"shuxiaoyuan","filename":"test\\/20220615\\/K4Ae7eZjHm.jpg","size":"4312532","mimeType":"image\\/jpeg","height":"2160","width":"3840","s":"\\/\\/demo\\/ali\\/oss\\/method4Notify"}
*/
'callbackBody' =>'filename=${object}&size=${size}&mimeType=${mimeType}&height=${imageInfo.height}&width=${imageInfo.width}',
'callbackBodyType' => "application/x-www-form-urlencoded"
];
$callback_string = json_encode($callback_param);
$base64_callback_body = base64_encode($callback_string);
// 当前时间
$now = time();
// 设置该 policy 超时时间(秒),即这个 policy 过了这个有效时间,将不能访问。
$expire = 36000;
$end = $now + $expire;
$expiration = $this->gmt_iso8601($end);
// 最大文件大小.用户可以自己设置
$condition = array(
0 => 'content-length-range',
1 => 0,
2 => 1024 * 1024 * 2000 // 2000M
);
$conditions[] = $condition;
// 表示用户上传的数据,必须是以$dir开始,不然上传会失败,这一步不是必须项,只是为了安全起见,防止用户通过policy上传到别人的目录。
$start = array(0 => 'starts-with', 1 => '$key', 2 => $dir);
$conditions[] = $start;
$arr = array('expiration' => $expiration, 'conditions' => $conditions);
$policy = json_encode($arr);
$base64_policy = base64_encode($policy);
$string_to_sign = $base64_policy;
$signature = base64_encode(hash_hmac('sha1', $string_to_sign, $key, true));
$response = array();
$response['accessid'] = $id;
$response['host'] = $host;
$response['policy'] = $base64_policy;
$response['signature'] = $signature;
$response['expire'] = $end;
$response['callback'] = $base64_callback_body;
$response['dir'] = $dir; // 这个参数是设置用户上传文件时指定的前缀。
return $response;//返回签名
}
对上述函数添加路由,请求这个路由就得到了对应的东西,类似这样:
{
"accessid": "LTAI5tDJqgdgdBAe8rwdFnt",
"host": "https://test.oss-cn-hangzhou.aliyuncs.com",
"policy": "eyJleHBpcmF0aW9uIjoiMjgdMi0xMS0xOFQwMjowMjoxNi4wMDBaIiwiY29uZGl0aW9ucyI6W1siY29udGVudC1sZW5ndGgtcmFuZ2UiLDAsMjA5NzE1MjAwMF0sWyJzdGFydHMtd2l0aCIsIiRrZXkiLCJ0cmFpbmluZ192aWRlb1wvIl1dfQ==",
"signature": "iV4QkB7qgdqEQLV3/W0ceSyrvpzA=",
"expire": 1668736936,
"callback": "eyJjYWxsYmFja1Vygd6Imh0dHBzOlwvXC9ocy5zZXIxMTkuY29tXC9hcGlcL3YxXC9jYWxsYmFjayIsImNhbGxiYWNrQm9keSI6ImZpbGVuYW1lPSR7b2JqZWN0fSZzaXplPSR7c2l6ZX0mbWltZVR5cGU9JHttaW1lVHlwZX0maGVpZ2h0PSR7aW1hZ2VJbmZvLmhlaWdodH0md2lkdGg9JHtpbWFnZUluZm8ud2lkdGh9IiwiY2FsbGJhY2tCb2R5VHlwZSI6ImFwcGxpY2F0aW9uXC94LXd3dy1mb3JtLXVybGVuY29kZWQifQ==",
"dir": "test/"
}
这时候前端就拿到了签名,就可以上传文件了:
这时就结束了吗?不是的,这个时候并没有验证回调,所以你还需要一个函数:
public function callback(Request $request){
$response = $request->all();
// 1.获取OSS的签名header和公钥url header
$authorizationBase64 = "";
$pubKeyUrlBase64 = "";
/*
* 注意:如果要使用HTTP_AUTHORIZATION头,你需要先在apache或者nginx中设置rewrite,以apache为例,修改
* 配置文件/etc/httpd/conf/httpd.conf(以你的apache安装路径为准),在DirectoryIndex index.php这行下面增加以下两行
RewriteEngine On
RewriteRule .* - [env=HTTP_AUTHORIZATION:%{HTTP:Authorization},last]
* */
if (isset($_SERVER['HTTP_AUTHORIZATION'])) {
$authorizationBase64 = $_SERVER['HTTP_AUTHORIZATION'];
}
if (isset($_SERVER['HTTP_X_OSS_PUB_KEY_URL'])) {
$pubKeyUrlBase64 = $_SERVER['HTTP_X_OSS_PUB_KEY_URL'];
}
if ($authorizationBase64 == '' || $pubKeyUrlBase64 == '') {
return $this->failed("上传失败");
}
// 2.获取OSS的签名
$authorization = base64_decode($authorizationBase64);
// 3.获取公钥
$pubKeyUrl = base64_decode($pubKeyUrlBase64);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $pubKeyUrl);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
$pubKey = curl_exec($ch);
if ($pubKey == "") {
return $this->failed("上传失败");
}
// 4.获取回调body
$body = file_get_contents('php://input');
// 5.拼接待签名字符串
$authStr = '';
$path = $_SERVER['REQUEST_URI'];
$pos = strpos($path, '?');
if ($pos === false) {
$authStr = urldecode($path) . "\n" . $body;
} else {
$authStr = urldecode(substr($path, 0, $pos)) . substr($path, $pos, strlen($path) - $pos) . "\n" . $body;
}
// 6.验证签名
$status = openssl_verify($authStr, $authorization, $pubKey, OPENSSL_ALGO_MD5);
if ($status == 1) {
header("Content-Type: application/json");
$url = 'https://anjk-hs.oss-cn-hangzhou.aliyuncs.com';
$data = [
// 这个地方应该拼接,不要写死
'file_url' => $response['filename'],
'data' => json_encode($response)
];
return $this->success(['data' => $data]);
} else {
return $this->failed("验签失败");
}
}
为这个函数添加路由,与生成签名里面的: $callbackUrl = “https://test.com/api/v1/callback";//这是回调函数的路由地址,相对应,oss会自动调用你的回调函数,这样就上传成功了。
基本上,oss官网的说辞,如果是小于5G就没有必要使用分片上传,所以到这里就结束了。
158bdfdf-aef3-a468-2f0d-889bf49753ee
本作品采用 知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议 (CC BY-NC-ND 4.0) 进行许可。