【问题标题】:Cannot send request to web application after Authentication Ticket was expired身份验证票证过期后无法向 Web 应用程序发送请求
【发布时间】:2021-10-29 09:19:24
【问题描述】:

我的web应用是Microsoft.NET.Sdk.Web项目,目标框架是net5.0。 我正在使用 Redis 来缓存 AuthenticationTicket。实现如下。

public class RedisCacheTicketStore : ITicketStore
{
    private readonly RedisCacheService cache;
    private readonly IConfiguration configuration;
    private readonly ILogger logger;

    public RedisCacheTicketStore(
        RedisCacheService redisCacheService, 
        IConfiguration configuration, 
        ILogger logger)
    {
        this.cache = redisCacheService;
        this.configuration = configuration;
        this.logger = logger;
    }

    public async Task<string> StoreAsync(AuthenticationTicket ticket)
    {
        var key = $"AuthSessionStore-{Guid.NewGuid()}";

        await RenewAsync(key, ticket);

        logger.Debug("The ticket {Key} was stored.", key);

        return key;
    }

    public Task RenewAsync(string key, AuthenticationTicket ticket)
    {
        var timeToLive = TimeSpan.FromMinutes(5); // For testing purpose
        //var timeToLive = TimeSpan.FromMinutes(configuration.GetValue("SessionCookieLifetimeMinutes", 60));

        var bytes = SerializeToBytes(ticket);

        cache.Set(key, bytes, timeToLive);

        logger.Debug("The ticket was renew and will be expire at {ExpiresAtUtc}", ticket.Properties.ExpiresUtc);

        return Task.CompletedTask;
    }

    public Task<AuthenticationTicket> RetrieveAsync(string key)
    {
        var bytes = cache.Get<byte[]>(key);
        var ticket = DeserializeFromBytes(bytes);

        logger.Debug("The ticket {Key} was retrieved.", key);

        return Task.FromResult(ticket);
    }

    public Task RemoveAsync(string key)
    {
        cache.Remove(key);

        logger.Debug("The ticket {Key} was removed.", key);

        return Task.CompletedTask;
    }

    private static byte[] SerializeToBytes(AuthenticationTicket source)
    {
        return TicketSerializer.Default.Serialize(source);
    }

    private static AuthenticationTicket DeserializeFromBytes(byte[] source)
    {
        return source == null ? null : TicketSerializer.Default.Deserialize(source);
    }
}

我在 Startup 类中注册 RedisCacheTicketStore 作为 ITicketStore 的自定义实现,如下所示。

public partial class Startup
{
    private void AddAuthentication(IServiceCollection services)
    {
        var sessionCookieLifetime = Configuration.GetValue("SessionCookieLifetimeMinutes", 60);

        services
            .AddAuthentication(options =>
            {
                options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
                options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
            })
            .AddCookie(options =>
            { 
                options.ExpireTimeSpan = TimeSpan.FromMinutes(2);
                options.SlidingExpiration = false; // For testing purpose
            })
            .AddOpenIdConnect(options =>
            {
                options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
                options.Authority = ApplicationSettings.AdalSettings.Authority;
                options.ClientId = ApplicationSettings.AdalSettings.ClientId;
                options.ResponseType = OpenIdConnectResponseType.CodeIdToken;  
                options.SaveTokens = true;
                options.ClientSecret = ApplicationSettings.AdalSettings.AppKey;
                options.Resource = ApplicationSettings.AdalSettings.ULTrackerResourceId;
                options.Events = new OpenIdConnectEvents
                {
                    OnAuthenticationFailed = OnAuthenticationFailed,
                    OnTokenValidated = OnSecurityTokenValidated
                };
            });

        services
            .AddOptions<CookieAuthenticationOptions>(CookieAuthenticationDefaults.AuthenticationScheme)
            .Configure<ITicketStore>((options, store) => options.SessionStore = store);

        services.AddSingleton(provider => 
            new RedisCacheService(
                ConnectionMultiplexer.Connect(Configuration["RedisConfigurationString"])));

        services.AddSingleton<ITicketStore, RedisCacheTicketStore>();
    }

    private Task OnAuthenticationFailed(AuthenticationFailedContext context)
    {
        Serilog.Log.Logger.Error(context.Exception, "Authentication failed");

        context.HandleResponse();
        context.Response.Redirect("/Error/AuthenticationFailed");

        return Task.CompletedTask;
    }

