首页  >  文章  >  web前端  >  优化大文件上传:安全地将客户端分段上传到 AWS S3

优化大文件上传:安全地将客户端分段上传到 AWS S3

Susan Sarandon
Susan Sarandon原创
2024-11-08 07:22:02388浏览

将大文件上传到云端可能具有挑战性 - 网络中断、浏览器限制和巨大的文件大小很容易破坏该过程。 Amazon S3(简单存储服务)是一种可扩展、高速、基于 Web 的云存储服务,专为数据和应用程序的在线备份和归档而设计。然而,将大文件上传到 S3 需要小心处理,以确保可靠性和性能。

AWS S3 的分段上传:这是一个功能强大的解决方案,可以将大文件分成更小的块,通过独立处理每个部分甚至并行上传部分来实现更快、更可靠的上传。这种方法不仅克服了文件大小限制(S3 需要对大于 5GB 的文件进行分段上传),而且还最大限度地降低了失败的风险,使其非常适合需要无缝、稳健的文件上传的应用程序。

在本指南中,我们将详细介绍客户端分段上传到 S3 的细节,向您展示为什么它是处理大文件的明智选择、如何安全地启动和运行它以及需要注意哪些挑战出去。我将提供分步说明、代码示例和最佳实践,以帮助您实现可靠的客户端文件上传解决方案。

准备好升级您的文件上传体验了吗?让我们开始吧!

服务器与客户端上传

设计文件上传系统时,您有两个主要选择:通过服务器上传文件(服务器端)或直接从客户端上传文件到 S3(客户端)。每种方法都有其优点和缺点。

服务器端上传

Optimizing Large File Uploads: Secure Client-Side Multipart Uploads to AWS S3

优点:

  • 增强的安全性:所有上传均由服务器管理,确保 AWS 凭证的安全。

  • 更好的错误处理:服务器可以更稳健地管理重试、日志记录和错误处理。

  • 集中处理:文件可以在存储到 S3 之前在服务器上进行验证、处理或转换。

缺点:

  • 更高的服务器负载:大量上传会消耗服务器资源(CPU、内存、带宽),这会影响性能并增加运营成本。

  • 潜在瓶颈:在高上传流量期间,服务器可能成为单点故障或性能瓶颈,导致上传缓慢或停机。

  • 成本增加:在服务器端处理上传可能需要扩展基础设施以处理峰值负载,从而增加运营费用。

客户端上传

Optimizing Large File Uploads: Secure Client-Side Multipart Uploads to AWS S3

优点:

  • 减少服务器负载:文件直接从用户设备发送到 S3,释放服务器资源。

  • 速度提高:由于绕过应用程序服务器,用户体验更快的上传速度。

  • 成本效率:无需服务器基础设施来处理大型上传,从而可能降低成本。

  • 可扩展性:非常适合在不给后端服务器造成压力的情况下扩展文件上传。

缺点:

  • 安全风险:需要仔细处理 AWS 凭证和权限。必须安全地生成预签名 URL,以防止未经授权的访问。

  • 有限控制:服务器端对上传的监督较少;错误处理和重试通常在客户端进行管理。

  • 浏览器限制:浏览器有内存和 API 限制,这可能会阻碍处理非常大的文件或影响低端设备上的性能。

实施安全客户端上传的分步指南

安全地实现客户端上传涉及前端应用程序和安全后端服务之间的协调。后端服务的主要作用是生成预签名 URL,允许客户端将文件直接上传到 S3,而无需暴露敏感的 AWS 凭证。

先决条件

  • AWS 账户:访问有权使用 S3 的 AWS 账户。
  • AWS 开发工具包知识:熟悉适用于 JavaScript 的 AWS 开发工具包 (v3) 或直接对 AWS 服务进行 API 调用。
  • 前端和后端开发技能:了解客户端(JavaScript、React 等)和服务器端(Node.js、Express 等)编程。

1. 设置正确的架构

要有效实现客户端上传,您需要:

  • 前端应用程序:处理文件选择、根据需要将文件拆分为多个部分,以及使用预签名 URL 将各个部分上传到 S3。
  • 后端服务:一个安全服务器,提供用于生成预签名 URL 以及初始化或完成分段上传的 API。它可以保证您的 AWS 凭证的安全并强制执行任何必要的业务逻辑或验证。

此架构确保敏感操作在后端安全处理,而前端则管理上传过程。

2. 后端创建上传服务

为什么使用预签名 URL?

预签名 URL 允许客户端直接与 S3 交互,执行上传文件等操作,而无需在客户端提供 AWS 凭证。它们是安全的,因为:

  • 它们是有时间限制的,并在指定期限后过期。
  • 它们可以被限制为特定操作(例如,PUT 用于上传)。
  • 它们特定于特定的 S3 对象密钥。

