.NET Framework4.8 Webforms应用iframe内Azure AD令牌刷新被拦截问题
问题描述
我们有一个运行在.NET Framework 4.8上的遗留WebForms应用,已更新为使用Azure/Entra身份验证,基于微软官方模板开发,常规场景运行正常,但存在以下问题:
- 身份验证获取的ID令牌包含过期时间,Owin检测到令牌过期时会向
login.microsoftonline.com请求刷新令牌,常规场景正常,但页面在iframe中时出现异常 - 报错信息:
Refused to display 'https://login.microsoftonline.com/' in a frame because it set
<code>X-Frame-Options</code>to<code>deny</code>
- 异常时
SecurityTokenValidated事件反复触发陷入循环,最终iframe显示损坏链接并抛出上述错误 - 需求:在iframe场景下无需用户交互,静默刷新令牌并更新认证Cookie
参考Owin启动代码:
Public Sub ConfigureAuth(ByVal app As IAppBuilder) app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType) app.UseCookieAuthentication(New CookieAuthenticationOptions() With {.CookieSameSite = SameSiteMode.Lax, .CookieName = "appname.Auth"}) app.UseOpenIdConnectAuthentication(New OpenIdConnectAuthenticationOptions() With { .ClientId = clientId, .Authority = authority, .RedirectUri = redirectUri, .PostLogoutRedirectUri = postLogoutRedirectUri, .Notifications = New OpenIdConnectAuthenticationNotifications() With { .AuthenticationFailed = Function(context) System.Threading.Tasks.Task.FromResult(0), .SecurityTokenValidated = Function(context) Return OnSecurityTokenValidated(context) End Function }} ) app.UseStageMarker(PipelineStage.Authenticate) End Sub
解决方案
1. 配置OpenID Connect以获取刷新令牌
默认情况下,OpenID Connect中间件不会请求刷新令牌,需要显式设置Scope包含offline_access,同时切换为授权码流以获取刷新令牌:
app.UseOpenIdConnectAuthentication(New OpenIdConnectAuthenticationOptions() With { .ClientId = clientId, .Authority = authority, .RedirectUri = redirectUri, .PostLogoutRedirectUri = postLogoutRedirectUri, .Scope = OpenIdConnectScope.OpenIdProfile + " offline_access", ' 添加离线访问权限以获取刷新令牌 .ResponseType = OpenIdConnectResponseType.CodeIdToken, ' 使用授权码流 .Notifications = New OpenIdConnectAuthenticationNotifications() With { .AuthenticationFailed = Function(context) ' 捕获iframe相关错误,避免循环重定向 If context.Exception.Message.Contains("X-Frame-Options") Then context.HandleResponse() context.Response.Redirect("/Error/IframeAuthExpired") ' 自定义静默处理逻辑或错误页 End If Return System.Threading.Tasks.Task.FromResult(0) End Function, .AuthorizationCodeReceived = Async Function(context) ' 用授权码交换令牌,并存储刷新令牌到身份票据 Dim tokenClient = New TokenClient( $"{authority}/oauth2/token", clientId, clientSecret ' 需要在Entra应用注册中配置客户端密钥 ) Dim tokenResponse = Await tokenClient.RequestAuthorizationCodeAsync( context.Code, redirectUri ) ' 将刷新令牌存入ClaimsIdentity context.AuthenticationTicket.Identity.AddClaim( New Claim("refresh_token", tokenResponse.RefreshToken) ) Return System.Threading.Tasks.Task.FromResult(0) End Function, .SecurityTokenValidated = Function(context) Return OnSecurityTokenValidated(context) End Function }} )
2. 自定义Cookie认证的静默刷新逻辑
修改CookieAuthenticationOptions,添加OnValidateIdentity事件,在令牌即将过期时使用刷新令牌静默获取新令牌,避免触发重定向:
app.UseCookieAuthentication(New CookieAuthenticationOptions() With { .CookieSameSite = SameSiteMode.None, ' iframe场景下需设置为None,同时确保HTTPS环境 .CookieName = "appname.Auth", .CookieSecure = CookieSecureOption.Always, ' 生产环境强制HTTPS .SlidingExpiration = True, .ExpireTimeSpan = TimeSpan.FromMinutes(30), .Provider = New CookieAuthenticationProvider() With { .OnValidateIdentity = Async Function(context) ' 检查ID令牌是否即将过期 Dim expClaim = context.Identity.FindFirst(ClaimTypes.Expiration) If expClaim IsNot Nothing Then Dim expirationDate = DateTimeOffset.Parse(expClaim.Value) ' 提前5分钟触发刷新 If expirationDate <= DateTimeOffset.UtcNow.AddMinutes(5) Then ' 从Claims中取出刷新令牌 Dim refreshToken = context.Identity.FindFirst("refresh_token")?.Value If Not String.IsNullOrEmpty(refreshToken) Then Try ' 使用刷新令牌获取新的ID令牌 Dim tokenClient = New TokenClient( $"{authority}/oauth2/token", clientId, clientSecret ) Dim tokenResponse = Await tokenClient.RequestRefreshTokenAsync(refreshToken) ' 更新身份票据中的令牌信息 Dim newIdentity = New ClaimsIdentity(context.Identity) ' 替换过期时间和ID令牌 newIdentity.RemoveClaim(newIdentity.FindFirst(ClaimTypes.Expiration)) newIdentity.AddClaim(New Claim(ClaimTypes.Expiration, DateTimeOffset.UtcNow.AddSeconds(tokenResponse.ExpiresIn).ToString())) newIdentity.RemoveClaim(newIdentity.FindFirst("id_token")) newIdentity.AddClaim(New Claim("id_token", tokenResponse.IdToken)) ' 更新刷新令牌(若返回新的刷新令牌) If Not String.IsNullOrEmpty(tokenResponse.RefreshToken) Then newIdentity.RemoveClaim(newIdentity.FindFirst("refresh_token")) newIdentity.AddClaim(New Claim("refresh_token", tokenResponse.RefreshToken)) End If ' 替换当前身份并标记需要续签Cookie context.ReplaceIdentity(newIdentity) context.ShouldRenew = True Catch ex As Exception ' 刷新失败时标记身份无效,后续可触发静默提示 context.RejectIdentity() End Try End If End If End If Return System.Threading.Tasks.Task.FromResult(0) End Function } })
3. 补充iframe场景适配
- 前端页面加载前,通过AJAX调用后端专门的刷新接口,提前检测并更新令牌,避免触发Owin的自动重定向逻辑
- 确保后端接口返回的Cookie设置
SameSite=None和Secure属性,适配iframe跨域场景 - 在
AuthenticationFailed事件中捕获iframe相关错误时,优先返回静默处理逻辑,而非重定向到登录页
4. Azure Entra配置要点
- 应用注册中需开启授权码流(在「认证」->「高级设置」中启用)
- 客户端密钥需妥善存储,避免硬编码(可使用配置文件或Azure Key Vault)
- 确保
RedirectUri已添加到应用注册的允许回调地址列表中
内容的提问来源于stack exchange,提问作者n4esa
相关产品推荐
相关产品推荐

