mirror of
https://github.com/crmeb/CRMEB.git
synced 2026-10-28 13:31:04 +00:00
fix(OSS上传): 支持 Signature V4 区域配置,管理端直传改走 V4 表单字段
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
5f342bb505
commit
f8ed83aa9a
@ -14,6 +14,7 @@ use crmeb\services\upload\BaseUpload;
|
||||
use crmeb\exceptions\AdminException;
|
||||
use crmeb\exceptions\UploadException;
|
||||
use OSS\Core\OssException;
|
||||
use OSS\Credentials\StaticCredentialsProvider;
|
||||
use OSS\Model\CorsConfig;
|
||||
use OSS\Model\CorsRule;
|
||||
use OSS\OssClient;
|
||||
@ -62,6 +63,12 @@ class Driver extends BaseUpload
|
||||
*/
|
||||
protected $storageRegion;
|
||||
|
||||
/**
|
||||
* OSS 区域 ID(V4 签名必填,如 cn-hangzhou)
|
||||
* @var string
|
||||
*/
|
||||
protected $regionId = '';
|
||||
|
||||
/**
|
||||
* 水印位置
|
||||
* @var string[]
|
||||
@ -96,7 +103,13 @@ class Driver extends BaseUpload
|
||||
$this->uploadUrl = rtrim(trim($config['uploadUrl'] ?? ''), '/');
|
||||
$this->storageName = trim($config['storageName'] ?? '');
|
||||
$this->cdn = rtrim(trim($config['cdn'] ?? ''), '/');
|
||||
$this->storageRegion = $this->normalizeEndpoint($config['storageRegion'] ?? '');
|
||||
// region:驱动配置里显式选择的区域;storageRegion:当前启用空间的区域/端点
|
||||
$configRegion = trim((string)($config['region'] ?? ''));
|
||||
$rawRegion = trim((string)($config['storageRegion'] ?? ''));
|
||||
$this->regionId = $this->resolveRegionId($configRegion) ?: $this->resolveRegionId($rawRegion);
|
||||
// 端点优先用空间区域;空间未填时回退驱动配置区域
|
||||
$endpointSource = $rawRegion !== '' ? $rawRegion : $configRegion;
|
||||
$this->storageRegion = $this->normalizeEndpoint($endpointSource);
|
||||
// 未配置自定义域名时,使用当前空间的公共访问域名。
|
||||
if (!$this->uploadUrl && $this->storageName && $this->storageRegion) {
|
||||
$this->uploadUrl = 'https://' . $this->storageName . '.' . parse_url($this->storageRegion, PHP_URL_HOST);
|
||||
@ -188,13 +201,48 @@ class Driver extends BaseUpload
|
||||
}
|
||||
|
||||
/**
|
||||
* 实例化 SDK 客户端
|
||||
* 从区域配置或 endpoint 解析 V4 所需 regionId(如 cn-hangzhou)
|
||||
* @param string $regionOrEndpoint 区域短名 / oss-cn-xxx / 完整 endpoint
|
||||
* @return string
|
||||
*/
|
||||
protected function resolveRegionId(string $regionOrEndpoint): string
|
||||
{
|
||||
$raw = trim($regionOrEndpoint);
|
||||
if ($raw === '') {
|
||||
return '';
|
||||
}
|
||||
if (preg_match('/^oss-([a-z0-9-]+)$/i', $raw, $m)) {
|
||||
return strtolower($m[1]);
|
||||
}
|
||||
if (preg_match('/^([a-z0-9-]+)$/i', $raw) && !str_contains($raw, '.')) {
|
||||
// 已是区域短名(cn-hangzhou)
|
||||
return strtolower($raw);
|
||||
}
|
||||
$host = parse_url(strpos($raw, '://') === false ? ('https://' . $raw) : $raw, PHP_URL_HOST) ?: '';
|
||||
if (preg_match('/^oss-([a-z0-9-]+?)(?:-internal)?\.aliyuncs\.com$/i', (string)$host, $m)) {
|
||||
return strtolower($m[1]);
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 实例化 SDK 客户端(强制 Signature V4:新 UID/新 Bucket 已禁用 V1)
|
||||
* @param string $endpoint 完整服务端点
|
||||
* @return OssClient
|
||||
*/
|
||||
protected function newOssClient(string $endpoint): OssClient
|
||||
{
|
||||
return new OssClient($this->accessKey, $this->secretKey, $endpoint);
|
||||
$regionId = $this->regionId ?: $this->resolveRegionId($endpoint);
|
||||
if ($regionId === '') {
|
||||
throw new UploadException('OSS:无法解析存储区域,请配置形如 oss-cn-hangzhou 的区域');
|
||||
}
|
||||
$this->regionId = $regionId;
|
||||
return new OssClient([
|
||||
'provider' => new StaticCredentialsProvider($this->accessKey, $this->secretKey),
|
||||
'endpoint' => $endpoint,
|
||||
'signatureVersion' => OssClient::OSS_SIGNATURE_VERSION_V4,
|
||||
'region' => $regionId,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -600,7 +648,8 @@ class Driver extends BaseUpload
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取OSS上传密钥
|
||||
* 获取OSS直传凭证(PostObject Signature V4)
|
||||
* 新 UID / 新 Bucket 已禁用 V1 表单签名,统一下发 OSS4-HMAC-SHA256 字段
|
||||
* @return mixed|void
|
||||
*/
|
||||
public function getTempKeys($callbackUrl = '', $dir = '')
|
||||
@ -609,23 +658,47 @@ class Driver extends BaseUpload
|
||||
if (!$this->accessKey || !$this->secretKey || !$this->uploadUrl) {
|
||||
return $this->setError('请先配置accessKey、secretKey和空间域名');
|
||||
}
|
||||
$expire = time() + 30;
|
||||
$regionId = $this->regionId ?: $this->resolveRegionId((string)$this->storageRegion);
|
||||
if ($regionId === '') {
|
||||
return $this->setError('请先配置有效的存储区域(如 oss-cn-hangzhou)');
|
||||
}
|
||||
$expire = time() + 1800;
|
||||
$utcNow = new \DateTimeImmutable('now', new \DateTimeZone('UTC'));
|
||||
$date = $utcNow->format('Ymd');
|
||||
$ossDate = $utcNow->format('Ymd\THis\Z');
|
||||
$credential = sprintf('%s/%s/%s/oss/aliyun_v4_request', $this->accessKey, $date, $regionId);
|
||||
|
||||
$base64CallbackBody = base64_encode(json_encode([
|
||||
'callbackUrl' => $callbackUrl,
|
||||
'callbackBody' => 'filename=${object}&size=${size}&mimeType=${mimeType}&height=${imageInfo.height}&width=${imageInfo.width}',
|
||||
'callbackBodyType' => "application/x-www-form-urlencoded"
|
||||
]));
|
||||
|
||||
$conditions = [
|
||||
['content-length-range', 0, 1048576000],
|
||||
['eq', '$x-oss-signature-version', 'OSS4-HMAC-SHA256'],
|
||||
['eq', '$x-oss-credential', $credential],
|
||||
['eq', '$x-oss-date', $ossDate],
|
||||
];
|
||||
if ($dir !== '') {
|
||||
$conditions[] = ['starts-with', '$key', $dir];
|
||||
}
|
||||
if ($this->storageName !== '') {
|
||||
$conditions[] = ['eq', '$bucket', $this->storageName];
|
||||
}
|
||||
|
||||
$policy = json_encode([
|
||||
'expiration' => $this->gmtIso8601($expire),
|
||||
'conditions' =>
|
||||
[
|
||||
[0 => 'content-length-range', 1 => 0, 2 => 1048576000],
|
||||
[0 => 'starts-with', 1 => '$key', 2 => $dir]
|
||||
]
|
||||
]);
|
||||
'conditions' => $conditions,
|
||||
], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
||||
$base64Policy = base64_encode($policy);
|
||||
$signature = base64_encode(hash_hmac('sha1', $base64Policy, $this->secretKey, true));
|
||||
// V4 Post 签名:对 Base64(policy) 做派生密钥 HMAC-SHA256,结果为十六进制
|
||||
$kDate = hash_hmac('sha256', $date, 'aliyun_v4' . $this->secretKey, true);
|
||||
$kRegion = hash_hmac('sha256', $regionId, $kDate, true);
|
||||
$kService = hash_hmac('sha256', 'oss', $kRegion, true);
|
||||
$kSigning = hash_hmac('sha256', 'aliyun_v4_request', $kService, true);
|
||||
$signature = hash_hmac('sha256', $base64Policy, $kSigning);
|
||||
|
||||
return [
|
||||
'accessid' => $this->accessKey,
|
||||
'host' => $this->uploadUrl,
|
||||
@ -634,7 +707,12 @@ class Driver extends BaseUpload
|
||||
'signature' => $signature,
|
||||
'expire' => $expire,
|
||||
'callback' => $base64CallbackBody,
|
||||
'type' => 'OSS'
|
||||
'type' => 'OSS',
|
||||
// PostObject V4 表单字段(前端直传必填)
|
||||
'signature_version' => 'OSS4-HMAC-SHA256',
|
||||
'x_oss_credential' => $credential,
|
||||
'x_oss_date' => $ossDate,
|
||||
'region' => $regionId,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@ -37,9 +37,10 @@
|
||||
"link": "https://www.aliyun.com/product/oss",
|
||||
"steps": [
|
||||
"第一步:注册阿里云账号,开通对象存储OSS服务",
|
||||
"第二步:进入OSS控制台,创建Bucket(空间名称不能重复)",
|
||||
"第三步(必选):在Bucket列表中,设置读写权限为【公共读】",
|
||||
"第四步(可选):使用自定义域名时,在Bucket详情中绑定域名并进行CNAME解析"
|
||||
"第二步:进入OSS控制台,创建Bucket(空间名称不能重复),并记下所在区域",
|
||||
"第三步(必选):在下方「区域配置」中选择与控制台一致的存储区域(Signature V4 必填)",
|
||||
"第四步(必选):在Bucket列表中,设置读写权限为【公共读】",
|
||||
"第五步(可选):使用自定义域名时,在Bucket详情中绑定域名并进行CNAME解析"
|
||||
]
|
||||
},
|
||||
"config_fields": [
|
||||
@ -58,6 +59,56 @@
|
||||
"required": true,
|
||||
"group": "密钥配置",
|
||||
"tip": "阿里云 SecretKey"
|
||||
},
|
||||
{
|
||||
"field": "region",
|
||||
"label": "存储区域",
|
||||
"type": "select",
|
||||
"required": true,
|
||||
"group": "区域配置",
|
||||
"tip": "Bucket 所在区域,Signature V4 必填;须与控制台区域一致(如华东1选杭州)",
|
||||
"options": [
|
||||
{
|
||||
"value": "oss-cn-hangzhou",
|
||||
"label": "华东1(杭州)"
|
||||
},
|
||||
{
|
||||
"value": "oss-cn-shanghai",
|
||||
"label": "华东2(上海)"
|
||||
},
|
||||
{
|
||||
"value": "oss-cn-qingdao",
|
||||
"label": "华北1(青岛)"
|
||||
},
|
||||
{
|
||||
"value": "oss-cn-beijing",
|
||||
"label": "华北2(北京)"
|
||||
},
|
||||
{
|
||||
"value": "oss-cn-zhangjiakou",
|
||||
"label": "华北3(张家口)"
|
||||
},
|
||||
{
|
||||
"value": "oss-cn-huhehaote",
|
||||
"label": "华北5(呼和浩特)"
|
||||
},
|
||||
{
|
||||
"value": "oss-cn-shenzhen",
|
||||
"label": "华南1(深圳)"
|
||||
},
|
||||
{
|
||||
"value": "oss-cn-hongkong",
|
||||
"label": "中国香港"
|
||||
},
|
||||
{
|
||||
"value": "oss-us-west-1",
|
||||
"label": "美国(硅谷)"
|
||||
},
|
||||
{
|
||||
"value": "oss-ap-southeast-1",
|
||||
"label": "新加坡"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"region_map": {
|
||||
|
||||
@ -139,7 +139,6 @@ export default {
|
||||
});
|
||||
},
|
||||
ossHttp(evfile, res, videoIng) {
|
||||
let that = this;
|
||||
let fileObject = evfile.target.files[0];
|
||||
let file = fileObject.name;
|
||||
let pos = file.lastIndexOf('.');
|
||||
@ -150,28 +149,33 @@ export default {
|
||||
let filename = this.getVideoName(suffix);
|
||||
let formData = new FormData();
|
||||
let data = res.data;
|
||||
// 注意formData里append添加的键的大小写
|
||||
formData.append('key', filename); // 存储在oss的文件路径
|
||||
formData.append('OSSAccessKeyId', data.accessid); // accessKeyId
|
||||
formData.append('policy', data.policy); // policy
|
||||
formData.append('Signature', data.signature); // 签名
|
||||
// 如果是base64文件,那么直接把base64字符串转成blob对象进行上传就可以了
|
||||
// PostObject Signature V4(新 Bucket 已禁用 V1:OSSAccessKeyId + Signature)
|
||||
formData.append('key', filename);
|
||||
formData.append('policy', data.policy);
|
||||
formData.append('success_action_status', 200);
|
||||
formData.append('x-oss-signature-version', data.signature_version || 'OSS4-HMAC-SHA256');
|
||||
formData.append('x-oss-credential', data.x_oss_credential || '');
|
||||
formData.append('x-oss-date', data.x_oss_date || '');
|
||||
formData.append('x-oss-signature', data.signature);
|
||||
// 兼容旧凭证(若后端仍返回 V1 字段)
|
||||
if (!data.x_oss_credential && data.accessid) {
|
||||
formData.append('OSSAccessKeyId', data.accessid);
|
||||
formData.append('Signature', data.signature);
|
||||
}
|
||||
formData.append('file', fileObject);
|
||||
formData.append('success_action_status', 200); // 成功后返回的操作码
|
||||
let url = data.host;
|
||||
let fileUrl = url + '/' + filename;
|
||||
let fileUrl = (data.cdn || url) + '/' + filename;
|
||||
videoIng(true, 100);
|
||||
return new Promise((resolve, reject) => {
|
||||
axios.defaults.withCredentials = false;
|
||||
axios
|
||||
.post(url, formData)
|
||||
.then(() => {
|
||||
// that.progress = 0;
|
||||
videoIng(false, 0);
|
||||
resolve({ url: fileUrl });
|
||||
})
|
||||
.catch((res) => {
|
||||
reject({ msg: res });
|
||||
.catch((err) => {
|
||||
reject({ msg: err });
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user