ASP.NET 单用户登录的经典解决方案,常见的需求是限制一个账号在同一时间只能有一个有效登录(即防止多地点、多浏览器同时登录同一账号)。下面是比较通用的实现思路和示例:


单用户登录经典解决方案思路

  1. 用户登录时生成唯一标识(如SessionID或Token),并保存到服务器(数据库或缓存)。
  2. 登录时验证该用户是否已登录(检查是否已有未失效的Token或SessionID)。
  3. 若已登录,则踢掉之前登录(让之前登录失效),或拒绝本次登录,根据需求选择。
  4. 每次请求时验证Token或SessionID是否匹配,若不匹配说明登录已被踢掉,强制登出。

具体实现方案示例(基于 ASP.NET MVC 或 WebForms)

假设数据库有 Users 表和一个用来存储登录状态的字段,比如 CurrentSessionId

1. 登录验证流程

// 用户登录成功后生成新的SessionId(Guid)
string newSessionId = Guid.NewGuid().ToString();

// 查询用户
var user = dbContext.Users.SingleOrDefault(u => u.UserName == inputUserName && u.Password == inputPassword);
if (user != null)
{
    // 如果user.CurrentSessionId不为空,则代表该用户已登录,选择踢掉或拒绝
    // 这里以踢掉为例,直接覆盖之前的SessionId

    user.CurrentSessionId = newSessionId;
    dbContext.SaveChanges();

    // 将newSessionId存入当前Session或Cookie
    Session["SessionId"] = newSessionId;

    // 登录成功
}
else
{
    // 登录失败
}

2. 每次请求验证(放在基类或全局过滤器)

protected void Application_AcquireRequestState(object sender, EventArgs e)
{
    var userName = /* 获取当前登录用户名 */;
    var sessionId = Session["SessionId"] as string;

    if (!string.IsNullOrEmpty(userName) && !string.IsNullOrEmpty(sessionId))
    {
        var user = dbContext.Users.SingleOrDefault(u => u.UserName == userName);
        if (user == null || user.CurrentSessionId != sessionId)
        {
            // SessionId不匹配,说明账号已被踢掉,执行强制退出
            Session.Clear();
            Response.Redirect("/Account/Login?message=LoggedOut");
        }
    }
}

3. 登出时清理SessionId

var user = dbContext.Users.SingleOrDefault(u => u.UserName == currentUserName);
if (user != null)
{
    user.CurrentSessionId = null;
    dbContext.SaveChanges();
}
Session.Clear();

额外建议

  • 使用缓存或分布式缓存(如 Redis)存储 SessionId,性能更好。
  • 结合 FormsAuthentication 或 ASP.NET Identity 统一管理身份验证。
  • 处理并发登录时的用户体验,比如通知用户“您的账号已在别处登录”。
  • 避免 Session 固定攻击,及时更新 SessionId。

好的!下面给你一个基于 ASP.NET MVC 和 ASP.NET Identity 的经典单用户登录限制完整示例,实现单账号只能同时在一个地方登录,后登录的会顶掉先登录的。


方案思路简述

  • 在用户登录时,生成一个唯一的登录令牌(LoginToken),保存到用户数据库字段和当前用户的 Claims中。
  • 每次请求时通过自定义中间件或过滤器验证当前用户的 LoginToken 是否和数据库一致。
  • 如果不一致,说明该账号在其他地方登录过,当前登录失效,要求重新登录。

代码示例(ASP.NET MVC + Identity)


1. 在 ApplicationUser 添加字段

public class ApplicationUser : IdentityUser
{
    // 新增字段,保存当前登录的唯一标识
    public string CurrentLoginToken { get; set; }
}

2. 登录时设置 LoginToken

