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
相关产品推荐
相关产品推荐

