使用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}`;
关键优化点
- 集中异步逻辑:用单独函数封装所有初始化操作,避免嵌套回调,代码结构更清晰。
- 简化凭证处理:直接返回JSON格式的凭证,减少字符串操作的冗余与潜在错误。
- 统一错误处理:将所有异步操作的错误集中捕获,避免分散的异常处理逻辑。
内容的提问来源于stack exchange,提问作者Koki
相关产品推荐
相关产品推荐

