如何在Angular + ASP.NET Core 8中实现单用户单活跃会话
单活跃会话实现方案(Angular + ASP.NET Core 8)
推荐方案:数据库追踪有效会话 + JWT校验结合实时推送
该方案可精准管控单用户单活跃会话,同时实现异地登录后的实时登出提示,兼顾安全性与用户体验,适配Web+移动跨平台场景。
后端(ASP.NET Core 8)实现步骤
创建会话追踪表
在数据库中新建UserActiveSessions表,核心字段如下:Id(主键)UserId(关联用户ID)SessionId(唯一会话标识,用GUID生成)TokenJti(JWT的JTI声明,用于唯一标识令牌)DeviceType(标记Web/移动端,用于个性化提示)ExpiresAt(会话过期时间)IsActive(标记会话是否有效)
JWT生成与会话绑定
生成JWT时加入Jti和SessionId声明,同时将会话记录写入数据库并标记为活跃状态:var sessionId = Guid.NewGuid(); var jwtSecurityToken = new JwtSecurityToken( issuer: _configuration["Jwt:Issuer"], audience: _configuration["Jwt:Audience"], claims: new List<Claim> { new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()), new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), new Claim("SessionId", sessionId.ToString()) }, expires: DateTime.UtcNow.AddHours(2), signingCredentials: signingCredentials ); // 写入会话记录 _dbContext.UserActiveSessions.Add(new UserActiveSession { UserId = user.Id, SessionId = sessionId, TokenJti = jwtSecurityToken.Id, DeviceType = loginRequest.DeviceType, // 从登录请求获取设备类型 ExpiresAt = jwtSecurityToken.ValidTo, IsActive = true }); await _dbContext.SaveChangesAsync();登录时清理旧会话
用户登录时,先将该用户所有活跃会话标记为失效,再创建新会话:var existingActiveSessions = await _dbContext.UserActiveSessions .Where(s => s.UserId == user.Id && s.IsActive) .ToListAsync(); foreach (var session in existingActiveSessions) { session.IsActive = false; } // 后续添加新会话逻辑...请求拦截与会话校验
自定义授权处理器,每次API请求时校验会话有效性:public class ActiveSessionAuthorizationHandler : AuthorizationHandler<ActiveSessionRequirement> { private readonly AppDbContext _dbContext; private readonly IHttpContextAccessor _httpContextAccessor; public ActiveSessionAuthorizationHandler(AppDbContext dbContext, IHttpContextAccessor httpContextAccessor) { _dbContext = dbContext; _httpContextAccessor = httpContextAccessor; } protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, ActiveSessionRequirement requirement) { var jti = context.User.FindFirst(JwtRegisteredClaimNames.Jti)?.Value; if (string.IsNullOrEmpty(jti)) { context.Fail(); return; } var validSession = await _dbContext.UserActiveSessions .FirstOrDefaultAsync(s => s.TokenJti == jti && s.IsActive); if (validSession == null) { _httpContextAccessor.HttpContext.Response.StatusCode = StatusCodes.Status401Unauthorized; await _httpContextAccessor.HttpContext.Response.WriteAsync("您的会话已终止,因账号在其他设备登录"); context.Fail(); return; } context.Succeed(requirement); } }在
Program.cs中注册该授权策略,保护需要登录验证的API接口。实时通知(可选但推荐)
用SignalR实现异地登录后的实时登出提示:- 创建SignalR Hub,用户登录后连接Hub并加入以
UserId命名的分组 - 清理旧会话时,通过Hub向旧会话的连接发送强制登出指令
- 创建SignalR Hub,用户登录后连接Hub并加入以
前端(Angular)实现步骤
JWT存储与请求拦截
- 登录成功后,将JWT和
SessionId存储在localStorage或sessionStorage - 实现HTTP拦截器,携带令牌并处理会话失效的响应:
@Injectable() export class AuthInterceptor implements HttpInterceptor { constructor(private authService: AuthService, private router: Router, private snackBar: MatSnackBar) {} intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { const token = this.authService.getToken(); if (token) { request = request.clone({ setHeaders: { Authorization: `Bearer ${token}` } }); } return next.handle(request).pipe( catchError((error: HttpErrorResponse) => { if (error.status === 401 && error.error.includes("会话已终止")) { this.authService.logout(); this.snackBar.open("您的会话已终止,因账号在其他设备登录", "关闭", { duration: 5000 }); this.router.navigate(['/login']); } return throwError(() => error); }) ); } }- 登录成功后,将JWT和
SignalR实时监听登出指令
集成SignalR客户端,监听后端推送的强制登出通知:@Injectable({ providedIn: 'root' }) export class SessionSignalRService { private hubConnection: signalR.HubConnection; startConnection(userId: string) { this.hubConnection = new signalR.HubConnectionBuilder() .withUrl('/sessionHub') .build(); this.hubConnection.start() .then(() => this.hubConnection.invoke('JoinGroup', userId)) .catch(err => console.error('SignalR连接失败:', err)); this.hubConnection.on('ForceLogout', (message) => { this.authService.logout(); this.snackBar.open(message, "关闭", { duration: 5000 }); this.router.navigate(['/login']); }); } }主动登出清理会话
用户主动登出时,调用后端API将当前会话标记为失效,并断开SignalR连接。
其他方案对比
- JWT黑名单:实现简单,但无法实时感知异地登录,需等待令牌过期或下次请求才会失效,用户体验差
- 传统服务器Session:依赖Cookie,移动端适配困难,跨平台兼容性差,不适合Web+移动场景
综上,数据库追踪有效会话+SignalR实时通知是最优方案,既能精准管控会话,又能实现实时提示,完全适配跨平台需求。
内容的提问来源于stack exchange,提问作者SP007
相关产品推荐
相关产品推荐