实施S3UploadService

在服务器上创建一个服务类,负责:

a.定义 S3 存储桶和区域
b.安全地建立 AWS 凭证。
c.提供生成预签名 URL 和管理分段上传的方法。

// services/S3UploadService.js

import {
  S3Client,
  CreateMultipartUploadCommand,
  CompleteMultipartUploadCommand,
  UploadPartCommand,
  AbortMultipartUploadCommand,
  PutObjectCommand,
  GetObjectCommand,
  DeleteObjectCommand,
} from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';

// Import credential providers
import {
  fromIni,
  fromInstanceMetadata,
  fromEnv,
  fromProcess,
} from '@aws-sdk/credential-providers';

export class S3UploadService {
  constructor() {
    this.s3BucketName = process.env.S3_BUCKET_NAME;
    this.s3Region = process.env.S3_REGION;

    this.s3Client = new S3Client({
      region: this.s3Region,
      credentials: this.getS3ClientCredentials(),
    });
  }

  // Method to generate AWS credentials securely
  getS3ClientCredentials() {
    if (process.env.NODE_ENV === 'development') {
      // In development, use credentials from environment variables
      return fromEnv();
    } else {
      // In production, use credentials from EC2 instance metadata or another secure method
      return fromInstanceMetadata();
    }
  }

  // Generate a presigned URL for single-part upload (PUT), download (GET), or deletion (DELETE)
  async generatePresignedUrl(key, operation) {
    let command;
    switch (operation) {
      case 'PUT':
        command = new PutObjectCommand({
          Bucket: this.s3BucketName,
          Key: key,
        });
        break;
      case 'GET':
        command = new GetObjectCommand({
          Bucket: this.s3BucketName,
          Key: key,
        });
        break;
      case 'DELETE':
        command = new DeleteObjectCommand({
          Bucket: this.s3BucketName,
          Key: key,
        });
        break;
      default:
        throw new Error(`Invalid operation "${operation}"`);
    }

    // Generate presigned URL
    return await getSignedUrl(this.s3Client, command, { expiresIn: 3600 }); // Expires in 1 hour
  }

  // Methods for multipart upload
  async createMultipartUpload(key) {
    const command = new CreateMultipartUploadCommand({
      Bucket: this.s3BucketName,
      Key: key,
    });
    const response = await this.s3Client.send(command);
    return response.UploadId;
  }

  async generateUploadPartUrl(key, uploadId, partNumber) {
    const command = new UploadPartCommand({
      Bucket: this.s3BucketName,
      Key: key,
      UploadId: uploadId,
      PartNumber: partNumber,
    });

    return await getSignedUrl(this.s3Client, command, { expiresIn: 3600 });
  }

  async completeMultipartUpload(key, uploadId, parts) {
    const command = new CompleteMultipartUploadCommand({
      Bucket: this.s3BucketName,
      Key: key,
      UploadId: uploadId,
      MultipartUpload: { Parts: parts },
    });
    return await this.s3Client.send(command);
  }

  async abortMultipartUpload(key, uploadId) {
    const command = new AbortMultipartUploadCommand({
      Bucket: this.s3BucketName,
      Key: key,
      UploadId: uploadId,
    });
    return await this.s3Client.send(command);
  }
}

注意:确保您的 AWS 凭证得到安全管理。在生产中,建议使用附加到 EC2 实例或 ECS 任务的 IAM 角色,而不是硬编码凭证或使用环境变量。

3. 实现后端 API 端点

在后端创建 API 端点来处理来自前端的请求。这些端点将利用 S3UploadService 来执行操作。

// controllers/S3UploadController.js

import { S3UploadService } from '../services/S3UploadService';

const s3UploadService = new S3UploadService();

export const generatePresignedUrl = async (req, res, next) => {
  try {
    const { key, operation } = req.body; // key is the S3 object key (file identifier)
    const url = await s3UploadService.generatePresignedUrl(key, operation);
    res.status(200).json({ url });
  } catch (error) {
    next(error);
  }
};

export const initializeMultipartUpload = async (req, res, next) => {
  try {
    const { key } = req.body;
    const uploadId = await s3UploadService.createMultipartUpload(key);
    res.status(200).json({ uploadId });
  } catch (error) {
    next(error);
  }
};

export const generateUploadPartUrls = async (req, res, next) => {
  try {
    const { key, uploadId, parts } = req.body; // parts is the number of parts
    const urls = await Promise.all(
      [...Array(parts).keys()].map(async (index) => {
        const partNumber = index + 1;
        const url = await s3UploadService.generateUploadPartUrl(key, uploadId, partNumber);
        return { partNumber, url };
      })
    );
    res.status(200).json({ urls });
  } catch (error) {
    next(error);
  }
};

