Express.js使用Azure托管身份无法在令牌过期后刷新访问令牌
问题:Azure托管身份令牌过期后无法刷新,导致Express应用无法连接数据库
我有一个Express.js应用,通过Azure托管身份连接Azure MySQL数据库。server.js文件导入并调用setupConnectionPool函数,可成功获取访问令牌并连接数据库。但24小时令牌过期后,我尝试的所有令牌刷新方案均未奏效。
我试过使用setInterval和setTimeout在令牌过期时重新调用setupConnectionPool,也尝试通过判断accessToken.expiresOnTimestamp < Date.now()来处理,但都没有成功。这是客户项目的最后一个待解决问题,恳请提供解决思路!
const mssql = require('mssql'); require('dotenv').config(); const { DefaultAzureCredential } = require("@azure/identity"); async function setupConnectionPool() { try { console.log(process.env.AZURE_DEV_MI_CLIENT_ID) const credential = new DefaultAzureCredential({ managedIdentityClientId: process.env.AZURE_DEV_MI_CLIENT_ID, }); console.log('credentialll', credential) let accessToken = await credential.getToken("https://database.windows.net/.default"); console.log('access token', accessToken) const refreshTime = accessToken.expiresOnTimestamp - Date.now(); console.log('refreshTime', refreshTime) const refreshToken = async () => { if(pool) { console.log('close pool') await pool.close(); } console.log('refresh token func call') try { console.log('try setupconnectionpool') await setupConnectionPool(); } catch(e) { console.log('catch e', e) } } setTimeout(() => { console.log('Get refresh token - 10000'); refreshToken(); }, (refreshTime - 10000)) const config = { user: "", password: "", server: process.env.DB_SERVER, authentication: { type: "azure-active-directory-access-token", options: { token: accessToken.token, }, }, options: { database: process.env.DB_NAME, encrypt: true, port: 1433, }, }; const pool = new mssql.ConnectionPool(config); await pool.connect(); console.log('pool', pool); return pool; } catch (error) { console.error("Error setting up connection pool:", error); throw error; } } module.exports = { setupConnectionPool }
核心问题分析
你的代码存在几个关键问题导致令牌刷新失败:
- 变量作用域错误:
refreshToken函数里引用的pool是setupConnectionPool内部的局部变量,刷新逻辑无法正确访问到当前的连接池实例,导致旧池无法关闭、新池无法替换。 - 实例未全局同步:重新调用
setupConnectionPool生成的新池,没有同步到业务代码使用的实例上,业务仍在使用带过期令牌的旧池。 - 无重试容错机制:刷新过程中如果出错,没有重试逻辑,直接中断刷新流程。
修正方案
1. 全局维护连接池实例
将连接池和身份凭证提升到模块级作用域,确保刷新逻辑能正确访问和替换实例:
const mssql = require('mssql'); require('dotenv').config(); const { DefaultAzureCredential } = require("@azure/identity"); // 模块级变量存储当前连接池和身份凭证 let pool = null; const credential = new DefaultAzureCredential({ managedIdentityClientId: process.env.AZURE_DEV_MI_CLIENT_ID, }); async function setupConnectionPool() { try { const accessToken = await credential.getToken("https://database.windows.net/.default"); console.log('获取新令牌,过期时间:', new Date(accessToken.expiresOnTimestamp)); // 安全关闭旧连接池 if (pool) { try { await pool.close(); console.log('旧连接池已关闭'); } catch (closeErr) { console.error('关闭旧池失败:', closeErr); } } const config = { user: "", password: "", server: process.env.DB_SERVER, authentication: { type: "azure-active-directory-access-token", options: { token: accessToken.token, }, }, options: { database: process.env.DB_NAME, encrypt: true, port: 1433, }, }; // 创建并连接新池 pool = new mssql.ConnectionPool(config); await pool.connect(); console.log('新连接池已建立'); // 提前10分钟刷新令牌,避免过期瞬间的连接失败 const refreshOffset = 10 * 60 * 1000; const refreshTime = accessToken.expiresOnTimestamp - Date.now() - refreshOffset; setTimeout(refreshToken, refreshTime); // 添加连接池异常监听,主动触发刷新 pool.on('error', (err) => { console.error('连接池异常,触发令牌刷新:', err); refreshToken(); }); return pool; } catch (error) { console.error("建立连接池失败:", error); // 刷新失败后5分钟重试 setTimeout(refreshToken, 5 * 60 * 1000); throw error; } } // 独立的刷新函数 async function refreshToken() { console.log('开始刷新令牌...'); try { await setupConnectionPool(); } catch (err) { console.error('令牌刷新失败:', err); } } // 暴露获取当前有效池的方法,业务代码统一用这个获取 function getConnectionPool() { return pool; } module.exports = { setupConnectionPool, getConnectionPool }
2. 业务代码适配修改
server.js中不要直接存储setupConnectionPool的返回值,每次通过getConnectionPool()获取当前有效的连接池:
const { setupConnectionPool, getConnectionPool } = require('./db-connector'); // 初始化数据库连接 async function initDB() { await setupConnectionPool(); } // 业务查询示例 async function fetchData() { const pool = getConnectionPool(); if (!pool) throw new Error('数据库连接池未初始化'); const result = await pool.request().query('SELECT * FROM your_table'); return result.recordset; }
3. 额外优化建议
- 增加详细日志,记录令牌过期时间、刷新触发时间、连接池状态,方便排查问题。
- 可以添加监控告警,当连接池连续刷新失败时,及时通知开发人员。
内容的提问来源于stack exchange,提问作者Mr. K
相关产品推荐
相关产品推荐

