我正在编写一个应用程序,以使用 EWS 托管 API 与 Exchange Online 进行通信,并利用 ADAL 库通过 OAuth 2.0 对我的应用程序进行身份验证。
访问令牌将在 60 分钟后过期。之后我需要刷新访问令牌。目前,我正在 StreamSubscriptionConnection OnNotificationEvent 处理程序以及 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;
}
即使认为上述方法“有效”,我觉得这不是处理这种情况的最佳方法,因为我非常需要确保每当与 EWS 通信时刷新我的访问令牌。如果我添加另一个事件处理程序,并且忘记在事件处理程序中添加令牌刷新逻辑,那么如果我的访问令牌过期,我可能会在处理该事件时收到 401,但我需要在事件处理程序中调用 EWS。
上面的代码是简化的,我可以在与 EWS 通信时放置 try catch,如果收到 401,我会刷新我的访问令牌并重试,但这并不能解决我上面提到的不便。
我认为应该有一种更简单的方法来处理这个问题,但我还没有找到正确的文档。这是我在开发时参考的材料。 https://blogs.msdn.microsoft.com/webdav_101/2015/05/11/best-practices-ews-authentication-and-access-issues/
另一种方法是当您通过 EWS Managed API 与 Exchange 在线通信时,您需要提供
exchangeService
对象。并且您需要捕获每个请求的 401 异常,并且在捕获此异常后,您需要重新设置 Credentials
对象的 exchangeService
属性或重新创建此对象。
我同意应该有更好的方法来处理令牌刷新。如果 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);
毫无疑问,这会增加流量/延迟,如果有更好的方法,这是可以避免的。如果有人有更好的方法 - 我洗耳恭听!
有一种微妙的方法可以检查您的令牌是否已过期。获得代币后
var authenticationResult = await authenticationContext.AcquireTokenAsync(exchangeOnlineServerName, myClientId, redirectUri, new PlatformParameters(promptBehavior));
您的authenticationResult对象具有ExpiresOn属性,您可以检查您的令牌是否已过期,然后刷新它。
类似:
if (DateTime.Compare(authenticationResult.ExpiresOn.LocalDateTime, DateTimeOffset.Now.LocalDateTime) <= 0)
{
// Do your refresh token logic
}
我必须在 .NET Core 应用程序中处理同样的问题(使用官方 EWS 托管 API 客户端的分支,它不会以任何方式触及这方面)。如 PaulG 的回答中所述,在完整请求之前主动执行
.Bind
会增加流量/延迟。并且容易受到计时故障的影响 - 绑定本身运行时令牌可能会超时,并且下一个请求无论如何都会失败。
希望我的单点“重试 401”解决方案可以帮助别人。
private async Task Example() {
var view = new FolderView(100);
view.Traversal = FolderTraversal.Shallow;
view.Offset = 0;
await EWS(service => service.FindFolders((FolderId)WellKnownFolderName.MsgFolderRoot, view));
}
private async Task<T> EWS<T>(Func<ExchangeService, Task<T>> action)
{
// just creates instance/returns one from cache and does some other unimportant stuff
var service = await _getExchangeServiceAndBuildFolderIdCache();
try
{
return await action(service);
}
// catch (ServiceRequestException e) when ((e.InnerException as System.Net.WebException)?.Response.StatusCode == StatusCode401Unauthorized)
// ^^ is the more correct version. Would be. Except e.InnerException.Response is already disposed here.
// Next best option is to just do substring match on message (and hope localization doesn't mess it up).
catch (ServiceRequestException e) when (e.InnerException?.Message.Contains("(401)") ?? false)
{
// this tries AcquireTokenSilent first (which has internal cache for access token, and transparently falls back to refresh token if one is available). If silent fails, it also tries full AcquireTokenXxx call
service.Credentials = await GetEWSCredentials();
if (service.Credentials == null)
{
throw;
}
return await action(service);
}
}