export const completeMultipartUpload = async (req, res, next) => {
  try {
    const { key, uploadId, parts } = req.body; // parts is an array of { ETag, PartNumber }
    const result = await s3UploadService.completeMultipartUpload(key, uploadId, parts);
    res.status(200).json({ result });
  } catch (error) {
    next(error);
  }
};

export const abortMultipartUpload = async (req, res, next) => {
  try {
    const { key, uploadId } = req.body;
    await s3UploadService.abortMultipartUpload(key, uploadId);
    res.status(200).json({ message: 'Upload aborted' });
  } catch (error) {
    next(error);
  }
};

在 Express 应用程序或您使用的任何框架中设置这些端点的路由。

4. 实现前端上传器类

前端将处理选择文件,根据文件大小决定是否执行单部分或分段上传,并管理上传过程。

一般来说,AWS 建议“当您的对象大小达到 100 MB 时,您应该考虑使用分段上传,而不是在单个操作中上传对象。”来源

// services/S3UploadService.js

import {
  S3Client,
  CreateMultipartUploadCommand,
  CompleteMultipartUploadCommand,
  UploadPartCommand,
  AbortMultipartUploadCommand,
  PutObjectCommand,
  GetObjectCommand,
  DeleteObjectCommand,
} from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';

// Import credential providers
import {
  fromIni,
  fromInstanceMetadata,
  fromEnv,
  fromProcess,
} from '@aws-sdk/credential-providers';

export class S3UploadService {
  constructor() {
    this.s3BucketName = process.env.S3_BUCKET_NAME;
    this.s3Region = process.env.S3_REGION;

    this.s3Client = new S3Client({
      region: this.s3Region,
      credentials: this.getS3ClientCredentials(),
    });
  }

  // Method to generate AWS credentials securely
  getS3ClientCredentials() {
    if (process.env.NODE_ENV === 'development') {
      // In development, use credentials from environment variables
      return fromEnv();
    } else {
      // In production, use credentials from EC2 instance metadata or another secure method
      return fromInstanceMetadata();
    }
  }

  // Generate a presigned URL for single-part upload (PUT), download (GET), or deletion (DELETE)
  async generatePresignedUrl(key, operation) {
    let command;
    switch (operation) {
      case 'PUT':
        command = new PutObjectCommand({
          Bucket: this.s3BucketName,
          Key: key,
        });
        break;
      case 'GET':
        command = new GetObjectCommand({
          Bucket: this.s3BucketName,
          Key: key,
        });
        break;
      case 'DELETE':
        command = new DeleteObjectCommand({
          Bucket: this.s3BucketName,
          Key: key,
        });
        break;
      default:
        throw new Error(`Invalid operation "${operation}"`);
    }

    // Generate presigned URL
    return await getSignedUrl(this.s3Client, command, { expiresIn: 3600 }); // Expires in 1 hour
  }

  // Methods for multipart upload
  async createMultipartUpload(key) {
    const command = new CreateMultipartUploadCommand({
      Bucket: this.s3BucketName,
      Key: key,
    });
    const response = await this.s3Client.send(command);
    return response.UploadId;
  }

  async generateUploadPartUrl(key, uploadId, partNumber) {
    const command = new UploadPartCommand({
      Bucket: this.s3BucketName,
      Key: key,
      UploadId: uploadId,
      PartNumber: partNumber,
    });

    return await getSignedUrl(this.s3Client, command, { expiresIn: 3600 });
  }

  async completeMultipartUpload(key, uploadId, parts) {
    const command = new CompleteMultipartUploadCommand({
      Bucket: this.s3BucketName,
      Key: key,
      UploadId: uploadId,
      MultipartUpload: { Parts: parts },
    });
    return await this.s3Client.send(command);
  }

  async abortMultipartUpload(key, uploadId) {
    const command = new AbortMultipartUploadCommand({
      Bucket: this.s3BucketName,
      Key: key,
      UploadId: uploadId,
    });
    return await this.s3Client.send(command);
  }
}

使用示例

// controllers/S3UploadController.js

import { S3UploadService } from '../services/S3UploadService';

const s3UploadService = new S3UploadService();

export const generatePresignedUrl = async (req, res, next) => {
  try {
    const { key, operation } = req.body; // key is the S3 object key (file identifier)
    const url = await s3UploadService.generatePresignedUrl(key, operation);
    res.status(200).json({ url });
  } catch (error) {
    next(error);
  }
};