public async Task<ActionResult> Login(LoginViewModel model)
{
    if (!ModelState.IsValid)
        return View(model);

    var user = await UserManager.FindAsync(model.UserName, model.Password);
    if (user != null)
    {
        // 生成新的登录令牌
        var newToken = Guid.NewGuid().ToString();

        // 更新数据库
        user.CurrentLoginToken = newToken;
        await UserManager.UpdateAsync(user);

        // 创建身份标识,并把登录令牌写入 Claims
        var identity = await UserManager.CreateIdentityAsync(user, DefaultAuthenticationTypes.ApplicationCookie);
        identity.AddClaim(new Claim("LoginToken", newToken));

        AuthenticationManager.SignOut(DefaultAuthenticationTypes.ApplicationCookie);
        AuthenticationManager.SignIn(new AuthenticationProperties { IsPersistent = model.RememberMe }, identity);

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

    ModelState.AddModelError("", "无效的用户名或密码");
    return View(model);
}

3. 自定义 OWIN 中间件或 MVC 过滤器验证 LoginToken

这里用 MVC 过滤器示例:

public class SingleLoginFilter : ActionFilterAttribute
{
    private UserManager<ApplicationUser> _userManager;

    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        var user = filterContext.HttpContext.User;
        if (user?.Identity?.IsAuthenticated ?? false)
        {
            _userManager = filterContext.HttpContext.GetOwinContext().GetUserManager<UserManager<ApplicationUser>>();
            var userId = user.Identity.GetUserId();
            var dbUser = _userManager.FindById(userId);

            var claimToken = ((ClaimsIdentity)user.Identity).FindFirst("LoginToken")?.Value;
            if (dbUser == null || string.IsNullOrEmpty(claimToken) || dbUser.CurrentLoginToken != claimToken)
            {
                // 登录失效,强制登出
                filterContext.HttpContext.GetOwinContext().Authentication.SignOut(DefaultAuthenticationTypes.ApplicationCookie);
                filterContext.Result = new RedirectResult("/Account/Login?message=AccountLoggedInElsewhere");
            }
        }
        base.OnActionExecuting(filterContext);
    }
}

在全局注册此过滤器FilterConfig.cs):

public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
    filters.Add(new HandleErrorAttribute());
    filters.Add(new SingleLoginFilter()); // 加入单点登录验证过滤器
}

4. 用户登出时清理 Token

public async Task<ActionResult> Logout()
{
    var userId = User.Identity.GetUserId();
    var user = await UserManager.FindByIdAsync(userId);
    if (user != null)
    {
        user.CurrentLoginToken = null;
        await UserManager.UpdateAsync(user);
    }

    AuthenticationManager.SignOut(DefaultAuthenticationTypes.ApplicationCookie);
    return RedirectToAction("Login", "Account");
}

总结

  • 登录时生成唯一 Token 并存库和 Claims
  • 每次请求校验 Token 是否匹配
  • 登出时清除 Token
  • 顶掉旧登录,保障单用户单会话

这里给你准备了一个简化版的 ASP.NET MVC(基于 .NET Framework)示例项目骨架,演示如何实现单用户登录(单会话)功能,包含:

  • 自定义 ApplicationUser 添加登录Token字段
  • 登录时生成Token并保存
  • 登录时将Token写入Claims
  • 全局过滤器验证Token一致性
  • 登出时清理Token

一、ApplicationUser.cs

using Microsoft.AspNet.Identity.EntityFramework;

public class ApplicationUser : IdentityUser
{
    // 新增登录Token字段
    public string CurrentLoginToken { get; set; }
}

二、AccountController.cs 关键方法

using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.Owin;
using Microsoft.Owin.Security;
using System;
using System.Security.Claims;
using System.Threading.Tasks;
using System.Web;
using System.Web.Mvc;

public class AccountController : Controller
{
    private ApplicationUserManager _userManager;
    private IAuthenticationManager AuthenticationManager => HttpContext.GetOwinContext().Authentication;

    public ApplicationUserManager UserManager
    {
        get { return _userManager ?? HttpContext.GetOwinContext().GetUserManager<ApplicationUserManager>(); }
        private set { _userManager = value; }
    }

    // 登录
    [HttpPost]
    [AllowAnonymous]
    [ValidateAntiForgeryToken]
    public async Task<ActionResult> Login(LoginViewModel model)
    {
        if (!ModelState.IsValid) return View(model);

        var user = await UserManager.FindAsync(model.UserName, model.Password);
        if (user != null)
        {
            // 生成登录Token
            var loginToken = Guid.NewGuid().ToString();
            user.CurrentLoginToken = loginToken;
            await UserManager.UpdateAsync(user);

            // 创建身份
            var identity = await UserManager.CreateIdentityAsync(user, DefaultAuthenticationTypes.ApplicationCookie);
            identity.AddClaim(new Claim("LoginToken", loginToken));

            AuthenticationManager.SignOut(DefaultAuthenticationTypes.ApplicationCookie);
            AuthenticationManager.SignIn(new AuthenticationProperties { IsPersistent = model.RememberMe }, identity);

            return RedirectToAction("Index", "Home");
        }
        ModelState.AddModelError("", "用户名或密码错误");
        return View(model);
    }

    // 登出
    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<ActionResult> Logout()
    {
        var userId = User.Identity.GetUserId();
        if (userId != null)
        {
            var user = await UserManager.FindByIdAsync(userId);
            if (user != null)
            {
                user.CurrentLoginToken = null;
                await UserManager.UpdateAsync(user);
            }
        }
        AuthenticationManager.SignOut(DefaultAuthenticationTypes.ApplicationCookie);
        return RedirectToAction("Login", "Account");
    }
}

三、单用户登录过滤器 SingleLoginFilter.cs

