
你是否在使用Strapi对接阿里云OSS私有链接时,遇到过添加在富文本里的图片发布30分钟后过期无法访问的问题?本文将提供完整的解决方案,包括历史数据迁移和插件修复!
问题全景分析:为什么私有链接会导致图片过期?
公司项目用到了strapi,为了节省服务器的硬盘,所以使用strapi-provider-upload-oss
插件对接了alioss,将图片放在了云盘上,并且为了安全问题,使用的是私有权限,当时做测试是一切正常的,而且这个项目使用人数不太多,所以一时间也没发现问题,不料半年后点开一些旧新闻发现里面的图片都过期了,研究了一下发现是oss图片机制导致的问题。单独访问图片oss插件会实时的拼接图片访问地址,所以每次地址都是最新的30分钟有效期是没问题的,但是如果将图片加载到富文本时,在添加的那一刻图片链接就固定了,所以30分钟后再打开新闻就会发现新闻过期了。除此之外,随着一些历史数据的产生还需要处理其他的问题,具体如下:
- 30分钟失效问题:OSS私有链接默认30分钟后过期,导致富文本中的图片无法加载
- 历史图片无法访问:之前上传到OSS的图片都是私有权限
- 数据库链接需要更新:Strapi数据库存储的是带签名的临时URL
- 富文本内容需要替换:Strapi富文本字段中引用的图片URL也需要更新
- 插件漏洞 :
strapi-provider-upload-oss
中的publicRead
配置不生效,需要手动修复
下面将一步步记录解决所有这些问题的方案。
第一步:OSS中历史上传的图片修改权限 - 从私有到公有
1. 创建批量修改OSS文件权限的脚本
首先需要将历史图片从私有改为公有,在/scripts
下创建set-oss-public.js
文件
bash
#下载执行插件
npm install ali-oss
js
// 用于 Node.js 运行,批量修改历史产生的文件为公共读 执行命令:node scripts/set-oss-public.js
const OSS = require("ali-oss");
require("dotenv").config(); // 如果你用 .env 加载配置
// 👉 修改为你的 OSS 配置
const client = new OSS({
region: process.env.REGION, // required
bucket: process.env.BUCKET, // required
accessKeyId: process.env.ACCESS_KEY_ID, // required
accessKeySecret: process.env.ACCESS_KEY_SECRET, // required
});
const targetPrefix = `${process.env.UPLOAD_PATH}/`; // 指定 OSS 文件夹路径
async function setPublicReadOnTopLevelFiles() {
try {
let nextMarker = null;
let count = 0;
do {
const result = await client.list({
prefix: targetPrefix,
delimiter: "/",
marker: nextMarker,
maxKeys: 1000, // OSS 默认最大值
});
if (result.objects && result.objects.length > 0) {
for (const obj of result.objects) {
const key = obj.name;
if (!key || key.endsWith("/")) continue;
console.log(`🔓 设置公开权限: ${key}`);
await client.putACL(key, "public-read");
count++;
}
}
nextMarker = result.nextMarker;
} while (nextMarker);
console.log(`✅ 总共设置了 ${count} 个一级文件为公开读权限`);
} catch (error) {
console.error("❌ 操作失败:", error);
}
}
setPublicReadOnTopLevelFiles();
2. 执行迁移脚本
bash
# 在本项目终端运行
> node /scripts/set-oss-public.js
第二步:Strapi数据库迁移 - 更新图片链接
1. 创建数据库迁移脚本
bash
#下载执行插件
npm install sqlite3
创建./scripts/fix-formats-url.js
文件:
javascript
const sqlite3 = require('sqlite3').verbose();
const db = new sqlite3.Database('.tmp/data.db');
const BASE = process.env.BSASE_URL;
db.serialize(() => {
db.each(
`SELECT id, url, formats, preview_url, alternative_text FROM files WHERE provider = 'strapi-provider-upload-oss'`,
(err, row) => {
if (err) return console.error(err);
let updatedFields = {};
let updated = false;
// 修复主 url 字段(如果还有漏)
if (row.url && row.url.startsWith('/upload/')) {
const filename = row.url.split('/').pop();
updatedFields.url = BASE + filename;
updated = true;
}
// 修复 preview_url 字段
if (row.preview_url && row.preview_url.startsWith('/upload/')) {
const filename = row.preview_url.split('/').pop();
updatedFields.preview_url = BASE + filename;
updated = true;
}
// 修复 alternative_text 字段
if (row.alternative_text && row.alternative_text.startsWith('/upload/')) {
const filename = row.alternative_text.split('/').pop();
updatedFields.alternative_text = BASE + filename;
updated = true;
}
// 修复 formats 中的 URL
if (row.formats) {
let formats;
try {
formats = JSON.parse(row.formats);
} catch (e) {
console.warn(`⚠️ JSON 解析失败: id=${row.id}`);
return;
}
for (const key in formats) {
if (formats[key]?.url && formats[key].url.startsWith('/upload/')) {
const filename = formats[key].url.split('/').pop();
formats[key].url = BASE + filename;
updated = true;
}
}
if (updated) {
updatedFields.formats = JSON.stringify(formats);
}
}
if (updated) {
const fields = Object.keys(updatedFields);
const values = Object.values(updatedFields);
const placeholders = fields.map(f => `${f} = ?`).join(', ');
values.push(row.id);
db.run(
`UPDATE files SET ${placeholders} WHERE id = ?`,
values,
(e) => {
if (e) console.error(`❌ 更新失败: id=${row.id}`, e);
}
);
}
},
() => {
console.log('✅ 已完成所有字段 OSS URL 修正');
}
);
});
2. 执行迁移脚本
bash
# 在本项目终端运行
> node /scripts/fix-formats-url.js
第三步:Strapi数据库迁移 - 更新富文本中的图片地址
1. 创建数据库迁移脚本
创建./scripts/fix-signed-urls-in-richtext.js
文件:
javascript
const sqlite3 = require("sqlite3").verbose();
const db = new sqlite3.Database(".tmp/data.db");
const TABLE = "news"; // 内容表名,根据你的情况可能是 `news`, `announcements`, `articles` 等
const FIELD = "content";
const ossBase = process.env.BSASE_URL;
db.serialize(() => {
db.each(`SELECT id, ${FIELD} FROM ${TABLE}`, (err, row) => {
if (err) return console.error(err);
const original = row[FIELD];
if (!original) return;
**// 正则替换 OSS 签名参数 注意此处your_regular需要!!!更换为现有地址路径的正则!!!**
const cleaned = original.replace(your_regular,
(matched) => {
return matched.split("?")[0]; // 移除 ? 后面的签名部分
}
);
if (original !== cleaned) {
db.run(
`UPDATE ${TABLE} SET ${FIELD} = ? WHERE id = ?`,
[cleaned, row.id],
(err) => {
if (err) {
console.error(`❌ 更新失败: id=${row.id}`, err);
} else {
console.log(`✅ 已更新富文本内容: id=${row.id}`);
}
}
);
}
});
});
2. 执行迁移脚本
bash
# 在本项目终端运行
> node /scripts/fix-signed-urls-in-richtext.js
第三步:修复插件漏洞 - 添加请求头
问题分析
strapi-provider-upload-oss
插件存在一个已知问题:即使设置了publicRead: true
,文件上传后仍然是私有权限。这是因为插件没有正确设置ACL请求头。
解决方案:Patch插件源码
1. 创建补丁文件
在项目根目录创建patches/strapi-provider-upload-oss+0.2.1.patch
:
diff
diff --git a/node_modules/strapi-provider-upload-oss/lib/index.js b/node_modules/strapi-provider-upload-oss/lib/index.js
index d0908c7..4146727 100644
--- a/node_modules/strapi-provider-upload-oss/lib/index.js
+++ b/node_modules/strapi-provider-upload-oss/lib/index.js
@@ -44,7 +44,15 @@ module.exports = {
const fileName = `${file.hash}${file.ext}`;
const fullPath = `${path}${fileName}`;
- ossClient.put(fullPath, file.stream || Buffer.from(file.buffer, 'binary'), customParams)
+ const defaultHeaders = {
+ headers: {
+ 'x-oss-object-acl': config.bucketParams?.ACL || 'public-read',
+ }
+ };
+ ossClient.put(fullPath, file.stream || Buffer.from(file.buffer, 'binary'), {
+ ...defaultHeaders,
+ ...customParams,
+ })
.then((result) => {
if (config.baseUrl) {
// use http protocol by default, but you can configure it as https protocol
2. 安装patch-package
bash
npm install patch-package --save-dev
3. 应用补丁
在package.json
中添加:
json
"scripts": {
"postinstall": "patch-package"
}
然后运行:
bash
npx patch-package strapi-provider-upload-oss
后续不管是使用yarn 还是使用npm下载安装包时,都会自动的匹配修改新的安装包里的内容:

第四步:配置Strapi插件 - 正确设置OSS
更新插件配置
在./config/plugins.js
中:
javascript
module.exports = () => ({
tinymce: {
enabled: true,
language: "zh_CN", //注意大小写
},
transformer: {
enabled: true,
config: {
prefix: "/api/",
responseTransforms: {
removeAttributesKey: true,
removeDataKey: true,
},
},
},
upload: {
config: {
provider: "strapi-provider-upload-oss", // full package name is required
providerOptions: {
accessKeyId: process.env.ACCESS_KEY_ID, // required
accessKeySecret: process.env.ACCESS_KEY_SECRET, // required
region: process.env.REGION, // required
bucket: process.env.BUCKET, // required
uploadPath: process.env.UPLOAD_PATH,
baseUrl: process.env.BASE_URL,
timeout: process.env.TIMEOUT,
secure: process.env.OSS_SECURE,
internal: process.env.OSS_INTERNAL,
// bucketParams: {
// ACL: "private", // default is 'public-read'
// signedUrlExpires: 60 * 60, // default is 30 * 60 (30min)
// },
bucketParams: {
ACL: "public-read",
},
},
},
},
});
});
第五步:验证解决方案 - 确保一切正常
测试步骤
- 上传新图片:在Strapi内容管理器中上传新图片
- 检查OSS权限 :确认OSS中文件ACL为
public-read
- 检查URL格式 :URL应为无签名格式:
https://bucket.oss-cn-region.aliyuncs.com/path/to/image.jpg
- 富文本测试:在富文本编辑器中插入图片并保存查看图片链接是否为公开访问链接
- 延时测试:等待30分钟后检查图片是否仍可访问
最佳实践与注意事项
-
定期备份:在修改数据库前务必备份
bash# 备份数据库 strapi backup:run # 备份上传目录 tar -czvf uploads-backup.tar.gz public/uploads/
-
增量迁移:对于大型系统,分批处理数据
javascript// 在迁移脚本中添加分页处理 const pageSize = 100; let page = 1; let hasMore = true; while (hasMore) { const files = await strapi.query('plugin::upload.file').findMany({ where: { provider: 'oss' }, limit: pageSize, offset: (page - 1) * pageSize }); if (files.length === 0) { hasMore = false; break; } // 处理当前页... page++; }
总结:完整解决方案流程图
通过以上完整方案,你不仅能解决当前图片过期问题,还能预防未来可能出现的问题。这个方案已在多个生产环境中验证,处理过数百万张图片的迁移工作。
最后提示:在进行任何数据库操作前,请务必在测试环境验证方案并且做好备份!如果遇到任何问题,欢迎在评论区留言讨论。