华为云OBS服务端签名直传唯一安全方案是POST表单签名(PostFormCredential),ThinkPHP调用Java SDK生成带时效和策略限制的签名参数,前端通过multipart/form-data提交,全程不暴露AK/SK。

ThinkPHP 项目里做华为云OBS服务端签名直传,核心不是“能不能传”,而是“怎么让前端拿不到AK/SK,又能让它合法上传”。直接在前端初始化 ObsClient 并传入长期密钥是绝对禁止的——这等于把家门钥匙贴在大门上。
为什么不能用 BrowserJS SDK 直连?
BrowserJS SDK 虽然封装了 putObject,但它要求前端持有 access_key_id 和 secret_access_key。而 ThinkPHP 后端若把这两个值透传给前端(哪怕加了加密),只要被反编译或抓包,密钥就彻底泄露。华为云控制台明确警告:长期 AK/SK 不得出现在客户端代码中。
真实项目中踩过的坑包括:
- 开发环境用测试 AK/SK 硬编码,上线后忘记替换,导致桶被恶意刷流量
- 用 JS 加密后再传给前端,结果被浏览器控制台一眼识破解密逻辑
- 误以为 CORS 配置好就能放开权限,结果攻击者绕过前端直接构造 PUT 请求写入恶意文件
必须用 POST 表单签名(Pre-Signed POST)
华为云 OBS 支持三种签名方式,但对 ThinkPHP + 前端直传组合,唯一安全、通用、兼容性好的方案是 POST 表单签名(即 PostFormCredential)。它生成的是带签名字段的 HTML 表单参数,前端只需用 FormData 提交,全程不接触 SK。
立即学习“PHP免费学习笔记(深入)”;
关键点:
- 签名有效期可控(建议设为 15–30 分钟),超时自动失效
- 签名内嵌
policy字段,可限制上传路径(key)、文件大小(content-length-range)、MIME 类型(Content-Type) - 小程序、WebView、PC 浏览器全部支持,没有兼容性问题
ThinkPHP 中调用 Java SDK 的 createPostSignature 方法即可生成,无需自己拼 policy 字符串或算 HMAC-SHA256 —— 自己实现极易出错,比如时间戳格式不对、base64 编码未去换行、policy JSON 键名大小写错误等,都会导致返回 403 SignatureDoesNotMatch。
ThinkPHP 控制器如何生成并返回签名参数
使用官方 esdk-obs-java SDK(推荐 3.25.10+),在 ThinkPHP 的控制器方法中调用:
public function getPresignedForPostUrl($fileName)
{
// 从配置读取
$endpoint = config('huawei.obs.endpoint');
$bucket = config('huawei.obs.bucket_name');
$ak = config('huawei.obs.access_key_id');
$sk = config('huawei.obs.secret_access_key');
<pre class='brush:php;toolbar:false;'>$obsClient = new ObsClient([
'ak' => $ak,
'sk' => $sk,
'endpoint' => $endpoint
]);
// 构造 policy:限定上传到 uploads/ 目录,最大 100MB,仅允许图片
$policy = json_encode([
['bucket' => $bucket],
['key' => "uploads/{$fileName}"],
['acl' => 'private'],
['success_action_status' => '201'],
['content-length-range' => [0, 100 * 1024 * 1024]],
['Content-Type' => 'image/*']
]);
$credentials = $obsClient->createPostSignature([
'bucket' => $bucket,
'expires' => 1800, // 单位秒
'policy' => base64_encode(str_replace(["\n", "\r"], '', $policy))
]);
return json([
'url' => "https://{$bucket}.{$endpoint}",
'fields' => [
'key' => "uploads/{$fileName}",
'acl' => 'private',
'success_action_status' => '201',
'policy' => $credentials['policy'],
'OSSAccessKeyId' => $credentials['accessKeyId'],
'signature' => $credentials['signature'],
]
]);}
注意几个易错细节:
-
key字段必须和 policy 里的key完全一致,否则 403 -
policybase64 编码前必须去除所有空白字符(str_replace那一行不能省) - 返回的
url是表单提交地址,不是文件访问地址;上传成功后 OBS 返回 XML,需解析其中的<Location>获取真实 URL
前端提交时别漏掉 hidden 字段和 enctype
前端拿到签名参数后,必须用原生 FormData 或 <form> 提交,且 enctype 必须为 multipart/form-data。常见错误是用 axios.post 直接发 JSON,这会导致签名字段被忽略,OBS 拒绝请求。
正确做法(Vue 示例):
<form :action="uploadData.url" method="post" enctype="multipart/form-data"> <input type="hidden" name="key" :value="uploadData.fields.key"> <input type="hidden" name="acl" :value="uploadData.fields.acl"> <input type="hidden" name="success_action_status" :value="uploadData.fields['success_action_status']"> <input type="hidden" name="policy" :value="uploadData.fields.policy"> <input type="hidden" name="OSSAccessKeyId" :value="uploadData.fields.OSSAccessKeyId"> <input type="hidden" name="signature" :value="uploadData.fields.signature"> <input type="file" name="file" @change="onFileChange"> </form>
如果非要用 axios(比如需要进度条),必须手动构造 FormData 并 append 所有 hidden 字段,再 append file —— 顺序无关,但字段名一个都不能少、不能错大小写。
最后提醒一句:上传回调(callback)虽然能简化后端处理,但它依赖额外配置 OSS 回调服务器,且回调地址必须公网可访问。中小项目建议先跳过回调,等前端上传成功后,由前端把返回的 Location 传给后端存数据库,更可控也更省事。



