    private async Task OnSecurityTokenValidated(TokenValidatedContext context)
    {
        try
        {
            var claimsIdentity = context.Principal.Identity as ClaimsIdentity;
            var roleClaims = context.Principal.Claims.SingleOrDefault(x => x.Type == Shared.Security.ClaimTypes.Role);

            if (roleClaims != null)
            {
                claimsIdentity.RemoveClaim(roleClaims);
            }

            var roles = await GetUserRolesAndPermissionsAsync(claimsIdentity.Name);

            claimsIdentity.AddRolesAndPermissions(roles);
        }
        catch (Exception exception) when (LogGetUserPermissionsException(exception))
        {
            throw;
        }

        async Task<Role[]> GetUserRolesAndPermissionsAsync(string userName)
        {
            var apiClient = new WebApiClient(
                ApplicationSettings.WebApiUrlAddressSettings.ULTrackerApiBaseAddress,
                new WebApiClientSettings());

            var response = await apiClient.GetAsyncWithFormattableUri($"users/permissions?username={userName}", needsAuthorize: false);
            response.EnsureSuccessStatusCode();

            var roles = await apiClient.ReadJsonContentAsync<Role[]>(response);
            return roles;
        }

        bool LogGetUserPermissionsException(Exception exception)
        {
            Serilog.Log.Logger.Error(exception, "An error has occurred while getting user permissions");
            return false;
        }
    }
}

我尝试将身份验证票证到期时间设置为 2 分钟,以查看 Cookie 票证到期时会发生什么。我可以按预期登录并访问受保护的页面。

AuthenticationTicket 存储在 Redis 中,如下所示

查看控制台中的日志时,到目前为止一切正常。

但 2 分钟后,Authentication Ticket 过期,我无法浏览任何应用程序的 URL。浏览器似乎无法向应用程序发送任何请求。

查看控制台中的日志时,没有任何反应。 我尝试刷新 URL“https://localhost:44327/Policy/Search”,浏览器提示“无法访问此站点。localhost 响应时间过长。”。

当我浏览 URL “https://localhost:44327”时也出现了同样的问题。我什至尝试使用 Postman 或 Firefox 或 Edge 发送请求,但控制台日志中没有任何反应。

有谁知道如何解决这个问题?如何处理分布式缓存中的 cookie 会话?我的期望是如果 cookie 过期,asp.net 核心框架应该返回登录页面。 非常感谢。

【问题讨论】:

  • 您的代码如何检查 cookie 数据是否过期?我的意思是可以添加一些逻辑代码,例如如果过期然后在验证 cookie 的地方重定向到登录 url。
  • @TinyWang 我只是将 Cookie 过期时间跨度设置为 2 分钟。我认为 .net 核心框架正在为我们处理过期逻辑。
  • 嗯,因为我不熟悉这个模块,你能告诉我你是按照哪个教程编写代码的吗?在我看来,应该有一些地方可以检查像过滤器一样工作的 cookie。您能否找到代码或配置,如果它们是注释,那么存储在 redis 中的数据将在您的项目中不起作用。这可能是解决问题的关键。
  • @TinyWang 我猜我的应用程序中某处存在无限循环。但我现在不知道如何检测。
  • 您是否在代码中添加断点来定位问题或发生错误时甚至没有进入控制器?

标签: asp.net-core authentication asp.net-core-mvc session-cookies openid-connect


【解决方案1】:

我找到了解决这个问题的方法。

调试的时候看到虽然我把AuthenticationTicket的ExpireTime设置为2分钟,但是Cookie的Expire Time为null。

在浏览器中创建 cookie 时,Expries/Max-Age 的值为“Session”。我不明白这是什么意思。

我猜当身份验证票过期时,浏览器向应用程序发送请求,但 asp.net 核心中间件不知何故忽略了 cookie,然后不响应浏览器。

我尝试如下明确设置 Cookie Expiration,然后它按预期工作。

.AddCookie(options =>
            {
                options.Cookie.MaxAge = TimeSpan.FromMinutes(2);
                options.ExpireTimeSpan = TimeSpan.FromMinutes(2);
                options.SlidingExpiration = false; // For testing purpose
            })

查看浏览器的 Cookie 时,我看到 Expires/Max-Age 现在有一个明确的日期时间。 Authentication Ticket 过期后,我刷新页面,发现 cookie 更新为新的 Expires/Max-Age。

查看浏览器网络时,我看到应用程序确实向身份服务器发送请求以验证令牌并更新 cookie。

但我仍然不明白 Cookie“会话”和 Cookie 有明确的 Expires/Max-Age 之间有什么区别。有人可以帮忙解释一下吗?

我不确定这个问题是 asp.net core 的错误还是设计的行为。

非常感谢。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-05-01
    • 1970-01-01
    • 2019-03-29
    • 1970-01-01
    • 2014-04-02
    相关资源
    最近更新 更多