using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.Owin;
using System.Linq;
using System.Security.Claims;
using System.Web;
using System.Web.Mvc;

public class SingleLoginFilter : ActionFilterAttribute
{
    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        var user = filterContext.HttpContext.User;
        if (user?.Identity?.IsAuthenticated ?? false)
        {
            var userManager = filterContext.HttpContext.GetOwinContext().GetUserManager<ApplicationUserManager>();
            var userId = user.Identity.GetUserId();
            var dbUser = userManager.FindById(userId);
            var claimToken = ((ClaimsIdentity)user.Identity).FindFirst("LoginToken")?.Value;

            if (dbUser == null || string.IsNullOrEmpty(claimToken) || dbUser.CurrentLoginToken != claimToken)
            {
                // 踢出登录
                filterContext.HttpContext.GetOwinContext().Authentication.SignOut(DefaultAuthenticationTypes.ApplicationCookie);
                filterContext.Result = new RedirectResult("/Account/Login?message=AccountLoggedInElsewhere");
            }
        }
        base.OnActionExecuting(filterContext);
    }
}

四、注册过滤器(FilterConfig.cs)

public class FilterConfig
{
    public static void RegisterGlobalFilters(GlobalFilterCollection filters)
    {
        filters.Add(new HandleErrorAttribute());
        filters.Add(new SingleLoginFilter());
    }
}

五、说明

  • 登录时保存唯一 Token 到数据库和 Claims。
  • 每次请求验证 Token 是否和数据库一致。
  • Token不一致时,说明用户已在别处登录,当前登录被踢出。
  • 登出时清空 Token。

好的,下面是基于 ASP.NET Core Identity 实现单用户登录限制的完整示例骨架,帮助你实现“单账号同一时间只能一个有效登录”功能。


ASP.NET Core Identity 单用户登录实现方案


1. 修改 ApplicationUser(扩展IdentityUser)

在 ApplicationUser 类里新增字段,用于存储当前登录的 Token:

public class ApplicationUser : IdentityUser
{
    public string CurrentLoginToken { get; set; }
}

2. 登录逻辑 — 生成并保存 Token

在登录接口中(比如 AccountController 的 Login 方法),登录成功后生成一个新的 Token,保存到数据库,并写入用户 Claims:

public async Task<IActionResult> Login(LoginViewModel model)
{
    if (!ModelState.IsValid) return View(model);

    var user = await _userManager.FindByNameAsync(model.UserName);
    if (user != null && await _userManager.CheckPasswordAsync(user, model.Password))
    {
        // 生成登录Token
        var newToken = Guid.NewGuid().ToString();

        user.CurrentLoginToken = newToken;
        await _userManager.UpdateAsync(user);

        // 创建身份Claims,包含登录Token
        var claims = new List<Claim>
        {
            new Claim(ClaimTypes.Name, user.UserName),
            new Claim("LoginToken", newToken)
        };

        var identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
        var principal = new ClaimsPrincipal(identity);

        await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
        await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, principal, new AuthenticationProperties
        {
            IsPersistent = model.RememberMe
        });

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

    ModelState.AddModelError("", "用户名或密码错误");
    return View(model);
}

3. 中间件或过滤器验证 Token

编写中间件或 MVC 过滤器,在每个请求验证数据库中的 CurrentLoginToken 与 Cookie 中的 LoginToken 是否一致:

public class SingleLoginMiddleware
{
    private readonly RequestDelegate _next;

    public SingleLoginMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task Invoke(HttpContext context, UserManager<ApplicationUser> userManager)
    {
        if (context.User.Identity.IsAuthenticated)
        {
            var userName = context.User.Identity.Name;
            var user = await userManager.FindByNameAsync(userName);

            var claimToken = context.User.FindFirst("LoginToken")?.Value;

            if (user == null || user.CurrentLoginToken != claimToken)
            {
                // Token不匹配,退出登录
                await context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
                context.Response.Redirect("/Account/Login?message=AccountLoggedInElsewhere");
                return;
            }
        }

        await _next(context);
    }
}

注册中间件:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    // ... 其他中间件
    app.UseAuthentication();
    app.UseMiddleware<SingleLoginMiddleware>(); // 放在身份认证后面
    app.UseAuthorization();
    // ... 其他中间件
}

4. 登出时清空 Token

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Logout()
{
    var user = await _userManager.GetUserAsync(User);
    if (user != null)
    {
        user.CurrentLoginToken = null;
        await _userManager.UpdateAsync(user);
    }

    await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
    return RedirectToAction("Login", "Account");
}

5. 总结

  • 登录时为用户生成唯一登录Token,保存数据库并写入Cookie Claims
  • 每次请求时中间件校验Token是否匹配
  • 不匹配则强制退出登录,防止多地多点登录
  • 登出时清空Token