ASP.NET Core 7+JWT限制在线用户数(最多5-6人)方案咨询
ASP.NET Core 7 + JWT 实现用户登录数量限制方案
JWT本身是无状态的,没法直接追踪客户端在线状态,核心思路是在服务端记录每个用户的有效JWT Token,通过统计未失效、未过期的Token数量来实现登录数限制,登出时标记对应Token失效即可排除统计。下面是具体实现步骤:
1. 设计Token存储表
创建一个简单的数据库表,用来记录用户的有效Token信息:
public class UserActiveToken { public int Id { get; set; } public string UserId { get; set; } // 关联用户ID public string TokenId { get; set; } // JWT的jti声明(每个Token唯一) public DateTime ExpiresAt { get; set; } // Token过期时间 public bool IsRevoked { get; set; } // 是否已登出/失效 }
对应的EF Core配置(可选):
protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<UserActiveToken>() .HasIndex(t => new { t.UserId, t.TokenId }) .IsUnique(); }
2. 登录时校验并生成Token
用户登录时,先统计当前有效Token数量,超过上限则拒绝登录(或挤掉最早的Token),然后生成带唯一jti的JWT并保存记录:
[HttpPost("login")] public async Task<IActionResult> Login(LoginRequest request) { // 1. 验证用户账号密码(此处省略业务逻辑) var user = await _userManager.FindByEmailAsync(request.Email); if (user == null || !await _userManager.CheckPasswordAsync(user, request.Password)) { return Unauthorized("账号或密码错误"); } var userId = user.Id; // 2. 统计当前有效Token数量 var activeTokenCount = await _dbContext.UserActiveTokens .CountAsync(t => t.UserId == userId && !t.IsRevoked && t.ExpiresAt > DateTime.UtcNow); const int MaxLoginCount = 5; // 设置最大登录数 if (activeTokenCount >= MaxLoginCount) { // 可选逻辑:挤掉最早的有效Token var oldestToken = await _dbContext.UserActiveTokens .Where(t => t.UserId == userId && !t.IsRevoked && t.ExpiresAt > DateTime.UtcNow) .OrderBy(t => t.ExpiresAt) .FirstOrDefaultAsync(); if (oldestToken != null) { oldestToken.IsRevoked = true; await _dbContext.SaveChangesAsync(); } // 或者直接拒绝登录:return BadRequest("当前登录设备数量已达上限"); } // 3. 生成带jti的JWT Token var tokenHandler = new JwtSecurityTokenHandler(); var key = Encoding.ASCII.GetBytes(_configuration["Jwt:SecretKey"]); var tokenDescriptor = new SecurityTokenDescriptor { Subject = new ClaimsIdentity(new[] { new Claim(ClaimTypes.NameIdentifier, userId), new Claim(ClaimTypes.Email, user.Email) }), Expires = DateTime.UtcNow.AddHours(2), SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature), Jti = Guid.NewGuid().ToString() // 生成唯一Token标识 }; var token = tokenHandler.CreateToken(tokenDescriptor); var tokenString = tokenHandler.WriteToken(token); // 4. 保存Token记录到数据库 await _dbContext.UserActiveTokens.AddAsync(new UserActiveToken { UserId = userId, TokenId = token.Jti, ExpiresAt = tokenDescriptor.Expires.Value, IsRevoked = false }); await _dbContext.SaveChangesAsync(); return Ok(new { AccessToken = tokenString }); }
3. 登出时标记Token失效
用户登出时,根据Token的jti标记对应记录为失效,这样就不会被计入登录数统计:
[HttpPost("logout")] [Authorize] public async Task<IActionResult> Logout() { var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); var authHeader = HttpContext.Request.Headers["Authorization"].ToString(); if (!string.IsNullOrEmpty(authHeader) && authHeader.StartsWith("Bearer ")) { var tokenString = authHeader["Bearer ".Length..]; var tokenHandler = new JwtSecurityTokenHandler(); if (tokenHandler.CanReadToken(tokenString)) { var jwtToken = tokenHandler.ReadJwtToken(tokenString); var jti = jwtToken.Claims.First(c => c.Type == "jti").Value; var activeToken = await _dbContext.UserActiveTokens .FirstOrDefaultAsync(t => t.UserId == userId && t.TokenId == jti && !t.IsRevoked); if (activeToken != null) { activeToken.IsRevoked = true; await _dbContext.SaveChangesAsync(); } } } return Ok("登出成功"); }
4. 中间件校验Token有效性
因为JWT本身无状态,需要在每次请求时校验Token是否在有效记录中,防止已登出/过期的Token继续使用:
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidateAudience = true, ValidateLifetime = true, ValidateIssuerSigningKey = true, ValidIssuer = _configuration["Jwt:Issuer"], ValidAudience = _configuration["Jwt:Audience"], IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(_configuration["Jwt:SecretKey"])) }; options.Events = new JwtBearerEvents { OnTokenValidated = async context => { var userId = context.Principal.FindFirstValue(ClaimTypes.NameIdentifier); var jti = context.Principal.FindFirstValue("jti"); using var scope = context.HttpContext.RequestServices.CreateScope(); var dbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>(); var tokenRecord = await dbContext.UserActiveTokens .FirstOrDefaultAsync(t => t.UserId == userId && t.TokenId == jti); if (tokenRecord == null || tokenRecord.IsRevoked || tokenRecord.ExpiresAt < DateTime.UtcNow) { context.Fail("Token已失效"); } } }; });
5. 定时清理过期Token
为了避免数据库存储冗余,可添加一个定时任务定期清理过期的Token记录:
public class CleanExpiredTokensService : IHostedService, IDisposable { private readonly IServiceScopeFactory _scopeFactory; private Timer _timer; public CleanExpiredTokensService(IServiceScopeFactory scopeFactory) { _scopeFactory = scopeFactory; } public Task StartAsync(CancellationToken cancellationToken) { // 每天凌晨1点清理一次过期Token var now = DateTime.Now; var delay = TimeSpan.FromHours(1 - now.Hour) + TimeSpan.FromMinutes(1 - now.Minute) + TimeSpan.FromSeconds(1 - now.Second); if (delay < TimeSpan.Zero) { delay += TimeSpan.FromDays(1); } _timer = new Timer(DoCleanup, null, delay, TimeSpan.FromDays(1)); return Task.CompletedTask; } private void DoCleanup(object state) { using var scope = _scopeFactory.CreateScope(); var dbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>(); var expiredTokens = dbContext.UserActiveTokens .Where(t => t.ExpiresAt < DateTime.UtcNow); dbContext.UserActiveTokens.RemoveRange(expiredTokens); dbContext.SaveChanges(); } public Task StopAsync(CancellationToken cancellationToken) { _timer?.Change(Timeout.Infinite, 0); return Task.CompletedTask; } public void Dispose() { _timer?.Dispose(); } }
然后在Program.cs中注册这个后台服务:
builder.Services.AddHostedService<CleanExpiredTokensService>();
关键说明
- 核心是利用JWT的
jti声明唯一标识每个Token,通过数据库记录Token的状态来追踪有效登录数 - 登出操作仅标记Token失效,无需删除记录(方便后续审计)
- 定时清理过期Token可降低数据库存储压力
内容的提问来源于stack exchange,提问作者Masoud Zare
相关产品推荐
相关产品推荐

