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

如何提前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时才获取新令牌,完全忽略了令牌即将过期的场景。

解决方案

解决步骤

  1. 新增状态变量:存储令牌过期时间、刷新定时器ID,以及并发刷新锁(避免重复刷新)
  2. 新增刷新判断逻辑:检查当前令牌是否处于「过期前5分钟」范围内,若是则触发刷新
  3. 实现自动刷新定时器:每次获取新令牌后,设置定时器在过期前5分钟自动触发刷新
  4. 处理并发请求:用锁机制确保同一时间只有一个刷新操作在执行

修改后的完整代码

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ý

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.11 16:34:55