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

React应用上传PDF至S3 Bucket时出现403权限禁止错误

React应用向S3 Bucket上传PDF时出现403 Forbidden错误

我在使用React应用向S3 Bucket上传PDF文件时触发403 Forbidden错误,错误信息不够清晰,相关代码及错误详情如下:

Flask生成预签名URL代码

from flask import Flask, jsonify, request
import boto3
import datetime
def create_file_upload_url_controller():
    try:
        s3_client = boto3.client('s3')
        print('create_file_upload_url_controller()')
        user_id = str(request.user.id)

        file_name = f"{user_id}_{datetime.datetime.utcnow()}"
        bucket_name = "docwise-large-files-dev"
        print(f'file_name: {file_name}')

        # Generate pre-signed URL for upload
        presigned_url = s3_client.generate_presigned_url(
            'put_object',
            Params={'Bucket': bucket_name, 'Key': file_name},
            ExpiresIn=3600,  # URL valid for 1 hour
            HttpMethod="PUT"
        )
        
        print(f'presigned_url:  {presigned_url}')

        return jsonify({'upload_url': presigned_url, 'file_name': file_name})

    except Exception as e:
        print(f'e: {e}')
        return jsonify({'error': str(e)}), 500

React文件上传组件代码

// FileUpload.js
import React, { useEffect, useState } from 'react';
import { connect } from 'react-redux';
import { addFile } from '../../../../../../actions/filesActions';
import { useNavigate, useParams } from 'react-router-dom';

const FileUpload = ({ onFileChange, addFile, conversation_id,files }) => {
  const { chatId } = useParams();

  useEffect( ()=>{
  },[files])

  const handleFileChange = (event) => {
    const selectedFiles = event.target.files;
    for (let i = 0; i < selectedFiles.length; i++) {
      const file = selectedFiles[i];
      const fileDetails = {
        conversation_id: chatId,
        file: file,
      };
      addFile(fileDetails);
    }
  };

  return (
    <div>
      <input type="file"  onChange={handleFileChange} />
    </div>
  );
};

const mapStateToProps = (state) => ({
  files: state.files.files
});

const mapActionsToProps = {
  addFile,
};

export default connect(mapStateToProps, mapActionsToProps)(FileUpload);

上传S3的Redux Action代码

// filesActions.js

export const ADD_PDF = 'ADD_PDF';
export const ADD_PDF_SUCCESS = 'ADD_PDF_SUCCESS';
export const ADD_PDF_FAIL = 'ADD_PDF_FAIL';
export const DELETE_PDF = 'DELETE_PDF';
export const UPDATE_PDF = 'UPDATE_PDF';

let baseUrl;
if (process.env.MODE === 'local') {
  baseUrl = `/api/file/`;
} else {
  baseUrl = `${process.env.REACT_APP_BACKEND_URL}/api/file/`;
}

export const addFile = (pdfDetails) => async (dispatch, getState) => {
  try {
    const { token } = getState().auth;

    const createFileUploadURLResponse = await fetch(`${baseUrl}create_file_upload_url`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${token}`,
      },
      body: null,
    });

    if (createFileUploadURLResponse.error != null) {
      console.log('error: ', createFileUploadURLResponse.error);
      return dispatch({
        type: ADD_PDF_FAIL,
        payload: createFileUploadURLResponse.error,
      });
    }

    const createFileUploadURLResponseData = await createFileUploadURLResponse.json();

    console.log(`createFileUploadURLResponseData:  ${createFileUploadURLResponseData.upload_url}`);
    console.log(`key : ${createFileUploadURLResponseData.file_name}`)

    console.log('pdfDetails.file:   ',pdfDetails.file);

    console.log('before uploading file to s3')

    const uploadFileResponse = await fetch(`${createFileUploadURLResponseData.upload_url}`, {
      method: 'PUT',
      headers: {
        'Content-Type': pdfDetails.file.type,  // Change to file's actual MIME type
        'Key': `${createFileUploadURLResponseData.file_name}`,
      },
      body: pdfDetails.file,
    });
    const uploadFileResponseData = await uploadFileResponse.json();

    console.log(`uploadFileResponseData:  ${JSON.stringify(uploadFileResponseData)}`);

  } catch (error) {
    console.log('error: ', error);
    return null;
  }
};

遇到的错误

filesActions.js:52     PUT https://docwise-large-files-dev.s3.amazonaws.com/Juan_Casas_Resume.pdf?AWSAccessKeyId=AKIAYNCJFQSXTCQHFGR3&Signature=GTb89cRSPxDQhp0TmTfuggDKPfc%3D&Expires=1692137811 403 (Forbidden)

排查修复方案

  1. 移除上传请求中的Key请求头
    预签名URL已经包含了对象Key的签名信息,额外在请求头中添加Key字段会导致签名不匹配,直接删除该字段:
const uploadFileResponse = await fetch(`${createFileUploadURLResponseData.upload_url}`, {
  method: 'PUT',
  headers: {
    'Content-Type': pdfDetails.file.type,  // 仅保留文件MIME类型
    // 移除此行:'Key': `${createFileUploadURLResponseData.file_name}`,
  },
  body: pdfDetails.file,
});
  1. 确保预签名Key与上传Key完全一致
    从错误URL可见,实际上传的Key是Juan_Casas_Resume.pdf,但后端生成的Key是{user_id}_{datetime}格式,两者不匹配。需保证前端完全使用后端返回的file_name作为S3对象的Key,不能替换为原文件名,同时检查后端日志输出的file_name是否与上传URL中的Key一致。

  2. 检查S3 Bucket的CORS配置
    确保Bucket的CORS规则允许PUT请求及前端域名,示例配置:

<?xml version="1.0" encoding="UTF-8"?>
<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<CORSRule>
    <AllowedOrigin>你的前端域名</AllowedOrigin>
    <AllowedMethod>PUT</AllowedMethod>
    <AllowedHeader>*</AllowedHeader>
    <ExposeHeader>ETag</ExposeHeader>
</CORSRule>
</CORSConfiguration>
  1. 验证IAM权限
    生成预签名URL的IAM用户必须拥有s3:PutObject权限,示例策略:
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": "s3:PutObject",
            "Resource": "arn:aws:s3:::docwise-large-files-dev/*"
        }
    ]
}
  1. 完善fetch响应错误处理
    fetch仅在网络错误时进入catch块,HTTP状态码错误需手动判断,修改后端请求的错误处理逻辑:
if (!createFileUploadURLResponse.ok) {
    const errorData = await createFileUploadURLResponse.json();
    console.log('error: ', errorData.error);
    return dispatch({
        type: ADD_PDF_FAIL,
        payload: errorData.error,
    });
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.13 00:37:49