如何基于express-session在Angular前端校验用户登录状态?
解决Angular前端与express-session会话状态不同步的问题
核心思路
因为httpOnly cookie的安全限制,前端无法直接读取会话状态,必须通过后端接口校验会话有效性,以此保证前端状态与后端会话完全同步,彻底解决LocalStorage残留导致的状态不一致问题。
1. 后端新增会话校验接口
在Express后端添加一个接口,用于验证当前会话是否有效(假设登录时已将用户信息存入req.session.user):
// routes/auth.js router.get('/check-session', (req, res) => { if (req.session?.user) { // 会话有效,返回登录状态和用户信息 res.status(200).json({ isSignedIn: true, user: req.session.user }); } else { // 会话过期或未登录 res.status(401).json({ isSignedIn: false }); } });
2. 改造Angular的AuthService
用BehaviorSubject维护全局登录状态,通过后端接口实时同步:
// auth.service.ts import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { BehaviorSubject, Observable } from 'rxjs'; import { tap, catchError } from 'rxjs/operators'; @Injectable({ providedIn: 'root' }) export class AuthService { // 用BehaviorSubject保存登录状态,支持组件订阅同步 private isSignedInSubject = new BehaviorSubject<boolean>(false); public isSignedIn$ = this.isSignedInSubject.asObservable(); constructor(private http: HttpClient) { // 应用初始化时自动校验会话状态 this.checkSession(); } // 调用后端接口校验会话 checkSession(): Observable<any> { return this.http.get('/api/auth/check-session').pipe( tap(res => this.isSignedInSubject.next(res.isSignedIn)), catchError(() => { // 请求失败(401)则标记为未登录,清空本地残留状态 this.isSignedInSubject.next(false); localStorage.removeItem('is_signed_in'); return []; }) ); } // 登录成功后主动更新状态 loginSuccess(): void { this.isSignedInSubject.next(true); } // 登出时同步后端与前端状态 logout(): Observable<any> { return this.http.post('/api/auth/logout', {}).pipe( tap(() => { this.isSignedInSubject.next(false); localStorage.removeItem('is_signed_in'); }) ); } }
3. 改造路由守卫
在路由守卫中调用校验接口,确保只有有效会话用户能访问受保护路由:
// auth.guard.ts import { Injectable } from '@angular/core'; import { CanActivate, Router } from '@angular/router'; import { AuthService } from './auth.service'; import { map } from 'rxjs/operators'; @Injectable({ providedIn: 'root' }) export class AuthGuard implements CanActivate { constructor(private authService: AuthService, private router: Router) {} canActivate() { return this.authService.checkSession().pipe( map(isValid => { if (isValid) return true; // 会话过期,跳转到登录页 this.router.navigate(['/login']); return false; }) ); } }
4. 组件同步登录状态(如导航栏)
通过订阅isSignedIn$自动同步UI状态:
<!-- 导航栏模板 --> <button *ngIf="isSignedIn | async" mat-button [matMenuTriggerFor]="userMenu"> 个人中心 </button> <button *ngIf="!(isSignedIn | async)" mat-button routerLink="/login"> 登录 </button>
// 导航栏组件 import { Component } from '@angular/core'; import { AuthService } from '../auth.service'; import { Observable } from 'rxjs'; @Component({ selector: 'app-navbar', templateUrl: './navbar.component.html' }) export class NavbarComponent { isSignedIn$: Observable<boolean>; constructor(private authService: AuthService) { this.isSignedIn$ = this.authService.isSignedIn$; } }
额外优化:全局拦截401状态
添加HTTP拦截器,统一处理后端返回的401(会话过期),自动同步前端状态并跳转:
// auth.interceptor.ts import { Injectable } from '@angular/core'; import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor } from '@angular/common/http'; import { Observable, throwError } from 'rxjs'; import { catchError } from 'rxjs/operators'; import { AuthService } from './auth.service'; import { Router } from '@angular/router'; @Injectable() export class AuthInterceptor implements HttpInterceptor { constructor(private authService: AuthService, private router: Router) {} intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { return next.handle(request).pipe( catchError(error => { if (error.status === 401) { this.authService.isSignedInSubject.next(false); localStorage.removeItem('is_signed_in'); this.router.navigate(['/login']); } return throwError(error); }) ); } }
内容的提问来源于stack exchange,提问作者Rok Benko
相关产品推荐
相关产品推荐

