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

如何在React/Next.js受保护路由访问AWS非public-read权限文件

实现方案拆解

一、后端上传PDF至AWS S3(私有权限)

首先确保S3桶默认权限为私有(无需额外设置public-read),后端通过IAM授权的账号完成上传,代码示例以Node.js + AWS SDK v3为例:

import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
import { Upload } from "@aws-sdk/lib-storage";

// 初始化S3客户端
const s3Client = new S3Client({
  region: "你的AWS区域",
  credentials: {
    accessKeyId: process.env.AWS_ACCESS_KEY_ID,
    secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
  },
});

// 上传PDF文件到S3
export async function uploadPdfToS3(fileBuffer, fileName) {
  try {
    const uploadParams = {
      Bucket: "你的桶名称",
      Key: `pdfs/${Date.now()}-${fileName}`, // 自定义存储路径,避免重名
      Body: fileBuffer,
      ContentType: "application/pdf",
      ACL: "private", // 明确设置私有,默认值也是private
    };

    // 大文件用分段上传,小文件可直接用PutObjectCommand
    const upload = new Upload({
      client: s3Client,
      params: uploadParams,
    });

    await upload.done();
    return { success: true, fileKey: uploadParams.Key };
  } catch (error) {
    console.error("S3上传失败:", error);
    throw new Error("文件上传失败");
  }
}

权限配置说明:给后端使用的IAM账号分配s3:PutObject权限,S3桶策略不要添加任何公开访问规则,确保只有授权账号能操作文件。

二、后端生成预签名URL(供前端临时访问)

私有文件无法直接访问,后端需为授权用户生成带过期时间的预签名URL,示例代码:

import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
import { GetObjectCommand } from "@aws-sdk/client-s3";

// 生成PDF的预签名访问链接
export async function getPdfPresignedUrl(fileKey) {
  try {
    const getObjectParams = {
      Bucket: "你的桶名称",
      Key: fileKey,
    };

    const command = new GetObjectCommand(getObjectParams);
    // 设置URL过期时间,建议15-30分钟(单位:秒)
    const signedUrl = await getSignedUrl(s3Client, command, { expiresIn: 900 });
    return signedUrl;
  } catch (error) {
    console.error("生成预签名URL失败:", error);
    throw new Error("获取文件链接失败");
  }
}

随后编写后端接口(如GET /api/pdf/:fileKey),接口需先校验用户身份(比如JWT验证),通过后返回预签名URL。

三、React/Next.js受保护路由配置

确保只有登录用户能访问PDF查看页面,以Next.js App Router为例,用Middleware做身份校验:

// middleware.js
import { NextResponse } from "next/server";
import { verifyAuthToken } from "./lib/auth";

export async function middleware(request) {
  const token = request.cookies.get("auth-token")?.value;
  if (!token) {
    return NextResponse.redirect(new URL("/login", request.url));
  }

  try {
    await verifyAuthToken(token);
    return NextResponse.next();
  } catch (error) {
    return NextResponse.redirect(new URL("/login", request.url));
  }
}

// 指定受保护路由的匹配规则
export const config = {
  matcher: "/protected/pdf/:path*",
};

四、用react-pdf组件渲染PDF

先安装依赖:

npm install @react-pdf/renderer

在受保护页面中,先调用后端接口获取预签名URL,再渲染PDF:

"use client";

import { useState, useEffect } from "react";
import { Document, Page, pdfjs } from "@react-pdf/renderer";

// 配置pdf.js的worker路径,避免加载失败
pdfjs.GlobalWorkerOptions.workerSrc = `//unpkg.com/pdfjs-dist@${pdfjs.version}/build/pdf.worker.min.js`;

export default function PdfViewer({ params }) {
  const [pdfUrl, setPdfUrl] = useState("");
  const [numPages, setNumPages] = useState(null);
  const [currentPage, setCurrentPage] = useState(1);

  useEffect(() => {
    const fetchPdfUrl = async () => {
      try {
        const res = await fetch(`/api/pdf/${params.fileKey}`);
        if (!res.ok) throw new Error("获取PDF链接失败");
        const data = await res.json();
        setPdfUrl(data.signedUrl);
      } catch (error) {
        console.error(error);
      }
    };

    fetchPdfUrl();
  }, [params.fileKey]);

  const handleDocumentLoad = ({ numPages }) => {
    setNumPages(numPages);
    setCurrentPage(1);
  };

  return (
    <div className="p-6 max-w-4xl mx-auto">
      {pdfUrl ? (
        <>
          <Document file={pdfUrl} onLoadSuccess={handleDocumentLoad}>
            <Page pageNumber={currentPage} />
          </Document>
          <div className="mt-4 flex items-center gap-3">
            <button
              onClick={() => setCurrentPage(prev => Math.max(prev - 1, 1))}
              disabled={currentPage === 1}
              className="px-3 py-1 bg-gray-200 rounded"
            >
              上一页
            </button>
            <span>第 {currentPage} 页 / 共 {numPages} 页</span>
            <button
              onClick={() => setCurrentPage(prev => Math.min(prev + 1, numPages))}
              disabled={currentPage === numPages}
              className="px-3 py-1 bg-gray-200 rounded"
            >
              下一页
            </button>
          </div>
        </>
      ) : (
        <div className="text-center py-10">加载中...</div>
      )}
    </div>
  );
}

关键注意事项

  • 预签名URL的过期时间需平衡安全性和用户体验,建议15-30分钟。
  • 后端接口必须严格校验用户身份,防止未授权用户获取文件链接。
  • S3桶权限需最小化,仅允许后端IAM账号执行上传和生成预签名的操作。

内容的提问来源于stack exchange,提问作者Ebrahim Khalil Amid

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.02 04:43:34