身份验证票证过期后无法向 Web 应用程序发送请求
Cannot send request to web application after Authentication Ticket was expired
我的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 class 中将 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;
}
}
}
我尝试将 Authentication Ticket Expire Time 设置为 2 分钟,以查看 Cookie Ticket 过期时会发生什么。我可以按预期登录并访问受保护的页面。
AuthenticationTicket 存储在 Redis 中如下
在控制台中查看日志时,到目前为止一切正常。
但是 2 分钟后,Authentication Ticket 过期了,我无法浏览任何 URL 的应用程序。浏览器似乎无法向应用程序发送任何请求。
查看控制台中的日志时,没有任何反应。
我尝试刷新 URL“https://localhost:44327/Policy/Search”,浏览器显示“无法访问此站点。本地主机响应时间过长。”。
当我浏览 URL "https://localhost:44327" 时出现同样的问题。我什至尝试使用 Postman、Firefox 或 Edge 发送请求,但控制台日志中没有任何反应。
有谁知道如何解决这个问题?如何处理分布式缓存中的 cookie 会话?我的期望是,如果 cookie 过期,asp.net 核心框架应该 return 登录页面。
非常感谢。
我找到了这个问题的解决方案。
调试时发现虽然我将AuthenticationTicket的ExpireTime设置为2分钟,但是Cookie的Expire Time为空
在浏览器中创建cookie时,Expries/Max-Age的值为“Session”。我不明白这是什么意思。
我想当身份验证票过期时,浏览器向应用程序发送请求,但 asp.net 核心中间件以某种方式忽略了 cookie,然后不响应浏览器。
我尝试如下所示明确设置 Cookie 过期时间,然后它按预期工作。
.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“Session”和Cookie has explicitExpires/Max-Age有什么区别。谁能帮忙解释一下?
我不确定这个问题是 asp.net 核心的错误还是设计的行为。
非常感谢。
我的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 class 中将 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;
}
}
}
我尝试将 Authentication Ticket Expire Time 设置为 2 分钟,以查看 Cookie Ticket 过期时会发生什么。我可以按预期登录并访问受保护的页面。
AuthenticationTicket 存储在 Redis 中如下
在控制台中查看日志时,到目前为止一切正常。
但是 2 分钟后,Authentication Ticket 过期了,我无法浏览任何 URL 的应用程序。浏览器似乎无法向应用程序发送任何请求。
查看控制台中的日志时,没有任何反应。 我尝试刷新 URL“https://localhost:44327/Policy/Search”,浏览器显示“无法访问此站点。本地主机响应时间过长。”。
当我浏览 URL "https://localhost:44327" 时出现同样的问题。我什至尝试使用 Postman、Firefox 或 Edge 发送请求,但控制台日志中没有任何反应。
有谁知道如何解决这个问题?如何处理分布式缓存中的 cookie 会话?我的期望是,如果 cookie 过期,asp.net 核心框架应该 return 登录页面。 非常感谢。
我找到了这个问题的解决方案。
调试时发现虽然我将AuthenticationTicket的ExpireTime设置为2分钟,但是Cookie的Expire Time为空
在浏览器中创建cookie时,Expries/Max-Age的值为“Session”。我不明白这是什么意思。
我想当身份验证票过期时,浏览器向应用程序发送请求,但 asp.net 核心中间件以某种方式忽略了 cookie,然后不响应浏览器。
我尝试如下所示明确设置 Cookie 过期时间,然后它按预期工作。
.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“Session”和Cookie has explicitExpires/Max-Age有什么区别。谁能帮忙解释一下?
我不确定这个问题是 asp.net 核心的错误还是设计的行为。
非常感谢。