Angular SSR中CanActivate Auth Guard的使用方法及兼容实现方案
嘿,这个问题确实是Angular SSR开发中常见的坑——传统的CanActivate守卫因为依赖浏览器环境或者异步处理的差异,在SSR模式下容易失效。不过别担心,我们可以通过适配SSR的特性来改造守卫,让它在服务器端和客户端都能正常工作。下面是一步步的实现方案:
1. 先搞懂SSR中
CanActivate的限制 服务器端渲染时,代码运行在Node.js环境,没有浏览器的window、localStorage等API;同时服务器需要等待守卫的异步逻辑完成后再渲染页面,否则可能输出未授权的内容。普通的CanActivate守卫如果没考虑这些差异,就会在SSR中出问题。
2. 创建支持SSR的
AuthGuard守卫 我们需要结合Angular的PlatformId(判断环境)和TransferState(服务器-客户端状态传递)来改造守卫,确保两端的认证逻辑都能正常执行。
import { Injectable, Inject, PLATFORM_ID } from '@angular/core'; import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, Router } from '@angular/router'; import { isPlatformServer } from '@angular/common'; import { TransferState, makeStateKey } from '@angular/platform-browser'; import { AuthService } from './auth.service'; import { Observable, of } from 'rxjs'; import { tap, catchError } from 'rxjs/operators'; // 定义TransferState的key,用来传递认证状态 const AUTH_STATE_KEY = makeStateKey<boolean>('isAuthenticated'); @Injectable({ providedIn: 'root' }) export class AuthGuard implements CanActivate { constructor( @Inject(PLATFORM_ID) private platformId: Object, private transferState: TransferState, private router: Router, private authService: AuthService ) {} canActivate( route: ActivatedRouteSnapshot, state: RouterStateSnapshot ): Observable<boolean> | Promise<boolean> | boolean { // 先检查TransferState:如果服务器已经处理过认证,直接用传递过来的状态 if (this.transferState.hasKey(AUTH_STATE_KEY)) { const isAuthenticated = this.transferState.get(AUTH_STATE_KEY, false); this.transferState.remove(AUTH_STATE_KEY); return this.handleAuthResult(isAuthenticated, state); } // 服务器端逻辑:从请求中获取认证信息(比如Cookie) if (isPlatformServer(this.platformId)) { return this.authService.isAuthenticatedOnServer().pipe( // 将认证状态存入TransferState,传递给客户端 tap(isAuthenticated => this.transferState.set(AUTH_STATE_KEY, isAuthenticated)), tap(isAuthenticated => this.handleAuthResult(isAuthenticated, state)), catchError(() => { this.transferState.set(AUTH_STATE_KEY, false); this.router.navigate(['/login']); return of(false); }) ); } // 客户端逻辑:从本地存储获取认证信息 return this.authService.isAuthenticated().pipe( tap(isAuthenticated => this.handleAuthResult(isAuthenticated, state)), catchError(() => { this.router.navigate(['/login'], { queryParams: { returnUrl: state.url } }); return of(false); }) ); } // 统一处理认证结果 private handleAuthResult(isAuthenticated: boolean, state: RouterStateSnapshot): boolean { if (!isAuthenticated) { this.router.navigate(['/login'], { queryParams: { returnUrl: state.url } }); return false; } return true; } }
3. 实现适配SSR的
AuthService 认证服务需要区分服务器端和客户端的逻辑,服务器端从请求对象获取认证信息(比如Cookie),客户端从本地存储获取:
import { Injectable, Inject, PLATFORM_ID } from '@angular/core'; import { isPlatformServer } from '@angular/common'; import { REQUEST } from '@nguniversal/express-engine/tokens'; import { Request } from 'express'; import { Observable, of } from 'rxjs'; @Injectable({ providedIn: 'root' }) export class AuthService { constructor( @Inject(PLATFORM_ID) private platformId: Object, @Inject(REQUEST) private request: Request // 服务器端注入Express请求对象 ) {} // 服务器端检查认证:从Cookie/请求头获取token并验证 isAuthenticatedOnServer(): Observable<boolean> { if (!isPlatformServer(this.platformId)) return of(false); // 示例:从Cookie获取token,实际项目中需要调用后端接口验证token有效性 const authToken = this.request.cookies['auth_token']; return of(!!authToken); } // 客户端检查认证:从localStorage获取token isAuthenticated(): Observable<boolean> { if (isPlatformServer(this.platformId)) return of(false); const authToken = localStorage.getItem('auth_token'); return of(!!authToken); } }
4. 在路由模块中正常使用守卫
和普通CanActivate守卫的用法完全一致,直接在路由配置中引用:
import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { DashboardComponent } from './dashboard/dashboard.component'; import { LoginComponent } from './login/login.component'; import { AuthGuard } from './auth.guard'; const routes: Routes = [ { path: 'dashboard', component: DashboardComponent, canActivate: [AuthGuard] // 直接使用改造后的守卫 }, { path: 'login', component: LoginComponent }, { path: '', redirectTo: '/dashboard', pathMatch: 'full' } ]; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule] }) export class AppRoutingModule { }
5. 关键注意事项
- 绝对不要在守卫中直接使用
window、localStorage等浏览器API,必须通过PlatformId判断环境后再执行对应逻辑。 - 用
TransferState传递认证状态可以避免客户端重复执行认证逻辑,提升渲染性能。 - 服务器端的认证必须依赖请求对象(
REQUEST)获取信息,因为服务器端没有本地存储。 - 确保守卫中的异步逻辑在服务器端渲染完成前完成,避免出现未授权内容被输出。
内容的提问来源于stack exchange,提问作者Kuldeep Sharma
相关产品推荐
相关产品推荐

