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

如何确保等待EWS返回OAuth令牌后再使用及高效刷新?

EWS邮件转发应用OAuth认证迁移的优化疑问

背景

我有一款老旧的邮件转发应用,每分钟收取一次邮件,将发件人地址与用户输入列表对比后按规则转发。原本通过Microsoft EWS插件使用Basic Authentication,现在该认证方式被淘汰,正在迁移至OAuth认证。

当前实现与临时问题

我已经实现了大部分功能,但存在临时 workaround,推测实现方式有误:

  1. 异步刷新令牌逻辑:编写了RefreshToken异步函数获取令牌,通过定时器根据令牌过期时间触发刷新:
Private Async Function RefreshToken() As Threading.Tasks.Task
    Dim cca = ConfidentialClientApplication.Create("myAppID").WithClientSecret("mySecret").WithTenantId("myTenantID").Build()
    Dim ewsScopes = New String() {"https://outlook.office365.com/.default"}
    Try
        MyRefreshTimer.Stop()
        Dim authResult = Await cca.AcquireTokenForClient(ewsScopes).ExecuteAsync()
        myToken = authResult.AccessToken
        MyRefreshTimer.Interval = CInt((authResult.ExpiresOn - Date.Now.ToUniversalTime).TotalMilliseconds)
        MyRefreshTimer.Start()
    Catch ex As Exception
        Debug.WriteLine(ex.Message)
    End Try
End Function
  1. 获取EWS客户端逻辑:编写GetEWSClient函数返回ExchangeService实例供业务调用:
Friend Function GetEWSClient() As ExchangeService
    Try
        Dim myEWSClient As New ExchangeService With {
            .Url = New Uri("https://outlook.office365.com/EWS/Exchange.asmx"),
            .Credentials = New OAuthCredentials(myToken),
            .ImpersonatedUserId = New ImpersonatedUserId(ConnectingIdType.SmtpAddress, My.Settings.EmailAddress)
        }
        myEWSClient.HttpHeaders.Add("X-AnchorMailbox", My.Settings.EmailAddress)
        Return myEWSClient
    Catch ex As Exception
        Debug.WriteLine(ex.Message)
        Return Nothing
    End Try
End Function
  1. 调用方式:
Dim newMsg As New EmailMessage(GetEWSClient)

存在的问题

应用启动时,有时令牌尚未获取完成就调用GetEWSClient,会因令牌为空导致失败。临时通过SetupExchangeConnection函数在启动时同步获取令牌及客户端:

Friend Async Function SetupExchangeConnection() As Threading.Tasks.Task(Of ExchangeService) ' Gets the initial connection working
    Dim cca = ConfidentialClientApplication.Create("myAppID").WithClientSecret("mySecret").WithTenantId("myTenantID").Build()
    Dim ewsScopes = New String() {"https://outlook.office365.com/.default"}
    Try
        Dim authResult = Await cca.AcquireTokenForClient(ewsScopes).ExecuteAsync()
        Dim ewsClient = New ExchangeService With {
                .Url = New Uri("https://outlook.office365.com/EWS/Exchange.asmx"),
                .Credentials = New OAuthCredentials(authResult.AccessToken),
                .ImpersonatedUserId = New ImpersonatedUserId(ConnectingIdType.SmtpAddress, My.Settings.EmailAddress)
            }
        ewsClient.HttpHeaders.Add("X-AnchorMailbox", My.Settings.EmailAddress)
        Return ewsClient
    Catch ex As Exception
        Debug.WriteLine(ex.Message)
        Return Nothing
    End Try
End Function

但想知道更规范的等待令牌返回的方式(避免使用sleep语句),同时想确认:使用定时器主动刷新令牌,还是捕获401 Unauthorized后再刷新并重试,哪种方式更合理?

优化后的尝试

根据建议,我将逻辑简化为单个异步GetEWSClient函数:

Friend Async Function GetEWSClient() As Threading.Tasks.Task(Of ExchangeService)
    Dim cca = ConfidentialClientApplication.Create("AppId").WithClientSecret("Secret").WithTenantId("TenantID").Build()
    Dim ewsScopes = New String() {"https://outlook.office365.com/.default"}
    Try
        Dim authResult = Await cca.AcquireTokenForClient(ewsScopes).ExecuteAsync()
        Dim myEWSClient As New ExchangeService With {
            .Url = New Uri("https://outlook.office365.com/EWS/Exchange.asmx"),
            .Credentials = New OAuthCredentials(authResult.AccessToken),
            .ImpersonatedUserId = New ImpersonatedUserId(ConnectingIdType.SmtpAddress, My.Settings.EmailAddress)
        }
        myEWSClient.HttpHeaders.Add("X-AnchorMailbox", My.Settings.EmailAddress)
        Return myEWSClient
    Catch ex As MsalClientException
        Debug.WriteLine("Error acquiring access token.  Details: " & ex.Message)
        Return Nothing
    Catch ex As Exception
        Debug.WriteLine("Error getting EWSClient.  Details: " & ex.Message)
        Return Nothing
    End Try
End Function

通过GetEWSClient.Result调用,目前运行正常,但仍想确认最优实现方案。


解答

一、规范的令牌等待方式(避免同步阻塞)

