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

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.XXX Cookie,当数量超过服务器请求头阈值时,触发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);

在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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.14 10:55:24