Amazon S3 提供方

页面摘要: @strapi/provider-upload-aws-s3 包允许你将媒体库资源存储在 Amazon S3 或任何兼容 S3 的服务(Cloudflare R2、Scaleway、MinIO、Tigris 等)上。本页介绍提供方配置、所需的 AWS 设置(IAM、CORS、中间件),以及加密、校验和、私有存储桶签名 URL 等扩展选项。

媒体库功能由一个名为 Upload 的后端服务器包提供支持,该包利用提供方机制。

Strapi 为媒体库维护 3 个提供方。本页介绍 Amazon S3 提供方的安装与配置。对于其他提供方,请参阅媒体库页面中的列表。

安装

要安装官方 Strapi 维护的 AWS S3 提供方,请在终端中运行以下命令:

Yarn

yarn add @strapi/provider-upload-aws-s3

NPM

npm install @strapi/provider-upload-aws-s3 --save

配置

提供方配置定义在/config/plugins 文件中。如果该文件不存在,请先创建它。当每个环境使用不同的提供方时,请在 /config/env/${yourEnvironment}/plugins.js|ts 中指定正确的配置(参见环境)。

提供方配置接受以下条目:

  • provider 用于定义提供方名称(即 amazon-s3)
  • providerOptions 用于定义在构建提供方时传递下去的选项(有关选项的完整列表,请参阅 AWS 文档;一些示例在专门的扩展提供方选项部分中给出)
  • actionOptions 用于定义分别直接传递给每个方法参数的选项。官方 AWS 文档列出了 upload/uploadStream 和 delete 的可用选项。

基础示例

JavaScript

module.exports = ({ env }) => ({
  // ...
  upload: {
    config: {
      provider: 'aws-s3',
      providerOptions: {
        baseUrl: env('CDN_URL'),
        rootPath: env('CDN_ROOT_PATH'),
        s3Options: {
          credentials: {
            accessKeyId: env('AWS_ACCESS_KEY_ID'),
            secretAccessKey: env('AWS_ACCESS_SECRET'),
          },
          region: env('AWS_REGION'),
          params: {
            ACL: env('AWS_ACL', 'public-read'),
            signedUrlExpires: env('AWS_SIGNED_URL_EXPIRES', 15 * 60),
            Bucket: env('AWS_BUCKET'),
          },
        },
      },
      actionOptions: {
        upload: {},
        uploadStream: {},
        delete: {},
      },
    },
  },
  // ...
});

TypeScript

export default ({ env }) => ({
  // ...
  upload: {
    config: {
      provider: 'aws-s3',
      providerOptions: {
        baseUrl: env('CDN_URL'),
        rootPath: env('CDN_ROOT_PATH'),
        s3Options: {
          credentials: {
            accessKeyId: env('AWS_ACCESS_KEY_ID'),
            secretAccessKey: env('AWS_ACCESS_SECRET'),
          },
          region: env('AWS_REGION'),
          params: {
            ACL: env('AWS_ACL', 'public-read'),
            signedUrlExpires: env('AWS_SIGNED_URL_EXPIRES', 15 * 60),
            Bucket: env('AWS_BUCKET'),
          },
        },
      },
      actionOptions: {
        upload: {},
        uploadStream: {},
        delete: {},
      },
    },
  },
  // ...
});

如果你将存储桶用作 CDN 并在自定义域名上分发内容,可以使用 baseUrl 和 rootPath 属性。使用环境配置来定义你的资源 URL 在 Strapi 中如何保存。

AWS SDK V3 URL 格式

该提供方使用 AWS SDK V3,默认对 S3 URL 使用 虚拟托管风格 URL。要改用路径风格 URL,请显式设置 baseUrl:

baseUrl: `https://s3.${process.env.AWS_REGION}.amazonaws.com/${process.env.AWS_BUCKET}`,
从较旧 Strapi 版本迁移 S3 凭据

S3 凭据的位置在 Strapi 各版本之间有所变化:选项必须嵌套在 s3Options 下,凭据应包裹在 credentials 对象中(将它们放在 s3Options 根目录仍然有效但已弃用)。有关完整的迁移信息,请参阅 Amazon S3 提供方凭据必须设置在 s3Options 下。

AWS 凭据提供方函数

你可以传递一个 AWS 凭据提供方函数(例如来自 @aws-sdk/credential-providers)给 s3Options.credentials,而不是静态的 credentials 对象。这允许在运行时解析并刷新凭据,而无需重启进程,当凭据在应用生命周期内发生变化时(例如临时凭据、凭据轮换)这很有用:

import { fromNodeProviderChain } from '@aws-sdk/credential-providers';

