IConfidentialClientApplication的AcquireTokenForClient周期性抛出异常求助
我构建了一个NotificationService工厂用于检查两个不同的邮箱,整体运行正常,但会周期性收到错误邮件。以下是抛出异常的方法及异常信息:
抛出异常的方法
private async Task<string> GetAccessToken() { try { string token = string.Empty; IConfidentialClientApplication app = ConfidentialClientApplicationBuilder.Create(_settings.AzureClientID) .WithCertificate(_settings.Certificate) .WithAuthority($"https://login.microsoftonline.com/{_settings.AzureTenantID}") .WithRedirectUri("https://daemon") .Build(); string[] scopes = new string[] { "https://graph.microsoft.com/.default" }; var result = await app.AcquireTokenForClient(scopes).ExecuteAsync(); return result.AccessToken; } catch (Exception ex) { _logger.LogError($"{DateTime.Now.ToString()}: Exception within GetAccessToken\r\n{ex}", ex); } return string.Empty; }
异常信息
2023年2月8日 上午9:28:19:GetAccessToken内发生异常
System.Net.Http.HttpRequestException: 发送请求时发生错误。
---> System.IO.IOException: 无法从传输连接读取数据:连接尝试失败,因为连接方在一段时间后未正确响应,或者已建立的连接失败,因为连接的主机未能响应。
---> System.Net.Sockets.SocketException (10060): 连接尝试失败,因为连接方在一段时间后未正确响应,或者已建立的连接失败,因为连接的主机未能响应。
--- 内部异常堆栈跟踪结束 ---
at System.Net.Sockets.Socket.AwaitableSocketAsyncEventArgs.ThrowException(SocketError error, CancellationToken cancellationToken)
at System.Net.Sockets.Socket.AwaitableSocketAsyncEventArgs.GetResult(Int16 token)
at System.Net.Security.SslStream.ReadAsyncInternal[TIOAdapter](TIOAdapter adapter, Memory1 buffer) at System.Net.Http.HttpConnection.SendAsyncCore(HttpRequestMessage request, Boolean async, CancellationToken cancellationToken) --- 内部异常堆栈跟踪结束 --- at System.Net.Http.HttpConnection.SendAsyncCore(HttpRequestMessage request, Boolean async, CancellationToken cancellationToken) at System.Net.Http.AuthenticationHelper.SendWithNtAuthAsync(HttpRequestMessage request, Uri authUri, Boolean async, ICredentials credentials, Boolean isProxyAuth, HttpConnection connection, HttpConnectionPool connectionPool, CancellationToken cancellationToken) at System.Net.Http.HttpConnectionPool.SendWithRetryAsync(HttpRequestMessage request, Boolean async, Boolean doRequestAuth, CancellationToken cancellationToken) at System.Net.Http.AuthenticationHelper.SendWithAuthAsync(HttpRequestMessage request, Uri authUri, Boolean async, ICredentials credentials, Boolean preAuthenticate, Boolean isProxyAuth, Boolean doRequestAuth, HttpConnectionPool pool, CancellationToken cancellationToken) at System.Net.Http.RedirectHandler.SendAsync(HttpRequestMessage request, Boolean async, CancellationToken cancellationToken) at System.Net.Http.HttpClient.SendAsyncCore(HttpRequestMessage request, HttpCompletionOption completionOption, Boolean async, Boolean emitTelemetryStartStop, CancellationToken cancellationToken) at Microsoft.Identity.Client.Http.HttpManager.ExecuteAsync(Uri endpoint, IDictionary2 headers, HttpContent body, HttpMethod method, ILoggerAdapter logger, CancellationToken cancellationToken)
at Microsoft.Identity.Client.Http.HttpManager.ExecuteWithRetryAsync(Uri endpoint, IDictionary2 headers, HttpContent body, HttpMethod method, ILoggerAdapter logger, Boolean doNotThrow, Boolean retry, CancellationToken cancellationToken) at Microsoft.Identity.Client.Http.HttpManager.SendPostAsync(Uri endpoint, IDictionary2 headers, HttpContent body, ILoggerAdapter logger, CancellationToken cancellationToken)
at Microsoft.Identity.Client.Http.HttpManager.SendPostAsync(Uri endpoint, IDictionary2 headers, IDictionary2 bodyParameters, ILoggerAdapter logger, CancellationToken cancellationToken)
at Microsoft.Identity.Client.OAuth2.OAuth2Client.ExecuteRequestAsync[T](Uri endPoint, HttpMethod method, RequestContext requestContext, Boolean expectErrorsOn200OK, Boolean addCommonHeaders, Func2 onBeforePostRequestData) at Microsoft.Identity.Client.OAuth2.OAuth2Client.GetTokenAsync(Uri endPoint, RequestContext requestContext, Boolean addCommonHeaders, Func2 onBeforePostRequestHandler)
at Microsoft.Identity.Client.OAuth2.TokenClient.SendHttpAndClearTelemetryAsync(String tokenEndpoint, ILoggerAdapter logger)
at Microsoft.Identity.Client.OAuth2.TokenClient.SendTokenRequestAsync(IDictionary`2 additionalBodyParameters, String scopeOverride, String tokenEndpointOverride, CancellationToken cancellationToken)
at Microsoft.Identity.Client.Internal.Requests.ClientCredentialRequest.FetchNewAccessTokenAsync(CancellationToken cancellationToken)
at Microsoft.Identity.Client.Internal.Requests.ClientCredentialRequest.ExecuteAsync(CancellationToken cancellationToken)
at Microsoft.Identity.Client.Internal.Requests.RequestBase.RunAsync(CancellationToken cancellationToken)
at Microsoft.Identity.Client.ApiConfig.Executors.ConfidentialClientExecutor.ExecuteAsync(AcquireTokenCommonParameters commonParameters, AcquireTokenForClientParameters clientParameters, CancellationToken cancellationToken)
at HIW.NotificationService.Service.GraphService.GetAccessToken() in C:\agent_work\60\s\HIW.NotificationService\Service\GraphService.cs:line 276
问题分析与解决建议
这个错误是Socket连接超时(错误码10060),本质是程序向Azure AD的token端点发起请求时,网络层面无法建立连接或连接超时,属于间歇性网络问题。可以从以下几个方向修复:
- 复用MSAL应用实例:当前代码每次调用
GetAccessToken都新建IConfidentialClientApplication实例,低效且易引发网络连接问题。应将实例设为类的私有成员,在构造函数中初始化一次,复用它。 - 启用令牌缓存:MSAL内置令牌缓存机制,复用实例后会自动缓存令牌,减少不必要的请求,降低触发网络问题的概率。
- 添加重试策略:给令牌请求添加重试逻辑,MSAL支持通过
.WithRetry()配置重试次数,也可以用Polly框架实现更灵活的瞬时故障重试。 - 检查网络环境:确认服务器到
login.microsoftonline.com的443端口连通性,排查是否有防火墙、代理偶尔拦截请求,或网络带宽波动导致超时。 - 设置请求超时:给MSAL的HTTP请求配置合理的超时时间,避免无限制等待。
修改后的代码示例
// 类私有成员,复用MSAL应用实例 private readonly IConfidentialClientApplication _msalApp; private readonly YourSettingsType _settings; // 构造函数中初始化应用实例 public GraphService(YourSettingsType settings) { _settings = settings; _msalApp = ConfidentialClientApplicationBuilder.Create(_settings.AzureClientID) .WithCertificate(_settings.Certificate) .WithAuthority($"https://login.microsoftonline.com/{_settings.AzureTenantID}") .WithRedirectUri("https://daemon") .Build(); } private async Task<string> GetAccessToken() { try { string[] scopes = new string[] { "https://graph.microsoft.com/.default" }; // 优先从缓存获取令牌,缓存失效时自动请求新令牌,并配置重试 var result = await _msalApp.AcquireTokenForClient(scopes) .WithRetry(3) // 配置3次重试 .ExecuteAsync(); return result.AccessToken; } catch (Exception ex) { _logger.LogError($"{DateTime.Now.ToString()}: Exception within GetAccessToken\r\n{ex}", ex); } return string.Empty; }
内容的提问来源于stack exchange,提问作者J Pacelli

