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

IdentityServer多客户端多标签页令牌失效问题求助

问题分析与解决思路

问题现象

两个客户端连接同一IdentityServer,用户已完成登录。在标签页1打开客户端1,从客户端1打开新标签页访问客户端2,客户端2成功向IdentityServer请求并获取令牌。返回标签页1发起fetch请求时失败,提示令牌过期,必须刷新页面才能重新获取有效令牌。

错误日志

2022-12-15 13:57:33.6372|1|INFO|Microsoft.AspNetCore.Hosting.Diagnostics|Request starting HTTP/2.0 POST https://srv/administration/details?handler=ChecktName application/json; charset=UTF-8 74
2022-12-15 13:57:33.6372|7|INFO|Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationHandler|Cookies was not authenticated. Failure message: Unprotect ticket failed
2022-12-15 13:57:33.6372|2|INFO|Microsoft.AspNetCore.Authorization.DefaultAuthorizationService|Authorization failed.
2022-12-15 13:57:33.6372|12|INFO|Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectHandler|AuthenticationScheme: oidc was challenged.
2022-12-15 13:57:33.6372|2|INFO|Microsoft.AspNetCore.Hosting.Diagnostics|Request finished in 1.6358ms 302 

已尝试的无效方案

曾认为是Data Protection问题,在启动配置中添加以下代码,但未解决问题:

public void ConfigureServices(IServiceCollection services)
{
   ....
   services.AddDataProtection();
   ....
}

补充信息

  • IdentityServer和所有客户端运行在同一服务器、同一域名下,仅端口不同。
  • 检查发现新标签页中refresh token被重新生成,导致第一个标签页的refresh token过期失效。

IdentityServer配置

services
        .AddIdentityServer(IdentityServerOptions)
        .AddInMemoryApiResources(Config.GetApis())
        .AddInMemoryApiScopes(Config.GetScoops())
        .AddInMemoryIdentityResources(Config.GetIdentityResources())
        .AddInMemoryClients(Config.GetClients()))

public static IEnumerable<Client> GetClients(IConfigurationSection section)
{
    var clients = new List<Client>();
    var eClients = new List<PlugIn>();
    section.Bind(eClients);

    foreach (PlugIn plugIn in eClients)
    {
        string lUrl = plugIn.Url;
        if (!string.IsNullOrEmpty(lUrl) && lUrl.EndsWith("/"))
        {
            lUrl = lUrl.Substring(0, lUrl.Length - 1);
        }

        if (plugIn.ClientConfig != null)
        {
            var client = plugIn.ClientConfig;
            client.AllowedGrantTypes = GrantTypes.CodeAndClientCredentials;

            if (!client.AllowedScopes.Contains(IdentityServerConstants.StandardScopes.OpenId))
            {
                client.AllowedScopes.Add(IdentityServerConstants.StandardScopes.OpenId);
            }
            if (!client.AllowedScopes.Contains(IdentityServerConstants.StandardScopes.Profile))
            {
                client.AllowedScopes.Add(IdentityServerConstants.StandardScopes.Profile);
            }
            if (!client.AllowedScopes.Contains("roles"))
            {
                client.AllowedScopes.Add("roles");
            }

            client.AllowedScopes.Add(IdentityServerConstants.LocalApi.ScopeName);
            client.AllowedScopes.Add(IdentityServerConstants.StandardScopes.OfflineAccess);

            client.AllowOfflineAccess = true;
            client.FrontChannelLogoutSessionRequired = false;
            client.UpdateAccessTokenClaimsOnRefresh = true;
            AddOidcSignInUrl(lUrl, client);
            client.RedirectUris.Add(lUrl + "/swagger/oauth2-redirect.html");
            client.PostLogoutRedirectUris.Add(lUrl + "/signout-callback-oidc");
            client.FrontChannelLogoutUri = lUrl + "/Clientlogout";
            client.AlwaysIncludeUserClaimsInIdToken = true;
            client.AlwaysSendClientClaims = true;
            client.AbsoluteRefreshTokenLifetime = 86400;
            client.SlidingRefreshTokenLifetime = 43200;

            if (client.AccessTokenLifetime < 3600)
            {
                client.AccessTokenLifetime = 3600;
            }

            client.RefreshTokenUsage = TokenUsage.ReUse;
            client.RefreshTokenExpiration = TokenExpiration.Absolute;
            clients.Add(client);
        }
    }
    return clients;
}

客户端配置

