.NET 6 Razor Pages集成Azure AD:静默令牌刷新与CORS问题解决
问题描述
我正在为.NET 6.0 Razor Pages Web应用集成Azure AD认证,已完成应用注册并实现用户登录,但面临令牌过期相关问题:
- 当前令牌约1小时过期,用户操作时会触发
/authorize端点调用并刷新页面,中断体验;期望实现12小时无中断会话,令牌后台自动静默刷新,12小时后需重新登录。 - 令牌过期时用户发起AJAX请求会触发302重定向至
/authorize端点,导致CORS错误,需重新刷新页面。
已手动添加认证配置(未使用脚手架),相关代码如下:
Program.cs
var builder = WebApplication.CreateBuilder(args); var config = builder.Configuration; var services = builder.Services; services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme) .AddMicrosoftIdentityWebApp( authOptions => { config.Bind("AzureAD", authOptions); authOptions.MaxAge = TimeSpan.FromHours(12); authOptions.SaveTokens = true; }, sessionOptions => { sessionOptions.Cookie.MaxAge = TimeSpan.FromHours(12); sessionOptions.Cookie.Name = "Custom-Cookie-Name"; sessionOptions.ExpireTimeSpan = TimeSpan.FromHours(12); sessionOptions.SlidingExpiration = false; }) .EnableTokenAcquisitionToCallDownstreamApi(config.GetValue<string>("GraphApi:Scopes")?.Split(' ')) .AddMicrosoftGraph(config.GetSection("GraphApi")) .AddSessionTokenCaches(); services.AddRazorPages(options => { options.Conventions.AddPageRoute("/Disclaimer", "/"); }) .AddMvcOptions(options => { var policy = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build(); options.Filters.Add(new AuthorizeFilter(policy)); }); services.AddHttpContextAccessor();
var app = builder.Build(); if (!app.Environment.IsDevelopment()) { app.UseExceptionHandler("/Error"); app.UseHsts(); } app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseRouting(); app.UseSession(); app.UseAuthentication(); app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapControllers(); endpoints.MapRazorPages(); }); app.UseSaveUserDetailsOnAuthentication(); app.UseIdentityPageInitialization(); app.MapRazorPages(); app.MapControllers(); app.Run();
Graph Middleware
public async Task InvokeAsync(HttpContext context, UserManager<ApplicationUser> userManager, GraphServiceClient graphServiceClient) { var page = context.GetRouteValue("page")?.ToString(); if (!page.IsNullOrEmpty() && page.Equals("/Disclaimer") && context.User.Identity?.IsAuthenticated == true) { var user = await graphServiceClient.Me .Request() .GetAsync() .ConfigureAwait(false);
现咨询:如何借助Microsoft Identity/MSAL实现令牌静默刷新、避免页面重载获取新令牌,以及处理客户端令牌过期引发的CORS问题?注:已了解Microsoft.Identity.Web v1.2.0+可处理AJAX场景,但仍需完整方案。
解决方案
一、实现后台静默刷新令牌,避免页面重载
Microsoft Identity Web 内置MSAL令牌静默刷新能力,通过调整配置和令牌获取逻辑即可实现:
- 调整OpenID Connect事件配置
在AddMicrosoftIdentityWebApp的authOptions中添加事件处理,优先静默刷新而非重定向:
authOptions.Events = new OpenIdConnectEvents { OnAuthorizationCodeReceived = async context => { // 将授权码兑换的令牌存入MSAL缓存,确保刷新令牌被保留 var tokenAcquisition = context.HttpContext.RequestServices.GetRequiredService<ITokenAcquisition>(); await tokenAcquisition.AddAccountToCacheFromAuthorizationCodeAsync( context.Principal, context.ProtocolMessage.Code, OpenIdConnectDefaults.AuthenticationScheme, context.Properties); context.HandleCodeRedemption(); }, OnRedirectToIdentityProvider = context => { // 仅当会话过期(超出MaxAge)时才触发重定向,令牌过期由MSAL静默处理 if (!context.Properties.IsRefreshTokenExpired()) { context.Response.StatusCode = StatusCodes.Status401Unauthorized; context.HandleResponse(); } return Task.CompletedTask; } };
- 通过ITokenAcquisition自动刷新令牌
调用下游API时,使用ITokenAcquisition主动获取令牌,MSAL会自动检测令牌过期并静默刷新:
// 在Graph Middleware中注入ITokenAcquisition public async Task InvokeAsync(HttpContext context, UserManager<ApplicationUser> userManager, GraphServiceClient graphServiceClient, ITokenAcquisition tokenAcquisition, IConfiguration config) { var page = context.GetRouteValue("page")?.ToString(); if (!page.IsNullOrEmpty() && page.Equals("/Disclaimer") && context.User.Identity?.IsAuthenticated == true) { // 自动获取/刷新令牌 var accessToken = await tokenAcquisition.GetAccessTokenForUserAsync( config.GetValue<string>("GraphApi:Scopes")?.Split(' ')); // 使用刷新后的令牌调用Graph var user = await graphServiceClient.Me .Request() .WithAccessToken(accessToken) .GetAsync() .ConfigureAwait(false); } }
- 确认会话与缓存配置有效性
保持现有会话配置的MaxAge=12小时、SlidingExpiration=false,确保会话严格12小时后过期;同时保留AddSessionTokenCaches(),让刷新令牌存储在会话中供静默刷新使用。
二、处理AJAX请求的302重定向与CORS问题
通过后端返回401替代302,配合前端拦截处理,解决AJAX场景的CORS问题:
- 后端配置:识别AJAX请求并返回401
修改AddMicrosoftIdentityWebApp的事件配置,针对AJAX请求返回401而非重定向:
services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme) .AddMicrosoftIdentityWebApp(authOptions => { config.Bind("AzureAD", authOptions); authOptions.MaxAge = TimeSpan.FromHours(12); authOptions.SaveTokens = true; authOptions.Events.OnRedirectToIdentityProvider = context => { if (IsAjaxRequest(context.Request)) { context.Response.StatusCode = StatusCodes.Status401Unauthorized; context.HandleResponse(); } return Task.CompletedTask; }; authOptions.Events.OnRedirectToIdentityProviderForSignOut = context => { if (IsAjaxRequest(context.Request)) { context.Response.StatusCode = StatusCodes.Status401Unauthorized; context.HandleResponse(); } return Task.CompletedTask; }; }, sessionOptions => { sessionOptions.Cookie.MaxAge = TimeSpan.FromHours(12); sessionOptions.Cookie.Name = "Custom-Cookie-Name"; sessionOptions.ExpireTimeSpan = TimeSpan.FromHours(12); sessionOptions.SlidingExpiration = false; }) .EnableTokenAcquisitionToCallDownstreamApi(config.GetValue<string>("GraphApi:Scopes")?.Split(' ')) .AddMicrosoftGraph(config.GetSection("GraphApi")) .AddSessionTokenCaches(); // 辅助方法:判断是否为AJAX请求 private static bool IsAjaxRequest(HttpRequest request) { return request.Headers["X-Requested-With"] == "XMLHttpRequest" || request.Headers["Accept"].Contains("application/json"); }
- 前端配置:拦截401响应并处理
在前端AJAX请求中添加全局拦截,根据会话状态处理401:
// jQuery示例 $.ajaxSetup({ statusCode: { 401: function() { // 检查会话Cookie是否存在,区分令牌过期还是会话过期 if (document.cookie.includes("Custom-Cookie-Name")) { // 调用静默刷新接口 $.get("/api/token/refresh-token") .done(function() { // 刷新成功后重新发起原请求或刷新页面 window.location.reload(); }) .fail(function() { // 静默刷新失败,跳转登录页 window.location.href = "/Account/Login"; }); } else { // 会话已过期,直接跳转登录页 window.location.href = "/Account/Login"; } } } });
- 添加静默刷新API端点(可选)
如果需要主动触发静默刷新,添加一个API控制器:
[ApiController] [Route("api/token")] public class TokenController : ControllerBase { private readonly ITokenAcquisition _tokenAcquisition; private readonly IConfiguration _config; public TokenController(ITokenAcquisition tokenAcquisition, IConfiguration config) { _tokenAcquisition = tokenAcquisition; _config = config; } [HttpGet("refresh-token")] public async Task<IActionResult> RefreshToken() { try { await _tokenAcquisition.GetAccessTokenForUserAsync( _config.GetValue<string>("GraphApi:Scopes")?.Split(' ')); return Ok(); } catch (MsalUiRequiredException) { // 需要用户交互,返回401 return Unauthorized(); } } }
三、额外注意事项
- 确认Azure AD应用注册中已启用刷新令牌(默认启用,需确保权限类型为
Delegated) - 升级Microsoft.Identity.Web到最新稳定版,确保特性兼容性
- 测试时可临时缩短
MaxAge(如10分钟)验证会话过期逻辑
内容的提问来源于stack exchange,提问作者Nakorr
相关产品推荐
相关产品推荐