export default ({ env }) => ({
  upload: {
    config: {
      provider: 'aws-s3',
      providerOptions: {
        s3Options: {
          credentials: fromNodeProviderChain(), // AWS SDK 动态解析凭据
          region: env('AWS_REGION'),
          params: {
            Bucket: env('AWS_BUCKET'),
          },
        },
      },
    },
  },
});

提供方函数必须返回一个解析为包含 accessKeyId 和 secretAccessKey 属性的凭据的 Promise。

WARNING

为确保提供方正常工作,你还需要配置 IAM 权限、存储桶 CORS 和 Strapi 安全中间件(参见必要设置)。

私有存储桶和签名 URL

如果你的存储桶配置为私有,请在 params 对象中将 ACL 选项设为 private。这确保文件 URL 被签名。

你可以通过在 params 对象中设置 signedUrlExpires 选项来定义签名 URL 的过期时间。默认值为 15 分钟。

NOTE

如果你使用 CDN,URL 将不会被签名。

module.exports = ({ env }) => ({
  // ...
  upload: {
    config: {
      provider: 'aws-s3',
      providerOptions: {
        s3Options: {
          credentials: {
            accessKeyId: env('AWS_ACCESS_KEY_ID'),
            secretAccessKey: env('AWS_ACCESS_SECRET'),
          },
          region: env('AWS_REGION'),
          params: {
            ACL: 'private', // <== 将 ACL 设为 private
            signedUrlExpires: env('AWS_SIGNED_URL_EXPIRES', 15 * 60),
            Bucket: env('AWS_BUCKET'),
          },
        },
      },
      actionOptions: {
        upload: {},
        uploadStream: {},
        delete: {},
      },
    },
  },
  // ...
});

必要设置

以下是为使你的 AWS S3 设置能与媒体库配合工作而需要考虑的最低配置操作。

IAM 策略操作

以下是 AWS S3 提供方工作所需的最低权限量:

"Action": [
  "s3:PutObject",
  "s3:GetObject",
  "s3:ListBucket",
  "s3:DeleteObject",
  "s3:PutObjectAcl"
],

存储桶 CORS 配置

要显示上传到 S3 的 GIF 和视频缩略图,请编辑你的存储桶 CORS 配置,以便缩略图在 Strapi 中正确显示。为此:

  1. 在 AWS 控制台中打开你的存储桶。
  2. 导航到 Permissions 标签页。
  3. 找到 Cross-origin resource sharing (CORS) 字段。
  4. 添加以下 CORS 策略(或根据你的需要进行调整):
[
  {
    "AllowedHeaders": ["*"],
    "AllowedMethods": ["GET"],
    "AllowedOrigins": ["YOUR STRAPI URL"],
    "ExposeHeaders": [],
    "MaxAgeSeconds": 3000
  }
]

安全中间件配置

默认的 Strapi 安全中间件设置会阻止媒体库中 S3 缩略图预览。修改 contentSecurityPolicy 设置以允许从你的 S3 存储桶加载媒体(详见中间件配置):

module.exports = [
  // ...
  {
    name: 'strapi::security',
    config: {
      // highlight-start
      contentSecurityPolicy: {
        useDefaults: true,
        directives: {
          'connect-src': ["'self'", 'https:'],
          'img-src': [
            "'self'",
            'data:',
            'blob:',
            'market-assets.strapi.io',
            'yourBucketName.s3.yourRegion.amazonaws.com',
          ],
          'media-src': [
            "'self'",
            'data:',
            'blob:',
            'market-assets.strapi.io',
            'yourBucketName.s3.yourRegion.amazonaws.com',
          ],
          upgradeInsecureRequests: null,
        },
      },
      // highlight-end
    },
  },
  // ...
];

如果你的存储桶名称包含点且 forcePathStyle 为 false,S3 使用目录风格 URL,例如:

s3.yourRegion.amazonaws.com/your.bucket.name/image.jpg

在这种情况下,对 img-src 和 media-src 指令使用 s3.yourRegion.amazonaws.com(不带存储桶名称)。

兼容 S3 的服务

AWS S3 提供方通过使用 endpoint 选项与兼容 S3 的服务配合工作。当兼容 S3 的提供方在其上传响应中返回有效 URL 时,提供方按原样信任该 URL,为提供方保留正确的格式(虚拟托管风格或路径风格)。对于在分块上传响应中返回格式错误 URL 的提供方(例如 IONOS、某些 MinIO 配置),提供方会回退到从 endpoint 配置构建 URL。

某些提供方需要在 s3Options 中设置 forcePathStyle: true。当提供方不支持虚拟托管风格 URL(例如 bucket.endpoint.com),而是使用路径风格 URL(例如 endpoint.com/bucket)时,需要此选项。

下表显示了每个提供方的兼容性设置:

