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

ASP.NET Core 6 MVC支付网关多屏验证:无Session/TempData实现单端登录

解决ASP.NET Core 6 MVC支付网关的会话唯一性与页面访问限制问题

针对你提出的不使用Session/TempData、限制同一账号多标签/浏览器访问指定页面、新登录自动注销旧会话的需求,以下是具体实现方案:

一、实现会话唯一性(新登录自动注销旧会话)

核心思路是给每个登录会话分配唯一标识,并存入分布式缓存(或数据库),每次请求验证当前会话标识是否与缓存中存储的有效标识一致,不一致则强制注销。

1. 登录时生成并绑定会话标识

在登录逻辑中,生成唯一SessionId,将其与用户ID关联存入缓存,同时把SessionId加入认证票据的Claims:

public async Task<IActionResult> Login(LoginViewModel model)
{
    // 省略用户身份验证逻辑
    var sessionId = Guid.NewGuid().ToString();
    // 缓存键格式:UserSession:用户ID,值为当前有效SessionId,过期时间与认证有效期匹配
    await _distributedCache.SetStringAsync($"UserSession:{model.UserId}", sessionId, 
        new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(2) });

    var claims = new List<Claim>
    {
        new Claim(ClaimTypes.NameIdentifier, model.UserId),
        new Claim("SessionId", sessionId)
    };

    var identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
    await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(identity));

    return RedirectToAction("Index", "Home");
}

2. 全局中间件验证会话有效性

创建自定义中间件,在每次请求时校验会话标识:

public class SessionValidationMiddleware
{
    private readonly RequestDelegate _next;
    private readonly IDistributedCache _cache;

    public SessionValidationMiddleware(RequestDelegate next, IDistributedCache cache)
    {
        _next = next;
        _cache = cache;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        if (context.User.Identity.IsAuthenticated)
        {
            var userId = context.User.FindFirstValue(ClaimTypes.NameIdentifier);
            var currentSessionId = context.User.FindFirstValue("SessionId");
            var validSessionId = await _cache.GetStringAsync($"UserSession:{userId}");

            if (currentSessionId != validSessionId)
            {
                await context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
                context.Response.Redirect("/Account/Login?reason=session_expired");
                return;
            }
        }

        await _next(context);
    }
}

// 在Program.cs中注册中间件(放在认证中间件之后)
app.UseAuthentication();
app.UseAuthorization();
app.UseMiddleware<SessionValidationMiddleware>();

当用户在新设备/标签页登录时,缓存中的SessionId会被更新,旧会话的标识匹配失败,自动触发注销。

二、禁止同一账号多标签/浏览器打开同一页面

通过给每个页面实例分配唯一标识,结合前端本地存储和后端缓存验证,实现页面的单实例访问限制。

1. 页面加载时生成并锁定实例

在目标页面(如支付页)的OnGet方法中,检查是否已有有效页面实例,若无则生成唯一PageInstanceId并存入缓存:

public async Task<IActionResult> OnGet()
{
    var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
    var pageLockKey = $"PageLock:{userId}:{Request.Path}";
    var existingInstanceId = await _distributedCache.GetStringAsync(pageLockKey);

    if (!string.IsNullOrEmpty(existingInstanceId))
    {
        // 已有页面实例,跳转至首页并提示
        return RedirectToAction("Index", "Home", new { msg = "该页面已在其他标签页打开" });
    }

    var pageInstanceId = Guid.NewGuid().ToString();
    await _distributedCache.SetStringAsync(pageLockKey, pageInstanceId, 
        new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30) });

    ViewData["PageInstanceId"] = pageInstanceId;
    return Page();
}

2. 前端验证与锁释放逻辑

在页面JS中,存储页面实例标识并验证,同时在页面卸载时通知后端释放锁:

const pageInstanceId = '@ViewData["PageInstanceId"]';
const pageLockKey = 'PaymentPage_Lock';

// 页面加载时校验是否已有有效实例
window.addEventListener('load', () => {
    const storedId = localStorage.getItem(pageLockKey);
    if (storedId && storedId !== pageInstanceId) {
        alert('该页面已在其他标签页打开,请关闭后重试');
        window.location.href = '/Home/Index';
        return;
    }
    localStorage.setItem(pageLockKey, pageInstanceId);
});

// 页面卸载时请求后端释放页面锁
window.addEventListener('beforeunload', async () => {
    await fetch('/Payment/ReleasePageLock', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ PageInstanceId: pageInstanceId })
    });
});

3. 后端释放页面锁接口

添加接口用于清除缓存中的页面锁:

[HttpPost]
public async Task<IActionResult> ReleasePageLock([FromBody] PageLockRequest request)
{
    var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
    var pageLockKey = $"PageLock:{userId}:{Request.Path}";
    var storedId = await _distributedCache.GetStringAsync(pageLockKey);

    if (storedId == request.PageInstanceId)
    {
        await _distributedCache.RemoveAsync(pageLockKey);
    }
    return Ok();
}

public class PageLockRequest
{
    public string PageInstanceId { get; set; }
}

关键注意点

  • 缓存选型:优先使用Redis等分布式缓存,避免单服务器部署时的缓存不一致问题;单服务器场景可使用内存缓存,但需注意服务器重启会丢失缓存数据。
  • 过期策略:合理设置会话和页面锁的过期时间,避免资源长期占用;也可结合前端心跳请求主动刷新过期时间。
  • 异常处理:缓存不可用时,建议添加降级逻辑(如允许访问并记录告警日志),避免影响核心业务流程。

内容的提问来源于stack exchange,提问作者Devansh Soni

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.07 19:32:03