PHP生成OSS直传签名

wen PHP项目 1

本文目录导读:

PHP生成OSS直传签名

  1. 文章标题:PHP生成OSS直传签名:从原理到实战,一篇掌握阿里云对象存储安全上传
  2. 目录导读

PHP生成OSS直传签名:从原理到实战,一篇掌握阿里云对象存储安全上传


目录导读

  1. 为什么需要“直传签名”?—— 绕过应用服务器的带宽瓶颈
  2. 核心机制拆解:Policy、Signature 与 OSS 的三角信任
  3. PHP 实现直传签名的完整代码解剖(含多环境配置)
  4. 签名生成中的 5 个致命陷阱与规避方案(附错误码对照)
  5. 前端配合:如何用生成的签名 + 表单直传 OSS(兼容性处理)
  6. 高频问答:关于有效期、覆盖上传、CDN 回源签名那些事

为什么需要“直传签名”?—— 绕过应用服务器的带宽瓶颈

在典型的 Web 应用中,如果用户上传图片先经过 PHP 应用服务器,再由服务器转发至 OSS,这会导致两个致命问题:带宽成本高(应用服务器下行流量翻倍)和 响应延迟大(大文件上传会占用 PHP 进程,导致阻塞)。OSS 直传签名 的核心理念是:客户端(浏览器/App)直接与 OSS 服务器通信,而应用服务器只负责生成一把“临时钥匙”——签名。

这把“钥匙”基于 阿里云 AccessKey Secret,通过特定算法生成,它限定了上传的目标 BucketObject 名称前缀最大文件大小 以及 有效期,没有这把签名,任何人无法往你的 Bucket 里写入文件,这既释放了应用服务器压力,又保障了数据安全。

核心机制拆解:Policy、Signature 与 OSS 的三角信任

要掌握 PHP 实现,必须先理解三个核心元素的协作关系:

  1. Policy(表单策略):一个 JSON 字符串,包含 expiration(过期时间,UTC 格式)、conditions(条件列表,如 bucketcontent-length-range 范围),这是“规则”的声明。
  2. Signature(签名):基于 Policy 字符串,使用 AccessKeySecret 进行 base64_encode(hash_hmac('sha1', base64_encode($policy), $accessKeySecret, true)) 计算出来的不可逆哈希值,这是“身份”的凭证。
  3. OSS 服务器:收到客户端请求后,OSS 会用它持有的 AccessKeyId 对应的 Secret 去解密签名,并比对 Policy 条件是否满足。三方缺一不可,且 Policy 必须与签名严格对应。

PHP 实现直传签名的完整代码解剖(含多环境配置)

以下代码为生产级实现,已过滤 SEO 垃圾站中的过时写法(如直接拼接字符串),采用面向对象风格:

<?php
/**
 * 生成 OSS 直传签名(兼容 ThinkPHP / Laravel / 原生)
 */
class OssDirectSigner
{
    private $accessKeyId;
    private $accessKeySecret;
    private $bucket;
    private $host; //  https://your-bucket.oss-cn-hangzhou.aliyuncs.com
    public function __construct($accessKeyId, $accessKeySecret, $bucket, $endpoint)
    {
        $this->accessKeyId = $accessKeyId;
        $this->accessKeySecret = $accessKeySecret;
        $this->bucket = $bucket;
        $this->host = "https://{$bucket}.{$endpoint}";
    }
    /**
     * 生成签名数组
     * @param string $dir 上传目录前缀
     * @param int $expireTime 有效期(秒)
     * @param int $maxSize 最大文件大小(KB)
     */
    public function getSignature($dir = 'uploads/', $expireTime = 300, $maxSize = 10240)
    {
        // 1. 构造 Policy 结构(必须按 OSS 官方要求排序)
        $now = time();
        $expire = $now + $expireTime;
        $policyJson = json_encode([
            'expiration' => gmdate('Y-m-d\TH:i:s.000\Z', $expire),
            'conditions' => [
                ['content-length-range', 0, $maxSize * 1024], // 0 ~ 10MB
                ['starts-with', '$key', $dir], // 限制前缀
                ['bucket' => $this->bucket] // 限定 Bucket
            ]
        ]);
        // 2. 计算签名(注意:Base64 后的 Policy 作为 HMAC 的明文)
        $base64Policy = base64_encode($policyJson);
        $signature = base64_encode(
            hash_hmac('sha1', $base64Policy, $this->accessKeySecret, true)
        );
        // 3. 返回前端所需的全部参数
        return [
            'accessid'       => $this->accessKeyId,
            'host'           => $this->host,
            'policy'         => $base64Policy,
            'signature'      => $signature,
            'expire'         => $expire,
            'dir'            => $dir,
            'callback'       => '', // 如需上传回调可在此添加
        ];
    }
}
// 使用示例(环境变量读取密钥,避免硬编码)
$signer = new OssDirectSigner(
    getenv('OSS_AK_ID'),
    getenv('OSS_AK_SECRET'),
    getenv('OSS_BUCKET'),
    getenv('OSS_ENDPOINT') // 如 oss-cn-shenzhen.aliyuncs.com
);
echo json_encode($signer->getSignature('user_avatar/2025/'));

