You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

.NET 6 Razor Pages集成Azure AD:静默令牌刷新与CORS问题解决

问题描述

我正在为.NET 6.0 Razor Pages Web应用集成Azure AD认证,已完成应用注册并实现用户登录,但面临令牌过期相关问题:

  1. 当前令牌约1小时过期,用户操作时会触发/authorize端点调用并刷新页面,中断体验;期望实现12小时无中断会话,令牌后台自动静默刷新,12小时后需重新登录。
  2. 令牌过期时用户发起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令牌静默刷新能力,通过调整配置和令牌获取逻辑即可实现:

  1. 调整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;
    }
};
  1. 通过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);
    }
}
  1. 确认会话与缓存配置有效性
    保持现有会话配置的MaxAge=12小时、SlidingExpiration=false,确保会话严格12小时后过期;同时保留AddSessionTokenCaches(),让刷新令牌存储在会话中供静默刷新使用。

二、处理AJAX请求的302重定向与CORS问题

通过后端返回401替代302,配合前端拦截处理,解决AJAX场景的CORS问题:

  1. 后端配置:识别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");
}
  1. 前端配置:拦截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";
            }
        }
    }
});
  1. 添加静默刷新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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.05 00:46:00