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

使用connect-mongo时,如何正确用async-await调用AWS Secrets Manager?

问题:AWS Secrets Manager获取凭证后配置connect-mongo的优雅实现

我用AWS Secrets Manager存储MongoDB Atlas的账号密码,数据库连接功能正常,但在使用connect-mongo实现会话存储时遇到问题:直接写死MongoDB URL可正常运行,但通过async/await从Secrets Manager获取凭证后,将async函数返回的Promise直接赋值给dbUrl,导致报错TypeError: connectionString.startsWith is not a function——原因是connect-mongo需要字符串类型的连接地址,而非Promise对象。

后续将所有业务代码塞进.then()回调后程序可正常运行,但代码结构臃肿,现寻求更优雅的实现方式。


相关代码与日志

retrieveSecrets.js

const AWS = require("aws-sdk");

module.exports = () => {
    // 配置AWS SDK
    const region = "[MY_REGION]";
    const client = new AWS.SecretsManager({ region });
    const SecretId = "[MY_SECRET_ID]";

    return new Promise((resolve, reject) => {
        // 从Secrets Manager获取凭证
        client.getSecretValue({ SecretId }, (err, data) => {
            if (err) {
                reject(err);
            } else {
                // 将获取的数据解析为JSON
                const secretsJSON = JSON.parse(data.SecretString);
                let secretsString = "";
                Object.keys(secretsJSON).forEach((key) => {
                    secretsString += `${key}=${secretsJSON[key]}\n`;
                });
                resolve(secretsString);
            }
        });
    });
};

原app.js(引发错误的片段)

const express = require('express');
const session = require('express-session');
const MongoDBStore = require('connect-mongo');
const app = express();
const retrieveSecrets = require("./retrieveSecrets");

const dbAddress = "[MY_DB_ADDRESS]"
const dbName = "[MY_DB_NAME]"

const getsecret = (async function () {
    try {
        const secretsString = await retrieveSecrets();
        const secretsArray = secretsString.split("="); // 此处无需await,split为同步方法
        return secretsArray;
    } catch (error) {
        // 记录错误并终止程序
        console.log("Error in setting environment variables", error);
        process.exit(-1);
    }
});

const connectMongoose = (async function () {
    const secretsArray = await getsecret();
    let url = `mongodb+srv://${secretsArray[0]}:${secretsArray[1]}@${dbAddress}/${dbName}`

    mongoose.connect(url);

});
connectMongoose();

const db = mongoose.connection;
db.on("error", console.error.bind(console, "连接错误:"));
db.once("open", () => {
    console.log("数据库已连接");
});

// 以上代码运行正常(通过Secrets Manager的账号密码连接MongoDB)

// *** 引发问题的代码 ***
const getUrl = (async function () {
    const secretsArray = await getsecret();
    let url = `mongodb+srv://${secretsArray[0]}:${secretsArray[1]}@${dbAddress}/${dbName}`
    return url;
});
const dbUrl = getUrl(); // 此处dbUrl是Promise对象,而非字符串

// 若直接设置URL如下,则运行正常
// const dbUrl = 'mongodb+srv://[MY_ID]:[MY_PASS]@[MY_DB_ADDRESS]/[MY_DB_NAME]';

const store = MongoDBStore.create({
    mongoUrl: dbUrl,
    touchAfter: 24 * 60 * 60,
    crypto: {
        secret: '[MY_SECRET]'
    }
});

store.on("error", function(e) {
    console.log("会话存储错误: ", e)
});

const sessionConfig = {
    store,
    name: 'session',
    secret: '[MY_SECRET]',
    resave: false,
    saveUninitialized: true,
    cookie: {
        httpOnly: true,
        expires: Date.now() + 1000 * 60 * 60 * 24 * 7,
        maxAge: 1000 * 60 * 60 * 24 * 7
    }
}
app.use(session(sessionConfig));

错误日志

