OSS 附件改为私有桶+内网Endpoint+服务器代理架构

背景:桶为私有、且只用内网 Endpoint(外网不可达),浏览器既无法直传、
也无法直连。原「浏览器直传 + 签名URL直连」两条路都失效,改为全站代理。

lib/oss.php:
- endpoint 自动转内网(*-internal),use_internal_endpoint 开关控制,内网走 http
- 移除浏览器直传凭证(PostObject),改为服务器端 PUT(流式 fb_oss_put_file)
- 新增 fb_oss_stream_get 流式读取(回调式,支持 Range,不整文件进内存)
- 签名支持 x-oss-process 子资源 + Authorization 头签名(Content-Type 入签)
- 新增 fb_oss_probe 连通性自检、fb_oss_error_text 错误归因(含跨地域提示)

media.php(新增): 附件代理输出,鉴权边界=对象键须在本站附件表(或本人待入库
  登记)且帖子对访客可见,防止本站沦为能读整个共用桶的开放代理;透传 Range。

upload.php: local/oss 统一走两步(领键→action=put),OSS 模式由服务器代理 PUT。
lib/db.php: 新增 pending_attachments 表 + idx_att_key 索引(迁移幂等自动建)。
lib/attach.php: 待入库登记(mark/clear/mine/gc),解决上传后入库前的预览鉴权。
cron.php: 清理过期待入库登记 + 桶连通性自检输出。
admin.php: 设置页加「使用内网 Endpoint」开关、存储体检显示实际连接地址与探测按钮。
README/config: 全面更新为代理架构(无需CORS、无需自定义域名、同地域要求)。

验证:本地 e2e 25/25 + 附件专项 13/13(含防开放代理/Range/内网改写);
线上真机上传→内网PUT→media.php代理读回→Range 全通过。
This commit is contained in:
2026-09-28 22:12:28 +08:00
parent cede838b5e
commit 1401ad899b
12 changed files with 866 additions and 246 deletions
+53
View File
@@ -394,10 +394,63 @@ function fb_attach_save(PDO $pdo, $threadId, $replyId, array $list) {
$i, $now,
]);
$n++;
// 转正成功即从「待入库」表移除(media.php 之后改走 attachments 表鉴权)
fb_attach_pending_clear($it['key']);
}
return $n;
}
/* ==================================================================
* 待入库附件登记
* ------------------------------------------------------------------
* 上传成功 → 登记;提交表单入库 → 转正并移除;一直没提交 → 由 cron 清理。
* 只为 media.php 的代理鉴权服务(上传后、入库前的缩略图预览)。
* ================================================================== */
/** 登记一条待入库附件 */
function fb_attach_pending_mark($key, $kind, $mime, $size) {
try {
$pdo = fb_db();
$pdo->prepare(
'INSERT INTO pending_attachments(oss_key, kind, mime, size, ip_hash, created_at)
VALUES(?,?,?,?,?,?)'
)->execute([(string)$key, (string)$kind, (string)$mime, (int)$size, fb_ip_hash(), time()]);
return true;
} catch (Throwable $e) { return false; }
}
/** 转正/放弃后移除登记(幂等) */
function fb_attach_pending_clear($key) {
try {
$pdo = fb_db();
$pdo->prepare('DELETE FROM pending_attachments WHERE oss_key = ?')->execute([(string)$key]);
return true;
} catch (Throwable $e) { return false; }
}
/** 该对象键是否由【当前访客】在有效期内登记过(media.php 鉴权用) */
function fb_attach_pending_mine($key, $ttl = 7200) {
try {
$pdo = fb_db();
$st = $pdo->prepare(
'SELECT id FROM pending_attachments
WHERE oss_key = ? AND ip_hash = ? AND created_at >= ? LIMIT 1'
);
$st->execute([(string)$key, fb_ip_hash(), time() - (int)$ttl]);
return (bool)$st->fetch();
} catch (Throwable $e) { return false; }
}
/** 清理过期的待入库登记(cron 调用;只删记录,不动 OSS 对象——那属于未提交的孤儿,另行处理) */
function fb_attach_pending_gc($ttl = 7200) {
try {
$pdo = fb_db();
$st = $pdo->prepare('DELETE FROM pending_attachments WHERE created_at < ?');
$st->execute([time() - (int)$ttl]);
return $st->rowCount();
} catch (Throwable $e) { return 0; }
}
/** 取某个主题的附件:['main' => [...], 'replies' => [reply_id => [...]]] */
function fb_attach_for_thread(PDO $pdo, $threadId) {
$st = $pdo->prepare('SELECT * FROM attachments WHERE thread_id = ? ORDER BY seq ASC, id ASC');