你当前用GetEWSClient.Result会导致同步阻塞,在UI线程中极易引发死锁,更规范的做法是全程使用异步/await模式:

  1. 修改业务调用逻辑:将调用GetEWSClient的代码改为异步,例如:
' 假设原调用代码在某个异步方法内
Dim ewsClient = Await GetEWSClient()
If ewsClient IsNot Nothing Then
    Dim newMsg As New EmailMessage(ewsClient)
    ' 后续业务逻辑
End If
  1. 应用启动时的初始化:在应用启动的异步入口方法中,先等待令牌初始化完成再启动业务定时器:
Private Async Sub MainForm_Load(sender As Object, e As EventArgs) Handles MyBase.Load
    ' 先初始化EWS客户端
    Dim initialClient = Await GetEWSClient()
    If initialClient Is Nothing Then
        ' 处理初始化失败逻辑,比如弹窗提示后退出
        MessageBox.Show("Failed to initialize EWS connection.")
        Me.Close()
        Return
    End If
    ' 初始化成功后启动业务定时器
    MailCheckTimer.Start()
End Sub

二、令牌刷新策略:主动刷新为主,被动重试兜底

两种策略各有优劣,推荐结合使用:

  1. 主动刷新的优势:

    • 避免因令牌过期导致的业务请求失败,提升稳定性;
    • 可以根据authResult.ExpiresOn设置定时器,提前1-2分钟刷新(比如设为过期时间的90%),规避网络延迟导致的令牌过期;
    • 对你这种每分钟执行一次的业务逻辑,主动刷新完全可以覆盖需求。
  2. 被动重试的必要性:

    • 即使主动刷新,也可能出现令牌提前失效(比如管理员撤销权限、令牌被吊销)的情况,此时捕获401错误并重试1次是必要的兜底方案;
    • 重试时注意限制次数,避免无限循环。

三、最优实现方案建议

结合你的场景,建议调整如下:

  1. 复用ConfidentialClientApplication实例:不要每次调用都新建cca实例,MSAL会自动缓存令牌,复用实例能提升性能并避免重复请求:
' 声明类级别的cca实例
Private ReadOnly _cca As IConfidentialClientApplication

' 在构造函数中初始化
Public Sub New()
    InitializeComponent()
    _cca = ConfidentialClientApplication.Create("AppId").WithClientSecret("Secret").WithTenantId("TenantID").Build()
End Sub
  1. 合并令牌获取与客户端创建逻辑,利用MSAL自动缓存:
Friend Async Function GetEWSClient() As Threading.Tasks.Task(Of ExchangeService)
    Dim ewsScopes = New String() {"https://outlook.office365.com/.default"}
    Try
        ' MSAL会自动从缓存获取有效令牌,缓存失效时自动刷新
        Dim authResult = Await _cca.AcquireTokenForClient(ewsScopes).ExecuteAsync()
        
        Dim myEWSClient As New ExchangeService With {
            .Url = New Uri("https://outlook.office365.com/EWS/Exchange.asmx"),
            .Credentials = New OAuthCredentials(authResult.AccessToken),
            .ImpersonatedUserId = New ImpersonatedUserId(ConnectingIdType.SmtpAddress, My.Settings.EmailAddress)
        }
        myEWSClient.HttpHeaders.Add("X-AnchorMailbox", My.Settings.EmailAddress)
        Return myEWSClient
    Catch ex As MsalClientException
        Debug.WriteLine("Error acquiring access token.  Details: " & ex.Message)
        Return Nothing
    Catch ex As Exception
        Debug.WriteLine("Error getting EWSClient.  Details: " & ex.Message)
        Return Nothing
    End Try
End Function
  1. 业务逻辑的异步改造:

    • 将每分钟执行的邮件收取逻辑改为异步方法;
    • 每次执行前通过Await GetEWSClient()获取有效客户端,利用MSAL自动缓存机制,无需手动管理令牌刷新。
  2. 添加401重试兜底:

    Private Async Sub MailCheckTimer_Tick(sender As Object, e As EventArgs) Handles MailCheckTimer.Tick
        MailCheckTimer.Stop()
        Try
            Dim ewsClient = Await GetEWSClient()
            If ewsClient Is Nothing Then
                Throw New Exception("Failed to get EWS client")
            End If
            ' 执行邮件收取转发逻辑
            Await ProcessEmailsAsync(ewsClient)
        Catch ex As WebException
            Dim response = TryCast(ex.Response, HttpWebResponse)
            If response IsNot Nothing AndAlso response.StatusCode = HttpStatusCode.Unauthorized Then
                ' 401错误,重试一次
                Debug.WriteLine("Token expired, retrying...")
                Dim ewsClient = Await GetEWSClient()
                If ewsClient IsNot Nothing Then
                    Await ProcessEmailsAsync(ewsClient)
                End If
            Else
                Debug.WriteLine("Error processing emails: " & ex.Message)
            End If
        Catch ex As Exception
            Debug.WriteLine("Error processing emails: " & ex.Message)
        Finally
            MailCheckTimer.Start()
        End Try
    End Sub
    
    Private Async Function ProcessEmailsAsync(ewsClient As ExchangeService) As Task
        ' 邮件收取、对比、转发的异步逻辑
    End Function
    

内容的提问来源于stack exchange,提问作者ADY

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.16 12:15:38