代码关键点conditions 数组的顺序会影响签名结果,特别是 starts-withcontent-length-range 的位置,必须与 OSS 服务端解析逻辑一致。

签名生成中的 5 个致命陷阱与规避方案

  1. 时间不同步:客户端时间与服务器时间偏差过大,导致 expiration 过早生效或失效。规避:统一使用服务器时间戳,前端不要依赖本地时间。
  2. Policy 中 Bucket 条件缺失:如果漏写 ['bucket' => $this->bucket],攻击者可修改请求头中的 Host 上传到任意 Bucket。规避:务必加入。
  3. 目录前缀伪造starts-with 条件若写死 $dir,但前端可篡改 key 字段。规避:后端必须校验前端提交的 key 是否以 $dir 开头,不信任前端。
  4. Base64 换行问题:某些环境会自动换行,导致签名不匹配。规避:生成 Policy 后使用 preg_replace('/\s+/', '', $base64Policy) 清除空白。
  5. 跨域 OPTIONS 预检:浏览器直传时,OSS 需要正确响应 CORS 预检。规避:在 OSS 控制台配置好 AllowedOriginAllowedMethod(POST)和 AllowedHeader(*)。

前端配合:如何用生成的签名 + 表单直传 OSS

后端返回 JSON 后,前端使用原生 FormData 构造请求(以 Vue 为例):

const formData = new FormData();
// 注意顺序:key 必须在最后,且不能使用 multipart 的额外字段
formData.append('key', signer.dir + 'avatar_' + Date.now() + '.jpg');
formData.append('policy', signer.policy);
formData.append('OSSAccessKeyId', signer.accessid);
formData.append('success_action_status', '200');
formData.append('signature', signer.signature);
formData.append('file', file);
fetch(signer.host, { method: 'POST', body: formData })
  .then(res => res.text())
  .then(result => console.log('上传成功', result));

注意key 字段如果不放在首位,某些浏览器会报 InvalidArgument,且 file 字段必须是最后一个。

高频问答

Q1:签名的有效期最少可以设置多长? A:官方建议最短为 1 秒,但考虑到网络延迟,一般建议 60~300 秒,过期后签名立即失效,OSS 返回 AccessDeniedInvalidArgument

Q2:如何实现“同名文件覆盖”? A:让前端生成的 key 包含固定文件名(如 user_1/default.jpg)即可,但请注意,OSS 本身不支持原子性覆盖,高并发下可能出现短暂读取到旧文件或新文件的中间态,建议配合版本控制。

Q3:直传大文件(超过 5GB)怎么办? A:直传签名仅适用于 5GB 以下文件,超过需使用分片上传,且分片上传不能使用表单签名,需使用 STS 临时凭证初始化上传 ID,这是另一套流程,不在此文讨论。

Q4:使用 CDN 后,签名规则需要改变吗? A:不需要,直传签名的计算只依赖 AccessKey 和 Policy,与是否解析到 CDN 域名无关,但注意:上传域名必须和签名时的host 一致,不能混用 CDN 域名与 OSS 原生域名。


本文基于阿里云官方文档及社区最佳实践撰写,所有代码均经过逻辑推演,实际部署时请自查 OSS 权限管理(建议使用 RAM 子账号最小权限),并开启服务端回调(Callback)以增强安全性。

抱歉,评论功能暂时关闭!