.... 
services.AddAuthentication(options =>
{
    options.DefaultScheme = "Cookies";
    options.DefaultChallengeScheme = "oidc";
})
.AddCookie("Cookies", options =>
{
    options.ExpireTimeSpan = TimeSpan.FromDays(14);
    options.SlidingExpiration = true;
    options.SessionStore = new DictionaryTicketStore(services);
    options.Cookie.Path = "/";
})
.AddOpenIdConnect("oidc", options =>
{
    options.Authority = this.CentralHubAdresse;
    options.ClientId = Assembly.GetEntryAssembly().GetName().Name;
    options.ClientSecret = "####";
    options.ResponseType = "code";
    options.GetClaimsFromUserInfoEndpoint = true;

    options.RequireHttpsMetadata = true;
    options.BackchannelHttpHandler = GetHandler();
    options.Scope.Add("roles");
    options.Scope.Add("offline_access");
    options.Scope.Add("IdentityServerApi");

    options.ClaimActions.MapJsonKey("role", "role", "role");
    options.TokenValidationParameters.RoleClaimType = "role";
    options.SaveTokens = true;
    options.UseTokenLifetime = false;
});

解决思路

1. 统一Data Protection密钥配置

仅添加services.AddDataProtection()不足以让多个应用共享密钥,需将密钥持久化到共享存储,并统一应用名称,确保所有应用(IdentityServer、客户端1、客户端2)使用相同的密钥环解密Cookie。示例配置:

services.AddDataProtection()
    .PersistKeysToFileSystem(new DirectoryInfo(@"C:\SharedDataProtectionKeys")) // 或共享网络路径
    .SetApplicationName("YourSharedAppName");

所有相关应用都需要配置相同的SetApplicationName和密钥存储位置。

2. 调整Refresh Token策略

当前RefreshTokenUsage = TokenUsage.ReUse结合RefreshTokenExpiration = TokenExpiration.Absolute的配置,会导致同一用户同一客户端获取新授权时,旧refresh token被直接吊销。可改为令牌轮换+滑动过期策略,支持自动刷新并延长有效期:

client.RefreshTokenUsage = TokenUsage.OneTimeOnly;
client.RefreshTokenExpiration = TokenExpiration.Sliding;

该配置下每次刷新令牌都会生成新的refresh token,旧token立即失效,但滑动过期会根据刷新操作延长绝对过期时间,减少因多标签页操作导致的token失效问题。

3. 优化客户端Cookie配置

  • 检查自定义DictionaryTicketStore是否存在ticket持久化或解密问题,可暂时移除该配置,使用默认内存存储验证是否解决问题。
  • 为每个客户端配置唯一的Cookie名称,避免同域名下不同客户端的Cookie冲突:
    .AddCookie("Cookies", options =>
    {
        // 其他配置...
        options.Cookie.Name = "Client1_Auth_Cookie"; // 客户端2设置为不同名称
    })
    

4. 实现客户端自动令牌刷新

在客户端OpenID Connect配置中添加令牌自动刷新逻辑,在令牌即将过期前主动刷新,避免因token过期导致请求失败:

options.Events.OnTokenValidated = async context =>
{
    var expiresAtStr = context.Properties.GetTokenValue("expires_at");
    if (DateTime.TryParse(expiresAtStr, out var expiration))
    {
        var timeUntilExpire = expiration - DateTime.UtcNow;
        // 提前5分钟刷新令牌
        if (timeUntilExpire < TimeSpan.FromMinutes(5))
        {
            var refreshToken = context.Properties.GetTokenValue("refresh_token");
            var tokenClient = context.HttpContext.RequestServices.GetRequiredService<IHttpClientFactory>().CreateClient();
            
            var tokenResponse = await tokenClient.RequestRefreshTokenAsync(new RefreshTokenRequest
            {
                Address = $"{options.Authority}/connect/token",
                ClientId = options.ClientId,
                ClientSecret = options.ClientSecret,
                RefreshToken = refreshToken
            });

            if (!tokenResponse.IsError)
            {
                // 更新存储的令牌信息
                context.Properties.UpdateTokenValue("access_token", tokenResponse.AccessToken);
                context.Properties.UpdateTokenValue("refresh_token", tokenResponse.RefreshToken);
                context.Properties.UpdateTokenValue("expires_at", 
                    DateTime.UtcNow.AddSeconds(tokenResponse.ExpiresIn).ToString("o"));
                
                // 重新登录以更新Cookie
                await context.HttpContext.SignInAsync("Cookies", context.Principal, context.Properties);
            }
        }
    }
};

5. 检查IdentityServer会话管理

确认IdentityServer的会话配置,若需支持同一用户多会话并发,可调整会话过期时间,并确保FrontChannelLogoutSessionRequired配置符合业务需求,避免注销一个会话影响其他正常会话。


内容的提问来源于stack exchange,提问作者user7555530

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.06 11:45:34