单订阅并发用户管控:页面异常关闭未登出问题解决方案咨询
高效解决未主动登出导致的并发登录名额占用问题
针对你遇到的用户直接关闭页面/浏览器未触发登出、占用并发名额的问题,结合jQuery前端和C#后端技术栈,推荐心跳延迟回收+优化beforeunload请求的组合方案,兼顾性能和需求:
核心思路
不用实时Socket监控,而是通过「前端心跳维持会话活跃状态」+「后端延迟回收机制」处理异常关闭场景,同时优化页面卸载时的登出请求,尽量区分刷新和关闭操作,双保险避免名额浪费。
前端实现(jQuery)
1. 优化beforeunload,区分刷新与关闭并发送可靠登出请求
利用navigator.sendBeacon替代普通AJAX,确保页面卸载时请求能被浏览器优先处理;同时通过浏览器API判断是否为刷新操作,避免误触发登出:
let isPageRefresh = false; // 监听页面卸载事件 window.addEventListener('beforeunload', function(e) { // 判断是否为刷新操作(兼容不同浏览器API) isPageRefresh = performance.navigation?.type === 1 || navigation?.type === 'reload'; if (!isPageRefresh) { // 用sendBeacon发送登出请求,参数需包含当前会话ID const logoutUrl = '/api/auth/logout'; const sessionId = '<从Cookie/页面存储中获取当前会话ID>'; navigator.sendBeacon(logoutUrl, JSON.stringify({ sessionId: sessionId })); } });
2. 定时心跳,维持会话活跃状态
页面加载后定时发送心跳请求,告诉后端用户仍在使用页面,避免被误判为异常关闭:
// 每3分钟发送一次心跳 setInterval(function() { const sessionId = '<从Cookie/页面存储中获取当前会话ID>'; $.post('/api/auth/heartbeat', { sessionId: sessionId }); }, 180000); // 180000毫秒 = 3分钟 // 可选:切后台时额外发送一次心跳,延长会话有效期 document.addEventListener('visibilitychange', function() { if (document.hidden) { const sessionId = '<从Cookie/页面存储中获取当前会话ID>'; $.post('/api/auth/heartbeat', { sessionId: sessionId }); } });
后端实现(C#)
1. 会话信息存储与管理
用Redis或数据库存储会话信息(生产环境禁止用静态字典),每个会话记录以下字段:
SessionId:会话唯一标识SubscriptionId:所属订阅IDLastActiveTime:最后活跃时间(心跳/操作更新)ExpireTime:预计回收时间IsLoggedOut:是否主动登出
示例代码(以内存字典为例,生产替换为分布式存储):
public class SessionInfo { public string SubscriptionId { get; set; } public DateTime LastActiveTime { get; set; } public DateTime ExpireTime { get; set; } public bool IsLoggedOut { get; set; } } // 建议用Redis替代内存字典,支持多实例部署 private static readonly ConcurrentDictionary<string, SessionInfo> _sessionStore = new();
2. 登录前的并发校验
用户登录时,先清理过期会话,再统计当前有效活跃会话数:
public bool CanUserLogin(string subscriptionId) { // 先清理已过期的会话 CleanExpiredSessions(); // 统计当前未注销、未过期的活跃会话数 var activeSessionCount = _sessionStore.Values.Count(s => s.SubscriptionId == subscriptionId && !s.IsLoggedOut && s.ExpireTime > DateTime.UtcNow); return activeSessionCount < 3; } // 清理过期会话的方法 private void CleanExpiredSessions() { var expiredSessionIds = _sessionStore.Where(kv => kv.Value.ExpireTime <= DateTime.UtcNow) .Select(kv => kv.Key) .ToList(); foreach (var id in expiredSessionIds) { _sessionStore.TryRemove(id, out _); } }
3. 心跳与登出接口实现
// 心跳接口:更新会话活跃时间与过期时间 [HttpPost("api/auth/heartbeat")] public IActionResult Heartbeat([FromForm] string sessionId) { if (_sessionStore.TryGetValue(sessionId, out var session)) { session.LastActiveTime = DateTime.UtcNow; // 延长过期时间(比如设置为当前时间+5分钟) session.ExpireTime = DateTime.UtcNow.AddMinutes(5); return Ok(); } return NotFound("会话不存在"); } // 登出接口:处理前端sendBeacon的请求 [HttpPost("api/auth/logout")] public IActionResult Logout([FromBody] dynamic request) { string sessionId = request.sessionId; if (_sessionStore.TryGetValue(sessionId, out var session)) { session.IsLoggedOut = true; session.ExpireTime = DateTime.UtcNow; // 立即标记为过期 return Ok(); } return NotFound("会话不存在"); }
4. 定时任务清理过期会话
用.NET的IHostedService或Hangfire实现定时任务,比如每10分钟清理一次过期会话,释放占用的名额:
public class SessionCleanupService : IHostedService, IDisposable { private Timer _timer; private readonly ConcurrentDictionary<string, SessionInfo> _sessionStore; public SessionCleanupService(ConcurrentDictionary<string, SessionInfo> sessionStore) { _sessionStore = sessionStore; } public Task StartAsync(CancellationToken cancellationToken) { // 每10分钟执行一次清理 _timer = new Timer(CleanExpiredSessions, null, TimeSpan.Zero, TimeSpan.FromMinutes(10)); return Task.CompletedTask; } private void CleanExpiredSessions(object state) { var expiredSessionIds = _sessionStore.Where(kv => kv.Value.ExpireTime <= DateTime.UtcNow) .Select(kv => kv.Key) .ToList(); foreach (var id in expiredSessionIds) { _sessionStore.TryRemove(id, out _); } } public Task StopAsync(CancellationToken cancellationToken) { _timer?.Change(Timeout.Infinite, 0); return Task.CompletedTask; } public void Dispose() { _timer?.Dispose(); } }
额外优化建议
- 用户交互优化:当并发名额满时,后端可返回「当前已有3个活跃会话,是否强制注销最长时间未活跃的会话?」,让用户主动选择,提升体验。
- 会话ID存储:前端将会话ID存在HttpOnly Cookie中,避免XSS风险。
- 超时时间配置:心跳间隔、过期时间可根据业务场景调整,比如办公场景设为10分钟,公共场景设为3分钟。
内容的提问来源于stack exchange,提问作者Yash Lathiya
相关产品推荐
相关产品推荐

