NestJS连接Azure PostgreSQL Flexible Server的Token自动刷新问题
NestJS + TypeORM 连接 Azure PostgreSQL 托管身份认证 Token 过期无法自动刷新问题
我们基于NestJS构建应用,使用TypeORM连接Azure PostgreSQL Flexible Server,采用托管身份无密码认证,通过Entra Token验证身份。应用启动后能正常连接数据库,但Token(有效期24小时)过期后,数据库连接无法自动刷新,导致应用崩溃。尝试用Azure Identity SDK在第23小时手动刷新Token,发现过了24小时仍未生成新Token。
当前核心代码如下:
DatabaseModule 代码
import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { ManagedIdentityCredential } from '@azure/identity'; @Module({ imports: [ TypeOrmModule.forRootAsync({ useFactory: async () => { const credential = new ManagedIdentityCredential(); // 仅在模块初始化时获取一次Token const tokenResponse = await credential.getToken('https://ossrdbms-aad.database.windows.net/.default'); return { type: 'postgres', host: '<YOUR_DATABASE_SERVER_NAME>.postgres.database.azure.com', port: 5432, username: '<YOUR_AAD_USER>@<YOUR_DATABASE_SERVER_NAME>', password: tokenResponse.token, // 固定使用初始化时的Token database: '<YOUR_DATABASE_NAME>', ssl: { rejectUnauthorized: false }, entities: [/* 你的实体类 */], synchronize: true, // 生产环境请设为false }; }, }), ], }) export class DatabaseModule {}
AppModule 代码
import { Module } from '@nestjs/common'; import { DatabaseModule } from './database/database.module'; @Module({ imports: [ DatabaseModule, // 其他模块 ], controllers: [], providers: [], }) export class AppModule {}
问题根源
当前代码的useFactory仅在NestJS模块初始化时执行一次,Token也只获取一次并固定配置到TypeORM连接中。当Token过期后,连接池中的所有连接都会因身份验证失败而无法正常工作,且没有机制自动刷新Token并重建连接。
手动刷新Token无效的原因是:即使外部获取了新Token,TypeORM的连接池已经使用旧Token创建了连接,不会自动更新连接的认证信息。
解决方案
方案1:利用TypeORM连接池的beforeConnect钩子动态获取Token
TypeORM支持在创建每个连接前执行钩子函数,可在这里动态获取最新的Token,确保每个新连接都使用有效凭证:
import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { ManagedIdentityCredential } from '@azure/identity'; @Module({ imports: [ TypeOrmModule.forRootAsync({ useFactory: () => { const credential = new ManagedIdentityCredential(); return { type: 'postgres', host: '<YOUR_DATABASE_SERVER_NAME>.postgres.database.azure.com', port: 5432, username: '<YOUR_AAD_USER>@<YOUR_DATABASE_SERVER_NAME>', database: '<YOUR_DATABASE_NAME>', ssl: { rejectUnauthorized: false }, entities: [/* 你的实体类 */], synchronize: true, // 每次创建连接前获取最新Token beforeConnect: async (connection) => { const tokenResponse = await credential.getToken('https://ossrdbms-aad.database.windows.net/.default'); connection.options.password = tokenResponse.token; }, }; }, }), ], }) export class DatabaseModule {}
方案2:配置连接池自动回收过期连接
结合beforeConnect钩子,调整连接池参数,让闲置连接在Token过期前被回收,强制后续请求创建使用新Token的连接:
// 在TypeORM配置中添加连接池参数 { // ...其他配置 pool: { max: 10, // 根据并发量设置最大连接数 idleTimeoutMillis: 3600000, // 1小时闲置后回收连接,确保在Token过期前更换 connectionTimeoutMillis: 20000, // 连接超时时间 }, beforeConnect: async (connection) => { const tokenResponse = await credential.getToken('https://ossrdbms-aad.database.windows.net/.default'); connection.options.password = tokenResponse.token; }, }
方案3:自定义Token刷新服务监听连接错误
创建服务监听数据库连接错误,当检测到Token过期相关错误时,主动刷新Token并重启连接:
import { Injectable, OnModuleInit } from '@nestjs/common'; import { Connection } from 'typeorm'; import { ManagedIdentityCredential } from '@azure/identity'; @Injectable() export class TokenRefreshService implements OnModuleInit { private credential = new ManagedIdentityCredential(); constructor(private readonly connection: Connection) {} async onModuleInit() { // 监听连接错误 this.connection.driver.connection.on('error', async (err) => { // 判断是否为Token过期类错误 if (err.message.includes('FATAL: password authentication failed for user') || err.message.includes('invalid token')) { await this.refreshTokenAndReconnect(); } }); // 定时刷新Token(提前1小时执行,避免过期) setInterval(async () => { await this.refreshTokenAndReconnect(); }, 23 * 60 * 60 * 1000); } private async refreshTokenAndReconnect() { try { const tokenResponse = await this.credential.getToken('https://ossrdbms-aad.database.windows.net/.default'); // 更新连接配置的密码 this.connection.options.password = tokenResponse.token; // 关闭现有连接池并重新建立连接 await this.connection.close(); await this.connection.connect(); } catch (err) { console.error('刷新Token并重建连接失败:', err); } } }
在DatabaseModule中注册该服务:
@Module({ imports: [ TypeOrmModule.forRootAsync({ // ...现有配置 }), ], providers: [TokenRefreshService], }) export class DatabaseModule {}
注意事项
- 生产环境必须关闭
synchronize: true,避免自动修改数据库结构导致数据风险。 - 连接池参数需根据应用并发量和Token有效期合理调整,确保闲置连接在Token过期前被回收。
ManagedIdentityCredential会自动缓存未过期的Token,调用getToken时无需手动处理缓存逻辑。
内容的提问来源于stack exchange,提问作者amn
相关产品推荐
相关产品推荐

