阿里云OSS实战:上传优化与性能调优经验分享

欧阳树潼 交互 阅读 1,419
赞 13 收藏
二维码
手机扫码查看
反馈

又踩坑了,OSS上传文件跨域报错

上周在给一个项目加图片上传功能,前端直接用阿里云OSS的Web直传,结果一上传就报CORS错误。控制台红了一片,Access to XMLHttpRequest at 'https://xxx.oss-cn-hangzhou.aliyuncs.com' from origin 'http://localhost:3000' has been blocked by CORS policy。我一开始以为是后端没配好,结果折腾了半天发现根本不是那么回事。

阿里云OSS实战:上传优化与性能调优经验分享

先说结论:OSS的CORS配置要手动开,而且不能只靠后端

我第一反应是让后端同事检查签名接口,是不是返回的policy或者callback有问题。他查了下说没问题,签名逻辑跑通了,测试脚本也能上传。那问题肯定出在前端请求上。我就开始翻阿里云文档,越看越迷糊——文档里一堆“服务端签名”“临时凭证”“STS”,但没一句说清楚CORS怎么配。

后来试了下发现,阿里云OSS默认是关闭CORS的!也就是说,哪怕你前端代码写得再漂亮,只要Bucket没显式开启CORS规则,浏览器直接拦掉请求,压根不会发到OSS。这和我以前用七牛云不一样,七牛新建空间默认就开了CORS,阿里云得自己手动配。

折腾半天才找到正确的CORS配置姿势

登录阿里云控制台,进OSS管理,找到对应Bucket,点“权限管理” → “跨域设置” → “创建规则”。这里有几个坑:

  • 来源(Allowed Origins)不能只写 *,虽然文档说支持通配符,但实测必须写具体的域名,比如 http://localhost:3000https://your-app.com。我一开始偷懒写了 *,结果还是报错。
  • 允许Methods必须包含 PUTPOST,因为直传用的是PUT方法(或者POST,看你用哪种方式)。
  • 允许Headers建议写 *,或者至少包含 x-oss-datex-oss-signatureContent-Type 这几个。我漏了 Content-Type,导致预检请求失败。
  • 暴露Headers(Expose Headers)可以留空,除非你要读取OSS返回的ETag之类的。

配完等了几分钟(据说有缓存),刷新页面,CORS错误终于没了。但新的问题来了:上传进度卡在100%,但文件没传上去,也没报错。

核心代码就这几行,但细节决定成败

我用的是最简单的Web直传方案:后端生成policy和signature,前端用XMLHttpRequest直接PUT到OSS。关键代码如下:

function uploadToOSS(file, { host, policy, signature, key }) {
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    xhr.open('PUT', ${host}/${key}, true);
    
    // 必须设置这些header,否则OSS会拒绝
    xhr.setRequestHeader('Content-Type', file.type || 'application/octet-stream');
    xhr.setRequestHeader('x-oss-date', new Date().toUTCString());
    xhr.setRequestHeader('x-oss-signature', signature);
    xhr.setRequestHeader('x-oss-policy', policy);

    xhr.upload.onprogress = (e) => {
      if (e.lengthComputable) {
        const percent = (e.loaded / e.total) * 100;
        console.log(上传进度: ${percent.toFixed(2)}%);
      }
    };

    xhr.onload = () => {
      if (xhr.status === 200) {
        resolve(${host}/${key});
      } else {
        reject(new Error(上传失败: ${xhr.status}));
      }
    };

    xhr.onerror = () => reject(new Error('网络错误'));
    xhr.send(file);
  });
}

这里注意几个细节:

  • host 必须是完整的OSS外网Endpoint,比如 https://my-bucket.oss-cn-hangzhou.aliyuncs.com,不能漏掉协议头。
  • key 是文件在OSS里的路径,比如 uploads/2024/05/avatar.jpg,要确保后端生成policy时允许这个前缀。
  • 所有自定义Header(x-oss-xxx)必须小写!虽然HTTP规范说header名不区分大小写,但某些浏览器或代理可能有问题,统一小写最安全。

我之前卡在100%就是因为没设 Content-Type,OSS默认当成binary/octet-stream处理,但我的policy里限制了只能上传image/*,所以被拒绝了。加上这一行就好了。

后端签名那块也别掉以轻心

虽然问题主要在前端,但后端生成的policy如果太严格也会导致上传失败。比如我同事一开始写的policy只允许特定文件名,结果我前端动态生成的key不符合规则,OSS直接403。正确的policy应该类似这样(Node.js示例):

const policy = {
  expiration: new Date(Date.now() + 30 * 60 * 1000).toISOString(), // 30分钟后过期
  conditions: [
    ['content-length-range', 0, 10 * 1024 * 1024], // 限制10MB以内
    ['starts-with', '$key', 'uploads/'], // 只允许上传到uploads目录
    ['starts-with', '$Content-Type', 'image/'] // 只允许图片
  ]
};
const base64Policy = Buffer.from(JSON.stringify(policy)).toString('base64');
const signature = crypto.createHmac('sha1', OSS_ACCESS_KEY_SECRET)
  .update(base64Policy)
  .digest('base64');

注意 conditions 里的 starts-with 是个好东西,既能限制目录,又能防止用户乱传文件类型。但如果你前端key是随机字符串(比如UUID),就得改成 ['starts-with', '$key', ''] 放宽限制。

还有一个小问题:本地开发时localhost的端口经常变

每次换端口都得去阿里云改CORS配置?太麻烦了。我的临时方案是:在CORS规则里多加几条,比如 http://localhost:3000http://localhost:8080http://127.0.0.1:5173。虽然不优雅,但省事。生产环境当然只留正式域名。

另外,有些团队会用内网穿透工具(比如ngrok)把本地服务映射到公网,这样就可以用固定域名调试,顺便避开CORS问题——不过这是另一个话题了。

总结一下踩过的坑

  • OSS默认关闭CORS,必须手动配置,且来源不能只写 *
  • 前端请求必须带齐 Content-Type 和所有 x-oss-xxx header
  • 后端policy的 conditions 要和前端key、Content-Type匹配
  • 本地开发时提前把常用端口加到CORS规则里

其实整个流程跑通后发现,OSS Web直传比想象中简单,核心就三步:后端签policy、前端带签名发PUT、OSS验证通过后存文件。但中间任何一个环节配错,都会导致静默失败(比如进度100%但没文件),排查起来特别耗时间。

以上是我踩坑后的总结,如果你有更好的方案欢迎评论区交流。比如现在也有用STS临时凭证的方式,安全性更高,不过对小项目来说可能有点重。这个技巧的拓展用法还有很多,后续会继续分享这类博客。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论