.NET Core & AzureAD:长运行进程中保留用户上下文调用下游API
解决长运行任务中调用下游API的用户上下文丢失问题
核心思路:长运行任务无法依赖请求上下文的用户令牌,必须提前获取**刷新令牌(Refresh Token)**并持久化,后续通过刷新令牌获取新的访问令牌来执行OBO(On-Behalf-Of)流程调用下游API。
步骤1:调整认证配置,确保获取刷新令牌
首先需要在配置中添加offline_access权限,让应用能获取刷新令牌。修改appsettings.json中的下游API配置:
"MyDownstreamApi": { "BaseUrl": "https://your-downstream-api-endpoint", "Scopes": "api://your-downstream-api-client-id/access_as_user offline_access" }
同时在Startup.cs的认证配置中,确保令牌获取逻辑支持刷新令牌:
services .AddMicrosoftIdentityWebApiAuthentication(Configuration) .EnableTokenAcquisitionToCallDownstreamApi() .AddDownstreamWebApi( "MyDownstreamApi", Configuration.GetSection("MyDownstreamApi")) .AddInMemoryTokenCaches();
步骤2:启动长任务时获取并存储刷新令牌
在PUT /long-running-logic的控制器方法中,获取当前用户的刷新令牌并与任务GUID绑定存储:
[HttpPut("long-running-logic")] public async Task<IActionResult> StartLongRunningTask([FromServices] ITokenAcquisition tokenAcquisition, [FromServices] ILongRunningTaskRegistry taskRegistry) { // 获取包含刷新令牌的认证结果 var authResult = await tokenAcquisition.GetAuthenticationResultForUserAsync( new[] { "api://your-downstream-api-client-id/access_as_user", "offline_access" }, includeRefreshToken: true); // 生成任务GUID var taskId = Guid.NewGuid(); // 存储任务信息:刷新令牌、用户ID、任务状态等 taskRegistry.RegisterTask(taskId, new LongRunningTaskInfo { RefreshToken = authResult.RefreshToken, UserId = User.FindFirstValue(ClaimTypes.NameIdentifier), Status = TaskStatus.Running }); // 启动后台长任务(建议用IHostedService或Hangfire等可靠框架,而非直接Task.Run) _ = Task.Run(async () => await ExecuteLongRunningTask(taskId, taskRegistry)); return Ok(new { TaskId = taskId }); }
步骤3:长任务中用刷新令牌获取访问令牌并调用下游API
首先在Startup.cs注册机密客户端应用,用于执行OBO流程:
services.AddSingleton<IConfidentialClientApplication>(sp => { var config = sp.GetRequiredService<IConfiguration>(); var azureAdConfig = config.GetSection("AzureAd").Get<MicrosoftIdentityOptions>(); return ConfidentialClientApplicationBuilder .Create(azureAdConfig.ClientId) .WithClientSecret(azureAdConfig.ClientSecret) .WithAuthority(new Uri($"{azureAdConfig.Instance}{azureAdConfig.TenantId}")) .Build(); });
然后在长任务逻辑中,使用刷新令牌获取访问令牌并调用下游API:
private async Task ExecuteLongRunningTask(Guid taskId, ILongRunningTaskRegistry taskRegistry, [FromServices] IConfidentialClientApplication confidentialClientApp, [FromServices] IConfiguration config) { var taskInfo = taskRegistry.GetTask(taskId); if (taskInfo == null) return; try { var downstreamApiScopes = new[] { "api://your-downstream-api-client-id/access_as_user" }; var azureAdConfig = config.GetSection("AzureAd").Get<MicrosoftIdentityOptions>(); // 用刷新令牌获取新的访问令牌 var result = await confidentialClientApp.AcquireTokenByRefreshToken(downstreamApiScopes, taskInfo.RefreshToken) .WithAccount(new Account(taskInfo.UserId, taskInfo.UserId, azureAdConfig.TenantId)) .ExecuteAsync(); // 调用下游API using var httpClient = new HttpClient(); httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", result.AccessToken); var downstreamBaseUrl = config.GetValue<string>("MyDownstreamApi:BaseUrl"); var response = await httpClient.GetAsync($"{downstreamBaseUrl}/your-target-endpoint"); // 更新任务状态和结果 taskInfo.Status = TaskStatus.Completed; taskInfo.Result = await response.Content.ReadAsStringAsync(); } catch (MsalUiRequiredException ex) { // 刷新令牌过期或无效,标记任务失败 taskInfo.Status = TaskStatus.Failed; taskInfo.ErrorMessage = "用户授权已过期,请重新发起请求"; } catch (Exception ex) { taskInfo.Status = TaskStatus.Failed; taskInfo.ErrorMessage = ex.Message; } finally { taskRegistry.UpdateTask(taskId, taskInfo); } }
关键注意事项
- 后台任务可靠性:避免直接用
Task.Run,建议使用IHostedService、Hangfire或Azure Functions等框架,确保任务在应用重启后不会丢失。 - 刷新令牌存储:刷新令牌是敏感信息,需加密存储(比如用加密数据库字段),禁止明文存储。
- 令牌过期处理:如果刷新令牌过期,无法再获取新的访问令牌,需通知用户重新发起请求。
- 权限配置:确保客户端应用在Azure AD中已被授予调用下游API的权限,且用户已同意相关权限。
内容的提问来源于stack exchange,提问作者gestj
相关产品推荐
相关产品推荐

