fix: OSS 上传令牌改为 POST 表单直传,修复智能填单拍照上传
前端按 OSS POST 表单直传方式编写(期望 policy/access_key_id/signature), 后端原返回 PUT 预签名 URL,字段不匹配导致上传失败。 改为生成 POST policy + HMAC-SHA1 签名,对齐前端已有的 FormData 组装逻辑。 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
01f7a38094
commit
9b8b0e4cd5
@ -47,20 +47,20 @@ class StorageService:
|
||||
self.export_publish_root.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
def create_upload_token(self, biz_type: str, biz_id: int, file_name: str, file_size: int) -> dict:
|
||||
"""生成前端直传 OSS 的上传令牌。
|
||||
"""生成前端直传 OSS 的 POST 表单上传令牌。
|
||||
|
||||
根据业务类型、业务 ID、文件名等信息,构建对象键并生成带签名的上传地址,
|
||||
供前端使用 PUT 方法直接上传文件到阿里云 OSS。
|
||||
根据业务类型、业务 ID、文件名等信息,构建对象键并生成带签名的 POST policy,
|
||||
供前端使用 POST 表单方式直接上传文件到阿里云 OSS。
|
||||
|
||||
参数:
|
||||
biz_type (str): 业务类型标识,用于构建对象键前缀(如 "file"、"smoke")
|
||||
biz_type (str): 业务类型标识,用于构建对象键前缀
|
||||
biz_id (int): 业务记录 ID,用于构建对象键中间段
|
||||
file_name (str): 原始文件名,用于提取扩展名和构建安全文件名
|
||||
file_size (int): 文件大小上限(字节),返回给前端做校验
|
||||
file_size (int): 文件大小上限(字节)
|
||||
|
||||
返回:
|
||||
dict: 包含 upload_url、file_url、headers、method、object_key、
|
||||
bucket_name、expire_at、max_file_size、content_type 等字段
|
||||
dict: 包含 upload_url、policy、access_key_id、signature、
|
||||
object_key、bucket_name、public_base_url、expire_at、content_type 等字段
|
||||
|
||||
被调用方:
|
||||
- FileService.create_upload_token() → API 文件上传令牌接口 /files/upload-token
|
||||
@ -68,16 +68,21 @@ class StorageService:
|
||||
object_key = self.build_object_key(biz_type, biz_id, file_name)
|
||||
expire_at = datetime.now() + timedelta(seconds=self.settings.oss_signed_url_expire_seconds)
|
||||
content_type = self._guess_content_type(file_name)
|
||||
signed_meta = self._build_signed_upload_url(object_key, expire_at, content_type)
|
||||
policy_b64 = self._build_post_policy(object_key, expire_at, file_size)
|
||||
signature = self._sign_post_policy(policy_b64)
|
||||
upload_url = (
|
||||
f"https://{self.settings.aliyun_oss_bucket}"
|
||||
f".{self.settings.aliyun_oss_endpoint}/"
|
||||
)
|
||||
return {
|
||||
"upload_url": signed_meta["upload_url"],
|
||||
"file_url": self.build_public_file_url(object_key),
|
||||
"headers": signed_meta["headers"],
|
||||
"method": "PUT",
|
||||
"upload_url": upload_url,
|
||||
"policy": policy_b64,
|
||||
"access_key_id": self.settings.aliyun_oss_access_key_id,
|
||||
"signature": signature,
|
||||
"object_key": object_key,
|
||||
"bucket_name": self.settings.aliyun_oss_bucket,
|
||||
"public_base_url": self.settings.aliyun_oss_public_base_url,
|
||||
"expire_at": expire_at.strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"max_file_size": file_size,
|
||||
"content_type": content_type,
|
||||
}
|
||||
|
||||
@ -177,50 +182,53 @@ class StorageService:
|
||||
return file_url[len(prefix) :]
|
||||
return file_url.split("://", 1)[-1].split("/", 1)[-1] if "/" in file_url.split("://", 1)[-1] else file_url
|
||||
|
||||
def _build_signed_upload_url(self, object_key: str, expire_at: datetime, content_type: str) -> dict:
|
||||
"""构建阿里云 OSS V1 预签名上传地址。
|
||||
def _build_post_policy(self, object_key: str, expire_at: datetime, file_size: int) -> str:
|
||||
"""构建阿里云 OSS POST policy 并 base64 编码。
|
||||
|
||||
按照 OSS V1 签名规则,使用 HMAC-SHA1 对请求信息进行签名,
|
||||
生成带过期时间的上传 URL,前端可直接使用 PUT 方法上传文件。
|
||||
policy 包含过期时间、目标 bucket、文件 key 和 Content-Type 约束,
|
||||
供前端 POST 表单直传时携带,OSS 服务端会校验 policy 合法性。
|
||||
|
||||
参数:
|
||||
object_key (str): OSS 对象键
|
||||
expire_at (datetime): 签名过期时间
|
||||
content_type (str): 文件 MIME 类型
|
||||
object_key: OSS 对象键
|
||||
expire_at: 过期时间
|
||||
file_size: 文件大小上限(字节)
|
||||
|
||||
返回:
|
||||
dict: 包含 upload_url(签名后的完整上传地址)和 headers(请求头)字段
|
||||
base64 编码的 policy 字符串
|
||||
|
||||
被调用方:
|
||||
- create_upload_token():生成上传令牌时调用
|
||||
"""
|
||||
# 当前按 OSS V1 预签名 URL 规则生成上传地址,便于前端直接联调真实 PUT 上传口径。
|
||||
expires = str(int(expire_at.timestamp()))
|
||||
encoded_key = quote(object_key.replace("\\", "/"), safe="/-_.*()")
|
||||
canonical_headers = "x-oss-object-acl:private\n"
|
||||
canonical_resource = f"/{self.settings.aliyun_oss_bucket}/{object_key}"
|
||||
string_to_sign = f"PUT\n\n{content_type}\n{expires}\n{canonical_headers}{canonical_resource}"
|
||||
signature = base64.b64encode(
|
||||
import json as _json
|
||||
policy = {
|
||||
"expiration": expire_at.strftime("%Y-%m-%dT%H:%M:%SZ"),
|
||||
"conditions": [
|
||||
{"bucket": self.settings.aliyun_oss_bucket},
|
||||
["eq", "$key", object_key],
|
||||
["content-length-range", 1, file_size],
|
||||
],
|
||||
}
|
||||
return base64.b64encode(_json.dumps(policy).encode("utf-8")).decode("utf-8")
|
||||
|
||||
def _sign_post_policy(self, policy_b64: str) -> str:
|
||||
"""使用 access_key_secret 对 base64 编码的 policy 进行 HMAC-SHA1 签名。
|
||||
|
||||
参数:
|
||||
policy_b64: base64 编码的 policy 字符串
|
||||
|
||||
返回:
|
||||
base64 编码的签名字符串
|
||||
|
||||
被调用方:
|
||||
- create_upload_token():生成上传令牌时调用
|
||||
"""
|
||||
return base64.b64encode(
|
||||
hmac.new(
|
||||
self.settings.aliyun_oss_access_key_secret.encode("utf-8"),
|
||||
string_to_sign.encode("utf-8"),
|
||||
policy_b64.encode("utf-8"),
|
||||
hashlib.sha1,
|
||||
).digest()
|
||||
).decode("utf-8")
|
||||
query_signature = quote(signature, safe="")
|
||||
upload_url = (
|
||||
f"https://{self.settings.aliyun_oss_bucket}.{self.settings.aliyun_oss_endpoint}/{encoded_key}"
|
||||
f"?OSSAccessKeyId={quote(self.settings.aliyun_oss_access_key_id, safe='')}"
|
||||
f"&Expires={expires}"
|
||||
f"&Signature={query_signature}"
|
||||
)
|
||||
return {
|
||||
"upload_url": upload_url,
|
||||
"headers": {
|
||||
"Content-Type": content_type,
|
||||
"x-oss-object-acl": "private",
|
||||
},
|
||||
}
|
||||
|
||||
def _guess_content_type(self, file_name: str) -> str:
|
||||
"""根据文件扩展名猜测 MIME 类型。
|
||||
|
||||
Loading…
Reference in New Issue
Block a user