You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

部署Node.js AWS S3应用:解决临时凭证过期致图片请求失败问题

问题描述

(说明:这不是EC2实例,仅作信息补充)
我开发了一个Node.js应用,通过app.use("/images", s3Proxy({...}))处理图片请求,图片会被上传至AWS S3存储桶并对外提供访问服务。我使用multer-s3将图片上传至该存储桶,这两个工具都需要使用凭证(access_key、secret_access_key和session_token)进行身份验证。

问题在于这些凭证是临时凭证,部署应用后凭证会过期,导致应用无法再处理图片请求,请问该如何解决这个问题?

const { v4: uuidv4 } = require("uuid");
require("dotenv").config();

const { S3Client } = require("@aws-sdk/client-s3");
const multerS3 = require("multer-s3");

const express = require("express");
const mongoose = require("mongoose");
const bodyParser = require("body-parser");
const multer = require("multer");

const cors = require("cors");

const { graphqlHTTP } = require("express-graphql");
const schema = require("./util/schema");
const resolver = require("./util/resolver");
const isAuth = require("./middleware/isAuth");
const s3Proxy = require("s3-proxy");

const app = express();

const s3 = new S3Client({
  credentials: {
    accessKeyId: process.env.AWS_ACCESS_KEY_ID,
    secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
    sessionToken: process.env.AWS_SESSION_TOKEN,
  },
  region: "eu-central-1",
});

app.options("*", cors());
app.use(cors());

app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());


const upload = multer({
  storage: multerS3({
    s3: s3,
    bucket: "express-app-recipe",
    metadata: function (req, file, cb) {
      cb(null, { fieldName: file.fieldname });
    },
    key: function (req, file, cb) {
      cb(null, uuidv4());
    },
  }),
});

app.use(
  "/images/",
  s3Proxy({
    bucket: "express-app-recipe",
    accessKeyId: process.env.AWS_ACCESS_KEY_ID,
    secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
    sessionToken: process.env.AWS_SESSION_TOKEN,
  })
);

app.use(isAuth);

app.post("/post-image", upload.single("image"), (req, res, next) => {
  if (!req.isAuth) {
    const error = new Error("Not authenticated");
    error.statusCode = 401;
    throw error;
  }

  if (!req.file) {
    const error = new Error("No file attached!");
    error.statusCode = 400;
    throw error;
  }


  return res.status(201).send({
    message: "File recieved",
    filePath: req.file.key,
  });
});

app.use(
  "/graphql",
  graphqlHTTP({
    schema: schema,
    rootValue: resolver,
    graphiql: true,
    customFormatErrorFn(err) {
      if (!err.originalError) {
        return err;
      }
      const data = err.originalError.data; // naš seznam izjem
      const message = err.message || "An error occured";
      const code = err.originalError.code;

      return { message: message, status: code, data: data };
    },
  })
);

app.use((error, req, res, next) => {
  console.log(error);
  const status = error.statusCode;
  const data = error.data;
  const message = error.message;
  res.status(status).json({ message: message, data: data });
});

mongoose
  .connect(process.env.DB_CONN)
  .then((result) => {
    app.listen(process.env.PORT);
  })
  .catch((err) => console.log(err));
解决方案

1. 使用AWS SDK默认凭证链(推荐)

AWS SDK for JavaScript v3(你使用的@aws-sdk/client-s3属于该版本)支持默认凭证链,会自动按优先级查找凭证:

  • 环境变量
  • 本地AWS配置文件(~/.aws/credentials)
  • 绑定到AWS服务的IAM角色(如ECS、EKS、Lambda等,SDK会自动获取并刷新临时凭证)
  • 容器凭证(ECS任务角色)

修改代码,移除硬编码的凭证配置,让SDK自动处理:

// 初始化S3Client时移除credentials配置
const s3 = new S3Client({
  region: "eu-central-1"
});

// 修改s3-proxy配置,移除凭证参数
app.use(
  "/images/",
  s3Proxy({
    bucket: "express-app-recipe",
    region: "eu-central-1"
  })
);

