C# 通过EWS托管API与Exchange联机对话时刷新访问令牌

C# 通过EWS托管API与Exchange联机对话时刷新访问令牌,c#,oauth-2.0,exchangewebservices,adal,ews-managed-api,C#,Oauth 2.0,Exchangewebservices,Adal,Ews Managed Api,我正在编写一个应用程序,使用EWS管理的API与Exchange联机通信,并使用ADAL库通过OAuth 2.0对我的应用程序进行身份验证 访问令牌在60分钟后过期。之后,我需要刷新访问令牌。目前,我正在StreamSubscriptionConnectionOnNotificationEvent处理程序以及OnDisconnect事件处理程序中使用以下代码刷新OAuth访问令牌 private void OnNotificationEventHandler(object sender, Not

我正在编写一个应用程序,使用EWS管理的API与Exchange联机通信,并使用ADAL库通过OAuth 2.0对我的应用程序进行身份验证

访问令牌在60分钟后过期。之后,我需要刷新访问令牌。目前,我正在StreamSubscriptionConnectionOnNotificationEvent处理程序以及OnDisconnect事件处理程序中使用以下代码刷新OAuth访问令牌

private void OnNotificationEventHandler(object sender, NotificationEventArgs args)
{
    exchangeService.Credentials = new OAuthCredentials(GetOAuthAccessToken().Result);

    // Do my work
}
我还在OnDisconnect事件处理程序中添加了相同的刷新访问令牌代码,因为StreamSubscriptionConnection最多只打开30分钟

private void OnDisconnectEventHandler(object sender, SubscriptionErrorEventArgs args)
{
    exchangeService.Credentials = new OAuthCredentials(GetOAuthAccessToken().Result);
    streamingSubscriptionConnection.Open();
}
这是我的访问令牌代码

private async Task<string> GetOAuthAccessToken(PromptBehavior promptBehavior = PromptBehavior.Auto)
{
    var authenticationContext = new AuthenticationContext(myAadTenant);

    var authenticationResult = await authenticationContext.AcquireTokenAsync(exchangeOnlineServerName, myClientId, redirectUri, new PlatformParameters(promptBehavior));

    return authenticationResult.AccessToken;
}
私有异步任务GetOAuthAccessToken(PromptBehavior=PromptBehavior.Auto) { var authenticationContext=新的authenticationContext(myAadTenant); var authenticationResult=wait authenticationContext.AcquireTokenAsync(exchangeOnlineServerName、myClientId、redirectUri、新平台参数(promptBehavior)); 返回authenticationResult.AccessToken; } 尽管上述方法“有效”,但我觉得这并不是处理这种情况的最佳方法,因为每当我与EWS通信时,我都需要确保刷新我的访问令牌。如果我添加了另一个事件处理程序,并且忘记在事件处理程序中添加令牌刷新逻辑,那么如果我的访问令牌过期,并且我需要在事件处理程序中调用EWS,那么在处理该事件时,我可能会得到401

上面的代码简化了,每当我与EWS通信时,我都可以设置try-catch,如果我得到401,我会刷新我的访问令牌并重试,但这并不能解决我上面提到的不便

我认为应该有一个更简单的方法来处理这个问题,但我还没有找到正确的文档。这是我在进行开发时参考的资料。

另一种方法是,当您通过EWS管理的API与Exchange联机通信时,需要提供
exchangeService
对象。您需要捕获每个请求的401异常,在获得此异常后,您需要为
exchangeService
对象重新设置
Credentials
属性或重新创建此对象。

我同意应该有更好的方法来处理令牌刷新。如果EWS API本身能够管理令牌刷新就好了,但作为一种解决方法,我所做的是

将对EWS服务的引用放入一个公共/内部属性中,该属性可以a)实例化尚未实例化的服务,b)确保身份验证令牌仍然有效(如果没有,则执行令牌刷新)。然后,我们需要确保该属性是EWS服务的单一访问点

大体上,这看起来像

public class Mailbox 
{
    private ExchangeService exchangeService;

    public ExchangeService ExchangeService
    {
        get
        {
            if (this.exchangeService == null)
            {
                // Initialise the service
                CreateExchangeService();
            }
            else
            {
                // Ensure token is still valid
                ValidateAuthentication();
            }

            return this.exchangeService;
        }
    }
}
我不打算详述
CreateExchangeService
,但
ValidateAuthentication
是对EWS的基本调用,如果未经验证,它将引发异常

private void ValidateAuthentication()
{
    try
    {
        Folder inbox = Folder.Bind(this.exchangeService, WellKnownFolderName.Inbox);
    }
    catch //(WebException webEx) when (((HttpWebResponse)webEx.Response).StatusCode == HttpStatusCode.Unauthorized)
    {
        RefreshOAuthCredentials();
    }
}
RefreshOAuthCredentials
将只刷新令牌。类似地,我在StreamingSubscription上还有一个
OnDisconnect
事件处理程序,它将尝试重新打开连接(如果重新打开失败,则重新授权)。为了简洁起见,这里没有显示这两个选项

但主要的一点是,我们现在有一个对EWS服务的引用,它在每次调用之前执行身份验证检查和重新授权(如果需要)。例如,绑定到邮件消息看起来像

var mailMessage = EmailMessage.Bind(this.mailbox.ExchangeService, itemId);

毫无疑问,这会增加流量/延迟,如果有更好的方法,这是可以避免的。如果有人有更好的办法,我洗耳恭听

为什么不使用一个计时器线程每n分钟刷新一次令牌,并让它与所有经过身份验证的web请求共享某种锁,这样就可以避免争用情况?如果EWS延长每个请求的超时时间,您甚至可以在每个成功的web请求中重置计时器。我不认为让计时器线程每n分钟刷新一次令牌就可以解决这个问题。令牌每60分钟过期一次,我的应用程序需要响应新邮件事件。因此,如果下一个计时器在65分钟标记时刷新令牌,但在62分钟标记时收到事件,那么我仍然会遇到问题。为了处理这个问题,我仍然需要在响应事件和调用EWS之前刷新令牌。我想知道我是否可以设置我的EWS实例,以便在需要时自动刷新令牌。然后不要将计时器设置为每65分钟刷新一次。lol。如果刷新周期为60分钟,我会每50分钟刷新一次。天哪,那是行不通的,因为在您已经拥有的OAuth访问令牌过期之前,您不会获得新的OAuth访问令牌。因此,在55分钟标记上调用AcquireTokenAsync()不会刷新令牌。当然,除非有一种我不知道的强制更新的方法。方法2是简单地重新获取令牌以响应请求出错。现在不是捕获401,而是刷新“凭据”,这或多或少是相同的。即使我尝试catch,我也需要为每次调用“exchangeService”都这样做。我试图找出是否有一个更优雅的解决方案,这样我就可以将try-catch逻辑放在一个地方,而不是放在整个代码中。如果我想执行try-catch 401或我现在正在执行的操作,我需要在调用“exchangeService”之前将其放在所有位置,因为其中任何一个都可能由于令牌过期而返回401。我还需要对我的所有事件处理程序执行相同的操作。要全局处理异常,我们可以使用
AppDomain.UnhandledException
(请参阅)。我们确实需要更好的令牌刷新解决方案,而不是尝试全局捕获异常并处理它们。为什么
exchangeService
本身不能处理这个问题?全局捕获充其量是笨拙的,在许多情况下都不起作用(例如一次打开多个EWS连接)