ProviderforcePathStyleACLNotes
IONOStrue支持格式错误的分块 Location 通过 endpoint 回退自动修复
MinIOtrue支持某些配置可能返回格式错误的 Location;通过 endpoint 回退自动修复
Contabotrue支持-
Hetznertrue支持-
DigitalOcean Spaces不需要支持返回正确的虚拟托管风格 URL
Wasabi不需要支持-
Scaleway不需要支持返回正确的虚拟托管风格 URL
Vultr不需要支持-
Backblaze B2不需要支持返回正确的虚拟托管风格 URL
Cloudflare R2不需要不支持从参数中省略 ACL
Tigris不需要支持单一全局端点;使用 region: 'auto'

Scaleway

module.exports = ({ env }) => ({
  // ...
  upload: {
    config: {
      provider: 'aws-s3',
      providerOptions: {
        // highlight-start
        s3Options: {
          credentials: {
            accessKeyId: env('SCALEWAY_ACCESS_KEY_ID'),
            secretAccessKey: env('SCALEWAY_ACCESS_SECRET'),
          },
          region: env('SCALEWAY_REGION'), // 例如 "fr-par"
          endpoint: env('SCALEWAY_ENDPOINT'), // 例如 "https://s3.fr-par.scw.cloud"
          params: {
            Bucket: env('SCALEWAY_BUCKET'),
          },
        },
        // highlight-end
      },
    },
  },
  // ...
});

IONOS / MinIO / Contabo

这些提供方需要 forcePathStyle: true,因为它们使用路径风格 URL 而不是虚拟托管风格 URL。

module.exports = ({ env }) => ({
  upload: {
    config: {
      provider: 'aws-s3',
      providerOptions: {
        // highlight-start
        s3Options: {
          credentials: {
            accessKeyId: env('S3_ACCESS_KEY_ID'),
            secretAccessKey: env('S3_ACCESS_SECRET'),
          },
          region: env('S3_REGION'),
          endpoint: env('S3_ENDPOINT'),
          forcePathStyle: true, // 这些提供方需要
          params: {
            Bucket: env('S3_BUCKET'),
          },
        },
        // highlight-end
      },
    },
  },
});

Cloudflare R2

Cloudflare R2 不支持 ACL。

module.exports = ({ env }) => ({
  upload: {
    config: {
      provider: 'aws-s3',
      providerOptions: {
        // highlight-start
        s3Options: {
          credentials: {
            accessKeyId: env('R2_ACCESS_KEY_ID'),
            secretAccessKey: env('R2_ACCESS_SECRET'),
          },
          region: 'auto',
          endpoint: env('R2_ENDPOINT'), // 例如 "https://<account-id>.r2.cloudflarestorage.com"
          params: {
            Bucket: env('R2_BUCKET'),
            // 不要设置 ACL - R2 不支持 ACL
          },
        },
        // highlight-end
      },
    },
  },
});

Tigris

Tigris 是全局分布的,并从单一端点为所有存储桶提供服务,因此 region 始终设为 auto。

module.exports = ({ env }) => ({
  upload: {
    config: {
      provider: 'aws-s3',
      providerOptions: {
        // highlight-start
        s3Options: {
          credentials: {
            accessKeyId: env('TIGRIS_ACCESS_KEY_ID'),
            secretAccessKey: env('TIGRIS_ACCESS_SECRET'),
          },
          region: 'auto',
          endpoint: 'https://t3.storage.dev',
          params: {
            Bucket: env('TIGRIS_BUCKET'),
          },
        },
        // highlight-end
      },
    },
  },
});

Tigris 存储桶还支持 快照和分支,以便在不复制数据的情况下暂存或回滚资源。

扩展提供方选项

providerOptions 内的 providerConfig 选项提供了用于数据完整性、安全性和成本优化的附加功能。

校验和验证

启用自动校验和计算以确保上传期间的数据完整性。SDK 在客户端计算校验和,S3 在服务器端验证校验和。

providerOptions: {
  s3Options: { /* ... */ },
  providerConfig: {
    checksumAlgorithm: 'CRC64NVME', // 选项:'CRC32'、'CRC32C'、'SHA1'、'SHA256'、'CRC64NVME'
  },
},

CRC64NVME 推荐用于现代硬件上的最佳性能。

条件写入(防止覆盖)

通过启用条件写入,防止由于竞态条件导致的意外文件覆盖。启用后,如果已存在具有相同键的对象,上传将失败。

providerConfig: {
  preventOverwrite: true,
},

存储类(仅 AWS S3)

通过为上传的对象指定存储类来优化存储成本。对不常访问的数据使用较低成本的类。

NOTE

存储类是 AWS S3 特有的。其他兼容 S3 的提供方(MinIO、DigitalOcean Spaces、IONOS、Wasabi)会忽略此设置。

