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

如何在Angular + ASP.NET Core 8中实现单用户单活跃会话

单活跃会话实现方案(Angular + ASP.NET Core 8)

推荐方案:数据库追踪有效会话 + JWT校验结合实时推送

该方案可精准管控单用户单活跃会话,同时实现异地登录后的实时登出提示,兼顾安全性与用户体验,适配Web+移动跨平台场景。


后端(ASP.NET Core 8)实现步骤

  1. 创建会话追踪表
    在数据库中新建UserActiveSessions表,核心字段如下:

    • Id(主键)
    • UserId(关联用户ID)
    • SessionId(唯一会话标识,用GUID生成)
    • TokenJti(JWT的JTI声明,用于唯一标识令牌)
    • DeviceType(标记Web/移动端,用于个性化提示)
    • ExpiresAt(会话过期时间)
    • IsActive(标记会话是否有效)
  2. 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();
    
  3. 登录时清理旧会话
    用户登录时,先将该用户所有活跃会话标记为失效,再创建新会话:

    var existingActiveSessions = await _dbContext.UserActiveSessions
        .Where(s => s.UserId == user.Id && s.IsActive)
        .ToListAsync();
    
    foreach (var session in existingActiveSessions)
    {
        session.IsActive = false;
    }
    // 后续添加新会话逻辑...
    
  4. 请求拦截与会话校验
    自定义授权处理器,每次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接口。

  5. 实时通知(可选但推荐)
    用SignalR实现异地登录后的实时登出提示:

    • 创建SignalR Hub,用户登录后连接Hub并加入以UserId命名的分组
    • 清理旧会话时,通过Hub向旧会话的连接发送强制登出指令

前端(Angular)实现步骤

  1. 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);
          })
        );
      }
    }
    
  2. 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']);
        });
      }
    }
    
  3. 主动登出清理会话
    用户主动登出时,调用后端API将当前会话标记为失效,并断开SignalR连接。


其他方案对比

  • JWT黑名单:实现简单,但无法实时感知异地登录,需等待令牌过期或下次请求才会失效,用户体验差
  • 传统服务器Session:依赖Cookie,移动端适配困难,跨平台兼容性差,不适合Web+移动场景

综上,数据库追踪有效会话+SignalR实时通知是最优方案,既能精准管控会话,又能实现实时提示,完全适配跨平台需求。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.01 13:24:58