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,默认对 S3 URL 使用 虚拟托管风格 URL。要改用路径风格 URL,请显式设置 baseUrl:
baseUrl: `https://s3.${process.env.AWS_REGION}.amazonaws.com/${process.env.AWS_BUCKET}`,
S3 凭据的位置在 Strapi 各版本之间有所变化:选项必须嵌套在 s3Options 下,凭据应包裹在 credentials 对象中(将它们放在 s3Options 根目录仍然有效但已弃用)。有关完整的迁移信息,请参阅 Amazon S3 提供方凭据必须设置在 s3Options 下。
你可以传递一个 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。
为确保提供方正常工作,你还需要配置 IAM 权限、存储桶 CORS 和 Strapi 安全中间件(参见必要设置)。
私有存储桶和签名 URL
如果你的存储桶配置为私有,请在 params 对象中将 ACL 选项设为 private。这确保文件 URL 被签名。
你可以通过在 params 对象中设置 signedUrlExpires 选项来定义签名 URL 的过期时间。默认值为 15 分钟。
如果你使用 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 中正确显示。为此:
- 在 AWS 控制台中打开你的存储桶。
- 导航到 Permissions 标签页。
- 找到 Cross-origin resource sharing (CORS) 字段。
- 添加以下 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)时,需要此选项。
下表显示了每个提供方的兼容性设置:
| Provider | forcePathStyle | ACL | Notes |
|---|---|---|---|
| IONOS | true | 支持 | 格式错误的分块 Location 通过 endpoint 回退自动修复 |
| MinIO | true | 支持 | 某些配置可能返回格式错误的 Location;通过 endpoint 回退自动修复 |
| Contabo | true | 支持 | - |
| Hetzner | true | 支持 | - |
| 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 选项提供了用于数据完整性、安全性和成本优化的附加功能。
- 为静态数据启用服务器端加密
- 启用校验和验证 以确保上传完整性
- 启用条件写入 以防止竞态条件
- 对敏感内容使用
ACL: 'private'配合签名 URL - 启用 S3 存储桶版本控制 以从意外删除中恢复
校验和验证
启用自动校验和计算以确保上传期间的数据完整性。SDK 在客户端计算校验和,S3 在服务器端验证校验和。
providerOptions: {
s3Options: { /* ... */ },
providerConfig: {
checksumAlgorithm: 'CRC64NVME', // 选项:'CRC32'、'CRC32C'、'SHA1'、'SHA256'、'CRC64NVME'
},
},
CRC64NVME 推荐用于现代硬件上的最佳性能。
条件写入(防止覆盖)
通过启用条件写入,防止由于竞态条件导致的意外文件覆盖。启用后,如果已存在具有相同键的对象,上传将失败。
providerConfig: {
preventOverwrite: true,
},
存储类(仅 AWS S3)
通过为上传的对象指定存储类来优化存储成本。对不常访问的数据使用较低成本的类。
存储类是 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 S3aws: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 提供方覆盖:
- 在你的应用中创建一个
/providers/aws-s3文件夹(更多信息请参阅本地提供方)。 - 在
aws-s3提供方中实现isPrivate()方法以返回true。 - 在
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 };
},
};
},
};