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

单订阅并发用户管控:页面异常关闭未登出问题解决方案咨询

高效解决未主动登出导致的并发登录名额占用问题

针对你遇到的用户直接关闭页面/浏览器未触发登出、占用并发名额的问题,结合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:所属订阅ID
  • LastActiveTime:最后活跃时间(心跳/操作更新)
  • 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();
    }
}

额外优化建议

  1. 用户交互优化:当并发名额满时,后端可返回「当前已有3个活跃会话,是否强制注销最长时间未活跃的会话?」,让用户主动选择,提升体验。
  2. 会话ID存储:前端将会话ID存在HttpOnly Cookie中,避免XSS风险。
  3. 超时时间配置:心跳间隔、过期时间可根据业务场景调整,比如办公场景设为10分钟,公共场景设为3分钟。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.21 07:23:13