$ nodemon app.js
[nodemon] 2.0.15
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): *.*
[nodemon] watching extensions: js,mjs,json
[nodemon] starting `node app.js`
Serving on port 3000
/home/ec2-user/dev/node_modules/mongodb-connection-string-url/lib/index.js:9
    return (connectionString.startsWith('mongodb://') ||
                             ^

TypeError: connectionString.startsWith is not a function
    at connectionStringHasValidScheme (/home/ec2-user/dev/node_modules/mongodb-connection-string-url/lib/index.js:9:30)
    at new ConnectionString (/home/ec2-user/dev/node_modules/mongodb-connection-string-url/lib/index.js:85:34)
    at parseOptions (/home/ec2-user/dev/node_modules/mongodb/lib/connection_string.js:217:17)
    at new MongoClient (/home/ec2-user/dev/node_modules/mongodb/lib/mongo_client.js:62:63)
    at Function.connect (/home/ec2-user/dev/node_modules/mongodb/lib/mongo_client.js:184:33)
    at new MongoStore (/home/ec2-user/dev/node_modules/connect-mongo/build/main/lib/MongoStore.js:110:46)
    at Function.create (/home/ec2-user/dev/node_modules/connect-mongo/build/main/lib/MongoStore.js:136:16)
    at Object.<anonymous> (/home/ec2-user/dev/app.js:103:28)
    at Module._compile (node:internal/modules/cjs/loader:1099:14)
    at Object.Module._extensions..js (node:internal/modules/cjs/loader:1153:10)

Node.js v17.7.2
[nodemon] app crashed - waiting for file changes before starting...

更新后可运行但结构臃肿的app.js片段

const express = require('express');
const session = require('express-session');
const MongoDBStore = require('connect-mongo');
const app = express();
const retrieveSecrets = require("./retrieveSecrets");

const dbAddress = "[MY_DB_ADDRESS]"
const dbName = "[MY_DB_NAME]"

const getsecret = (async function () {
    try {
        const secretsString = await retrieveSecrets();
        const secretsArray = secretsString.split("=");
        return secretsArray;
    } catch (error) {
        // 记录错误并终止程序
        console.log("Error in setting environment variables", error);
        process.exit(-1);
    }
});

const connectMongoose = (async function () {
    const secretsArray = await getsecret();
    let url = `mongodb+srv://${secretsArray[0]}:${secretsArray[1]}@${dbAddress}/${dbName}`

    mongoose.connect(url);

});
connectMongoose();

const db = mongoose.connection;
db.on("error", console.error.bind(console, "连接错误:"));
db.once("open", () => {
    console.log("数据库已连接");
});

// 以上代码运行正常(通过Secrets Manager的账号密码连接MongoDB)

const getUrl = (async function () {
    const secretsArray = await getsecret();
    let url = `mongodb+srv://${secretsArray[0]}:${secretsArray[1]}@${dbAddress}/${dbName}`
    return url;
});

// *** 所有业务代码塞进then回调,结构臃肿 ***
const url = getUrl();
url.then((dbUrl) => {
    const store = MongoDBStore.create({
        mongoUrl: dbUrl,
        touchAfter: 24 * 60 * 60,
        crypto: {
            secret: dbSecret
        }
    });

    store.on("error", function(e) {
        console.log("会话存储错误: ", e)
    });
    
    const sessionConfig = {
        store,
        name: 'session',
        secret: dbSecret,
        resave: false,
        saveUninitialized: true,
        cookie: {
            httpOnly: true,
            expires: Date.now() + 1000 * 60 * 60 * 24 * 7,
            maxAge: 1000 * 60 * 60 * 24 * 7
        }
    }
    app.use(session(sessionConfig));

    app.use(flash());

    app.use(passport.initialize());
    app.use(passport.session());
    passport.use(new LocalStrategy(User.authenticate()));

    passport.serializeUser(User.serializeUser());
    passport.deserializeUser(User.deserializeUser());

    app.use((req, res, next) => {
        res.locals.currentUser = req.user;
        res.locals.success = req.flash('success');
        res.locals.error = req.flash('error');
        next();
    })

    app.use('/', usersRoutes);
    app.use('/abc', abcRoutes);
    app.use('/abc/:id/reviews', reviewRoutes);


    app.get('/', (req, res) => {
        res.render('home')
    });

    app.all('*', (req, res, next) => {
        next(new ExpressError('Page Not Found', 404))
    })

    app.use((err, req, res, next) => {
        const { statusCode = 500 } = err;
        if (!err.message) err.message = 'Something went wrong!';
        res.status(statusCode).render('error', { err });
    })

    app.listen(3000, () => {
        console.log('Serving on port 3000')
    });
});

优雅解决方案

方案1:用初始化函数包裹异步逻辑(兼容全版本Node.js)

将所有异步初始化逻辑封装到单独函数中,避免嵌套回调,保持代码结构清晰:

const express = require('express');
const session = require('express-session');
const MongoDBStore = require('connect-mongo');
const mongoose = require('mongoose');
const flash = require('connect-flash');
const passport = require('passport');
const LocalStrategy = require('passport-local');
const User = require('./models/User');
const retrieveSecrets = require("./retrieveSecrets");
const usersRoutes = require('./routes/users');
const abcRoutes = require('./routes/abc');
const reviewRoutes = require('./routes/reviews');
const ExpressError = require('./utils/ExpressError');

const app = express();
const dbAddress = "[MY_DB_ADDRESS]";
const dbName = "[MY_DB_NAME]";
const dbSecret = "[MY_SECRET]";

// 封装初始化逻辑
async function initApp() {
    try {
        // 1. 获取并解析凭证
        const secretsString = await retrieveSecrets();
        const secretsArray = secretsString.split("=");
        const dbUrl = `mongodb+srv://${secretsArray[0]}:${secretsArray[1]}@${dbAddress}/${dbName}`;

        // 2. 连接MongoDB
        await mongoose.connect(dbUrl);
        const db = mongoose.connection;
        db.on("error", console.error.bind(console, "连接错误:"));
        db.once("open", () => console.log("数据库已连接"));

        // 3. 配置会话存储
        const store = MongoDBStore.create({
            mongoUrl: dbUrl,
            touchAfter: 24 * 60 * 60,
            crypto: { secret: dbSecret }
        });
        store.on("error", e => console.log("会话存储错误: ", e));

        // 4. 配置会话与中间件
        const sessionConfig = {
            store,
            name: 'session',
            secret: dbSecret,
            resave: false,
            saveUninitialized: true,
            cookie: {
                httpOnly: true,
                expires: Date.now() + 1000 * 60 * 60 * 24 * 7,
                maxAge: 1000 * 60 * 60 * 24 * 7
            }
        };
        app.use(session(sessionConfig));

        app.use(flash());
        app.use(passport.initialize());
        app.use(passport.session());
        passport.use(new LocalStrategy(User.authenticate()));
        passport.serializeUser(User.serializeUser());
        passport.deserializeUser(User.deserializeUser());

        // 5. 配置路由与错误处理
        app.use((req, res, next) => {
            res.locals.currentUser = req.user;
            res.locals.success = req.flash('success');
            res.locals.error = req.flash('error');
            next();
        });

        app.use('/', usersRoutes);
        app.use('/abc', abcRoutes);
        app.use('/abc/:id/reviews', reviewRoutes);

        app.get('/', (req, res) => res.render('home'));

        app.all('*', (req, res, next) => next(new ExpressError('Page Not Found', 404)));

        app.use((err, req, res, next) => {
            const { statusCode = 500 } = err;
            if (!err.message) err.message = 'Something went wrong!';
            res.status(statusCode).render('error', { err });
        });

        // 6. 启动服务
        app.listen(3000, () => console.log('Serving on port 3000'));
    } catch (error) {
        console.log("应用初始化失败: ", error);
        process.exit(-1);
    }
}

// 执行初始化
initApp();

方案2:优化凭证解析逻辑(减少冗余操作)

修改retrieveSecrets.js直接返回JSON对象,避免字符串拼接与分割的冗余:

const AWS = require("aws-sdk");

module.exports = async () => {
    const region = "[MY_REGION]";
    const client = new AWS.SecretsManager({ region });
    const SecretId = "[MY_SECRET_ID]";

    try {
        const data = await client.getSecretValue({ SecretId }).promise();
        return JSON.parse(data.SecretString); // 直接返回解析后的JSON对象
    } catch (err) {
        throw err;
    }
};

在initApp中使用:

// 获取凭证
const secrets = await retrieveSecrets();
// 假设Secrets Manager中存储的字段为username和password
const dbUrl = `mongodb+srv://${secrets.username}:${secrets.password}@${dbAddress}/${dbName}`;

关键优化点

  1. 集中异步逻辑:用单独函数封装所有初始化操作,避免嵌套回调,代码结构更清晰。
  2. 简化凭证处理:直接返回JSON格式的凭证,减少字符串操作的冗余与潜在错误。
  3. 统一错误处理:将所有异步操作的错误集中捕获,避免分散的异常处理逻辑。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.20 09:49:14