如何在ASP.NET Core中用缓存阻止并发登录?找不到Global.asax该怎么处理
ASP.NET Core 实现单用户单会话(替代传统Global.asax方案)
首先明确:ASP.NET Core 中没有 Global.asax——它采用模块化的中间件和依赖注入架构,替代了传统ASP.NET的全局事件模型。下面把你参考的「禁止并发登录」方案适配到ASP.NET Core中,同时翻译原方案的核心逻辑:
原参考方案核心逻辑(翻译自2008年的文章)
在传统ASP.NET里,开发者会在Global.asax的Session_Start事件中做以下操作:
- 获取当前登录用户的用户名(通过FormsAuthentication)
- 在Application全局状态中维护「用户名-活跃会话ID」的映射
- 当检测到用户已有活跃会话时,调用
Session.Abandon()销毁旧会话,强制用户只能保持一个登录状态
ASP.NET Core 适配实现步骤
Core里没有Application全局状态,我们用分布式缓存(替代Application)+ 身份验证事件(替代Global.asax事件)来实现,以下是具体代码:
1. 注册必要服务(Program.cs)
var builder = WebApplication.CreateBuilder(args); // 添加分布式缓存(开发用内存缓存,生产建议用Redis/SQL Server缓存) builder.Services.AddDistributedMemoryCache(); // 配置Session builder.Services.AddSession(options => { options.IdleTimeout = TimeSpan.FromMinutes(30); // 会话超时时间 options.Cookie.HttpOnly = true; options.Cookie.IsEssential = true; }); // 配置Cookie身份验证,并添加会话检查逻辑 builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) .AddCookie(options => { options.Events = new CookieAuthenticationEvents { // 用户登录时,检查并替换旧会话 OnSigningIn = async context => { var userId = context.Principal.FindFirstValue(ClaimTypes.NameIdentifier); var cache = context.HttpContext.RequestServices.GetRequiredService<IDistributedCache>(); // 获取用户已有的活跃会话ID var existingSessionId = await cache.GetStringAsync($"ActiveSession_{userId}"); if (!string.IsNullOrEmpty(existingSessionId)) { // 标记旧会话为无效 await cache.RemoveAsync($"SessionValid_{existingSessionId}"); } // 存储当前会话ID为用户的活跃会话 var currentSessionId = context.HttpContext.Session.Id; await cache.SetStringAsync($"ActiveSession_{userId}", currentSessionId); await cache.SetStringAsync($"SessionValid_{currentSessionId}", "true", new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30) }); }, // 每次请求验证会话有效性 OnValidatePrincipal = async context => { var userId = context.Principal.FindFirstValue(ClaimTypes.NameIdentifier); if (userId == null) return; var cache = context.HttpContext.RequestServices.GetRequiredService<IDistributedCache>(); var currentSessionId = context.HttpContext.Session.Id; // 检查当前会话是否是用户的活跃会话 var activeSessionId = await cache.GetStringAsync($"ActiveSession_{userId}"); var isSessionValid = await cache.GetStringAsync($"SessionValid_{currentSessionId}"); if (activeSessionId != currentSessionId || isSessionValid != "true") { // 会话无效,强制注销 context.RejectPrincipal(); await context.HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); } }, // 用户注销时清理缓存 OnSigningOut = async context => { var userId = context.Principal?.FindFirstValue(ClaimTypes.NameIdentifier); if (userId == null) return; var cache = context.HttpContext.RequestServices.GetRequiredService<IDistributedCache>(); var currentSessionId = context.HttpContext.Session.Id; await cache.RemoveAsync($"ActiveSession_{userId}"); await cache.RemoveAsync($"SessionValid_{currentSessionId}"); } }; }); // 添加MVC等其他服务 builder.Services.AddControllersWithViews();
2. 启用中间件(Program.cs)
要确保中间件顺序正确,Session必须在Authentication之前:
var app = builder.Build(); // ...(其他中间件,比如静态文件) app.UseSession(); // 先启用Session app.UseAuthentication(); // 再启用身份验证 app.UseAuthorization(); app.MapControllerRoute( name: "default", pattern: "{controller=Home}/{action=Index}/{id?}"); app.Run();
注意事项
- 生产环境缓存选择:不要用
AddDistributedMemoryCache,改用AddStackExchangeRedisCache或AddSqlServerCache,避免应用重启后会话数据丢失 - 扩展功能:可以修改逻辑支持多会话限制(比如允许用户同时登录2个设备),只需调整缓存中存储的会话ID列表即可
- 会话超时:确保缓存过期时间和Session超时时间保持一致,避免出现无效会话残留
内容的提问来源于stack exchange,提问作者sellavsert
相关产品推荐
相关产品推荐