如果应用部署在AWS托管服务上,直接给服务分配具备S3读写权限的IAM角色,SDK会自动维护临时凭证的刷新,无需手动管理。

2. 实现凭证自动刷新逻辑(非AWS环境适用)

若必须使用临时凭证,可通过AWS STS服务定时获取并更新凭证:

  • 使用@aws-sdk/client-sts调用AssumeRole接口获取临时凭证
  • 设置定时器,在凭证过期前(如提前5分钟)重新获取并更新S3Client、multer-s3和s3-proxy的配置

简化示例代码:

const { STSClient, AssumeRoleCommand } = require("@aws-sdk/client-sts");

let s3Client;
let upload;
let s3ProxyMiddleware;

async function refreshCredentials() {
  const stsClient = new STSClient({
    credentials: {
      accessKeyId: process.env.AWS_LONG_TERM_ACCESS_KEY,
      secretAccessKey: process.env.AWS_LONG_TERM_SECRET_KEY
    },
    region: "eu-central-1"
  });

  const command = new AssumeRoleCommand({
    RoleArn: "arn:aws:iam::ACCOUNT_ID:role/YOUR_ROLE",
    RoleSessionName: "node-app-session"
  });

  const response = await stsClient.send(command);
  const credentials = response.Credentials;

  // 更新S3Client
  s3Client = new S3Client({
    credentials: {
      accessKeyId: credentials.AccessKeyId,
      secretAccessKey: credentials.SecretAccessKey,
      sessionToken: credentials.SessionToken
    },
    region: "eu-central-1"
  });

  // 更新multer-s3配置
  upload = multer({
    storage: multerS3({
      s3: s3Client,
      bucket: "express-app-recipe",
      metadata: (req, file, cb) => cb(null, { fieldName: file.fieldname }),
      key: (req, file, cb) => cb(null, uuidv4()),
    }),
  });

  // 更新s3-proxy中间件
  s3ProxyMiddleware = s3Proxy({
    bucket: "express-app-recipe",
    accessKeyId: credentials.AccessKeyId,
    secretAccessKey: credentials.SecretAccessKey,
    sessionToken: credentials.SessionToken,
    region: "eu-central-1"
  });

  // 设置下一次刷新时间(过期前5分钟)
  const refreshTime = credentials.Expiration.getTime() - Date.now() - 5 * 60 * 1000;
  setTimeout(refreshCredentials, refreshTime);
}

// 初始化凭证并启动应用
refreshCredentials().then(() => {
  app.post("/post-image", upload.single("image"), (req, res, next) => {
    // 原接口逻辑
  });

  app.use("/images/", (req, res, next) => s3ProxyMiddleware(req, res, next));

  mongoose.connect(process.env.DB_CONN)
    .then(() => app.listen(process.env.PORT))
    .catch(err => console.log(err));
});

3. 使用S3预签名URL替代s3-proxy

无需通过应用代理图片请求,上传成功后生成预签名URL返回给前端,前端直接访问S3:

const { GetObjectCommand } = require("@aws-sdk/client-s3");
const { getSignedUrl } = require("@aws-sdk/s3-request-presigner");

app.post("/post-image", upload.single("image"), async (req, res, next) => {
  if (!req.isAuth) {
    const error = new Error("Not authenticated");
    error.statusCode = 401;
    throw error;
  }

  if (!req.file) {
    const error = new Error("No file attached!");
    error.statusCode = 400;
    throw error;
  }

  // 生成有效期1小时的预签名URL
  const command = new GetObjectCommand({
    Bucket: "express-app-recipe",
    Key: req.file.key
  });
  const signedUrl = await getSignedUrl(s3Client, command, { expiresIn: 3600 });

  return res.status(201).send({
    message: "File received",
    filePath: signedUrl
  });
});

此方式减少应用负担,同时避免s3-proxy长期持有凭证的问题,上传部分仍可结合默认凭证链自动刷新。


内容的提问来源于stack exchange,提问作者Python_Slayer

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.13 10:25:57