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:
+31
-22
@@ -83,32 +83,35 @@ return [
|
||||
'close_mail_subject' => '关于你反馈的问题《%s》', // %s 会替换成问题标题
|
||||
],
|
||||
|
||||
/* ---------------- 附件上传(阿里云 OSS 直传) ----------------
|
||||
* 浏览器把文件「直接」传到 OSS,不经过本站服务器 —— 因此不占用服务器磁盘、不消耗服务器带宽,
|
||||
* 也就不会出现"硬盘被挤爆"的风险。服务器只做两件事:
|
||||
* 1) 下发一段有时效的上传凭证(签名),限制能传到哪个路径、多大、什么类型;
|
||||
* 2) 提交表单时校验对象确实存在且合规,并把元信息写进 SQLite。
|
||||
/* ---------------- 附件上传(阿里云 OSS · 服务器代理) ----------------
|
||||
* ★ 架构:私有桶 + 内网 Endpoint,读写全部经服务器代理
|
||||
* 桶设为【私有】、且只用【内网 Endpoint】(*-internal.aliyuncs.com,仅同地域 ECS 可达),
|
||||
* 浏览器既不直传、也不直连。数据流:
|
||||
* 上传:浏览器 → upload.php → 服务器 PUT 到内网 OSS
|
||||
* 读取:浏览器 → media.php → 服务器 GET 内网 OSS 并流式转发(支持 Range)
|
||||
* 好处:桶可彻底私有、公网完全看不到 Endpoint、AccessKey 永不出服务器。
|
||||
* 代价:附件流量经过服务器(占带宽),需保证服务器磁盘临时空间够放单个上传文件。
|
||||
*
|
||||
* ★ 凭证方式:ECS 实例 RAM 角色(唯一支持的方式,服务器上不保存长期 AccessKey)
|
||||
* 给这台 ECS 绑定一个 RAM 角色后,程序从内网元数据服务
|
||||
* (http://100.100.100.200,链路本地地址、不出公网)读取一份「有时效、
|
||||
* 会自动周期轮换」的临时凭证来签名。AccessKey 永不落盘,也永不进本文件。
|
||||
* ★ 凭证方式:ECS 实例 RAM 角色(推荐,服务器上不保存长期 AccessKey)或静态 AccessKey。
|
||||
* 实例角色下,程序从内网元数据服务(http://100.100.100.200,链路本地、不出公网)
|
||||
* 读取一份「有时效、会自动轮换」的临时凭证来签名。AccessKey 永不落盘、永不进本文件。
|
||||
*
|
||||
* ★ 开通步骤(详见 README.md「5.1 开通与配置」):
|
||||
* ① 创建 Bucket(读写权限建议「公共读」;想更严可用 private,本程序会自动签发访问 URL)
|
||||
* ② RAM 建权限策略:只授予该 Bucket(某前缀下)的 oss:PutObject / oss:GetObject / oss:DeleteObject
|
||||
* ★ 开通步骤(详见 README.md):
|
||||
* ① 创建【与 ECS 同地域】的 Bucket,读写权限设为「私有」
|
||||
* ⚠️ 内网 Endpoint 跨地域不通!ECS 在哪个地域,桶就建在哪个地域。
|
||||
* ② RAM 建权限策略:只授予该 Bucket(某前缀下)的 oss:PutObject / GetObject / DeleteObject
|
||||
* ③ RAM 建角色(如 FeedLiteOssRole)并赋予上一步的策略
|
||||
* ④ ECS 控制台把该角色【授予本实例】
|
||||
* ⑤ Bucket → 数据安全 → 跨域设置(CORS):来源填你的站点域名,方法勾 POST/GET/HEAD
|
||||
* ⑥ 把 endpoint / bucket / instance_role 填到下面,最后 enabled = true
|
||||
* ⑦(可选但强烈推荐)绑定自定义域名,避免默认域名带 Content-Disposition 被迫下载
|
||||
* ⑤ 把 endpoint / bucket / instance_role 填到下面,use_internal_endpoint 保持 true,enabled = true
|
||||
* ⑥ 后台「站点设置 → 存储体检」确认桶连通性为 ✅
|
||||
* (代理架构下【不需要】配置 CORS,也【不需要】绑定自定义域名)
|
||||
*
|
||||
* ★ 前提:站点必须运行在【阿里云 ECS】上并且已绑定 RAM 角色;
|
||||
* 未绑定则取不到凭证,上传接口会明确报错(其它功能不受影响)。
|
||||
*/
|
||||
'oss' => [
|
||||
'enabled' => false, // 填好参数后改为 true 才启用上传
|
||||
'endpoint' => 'oss-cn-beijing.aliyuncs.com', // 不含 http://,如 oss-cn-hangzhou.aliyuncs.com
|
||||
'endpoint' => 'oss-cn-qingdao.aliyuncs.com', // 不含 http://;填公网或内网均可,程序按下方开关自动转换
|
||||
'bucket' => 'your-bucket-name',
|
||||
|
||||
// ★ ECS 实例 RAM 角色名 —— 必须与【ECS 实例实际绑定的角色名】逐字一致。
|
||||
@@ -117,6 +120,12 @@ return [
|
||||
// 建的是 MyOssRole,授予实例后生效名可能是系统预置策略对应的角色),以查询结果为准。
|
||||
'instance_role' => '',
|
||||
|
||||
// ★ 是否用内网 Endpoint(默认开)。开启后程序自动把 endpoint 改写成 *-internal 形式,
|
||||
// 走阿里云内网访问 OSS:免公网流量费、延迟更低,且公网完全无法直连该地址。
|
||||
// ⚠️ 仅当【桶与本机 ECS 同地域】时可用;跨地域时内网不可达,必须关掉本开关
|
||||
// (回落公网 Endpoint)或改用同地域的桶。后台「存储体检」会探测并提示。
|
||||
'use_internal_endpoint' => true,
|
||||
|
||||
// ---- 以下一般不用改 ----
|
||||
'metadata_url' => '', // 留空=http://100.100.100.200(ECS 内网元数据服务)
|
||||
'metadata_v2' => false, // true=加固模式(先取一次性 token 再读元数据,防 SSRF 探测)
|
||||
@@ -124,13 +133,13 @@ return [
|
||||
'credential_cache' => '', // 凭证缓存文件;留空=与数据库同目录的 oss.sts.php
|
||||
'credential_refresh_margin' => 300, // 到期前多少秒就提前换新(秒)
|
||||
|
||||
'domain' => '', // 自定义访问域名,如 https://fb-img.example.com(留空用默认域名)
|
||||
'url_scheme' => 'https', // 拼接访问地址时使用的协议
|
||||
'domain' => '', // 代理架构下【不再使用】:附件一律经本站 media.php 输出,无需自定义域名
|
||||
'url_scheme' => 'https', // 公网 Endpoint 时使用的协议(内网 Endpoint 强制走 http)
|
||||
'prefix' => 'fb', // 对象键前缀:fb/2026/09/xxxxxxxx.jpg
|
||||
'visibility' => 'public', // public=直接可读 | private=展示时生成签名 URL
|
||||
'sign_ttl' => 600, // 上传凭证有效期(秒)
|
||||
'link_ttl' => 604800, // private 模式下展示链接的有效期(秒)
|
||||
'image_process' => true, // 列表缩略图使用 OSS 图片处理(免费额度内,省流量)
|
||||
'visibility' => 'private', // 记录桶的读写权限:代理架构下推荐 private(浏览器永不直连桶)
|
||||
'sign_ttl' => 600, // 服务器内部签名 URL 的有效期(秒),仅服务器侧使用
|
||||
'link_ttl' => 604800, // 兼容保留;代理读取时签名有效期由代码收敛到 ≤300 秒
|
||||
'image_process' => true, // 列表缩略图使用 OSS 图片处理(服务端缩放后再转发,省下行流量)
|
||||
'verify' => true, // 提交时 HEAD 复核对象(防伪造/防超限)
|
||||
'host_override' => '', // 仅特殊网关/联调使用;留空自动按 bucket.endpoint 拼接
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user