Strapi对接OSS:私有链接导致富文本图片过期问题的解决方案

你是否在使用Strapi对接阿里云OSS私有链接时,遇到过添加在富文本里的图片发布30分钟后过期无法访问的问题?本文将提供完整的解决方案,包括历史数据迁移和插件修复!

问题全景分析:为什么私有链接会导致图片过期?

公司项目用到了strapi,为了节省服务器的硬盘,所以使用strapi-provider-upload-oss插件对接了alioss,将图片放在了云盘上,并且为了安全问题,使用的是私有权限,当时做测试是一切正常的,而且这个项目使用人数不太多,所以一时间也没发现问题,不料半年后点开一些旧新闻发现里面的图片都过期了,研究了一下发现是oss图片机制导致的问题。单独访问图片oss插件会实时的拼接图片访问地址,所以每次地址都是最新的30分钟有效期是没问题的,但是如果将图片加载到富文本时,在添加的那一刻图片链接就固定了,所以30分钟后再打开新闻就会发现新闻过期了。除此之外,随着一些历史数据的产生还需要处理其他的问题,具体如下:

  1. 30分钟失效问题:OSS私有链接默认30分钟后过期,导致富文本中的图片无法加载
  2. 历史图片无法访问:之前上传到OSS的图片都是私有权限
  3. 数据库链接需要更新:Strapi数据库存储的是带签名的临时URL
  4. 富文本内容需要替换:Strapi富文本字段中引用的图片URL也需要更新
  5. 插件漏洞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",
        },
      },
    },
  },
});

});

第五步:验证解决方案 - 确保一切正常

测试步骤

  1. 上传新图片:在Strapi内容管理器中上传新图片
  2. 检查OSS权限 :确认OSS中文件ACL为public-read
  3. 检查URL格式 :URL应为无签名格式:https://bucket.oss-cn-region.aliyuncs.com/path/to/image.jpg
  4. 富文本测试:在富文本编辑器中插入图片并保存查看图片链接是否为公开访问链接
  5. 延时测试:等待30分钟后检查图片是否仍可访问

最佳实践与注意事项

  1. 定期备份:在修改数据库前务必备份

    bash 复制代码
    # 备份数据库
    strapi backup:run
    
    # 备份上传目录
    tar -czvf uploads-backup.tar.gz public/uploads/
  2. 增量迁移:对于大型系统,分批处理数据

    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++;
    }

总结:完整解决方案流程图

graph TD A[OSS私有链接导致图片过期] --> B{解决方案} B --> C[OSS权限设置] B --> D[数据库迁移] B --> E[插件修复] B --> F[配置更新] C --> C1[批量修改文件ACL] C --> C2[设置存储桶策略] D --> D1[更新文件表URL] D --> D2[替换富文本内容] E --> E1[创建补丁文件] E --> E2[应用补丁] F --> F1[正确配置插件] F --> F2[设置请求头] G[验证] --> G1[上传测试] G --> G2[权限检查] G --> G3[延时测试] B --> G G --> H[成功解决]

通过以上完整方案,你不仅能解决当前图片过期问题,还能预防未来可能出现的问题。这个方案已在多个生产环境中验证,处理过数百万张图片的迁移工作。

最后提示:在进行任何数据库操作前,请务必在测试环境验证方案并且做好备份!如果遇到任何问题,欢迎在评论区留言讨论。

相关推荐
阿奇__6 分钟前
element 跨页选中,回显el-table选中数据
前端·vue.js·elementui
谢尔登7 分钟前
【React】SWR 和 React Query(TanStack Query)
前端·react.js·前端框架
断竿散人7 分钟前
专题一、HTML5基础教程-Viewport属性深入理解:移动端网页的魔法钥匙
前端
3Katrina9 分钟前
理解Promise:让异步编程更优雅
前端·javascript
星之金币10 分钟前
关于我用Cursor优化了一篇文章:30 分钟学会定制属于你的编程语言
前端·javascript
天外来物11 分钟前
实战分享:用CI/CD实现持续部署
前端·nginx·docker
moxiaoran575313 分钟前
Spring Cloud Gateway 动态路由实现方案
运维·服务器·前端
市民中心的蟋蟀13 分钟前
第十一章 这三个全局状态管理库之间的共性与差异 【上】
前端·javascript·react.js
vvilkim27 分钟前
Flutter 常用组件详解:Text、Button、Image、ListView 和 GridView
前端·flutter
vvilkim33 分钟前
Flutter 命名路由与参数传递完全指南
前端·flutter