PHP实现服务端签名阿里云OSS直传实践
概述
本教程介绍如何在Web端通过表单上传方式直接上传数据到OSS。Web端常见的上传方法是用户在浏览器或App端上传文件到应用服务器,应用服务器再把文件上传到OSS。这种方式需通过应用服务器中转,传输效率明显低于数据直传至OSS的方式。
这里使用在服务端完成签名,然后通过表单直传数据到OSS
服务端签名直传
服务端签名直传是指在服务端生成签名,将签名返回给客户端,然后客户端使用签名上传文件到OSS。由于服务端签名直传无需将访问密钥暴露在前端页面,相比客户端签名直传具有更高的安全性。本文介绍如何进行服务端签名直传。
请求流程服务端签名直传流程如下图所示
图片来源:阿里云
复制
/**
* @desc 获取上传策略签名
* @param array $param
* @return array
* @author Tinywan(ShaoBo Wan)
*/
public static function getUploadPolicy(array $param): array
{
$param[type] = images;
$config = [
accessKeyId => xxxxxxxxxxxxxxx,
accessKeySecret => xxxxxxxxxxxxxxxxxxxxxxxxxxx,
callbackUrl => https://oss.tinywan.com/aliyun/oss-upload-callback,
images => [
bucket => images,
host => https://images.tinywan.com,
endpoint => oss-cn-hangzhou.aliyuncs.com,
ContentLengthMin => 10,
ContentLengthMax => 20 * 1024 * 1024,
]
];
$bucket = $config[$param[type]];
// 文件路径和文件名
$dir = self::PROJECT_BUCKET . DIRECTORY_SEPARATOR . env(env_name) . DIRECTORY_SEPARATOR . self::getDirectoryPath($param[type], $param[dirname]);
$key = $dir . self::getRandomFilename($param[ext]);
// 过期时间
$expiration = self::getExpireTime(self::EXPIRE_TIME);
// 参数设置
$policyParams = [
expiration => $expiration,
conditions => [
[starts-with, $key, $dir],
[content-length-range, $bucket[ContentLengthMin], $bucket[ContentLengthMax]]
]
];
$policyBase64 = self::getPolicyBase64($policyParams);
$signature = self::getSignature($policyBase64, $config[accessKeySecret]);
// 回调
$callbackParam = [
callbackUrl => $config[callbackUrl],
callbackBody => filename=${object}&size=${size}&mimeType=${mimeType}&height=${imageInfo.height}&width=${imageInfo.width},
callbackBodyType => application/x-www-form-urlencoded,
];
$callbackString = json_encode($callbackParam);
$base64CallbackBody = base64_encode($callbackString);
return [
access_id => $config[accessKeyId],
host => https:// . $bucket[bucket] . . . $bucket[endpoint], // $host的格式为 bucketname.endpoint
policy => $policyBase64,
signature => $signature,
expire => $expiration,
callback => $base64CallbackBody,
dir => $dir,
key => $key,
url => $bucket[host] . DIRECTORY_SEPARATOR . $key
];
}1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.34.35.36.37.38.39.40.41.42.43.44.45.46.47.48.49.50.51.52.53.54.55.56.57.58.59.
复制
/**
* @desc: 获取参数base64
* @param $policyParams
* @return string
* @author Tinywan(ShaoBo Wan)
*/
private static function getPolicyBase64($policyParams): string
{
return base64_encode(json_encode($policyParams));
}1.2.3.4.5.6.7.8.9.10.
复制
/**
* @desc: 获取签名
* @param string $policyBase64
* @param string $accessKeySecret
* @return string
* @author Tinywan(ShaoBo Wan)
*/
private static function getSignature(string $policyBase64, string $accessKeySecret): string
{
return base64_encode(hash_hmac(sha1, $policyBase64, $accessKeySecret, true));
}1.2.3.4.5.6.7.8.9.10.11.
复制
/**
* @desc: 获取过期时间
* @param int $time
* @return array|false|string|string[]
* @author Tinywan(ShaoBo Wan)
*/
private static function getExpireTime(int $time)
{
return str_replace(+00:00, .000Z, gmdate(c, time() + $time));
}1.2.3.4.5.6.7.8.9.10.
复制
/**
* @desc: 获取按照月份分隔的文件夹路径
* @param string $type
* @param string $directoryName eg: img/video
* @return string
* @author Tinywan(ShaoBo Wan)
*/
private static function getDirectoryPath(string $type, string $directoryName): string
{
if ($type === img) {
return $directoryName . DIRECTORY_SEPARATOR . date(Y-m) . DIRECTORY_SEPARATOR;
}
return date(Y-m) . DIRECTORY_SEPARATOR;
}1.2.3.4.5.6.7.8.9.10.11.12.13.14.
复制
/**
* @desc: 获取一个随机的文件名
* @param string $extend eg: jpg
* @return string
* @author Tinywan(ShaoBo Wan)
*/
private static function getRandomFilename(string $extend): string
{
return \Ramsey\Uuid\Uuid::uuid4()->toString() . . . $extend;
}1.2.3.4.5.6.7.8.9.10.
微信小程序客户端
请求上传参数复制
{
"type": "images",
"dirname": "images",
"ext": "png"
}1.2.3.4.5.
复制
{
"code": 0,
"msg": "success",
"data": {
"access_id": "xxxxxxxxxxxxxxxxx",
"host": "https://tinywan-images.oss-cn-hangzhou.aliyuncs.com",
"policy": "eyJlexxxxxxxxxxxxxxxxxxxx==",
"signature": "YrlQxxxxxxxxxxxxxxxxxxxxxxtiI=",
"expire": "2024-05-22T09:46:46.000Z",
"callback": "eyJxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx0=",
"dir": "ai/2024-05/",
"key": "ai/2024-05/14b796b1-54ef-48d9-83a3-cde7cbc298e7.png",
"url": "https://images.tinywan.com/ai/2024-05/14b796b1-54ef-48d9-83a3-cde7cbc298e7.png"
}
}1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.
“
客户端拿到文件名后缀后,传给服务端,获取签名和文件名等必要的上传参数,让更多的工作在服务端完成
复制
/** 获取上传文件扩展名 */
function getFilePathExtention(filePath) {
return filePath.split(.).slice(-1)[0];
}
/** 上传到阿里云oss */
function uploadFileAsync(config, filePath) {
console.log(config);
return new Promise((resolve, reject) => {
wx.uploadFile({
url: config.host, // 开发者服务器的URL。
filePath: filePath,
name: file, // 必须填file。
formData: {
key: config.key,
policy: config.policy,
OSSAccessKeyId: config.accessKeyId,
signature: config.signature
},
success: (res) => {
console.log(res);
if (res.statusCode === 204) {
resolve();
} else {
reject(上传失败);
}
},
fail: (err) => {
console.log(err);
},
});
});
}
/** 上传文件 */
export async function uploadFile(filePath, dirname = image) {
console.log(filePath);
let ext = getFilePathExtention(filePath);
// 改方法通过接口获取服务端生成的上传签名
const resParams = await Http.AliOssGetUploadParams({
ext,
dirname,
});
await uploadFileAsync(resParams.data, filePath);
return resParams;
}1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.34.35.36.37.38.39.40.41.42.43.44.45.46.47.48.49.50.51.
上传回调
大多数情况下,用户上传文件后,应用服务器需要知道用户上传了哪些文件以及文件名;如果上传了图片,还需要知道图片的大小等,为此OSS提供了上传回调方案。
流程介绍当用户要上传一个文件到OSS,而且希望将上传的结果返回给应用服务器时,需要设置一个回调函数,将请求告知应用服务器。用户上传完文件后,不会直接得到返回结果,而是先通知应用服务器,再把结果转达给用户。
图片
复制
/**
* @desc: OSS上传回调
* @throws ForbiddenHttpException
* @return Response
* @author Tinywan(ShaoBo Wan)
*/
public function ossUploadCallback(): Response
{
// 1.请求头参数
$header = $this->request->header();
$authorizationBase64 = $header[authorization] ?? ;
$pubKeyUrlBase64 = $header[x-oss-pub-key-url] ?? ;
if ($authorizationBase64 == || $pubKeyUrlBase64 == ) {
throw new \tinywan\exception\ForbiddenHttpException();
}
// 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);
$publicKey = curl_exec($ch);
if ($publicKey == ) {
throw new \tinywan\exception\ForbiddenHttpException();
}
// 4.获取回调body
$body = $this->request->getInput();
// 5.拼接待签名字符串
$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.验证签名
$verifyRes = openssl_verify($authStr, $authorization, $publicKey, OPENSSL_ALGO_MD5);
if ($verifyRes === 1) {
return response_json(success, 200, $this->request->post());
}
throw new \tinywan\exception\ForbiddenHttpException();
}1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.34.35.36.37.38.39.40.41.42.43.44.
阅读剩余
THE END