export const initializeMultipartUpload = async (req, res, next) => {
  try {
    const { key } = req.body;
    const uploadId = await s3UploadService.createMultipartUpload(key);
    res.status(200).json({ uploadId });
  } catch (error) {
    next(error);
  }
};

export const generateUploadPartUrls = async (req, res, next) => {
  try {
    const { key, uploadId, parts } = req.body; // parts is the number of parts
    const urls = await Promise.all(
      [...Array(parts).keys()].map(async (index) => {
        const partNumber = index + 1;
        const url = await s3UploadService.generateUploadPartUrl(key, uploadId, partNumber);
        return { partNumber, url };
      })
    );
    res.status(200).json({ urls });
  } catch (error) {
    next(error);
  }
};

export const completeMultipartUpload = async (req, res, next) => {
  try {
    const { key, uploadId, parts } = req.body; // parts is an array of { ETag, PartNumber }
    const result = await s3UploadService.completeMultipartUpload(key, uploadId, parts);
    res.status(200).json({ result });
  } catch (error) {
    next(error);
  }
};

export const abortMultipartUpload = async (req, res, next) => {
  try {
    const { key, uploadId } = req.body;
    await s3UploadService.abortMultipartUpload(key, uploadId);
    res.status(200).json({ message: 'Upload aborted' });
  } catch (error) {
    next(error);
  }
};

5. 安全注意事项和最佳实践

  • 限制预签名 URL 权限:确保预签名 URL 仅授予必要的权限(例如,仅允许上传的 PUT 操作)。
  • 设置适当的过期时间:预签名 URL 应在合理的时间(例如 15 分钟到 1 小时)后过期,以最大限度地减少误用的时间。
  • 验证文件元数据:在后端,验证从客户端发送的任何元数据或参数以防止操纵(例如,强制执行允许的文件类型或大小)。
  • 使用 HTTPS:始终使用 HTTPS 进行客户端和后端之间的通信,以及访问 S3 时,以保护传输中的数据。
  • 监控和日志:在后端和 S3 上实施日志记录和监控,以检测任何异常活动或错误。

6. 其他注意事项

限制对象大小

虽然 AWS S3 支持大小高达 5 TiB(太字节)的对象,但由于浏览器限制和客户端资源限制,直接从浏览器上传如此大的文件是不切实际的,而且通常是不可能的。处理非常大的文件时,浏览器可能会崩溃或变得无响应,特别是需要在内存中处理它们时。

推荐:
  • 设置实际限制:定义您的应用程序支持客户端上传的最大文件大小(例如,100 GB 或更少)。
  • 通知用户:向用户提供有关允许的最大文件大小的反馈,并在开始上传之前在客户端处理验证。

重试策略

上传大文件会增加上传过程中网络中断或失败的风险。实施稳健的重试策略对于增强用户体验并确保成功上传至关重要。

策略
  • 自动重试:在提示用户之前自动重试失败的部分有限次数。
  • 可恢复上传:跟踪上传的部分,以便上传可以从中断处恢复,而不是重新开始。
  • 错误处理:如果重试失败,向用户提供信息丰富的错误消息,可能会建议检查网络连接等操作。

分段上传清理

不完整的分段上传可能会累积在您的 S3 存储桶中,消耗存储空间并可能产生费用。

注意事项
  • 中止未完成的上传:如果上传失败或被取消,请确保您的应用程序调用 AbortMultipartUpload API 来清理所有已上传的部分。
  • 生命周期规则:配置 S3 生命周期策略以在一段时间(例如 7 天)后自动中止不完整的分段上传。这有助于管理存储成本并保持水桶清洁。

生命周期规则配置示例:

// services/S3UploadService.js

import {
  S3Client,
  CreateMultipartUploadCommand,
  CompleteMultipartUploadCommand,
  UploadPartCommand,
  AbortMultipartUploadCommand,
  PutObjectCommand,
  GetObjectCommand,
  DeleteObjectCommand,
} from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';

// Import credential providers
import {
  fromIni,
  fromInstanceMetadata,
  fromEnv,
  fromProcess,
} from '@aws-sdk/credential-providers';

export class S3UploadService {
  constructor() {
    this.s3BucketName = process.env.S3_BUCKET_NAME;
    this.s3Region = process.env.S3_REGION;

    this.s3Client = new S3Client({
      region: this.s3Region,
      credentials: this.getS3ClientCredentials(),
    });
  }

  // Method to generate AWS credentials securely
  getS3ClientCredentials() {
    if (process.env.NODE_ENV === 'development') {
      // In development, use credentials from environment variables
      return fromEnv();
    } else {
      // In production, use credentials from EC2 instance metadata or another secure method
      return fromInstanceMetadata();
    }
  }