providerConfig: {
  storageClass: 'INTELLIGENT_TIERING', // 自动优化成本
},

可用的存储类:

  • STANDARD - 频繁访问的数据(默认)
  • INTELLIGENT_TIERING - 自动成本优化
  • STANDARD_IA - 不常访问的数据
  • ONEZONE_IA - 不常访问,单可用区
  • GLACIER - 归档存储
  • DEEP_ARCHIVE - 长期归档
  • GLACIER_IR - Glacier 即时检索

服务器端加密

为满足合规要求(GDPR、HIPAA 等)配置服务器端加密。

providerConfig: {
  encryption: {
    type: 'AES256', // S3 托管加密
  },
},

对于 KMS 托管加密(仅 AWS S3):

providerConfig: {
  encryption: {
    type: 'aws:kms',
    kmsKeyId: env('AWS_KMS_KEY_ID'),
  },
},

可用的加密类型:

  • AES256 - S3 托管密钥(SSE-S3)——大多数兼容 S3 的提供方支持
  • aws:kms - AWS KMS 托管密钥(SSE-KMS)——仅 AWS S3
  • aws:kms:dsse - 使用 KMS 的双层 SSE——仅 AWS S3

对象标签

对上传的对象应用标签,用于成本分配、生命周期策略和组织。

providerConfig: {
  tags: {
    project: 'website',
    environment: 'production',
    team: 'backend',
  },
},

分块上传配置

为大文件配置分块上传行为。

providerConfig: {
  multipart: {
    partSize: 10 * 1024 * 1024, // 每块 10MB
    queueSize: 4,               // 并行上传数量
    leavePartsOnError: false,   // 失败时清理
  },
},

完整配置示例

module.exports = ({ env }) => ({
  upload: {
    config: {
      provider: 'aws-s3',
      providerOptions: {
        baseUrl: env('CDN_URL'),
        rootPath: env('CDN_ROOT_PATH'),
        s3Options: {
          credentials: {
            accessKeyId: env('AWS_ACCESS_KEY_ID'),
            secretAccessKey: env('AWS_ACCESS_SECRET'),
          },
          region: env('AWS_REGION'),
          params: {
            ACL: 'private',
            signedUrlExpires: 15 * 60,
            Bucket: env('AWS_BUCKET'),
          },
        },
        providerConfig: {
          checksumAlgorithm: 'CRC64NVME',
          preventOverwrite: true,
          storageClass: 'INTELLIGENT_TIERING',
          encryption: {
            type: 'aws:kms',
            kmsKeyId: env('AWS_KMS_KEY_ID'),
          },
          tags: {
            application: 'strapi',
            environment: env('NODE_ENV'),
          },
          multipart: {
            partSize: 10 * 1024 * 1024,
            queueSize: 4,
          },
        },
      },
    },
  },
});

自定义提供方覆盖(私有 S3 提供方) {#private-aws-s3-provider}

对于大多数私有存储桶用例,在提供方配置中设置 ACL: 'private'(参见私有存储桶和签名 URL)就足够了。提供方会自动处理 URL 签名。

但是,如果你需要对签名逻辑进行完全控制,可以在本地覆盖提供方。这可用于为每个文件设置自定义过期时间、条件访问规则,或与外部授权服务集成。

要创建自定义的 aws-s3 提供方覆盖:

  1. 在你的应用中创建一个 /providers/aws-s3 文件夹(更多信息请参阅本地提供方)。
  2. 在 aws-s3 提供方中实现 isPrivate() 方法以返回 true。
  3. 在 aws-s3 提供方中实现 getSignedUrl(file) 方法以为给定文件生成签名 URL。

JavaScript

module.exports = {
  init: (config) => {
    const s3 = new AWS.S3(config);

    return {
      async upload(file) {
        // code to upload file to S3
      },

      async delete(file) {
        // code to delete file from S3
      },

      async isPrivate() {
        return true;
      },

      async getSignedUrl(file) {
        const params = {
          Bucket: config.params.Bucket,
          Key: file.path,
          Expires: 60, // URL expiration time in seconds
        };

        const signedUrl = await s3.getSignedUrlPromise("getObject", params);
        return { url: signedUrl };
      },
    };
  },
};

TypeScript

export = {
  init: (config) => {
    const s3 = new AWS.S3(config);

    return {
      async upload(file) {
        // code to upload file to S3
      },

      async delete(file) {
        // code to delete file from S3
      },

      async isPrivate() {
        return true;
      },

      async getSignedUrl(file) {
        const params = {
          Bucket: config.params.Bucket,
          Key: file.path,
          Expires: 60, // URL expiration time in seconds
        };

        const signedUrl = await s3.getSignedUrlPromise("getObject", params);
        return { url: signedUrl };
      },
    };
  },
};