导读:官网的招聘投递、合作留言、售后工单几乎都要传附件(简历、截图、合同)。常见做法是把文件 POST 到业务服务器再转存对象存储,结果大文件占满带宽、请求超时丢件,甚至把永久密钥放进前端造成越权。本文给出一套可直接落地的闭环:前端拿临时凭证直传对象存储、服务端回调验签落库、定时清理孤儿文件,附件与业务单据强绑定,不丢、不重、不被越权访问。
很多官网表单第一版这样写:前端把文件和文字字段一起 multipart/form-data POST 到后端,后端收完整文件再上传对象存储。小文件没问题,简历动辄十几 MB、截图几张就上百 MB 时会暴露三个问题:
直传把"传文件"这件事交给对象存储的就近接入点,后端只负责签发一张"只能往指定目录写、几分钟后过期"的临时凭证,自己不碰文件流。
临时凭证要遵循最小权限:只允许对"本次表单会话专属目录"执行写入,不给读、不列桶、不能碰其他路径。后端在用户进入表单页时先创建一个上传会话,返回会话号与对象键前缀,再据此签发凭证。
import time, uuid
def issue_upload_credential(form_id, user_ip):
session_id = uuid.uuid4().hex
# 对象键按业务隔离:附件类型/表单号/会话号/文件名
prefix = f"form-attach/{form_id}/{session_id}/"
policy = {
"version": "2.0",
"statement": [{
"effect": "allow",
"action": ["cos:PostObject", "cos:PutObject", "cos:InitiateMultipartUpload"],
"resource": [f"qcs::cos:*:*:bucket-name/{prefix}*"],
"condition": {
"numeric_less_than": {"cos:content-length": 20 * 1024 * 1024},
"date_less_than": {"cos:expire-time":
time.strftime("%Y-%m-%dT%H:%M:%SZ",
time.gmtime(time.time() + 600))}
}
}]
}
save_session(session_id, form_id, prefix, user_ip, ttl=600)
cred = sts_client.get_credential_of_policy(policy)
return {"session_id": session_id, "prefix": prefix, "credential": cred}要点有三:资源路径锁到 session_id 这一级,A 会话拿不到 B 会话的目录;用条件把单文件大小和有效期钉死;凭证有效期 5–10 分钟即可,传完就作废,不必为了"省事"发一小时。
前端不能凭"上传进度走到 100%"就告诉后端附件已就绪——弱网下可能传了一半断流。可靠做法是让对象存储在写入完成后回调业务后端,后端验明回调真身、校验对象键归属本次会话,再把附件登记为"已上传、待绑定"。
import hmac, hashlib, json
def upload_callback(request):
# 1) 用回调密钥校验签名,拒绝伪造回调
raw = request.body
sign = request.headers.get("Authorization", "")
expect = hmac.new(CALLBACK_SECRET, raw, hashlib.sha1).hexdigest()
if not hmac.compare_digest("sha1=" + expect, sign):
return 401, {"code": "bad_sign"}
data = json.loads(raw)
key, size = data["key"], int(data.get("size", 0))
# 2) 对象键必须落在已登记的会话前缀下,防止窜改 key 绑定别人的文件
sess = get_session(data.get("session_id"))
if not sess or not key.startswith(sess["prefix"]):
return 403, {"code": "key_mismatch"}
# 3) 幂等登记:同一对象键重复回调只落一条
upsert_attachment(key=key, form_id=sess["form_id"],
session_id=sess["session_id"], size=size,
status="uploaded")
return 200, {"code": "ok"}前端提交表单时只回传 session_id 和已确认的对象键列表,后端按会话把附件从"已上传"改成"已绑定单据"。这样即使前端重复点提交,附件与单据的绑定也是幂等的。
直传必然产生孤儿文件:用户选了文件传到一半关页面、回调丢失、或提交表单时没勾选这些附件。它们不会自己消失,会一直占用存储。需要两道清理:
uploaded、没有绑定到任何单据的对象登记,删除对象并写审计日志。-- 附件登记表:对象键唯一,保证回调与清理都幂等
CREATE TABLE form_attachment (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
form_id VARCHAR(32) NOT NULL,
session_id VARCHAR(64) NOT NULL,
object_key VARCHAR(255) NOT NULL,
size_bytes BIGINT NOT NULL DEFAULT 0,
status VARCHAR(16) NOT NULL DEFAULT 'uploaded', -- uploaded/bound/deleted
created_at DATETIME NOT NULL,
bound_at DATETIME NULL,
UNIQUE KEY uk_object_key (object_key),
KEY idx_status_created (status, created_at)
);def cleanup_orphan_objects(now):
# 会话有效期 10 分钟,宽限到 1 小时仍未绑定即判定为孤儿
rows = query("SELECT * FROM form_attachment "
"WHERE status='uploaded' AND created_at < %s", now - 3600)
for r in rows:
delete_object(r["object_key"]) # 删除前对象键仍受会话前缀约束
execute("UPDATE form_attachment SET status='deleted' WHERE id=%s", r["id"])删除走"先标记、再删对象、回写状态",任务中途失败重跑也不会重复计费或漏删。
官网这类低频、文件不大的对外表单,直传方案用对象存储的标准临时凭证与上传回调即可,不必自建文件服务;会话前缀、回调验签、附件登记表、孤儿清理四件事建议作为统一的"附件中台"沉淀,招聘、留言、工单、报备等所有带附件的表单共用一套,避免每个表单各写一套密钥和回调。私密度高的附件在绑定后把对象权限切为私有,按单据归属做带时效的签名 URL 下载。
A:用对象存储分片直传并配置"未完成分片自动中止",业务侧再把超过会话有效期仍未绑定单据的对象登记判定为孤儿,定时删除并回写状态,两层兜底即可。
A:不会,只要把临时策略的资源限制在本次会话专属前缀、动作限制为写入、并用条件钉死文件大小和有效期,凭证几分钟后即失效,无法越权访问其他路径。
A:对象存储写入后服务端回调验签,校验对象键属于该会话再登记为已上传,表单提交时按会话号幂等绑定到单据,重复提交也只绑一次。
以上为附件直传的一套工程化闭环实践,具体凭证时效、大小阈值与清理周期请按自身业务和对象存储厂商的实时文档调整。核心思路只有一句:后端发"最小权限、短时有效"的凭证,文件走近路,真实性和归属交给服务端回调与幂等登记兜底。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。