  // Generate a presigned URL for single-part upload (PUT), download (GET), or deletion (DELETE)
  async generatePresignedUrl(key, operation) {
    let command;
    switch (operation) {
      case 'PUT':
        command = new PutObjectCommand({
          Bucket: this.s3BucketName,
          Key: key,
        });
        break;
      case 'GET':
        command = new GetObjectCommand({
          Bucket: this.s3BucketName,
          Key: key,
        });
        break;
      case 'DELETE':
        command = new DeleteObjectCommand({
          Bucket: this.s3BucketName,
          Key: key,
        });
        break;
      default:
        throw new Error(`Invalid operation "${operation}"`);
    }

    // Generate presigned URL
    return await getSignedUrl(this.s3Client, command, { expiresIn: 3600 }); // Expires in 1 hour
  }

  // Methods for multipart upload
  async createMultipartUpload(key) {
    const command = new CreateMultipartUploadCommand({
      Bucket: this.s3BucketName,
      Key: key,
    });
    const response = await this.s3Client.send(command);
    return response.UploadId;
  }

  async generateUploadPartUrl(key, uploadId, partNumber) {
    const command = new UploadPartCommand({
      Bucket: this.s3BucketName,
      Key: key,
      UploadId: uploadId,
      PartNumber: partNumber,
    });

    return await getSignedUrl(this.s3Client, command, { expiresIn: 3600 });
  }

  async completeMultipartUpload(key, uploadId, parts) {
    const command = new CompleteMultipartUploadCommand({
      Bucket: this.s3BucketName,
      Key: key,
      UploadId: uploadId,
      MultipartUpload: { Parts: parts },
    });
    return await this.s3Client.send(command);
  }

  async abortMultipartUpload(key, uploadId) {
    const command = new AbortMultipartUploadCommand({
      Bucket: this.s3BucketName,
      Key: key,
      UploadId: uploadId,
    });
    return await this.s3Client.send(command);
  }
}

从主线程处理分段上传

上传大文件可能会占用大量资源,并可能导致浏览器主线程无响应,从而导致用户体验不佳。

解决方案:
  • 使用 Web Workers:将上传过程卸载到 Web Worker。 Web Workers 在后台运行,与 Web 应用程序的主执行线程分开,允许您执行资源密集型操作而不阻塞 UI。
好处:
  • 提高性能:释放主线程,确保 UI 在上传过程中保持响应。
  • 减少内存使用:帮助更有效地管理内存,因为可以在工作线程内处理大数据处理。
  • 增强稳定性:降低浏览器在大量上传期间无响应或崩溃的风险。

7. 浏览器兼容性注意事项

在实现客户端分段上传时,浏览器兼容性确实是一个问题。不同的浏览器可能对处理大文件上传所需的 API 和功能有不同级别的支持,例如 *文件 API、Blob 切片、Web Workers 和网络请求处理* 。成功应对这些差异对于确保在所有受支持的浏览器上获得一致且可靠的用户体验至关重要。

兼容性问题:

  • 文件 API 和 Blob 方法:大多数现代浏览器支持 Blob.slice(),但较旧的浏览器可能使用 Blob.webkitSlice() 或 Blob.mozSlice()。
  • Web Workers:在现代浏览器中受支持,但在某些较旧的浏览器中不受支持,或者在 Internet Explorer 中受到限制。
  • Fetch API 和 XMLHttpRequest:虽然 fetch() 得到广泛支持,但使用 fetch() 的上传进度事件并非在所有浏览器上一致可用。
  • 最大并发连接数:根据支持的浏览器之间的最低公分母限制同时上传的数量(例如 6 个并发连接)。
  • 内存限制:以小块的形式处理文件,并避免一次将整个文件加载到内存中。
  • CORS:配置 S3 CORS 策略以支持必要的 HTTP 方法(例如,PUT、POST)和标头。

结论

通过使用预签名 URL 和分段上传实现客户端上传,您可以高效地直接处理任意大小的文件上传到 S3,从而减少服务器负载并提高性能。请记住,通过安全地管理 AWS 凭证并限制预签名 URL 的权限和生命周期,将安全性放在首位。

本指南提供了使用 AWS S3、AWS SDK for JavaScript 和预签名 URL 设置安全且可扩展的文件上传系统的分步方法。通过提供的代码示例和最佳实践,您就可以很好地增强应用程序的文件上传功能。

以上是优化大文件上传:安全地将客户端分段上传到 AWS S3的详细内容。更多信息请关注PHP中文网其他相关文章!

声明:
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn