IdentityServer4单点登录偶发重定向循环问题求助
IdentityServer4 单点登录偶发重定向循环及400请求头过长问题
问题现象
- 基于IdentityServer4实现的SSO多数场景运行正常,偶发重定向循环,需重启应用才能恢复
- 循环持续后触发HTTP 400错误:
Bad request - Request Too Long(请求头过长) - 网络请求可见客户端在IdentityServer与
signin-oidc重定向页间反复跳转 - 应用监控显示流程:客户端首页返回401 →
signin-oidc返回302 → 调用IdentityServer的connect/token、connect/userinfo获取用户声明 → 回到首页再次返回401,循环往复(IdentityServer侧显示用户已认证,但客户端判定未认证)
排查结论
- 客户端为MVC应用,使用Session/TempData的功能模块是触发问题的核心场景
- 异常触发条件:用户访问Session/TempData模块后登出,或其他用户登录时,IdentityServer完成认证并成功调用
userinfo接口,但客户端未生成认证Cookie(AudDiscoveryAuth),首页请求因缺失该Cookie返回401,进而触发重定向循环 - 每次重定向都会新增
OpenIdConnect.nonce.XXXCookie,当数量超过服务器请求头阈值时,触发400错误
客户端配置代码
public void Configuration(IAppBuilder app) { string baseClientAddress = ConfigurationManager.AppSettings["ApplicationUrl"]; int slidingExpiryHrs = Convert.ToInt32(ConfigurationManager.AppSettings["SlidingExpiryHrs"]); int slidingExpiryMins = Convert.ToInt32(ConfigurationManager.AppSettings["SlidingExpiryMins"]); TimeSpan expireTimeSpan = new TimeSpan(slidingExpiryHrs, slidingExpiryMins, 0); JwtSecurityTokenHandler.DefaultInboundClaimTypeMap = new Dictionary<string, string>(); app.UseCookieAuthentication(new CookieAuthenticationOptions { AuthenticationType = DefaultAuthenticationType, CookieName = "AudDiscoveryAuth", ExpireTimeSpan = expireTimeSpan, SlidingExpiration = true }); app.UseOpenIdConnectAuthenticationPatched(new OpenIdConnectAuthenticationOptions { ClientId = "ratingspro.web", Authority = IdsvrConstants.BaseAddress, RedirectUri = baseClientAddress + "signin-oidc/", PostLogoutRedirectUri = baseClientAddress + "signout-callback-oidc/", ResponseType = "code id_token", Scope = "openid api1 ratingspro.webapi offline_access", UseTokenLifetime = false, SignInAsAuthenticationType = DefaultAuthenticationType, RequireHttpsMetadata = true, Notifications = new OpenIdConnectAuthenticationNotifications { AuthorizationCodeReceived = async n => { var client = HttpClientFactory.Create(); var tokenResponse = await client.RequestAuthorizationCodeTokenAsync(new AuthorizationCodeTokenRequest { Address = IdsvrConstants.TokenEndpoint, ClientId = "ratingspro.web", ClientSecret = "secret", Code = n.Code, RedirectUri = n.RedirectUri, }); if (tokenResponse.IsError) { LogHelper.LogMessage("RatingsproApp: Startup => tokenResponseError: " + tokenResponse.Error); throw new AuthenticationException(tokenResponse.Error); } var userInfoResponse = await client.GetUserInfoAsync(new UserInfoRequest { Address = IdsvrConstants.UserInfoEndpoint, Token = tokenResponse.AccessToken }); if (userInfoResponse.IsError) { throw new AuthenticationException(userInfoResponse.Error); } var claims = userInfoResponse.Claims; if (claims.Any(c => c.Type == "ApplicationAccessDenied")) { throw new AuthenticationException(claims.FirstOrDefault(c => c.Type == "ApplicationAccessDenied").Value); } // create new identity var id = new ClaimsIdentity(n.AuthenticationTicket.Identity.AuthenticationType); id.AddClaims(claims); id.AddClaim(new Claim("AccessToken", tokenResponse.AccessToken)); id.AddClaim(new Claim("expires_at", DateTime.Now.AddSeconds(tokenResponse.ExpiresIn).ToLocalTime().ToString(CultureInfo.InvariantCulture))); id.AddClaim(new Claim("refresh_token", tokenResponse.RefreshToken)); id.AddClaim(new Claim("id_token", n.ProtocolMessage.IdToken)); client.Dispose(); n.AuthenticationTicket = new AuthenticationTicket( new ClaimsIdentity(id.Claims, n.AuthenticationTicket.Identity.AuthenticationType, "name", "role"), n.AuthenticationTicket.Properties); }, RedirectToIdentityProvider = n => { if (n.ProtocolMessage.RequestType == OpenIdConnectRequestType.Logout) { var idTokenHint = n.OwinContext.Authentication.User.FindFirst("id_token").Value; n.ProtocolMessage.IdTokenHint = idTokenHint; } return Task.FromResult(0); }, AuthenticationFailed = x => { x.ProtocolMessage.RedirectUri = "/"; return Task.CompletedTask; } } }); }
解决方案
从问题根源和配置分析,问题核心在于Session与认证Cookie的冲突以及认证票据未正确持久化,可通过以下措施修复:
1. 分离Session与认证Cookie
MVC默认Session会复用应用主Cookie,易与认证Cookie产生冲突。修改Session配置,指定独立的Session Cookie:
// 在Startup的Configuration方法中添加 app.UseSession(new SessionStateOptions { CookieName = "AudDiscoverySession", IdleTimeout = TimeSpan.FromMinutes(30) });
2. 登出时清理所有关联数据
在登出逻辑中,不仅触发OIDC登出,还要清理Session和残留的nonce Cookie:
public ActionResult Logout() { // 销毁Session Session.Abandon(); // 清理所有OpenIdConnect nonce Cookie foreach (var cookieKey in Request.Cookies.AllKeys.Where(k => k.StartsWith("OpenIdConnect.nonce"))) { Response.Cookies[cookieKey].Expires = DateTime.Now.AddDays(-1); } // 触发OIDC登出流程 Request.GetOwinContext().Authentication.SignOut( DefaultAuthenticationType, OpenIdConnectAuthenticationDefaults.AuthenticationType); return RedirectToAction("Index", "Home"); }
3. 修复认证票据持久化逻辑
在AuthorizationCodeReceived通知中,确保认证票据的持久化属性正确设置:
// 替换原有AuthenticationTicket创建代码 var existingProperties = n.AuthenticationTicket.Properties; // 设置票据持久化及过期时间 existingProperties.IsPersistent = true; existingProperties.ExpiresUtc = DateTime.UtcNow.Add(expireTimeSpan); n.AuthenticationTicket = new AuthenticationTicket( new ClaimsIdentity(id.Claims, n.AuthenticationTicket.Identity.AuthenticationType, "name", "role"), existingProperties);
4. 自动清理旧nonce Cookie
在OIDC配置中添加MessageReceived通知,每次接收OIDC消息时清理旧的nonce Cookie:
// 在OpenIdConnectAuthenticationNotifications中添加 MessageReceived = n => { // 清理所有已存在的nonce Cookie foreach (var cookie in n.OwinContext.Request.Cookies.Where(c => c.Key.StartsWith("OpenIdConnect.nonce"))) { n.OwinContext.Response.Cookies.Delete(cookie.Key); } return Task.CompletedTask; }
5. 优化认证Cookie属性
确保认证Cookie的SameSite和Secure属性适配部署环境:
app.UseCookieAuthentication(new CookieAuthenticationOptions { // 现有配置... CookieSameSite = SameSiteMode.Lax, CookieSecure = CookieSecureOption.Always // 仅HTTPS环境启用 });
内容的提问来源于stack exchange,提问作者spherexmanish
相关产品推荐
相关产品推荐

