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>
|
/// <summary>
|
||||||
/// 预签名上传信息(COS POST Object 方式)
|
/// 预签名上传信息(COS PUT 预签名URL方式)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class PresignedUploadInfo
|
public class PresignedUploadInfo
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// COS上传地址(POST Object 目标URL)
|
/// 预签名上传URL(PUT方式直传)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string UploadUrl { get; set; } = string.Empty;
|
public string UploadUrl { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
|
@ -30,31 +30,6 @@ public class PresignedUploadInfo
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string FileUrl { get; set; } = string.Empty;
|
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>
|
/// <summary>
|
||||||
/// URL过期时间(秒)
|
/// URL过期时间(秒)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ namespace MiAssessment.Core.Services;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 上传配置服务实现
|
/// 上传配置服务实现
|
||||||
/// 从Admin库读取COS配置,生成POST Object签名供小程序直传
|
/// 从Admin库读取COS配置,生成PUT预签名URL供小程序直传
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class UploadConfigService : IUploadConfigService
|
public class UploadConfigService : IUploadConfigService
|
||||||
{
|
{
|
||||||
|
|
@ -60,53 +60,49 @@ public class UploadConfigService : IUploadConfigService
|
||||||
var uniqueFileName = $"{timestamp}_{guid}{extension}";
|
var uniqueFileName = $"{timestamp}_{guid}{extension}";
|
||||||
var objectKey = $"{UploadBasePath}/{datePath}/{uniqueFileName}";
|
var objectKey = $"{UploadBasePath}/{datePath}/{uniqueFileName}";
|
||||||
|
|
||||||
// 生成POST Object签名信息
|
// 生成PUT预签名URL
|
||||||
var host = $"{setting.Bucket}.cos.{setting.Region}.myqcloud.com";
|
var presignedUrl = GeneratePresignedUrl(setting, objectKey, "PUT", contentType, DefaultExpiresInSeconds);
|
||||||
var uploadUrl = $"https://{host}";
|
|
||||||
var fileUrl = GenerateAccessUrl(setting.Domain!, objectKey);
|
var fileUrl = GenerateAccessUrl(setting.Domain!, objectKey);
|
||||||
|
|
||||||
var startTime = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
_logger.LogInformation("生成预签名URL成功: {ObjectKey}", objectKey);
|
||||||
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);
|
|
||||||
|
|
||||||
return new PresignedUploadInfo
|
return new PresignedUploadInfo
|
||||||
{
|
{
|
||||||
UploadUrl = uploadUrl,
|
UploadUrl = presignedUrl,
|
||||||
FileUrl = fileUrl,
|
FileUrl = fileUrl,
|
||||||
Key = objectKey,
|
|
||||||
Policy = policyBase64,
|
|
||||||
SecretId = setting.AccessKeyId!,
|
|
||||||
KeyTime = keyTime,
|
|
||||||
Signature = signature,
|
|
||||||
ExpiresIn = DefaultExpiresInSeconds
|
ExpiresIn = DefaultExpiresInSeconds
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 生成POST Object的policy JSON
|
/// 生成腾讯云COS预签名URL(PUT方式)
|
||||||
/// 参考: https://cloud.tencent.com/document/product/436/14690
|
|
||||||
/// </summary>
|
/// </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 host = $"{setting.Bucket}.cos.{setting.Region}.myqcloud.com";
|
||||||
var policy = $$"""
|
var urlPath = $"/{objectKey}";
|
||||||
{"expiration":"{{expiration}}","conditions":[{"q-sign-algorithm":"sha1"},{"q-ak":"{{secretId}}"},{"q-sign-time":"{{keyTime}}"},{"key":"{{objectKey}}"}]}
|
|
||||||
""".Trim();
|
|
||||||
|
|
||||||
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>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
/**
|
/**
|
||||||
* COS直传工具
|
* COS直传工具
|
||||||
* 通过POST Object方式将文件直传到腾讯云COS
|
* 通过PUT预签名URL将文件直传到腾讯云COS
|
||||||
* 参考: https://cloud.tencent.com/document/product/436/14690
|
* 使用 uni.getFileSystemManager().readFile 读取文件二进制数据
|
||||||
|
* 再通过 uni.request PUT 方式上传(uni.uploadFile 只支持 POST)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { getPresignedUploadUrl } from '@/api/user.js'
|
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 mimeMap = { jpg: 'image/jpeg', jpeg: 'image/jpeg', png: 'image/png', gif: 'image/gif', webp: 'image/webp' }
|
||||||
const contentType = mimeMap[ext] || 'image/png'
|
const contentType = mimeMap[ext] || 'image/png'
|
||||||
|
|
||||||
// 2. 获取POST Object签名信息
|
// 2. 获取预签名URL
|
||||||
const presignedRes = await getPresignedUploadUrl(fileName, contentType)
|
const presignedRes = await getPresignedUploadUrl(fileName, contentType)
|
||||||
if (!presignedRes || presignedRes.code !== 0 || !presignedRes.data) {
|
if (!presignedRes || presignedRes.code !== 0 || !presignedRes.data) {
|
||||||
throw new Error(presignedRes?.message || '获取上传地址失败')
|
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) => {
|
await new Promise((resolve, reject) => {
|
||||||
uni.uploadFile({
|
uni.getFileSystemManager().readFile({
|
||||||
url: uploadUrl,
|
|
||||||
filePath: tempFilePath,
|
filePath: tempFilePath,
|
||||||
name: 'file',
|
success: (readRes) => {
|
||||||
formData: {
|
// 使用PUT方法上传二进制数据
|
||||||
'key': key,
|
uni.request({
|
||||||
'policy': policy,
|
url: uploadUrl,
|
||||||
'q-sign-algorithm': 'sha1',
|
method: 'PUT',
|
||||||
'q-ak': secretId,
|
data: readRes.data,
|
||||||
'q-key-time': keyTime,
|
header: {
|
||||||
'q-sign-time': keyTime,
|
'Content-Type': contentType
|
||||||
'q-signature': signature
|
},
|
||||||
|
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) => {
|
fail: (err) => reject(new Error('读取文件失败: ' + err.errMsg))
|
||||||
// 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 || '上传失败'))
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user