You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Next.js 13使用AWS SDK上传图片至S3时持续出现403禁止访问错误

Next.js中AWS S3上传403错误排查(除密钥无效外的原因及代码问题分析)

除accessKeyId/secretAccessKey无效外的403原因

  • IAM权限不足:即使密钥有效,对应的IAM用户未被授予s3:PutObject权限,或权限未覆盖目标Bucket。需检查IAM策略是否明确允许对指定Bucket执行上传操作。
  • Bucket策略限制:S3 Bucket自身的策略可能拒绝请求,比如设置了IP白名单限制、来源域名拦截,或未配置允许上传的规则。
  • CORS配置错误:若前端直接与S3交互(如你的当前实现),Bucket的CORS规则未允许Next.js域名发起PUT请求,会触发403。需确保CORS规则包含你的域名及PUT方法。
  • 签名/区域不匹配:签名版本(你用了v4)或配置的region与Bucket实际所在区域不一致,会导致签名无效,返回403。
  • 对象Key权限冲突:若要上传的Key已存在,且该对象的ACL不允许当前用户覆盖,会触发403。
  • Bucket公共访问设置:Bucket启用了「阻止公共访问」规则,而请求未通过合法身份验证流程,也会导致403。

你的代码存在的问题

  1. 密钥暴露风险:使用NEXT_PUBLIC_前缀的环境变量存储AWS密钥,会将密钥打包到前端代码中完全暴露给用户,这是严重的安全漏洞。正确做法是将上传逻辑放在后端API路由,前端通过调用后端接口完成上传,避免直接在前端初始化S3客户端。
  2. ContentEncoding配置错误:你设置了ContentEncoding: 'base64',但上传的是原始File对象而非base64编码内容。这会导致S3接收到的内容与声明的编码不匹配,可能触发签名验证失败,进而返回403。应移除该配置,除非你确实将File转换为base64字符串上传。
  3. 错误处理缺失细节:onError未传递具体错误信息,无法排查AWS返回的具体错误原因(比如是权限问题还是签名问题)。建议修改为onError(error.message),以便定位问题。
  4. 可选:ACL配置缺失:若Bucket默认ACL不允许当前操作,可能需要在上传参数中添加ACL字段(如ACL: 'public-read'),具体取决于你的业务需求。

修正后的实现思路

将上传逻辑移至Next.js后端API路由,通过预签名URL方式完成上传(更安全且符合AWS最佳实践):

后端API路由(pages/api/upload-s3.ts)

import { S3 } from 'aws-sdk';
import type { NextApiRequest, NextApiResponse } from 'next';

const s3 = new S3({
  accessKeyId: process.env.AWS_ACCESS_KEY_ID,
  secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
  region: process.env.AWS_REGION,
  signatureVersion: 'v4',
});

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  if (req.method !== 'POST') {
    return res.status(405).json({ message: '仅支持POST请求' });
  }

  try {
    const { key, fileType } = req.body;
    const params = {
      Bucket: process.env.AWS_S3_BUCKET!,
      Key: key,
      ContentType: fileType,
      Expires: 60, // 预签名URL有效期60秒
    };

    const signedUrl = s3.getSignedUrl('putObject', params);
    res.status(200).json({ signedUrl });
  } catch (error) {
    res.status(500).json({ message: (error as Error).message });
  }
}

前端上传逻辑

export const uploadObject = async (
  file: File,
  key: string,
  onProgress: (percent: number) => void,
  onSuccess: () => void,
  onError: (message: string) => void
) => {
  try {
    // 调用后端获取预签名URL
    const apiRes = await fetch('/api/upload-s3', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ key, fileType: file.type }),
    });

    if (!apiRes.ok) throw new Error('获取上传地址失败');
    const { signedUrl } = await apiRes.json();

    // 使用预签名URL上传文件
    const uploadRes = await fetch(signedUrl, {
      method: 'PUT',
      body: file,
      headers: { 'Content-Type': file.type },
    });

    if (!uploadRes.ok) throw new Error('文件上传失败');
    onSuccess();
  } catch (error) {
    onError((error as Error).message);
  }
};

内容的提问来源于stack exchange,提问作者Vadim

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.05 13:16:21