fix(upload): 改回PUT预签名URL方式,前端用readFile+uni.request PUT上传
- 后端恢复PUT预签名URL生成,移除POST Object的policy/signature逻辑 - 前端改用uni.getFileSystemManager().readFile读取二进制数据 - 再通过uni.request PUT方式直传COS(uni.uploadFile只支持POST) - 参考验证过的CosUploadService实现
This commit is contained in:
parent
66df292628
commit
4bba7f8e53
|
|
@ -16,12 +16,12 @@ public interface IUploadConfigService
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// 预签名上传信息(COS POST Object 方式)
|
||||
/// 预签名上传信息(COS PUT 预签名URL方式)
|
||||
/// </summary>
|
||||
public class PresignedUploadInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// COS上传地址(POST Object 目标URL)
|
||||
/// 预签名上传URL(PUT方式直传)
|
||||
/// </summary>
|
||||
public string UploadUrl { get; set; } = string.Empty;
|
||||
|
||||
|
|
@ -30,31 +30,6 @@ public class PresignedUploadInfo
|
|||
/// </summary>
|
||||
public string FileUrl { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// COS对象Key(文件路径)
|
||||
/// </summary>
|
||||
public string Key { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Base64编码的上传策略
|
||||
/// </summary>
|
||||
public string Policy { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// SecretId
|
||||
/// </summary>
|
||||
public string SecretId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 密钥有效时间范围(Unix时间戳格式: startTime;endTime)
|
||||
/// </summary>
|
||||
public string KeyTime { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// HMAC-SHA1签名
|
||||
/// </summary>
|
||||
public string Signature { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// URL过期时间(秒)
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ namespace MiAssessment.Core.Services;
|
|||
|
||||
/// <summary>
|
||||
/// 上传配置服务实现
|
||||
/// 从Admin库读取COS配置,生成POST Object签名供小程序直传
|
||||
/// 从Admin库读取COS配置,生成PUT预签名URL供小程序直传
|
||||
/// </summary>
|
||||
public class UploadConfigService : IUploadConfigService
|
||||
{
|
||||
|
|
@ -60,53 +60,49 @@ public class UploadConfigService : IUploadConfigService
|
|||
var uniqueFileName = $"{timestamp}_{guid}{extension}";
|
||||
var objectKey = $"{UploadBasePath}/{datePath}/{uniqueFileName}";
|
||||
|
||||
// 生成POST Object签名信息
|
||||
var host = $"{setting.Bucket}.cos.{setting.Region}.myqcloud.com";
|
||||
var uploadUrl = $"https://{host}";
|
||||
// 生成PUT预签名URL
|
||||
var presignedUrl = GeneratePresignedUrl(setting, objectKey, "PUT", contentType, DefaultExpiresInSeconds);
|
||||
var fileUrl = GenerateAccessUrl(setting.Domain!, objectKey);
|
||||
|
||||
var startTime = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||
var endTime = startTime + DefaultExpiresInSeconds;
|
||||
var keyTime = $"{startTime};{endTime}";
|
||||
|
||||
// 生成policy
|
||||
var policy = GeneratePostPolicy(objectKey, setting.AccessKeyId!, keyTime);
|
||||
var policyBase64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(policy));
|
||||
|
||||
// 生成签名: SignKey -> StringToSign(SHA1 of raw policy) -> Signature
|
||||
var signKey = HmacSha1(setting.AccessKeySecret!, keyTime);
|
||||
var stringToSign = Sha1Hash(policy);
|
||||
var signature = HmacSha1(signKey, stringToSign);
|
||||
|
||||
_logger.LogInformation("生成POST Object签名成功: {ObjectKey}", objectKey);
|
||||
_logger.LogInformation("生成预签名URL成功: {ObjectKey}", objectKey);
|
||||
|
||||
return new PresignedUploadInfo
|
||||
{
|
||||
UploadUrl = uploadUrl,
|
||||
UploadUrl = presignedUrl,
|
||||
FileUrl = fileUrl,
|
||||
Key = objectKey,
|
||||
Policy = policyBase64,
|
||||
SecretId = setting.AccessKeyId!,
|
||||
KeyTime = keyTime,
|
||||
Signature = signature,
|
||||
ExpiresIn = DefaultExpiresInSeconds
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 生成POST Object的policy JSON
|
||||
/// 参考: https://cloud.tencent.com/document/product/436/14690
|
||||
/// 生成腾讯云COS预签名URL(PUT方式)
|
||||
/// </summary>
|
||||
private static string GeneratePostPolicy(string objectKey, string secretId, string keyTime)
|
||||
private static string GeneratePresignedUrl(UploadSetting setting, string objectKey, string httpMethod, string contentType, int expiresInSeconds)
|
||||
{
|
||||
var expiration = DateTimeOffset.UtcNow.AddSeconds(DefaultExpiresInSeconds).ToString("yyyy-MM-ddTHH:mm:ss.fffZ");
|
||||
var startTime = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||
var endTime = startTime + expiresInSeconds;
|
||||
var keyTime = $"{startTime};{endTime}";
|
||||
|
||||
// 手动拼接JSON,因为COS要求key中包含连字符(q-sign-algorithm等)
|
||||
var policy = $$"""
|
||||
{"expiration":"{{expiration}}","conditions":[{"q-sign-algorithm":"sha1"},{"q-ak":"{{secretId}}"},{"q-sign-time":"{{keyTime}}"},{"key":"{{objectKey}}"}]}
|
||||
""".Trim();
|
||||
var host = $"{setting.Bucket}.cos.{setting.Region}.myqcloud.com";
|
||||
var urlPath = $"/{objectKey}";
|
||||
|
||||
return policy;
|
||||
// 1. SignKey
|
||||
var signKey = HmacSha1(setting.AccessKeySecret!, keyTime);
|
||||
|
||||
// 2. HttpString
|
||||
var httpString = $"{httpMethod.ToLowerInvariant()}\n{urlPath}\n\nhost={host.ToLowerInvariant()}\n";
|
||||
|
||||
// 3. StringToSign
|
||||
var sha1HttpString = Sha1Hash(httpString);
|
||||
var stringToSign = $"sha1\n{keyTime}\n{sha1HttpString}\n";
|
||||
|
||||
// 4. Signature
|
||||
var signature = HmacSha1(signKey, stringToSign);
|
||||
|
||||
// 5. Authorization
|
||||
var authorization = $"q-sign-algorithm=sha1&q-ak={setting.AccessKeyId}&q-sign-time={keyTime}&q-key-time={keyTime}&q-header-list=host&q-url-param-list=&q-signature={signature}";
|
||||
|
||||
return $"https://{host}{urlPath}?{authorization}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
/**
|
||||
* COS直传工具
|
||||
* 通过POST Object方式将文件直传到腾讯云COS
|
||||
* 参考: https://cloud.tencent.com/document/product/436/14690
|
||||
* 通过PUT预签名URL将文件直传到腾讯云COS
|
||||
* 使用 uni.getFileSystemManager().readFile 读取文件二进制数据
|
||||
* 再通过 uni.request PUT 方式上传(uni.uploadFile 只支持 POST)
|
||||
*/
|
||||
|
||||
import { getPresignedUploadUrl } from '@/api/user.js'
|
||||
|
|
@ -41,39 +42,39 @@ export async function chooseAndUploadImage(options = {}) {
|
|||
const mimeMap = { jpg: 'image/jpeg', jpeg: 'image/jpeg', png: 'image/png', gif: 'image/gif', webp: 'image/webp' }
|
||||
const contentType = mimeMap[ext] || 'image/png'
|
||||
|
||||
// 2. 获取POST Object签名信息
|
||||
// 2. 获取预签名URL
|
||||
const presignedRes = await getPresignedUploadUrl(fileName, contentType)
|
||||
if (!presignedRes || presignedRes.code !== 0 || !presignedRes.data) {
|
||||
throw new Error(presignedRes?.message || '获取上传地址失败')
|
||||
}
|
||||
|
||||
const { uploadUrl, fileUrl, key, policy, secretId, keyTime, signature } = presignedRes.data
|
||||
const { uploadUrl, fileUrl } = presignedRes.data
|
||||
|
||||
// 3. POST Object方式直传COS
|
||||
// 3. 读取文件二进制数据,然后用PUT方式上传到COS
|
||||
await new Promise((resolve, reject) => {
|
||||
uni.uploadFile({
|
||||
url: uploadUrl,
|
||||
uni.getFileSystemManager().readFile({
|
||||
filePath: tempFilePath,
|
||||
name: 'file',
|
||||
formData: {
|
||||
'key': key,
|
||||
'policy': policy,
|
||||
'q-sign-algorithm': 'sha1',
|
||||
'q-ak': secretId,
|
||||
'q-key-time': keyTime,
|
||||
'q-sign-time': keyTime,
|
||||
'q-signature': signature
|
||||
success: (readRes) => {
|
||||
// 使用PUT方法上传二进制数据
|
||||
uni.request({
|
||||
url: uploadUrl,
|
||||
method: 'PUT',
|
||||
data: readRes.data,
|
||||
header: {
|
||||
'Content-Type': contentType
|
||||
},
|
||||
success: (res) => {
|
||||
if (res.statusCode === 200) {
|
||||
resolve(res)
|
||||
} else {
|
||||
console.error('COS上传失败:', res.statusCode, res.data)
|
||||
reject(new Error(`上传失败,状态码: ${res.statusCode}`))
|
||||
}
|
||||
},
|
||||
fail: (err) => reject(new Error('上传COS失败: ' + (err.errMsg || '网络错误')))
|
||||
})
|
||||
},
|
||||
success: (res) => {
|
||||
// COS POST Object成功返回 200 或 204
|
||||
if (res.statusCode >= 200 && res.statusCode < 300) {
|
||||
resolve(res)
|
||||
} else {
|
||||
console.error('COS上传失败:', res.statusCode, res.data)
|
||||
reject(new Error(`上传失败,状态码: ${res.statusCode}`))
|
||||
}
|
||||
},
|
||||
fail: (err) => reject(new Error(err.errMsg || '上传失败'))
|
||||
fail: (err) => reject(new Error('读取文件失败: ' + err.errMsg))
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user