如何确保等待EWS返回OAuth令牌后再使用及高效刷新?
EWS邮件转发应用OAuth认证迁移的优化疑问
背景
我有一款老旧的邮件转发应用,每分钟收取一次邮件,将发件人地址与用户输入列表对比后按规则转发。原本通过Microsoft EWS插件使用Basic Authentication,现在该认证方式被淘汰,正在迁移至OAuth认证。
当前实现与临时问题
我已经实现了大部分功能,但存在临时 workaround,推测实现方式有误:
- 异步刷新令牌逻辑:编写了
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
- 获取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
- 调用方式:
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模式:
- 修改业务调用逻辑:将调用
GetEWSClient的代码改为异步,例如:
' 假设原调用代码在某个异步方法内 Dim ewsClient = Await GetEWSClient() If ewsClient IsNot Nothing Then Dim newMsg As New EmailMessage(ewsClient) ' 后续业务逻辑 End If
- 应用启动时的初始化:在应用启动的异步入口方法中,先等待令牌初始化完成再启动业务定时器:
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
二、令牌刷新策略:主动刷新为主,被动重试兜底
两种策略各有优劣,推荐结合使用:
主动刷新的优势:
- 避免因令牌过期导致的业务请求失败,提升稳定性;
- 可以根据
authResult.ExpiresOn设置定时器,提前1-2分钟刷新(比如设为过期时间的90%),规避网络延迟导致的令牌过期; - 对你这种每分钟执行一次的业务逻辑,主动刷新完全可以覆盖需求。
被动重试的必要性:
- 即使主动刷新,也可能出现令牌提前失效(比如管理员撤销权限、令牌被吊销)的情况,此时捕获401错误并重试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
- 合并令牌获取与客户端创建逻辑,利用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
业务逻辑的异步改造:
- 将每分钟执行的邮件收取逻辑改为异步方法;
- 每次执行前通过
Await GetEWSClient()获取有效客户端,利用MSAL自动缓存机制,无需手动管理令牌刷新。
添加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
相关产品推荐
相关产品推荐

