如何提前5分钟刷新Microsoft client_credentials令牌?旧令牌问题求助
解决Microsoft令牌提前5分钟自动刷新的问题
我有一个使用@azure/msal-node获取Microsoft令牌的服务类,代码能正常获取令牌,但无法实现令牌过期前5分钟自动刷新的需求,尝试多次后定时刷新始终拿到旧令牌,求解决办法。
原代码
import { ConfidentialClientApplication } from '@azure/msal-node' import { ConfigurationService } from './configuration/configuration.class.js' export class TokenService { constructor(app) { this.app = app this.msalApplication = null this.accessToken = null } async initialize(configData) { try { // Find the values you need in the response data const clientId = configData.find((item) => item.setting === 'clientId')?.value const tenantId = configData.find((item) => item.setting === 'tenantId')?.value const clientSecret = configData.find((item) => item.setting === 'clientSecret')?.value // Check if all required values are present if (!clientId || !tenantId || !clientSecret) { throw new Error('Missing configuration values') } // Configure the MSAL application with the fetched values this.msalApplication = new ConfidentialClientApplication({ auth: { clientId, authority: `https://login.microsoftonline.com/${tenantId}`, clientSecret, grant_type: 'client_credentials' } }) } catch (error) { console.error('Error initializing TokenService:', error) throw error } } async getToken() { if (!this.msalApplication) { // Fetch the configuration values from the database using your ConfigurationService const configService = new ConfigurationService({ Model: this.app.get('mssqlClient'), name: 'application_config' // Make sure this matches your FeathersJS database configuration }) const configData = await configService.find() await this.initialize(configData) } // 如果没有有效令牌或即将过期,请获取新令牌 if (!this.accessToken) { try { const tokenResponse = await this.msalApplication.acquireTokenByClientCredential({ scopes: ['https://graph.microsoft.com/.default'] }) this.accessToken = tokenResponse.accessToken return this.accessToken } catch (error) { console.error('Error acquiring token:', error) this.accessToken = null throw error } } return this.accessToken } }
问题分析
原代码的核心问题是仅存储了accessToken本身,未记录令牌过期时间,也没有提前刷新的触发逻辑。getToken方法仅在accessToken为null时才获取新令牌,完全忽略了令牌即将过期的场景。
解决方案
解决步骤
- 新增状态变量:存储令牌过期时间、刷新定时器ID,以及并发刷新锁(避免重复刷新)
- 新增刷新判断逻辑:检查当前令牌是否处于「过期前5分钟」范围内,若是则触发刷新
- 实现自动刷新定时器:每次获取新令牌后,设置定时器在过期前5分钟自动触发刷新
- 处理并发请求:用锁机制确保同一时间只有一个刷新操作在执行
修改后的完整代码
import { ConfidentialClientApplication } from '@azure/msal-node' import { ConfigurationService } from './configuration/configuration.class.js' export class TokenService { constructor(app) { this.app = app this.msalApplication = null this.accessToken = null this.tokenExpiresAt = null // 令牌过期时间(时间戳) this.refreshTimer = null // 刷新定时器ID this.isRefreshing = false // 并发刷新锁 this.refreshOffset = 5 * 60 * 1000 // 提前5分钟刷新,单位毫秒 } async initialize(configData) { try { const clientId = configData.find((item) => item.setting === 'clientId')?.value const tenantId = configData.find((item) => item.setting === 'tenantId')?.value const clientSecret = configData.find((item) => item.setting === 'clientSecret')?.value if (!clientId || !tenantId || !clientSecret) { throw new Error('Missing configuration values') } this.msalApplication = new ConfidentialClientApplication({ auth: { clientId, authority: `https://login.microsoftonline.com/${tenantId}`, clientSecret // grant_type无需手动配置,msal-node会自动处理client credentials流程 } }) } catch (error) { console.error('Error initializing TokenService:', error) throw error } } // 判断令牌是否需要刷新 isTokenNeedRefresh() { if (!this.accessToken || !this.tokenExpiresAt) return true // 当前时间 + 提前刷新时间 >= 过期时间,触发刷新 return Date.now() + this.refreshOffset >= this.tokenExpiresAt } // 取消现有刷新定时器 cancelRefreshTimer() { if (this.refreshTimer) { clearTimeout(this.refreshTimer) this.refreshTimer = null } } // 获取新令牌并设置自动刷新定时器 async fetchNewToken() { try { const tokenResponse = await this.msalApplication.acquireTokenByClientCredential({ scopes: ['https://graph.microsoft.com/.default'] }) this.accessToken = tokenResponse.accessToken // 计算过期时间:当前时间 + 令牌有效期(expiresIn为秒,转毫秒) this.tokenExpiresAt = Date.now() + tokenResponse.expiresIn * 1000 // 设置新的刷新定时器:在过期前5分钟触发 this.cancelRefreshTimer() this.refreshTimer = setTimeout(() => { this.fetchNewToken().catch(err => console.error('Auto refresh token failed:', err)) }, this.tokenExpiresAt - Date.now() - this.refreshOffset) return this.accessToken } catch (error) { console.error('Error acquiring token:', error) this.accessToken = null this.tokenExpiresAt = null throw error } } async getToken() { if (!this.msalApplication) { const configService = new ConfigurationService({ Model: this.app.get('mssqlClient'), name: 'application_config' }) const configData = await configService.find() await this.initialize(configData) } // 需要刷新且无正在进行的刷新操作,触发刷新 if (this.isTokenNeedRefresh()) { if (this.isRefreshing) { // 等待正在进行的刷新完成 await new Promise(resolve => { const checkInterval = setInterval(() => { if (!this.isRefreshing) { clearInterval(checkInterval) resolve() } }, 100) }) } else { this.isRefreshing = true try { await this.fetchNewToken() } finally { this.isRefreshing = false } } } return this.accessToken } }
关键改动说明
- 状态变量扩展:新增
tokenExpiresAt记录过期时间、refreshTimer管理定时器、isRefreshing避免并发刷新冲突 - 刷新判断逻辑:
isTokenNeedRefresh方法精准判断是否需要提前刷新 - 自动刷新机制:每次获取新令牌后自动设置定时器,确保在过期前5分钟触发刷新
- 并发处理:通过锁机制避免多个请求同时触发刷新,减少无效请求
- 冗余配置移除:删除
grant_type配置,msal-node会自动处理client credentials授权流程
内容的提问来源于stack exchange,提问作者Daniel Tršťanský
相关产品推荐
相关产品推荐

