Angular 12 SSR中canActivate守卫路由页面重载失效问题
问题根因
核心问题是Angular Universal(SSR)的服务端运行环境不存在浏览器专属API,和你用什么状态管理库没关系:
- 浏览器刷新页面时,请求先打到Nginx,再转发给Node端的SSR服务渲染首屏。Node环境里没有
window、localStorage、DOM这些浏览器对象,你代码里直接调用localStorage.getItem("currentUser")在服务端执行时要么触发异常被容错为null,要么根本读不到浏览器本地存储的内容,导致AuthGuard判定用户未登录,直接拦截路由。 - 之前试的RxJS、NgRX、NGXS持久化方案本质还是默认读写localStorage,没做SSR环境适配,自然解决不了问题。
修复步骤
1. 先做环境兼容,解决登录态跨端读取问题
localStorage只存在浏览器端,SSR渲染阶段拿不到这部分数据,必须把登录态同步存到Cookie里——浏览器发刷新请求时会自动携带同域Cookie到服务端,SSR层可以直接从请求头读取Cookie判断登录状态,首屏判定就不会出错。
第一步先改造AuthService:
import { PLATFORM_ID, Inject, Injectable, Optional } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; import { REQUEST } from '@nguniversal/express-engine/tokens'; import { Request } from 'express'; @Injectable({ providedIn: 'root' }) export class AuthService { constructor( @Inject(PLATFORM_ID) private platformId: Object, @Optional() @Inject(REQUEST) private request: Request // 原有其他依赖比如http、多语言相关服务保留 ) {} // 改造login方法,登录成功后同时存localStorage和Cookie login(email: string, password: string) { let input = new FormData(); const headers = { "APP-TOKEN": "GV5TmkI8lCyPqBTLExji", "Accept-Language": this.currentLang, }; input.append("email", email); input.append("password", password); return this.http .post<any>(`${this.serverUrl}api/v1/login`, input, { headers: headers }) .pipe( map((user) => { if (user.status === true || user.count === true) { // 原有localStorage存储逻辑保留 if (isPlatformBrowser(this.platformId)) { localStorage.setItem("currentUser", JSON.stringify(user.data)); localStorage.setItem("isLoggedIn", "true"); localStorage.removeItem("guest-user-id"); localStorage.setItem("loginCall", "true"); // 新增同步存Cookie,7天有效期,全站可访问 const expireDate = new Date(Date.now() + 7 * 24 * 3600 * 1000); document.cookie = `currentUser=${encodeURIComponent(JSON.stringify(user.data))};expires=${expireDate.toUTCString()};path=/;SameSite=Lax`; } } return user; }), catchError(this.handleError) ); } // 改造isLoggedIn方法,兼容服务端/浏览器端读取 isLoggedIn(): boolean { // 浏览器环境直接读localStorage if (isPlatformBrowser(this.platformId)) { return !!localStorage.getItem("currentUser"); } // 服务端环境从请求Cookie里读登录态 if (this.request?.headers?.cookie) { const cookieList = this.request.headers.cookie.split(';'); const userCookie = cookieList.find(item => item.trim().startsWith('currentUser=')); return !!userCookie; } return false; } // 原有其他方法保留 }
记得在app.server.module.ts里加REQUEST的提供者,不然服务端拿不到请求对象:
import { NgModule } from '@angular/core'; import { ServerModule } from '@angular/platform-server'; import { REQUEST } from '@nguniversal/express-engine/tokens'; import { AppModule } from './app.module'; import { AppComponent } from './app.component'; @NgModule({ imports: [ AppModule, ServerModule, ], bootstrap: [AppComponent], providers: [ { provide: REQUEST, useFactory: () => (global as any).req, } ] }) export class AppServerModule {}
2. 改造路由守卫,避免服务端执行浏览器专属逻辑
你现在的守卫里直接调用SweetAlert弹窗,这玩意依赖DOM,服务端根本跑不了,必须加环境判断,服务端直接做路由跳转,不执行弹窗逻辑:
import { Injectable } from '@angular/core'; import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, UrlTree, Router } from '@angular/router'; import { PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; import { AuthService } from './auth.service'; import Swal from 'sweetalert2'; @Injectable({ providedIn: 'root' }) export class AuthGuard implements CanActivate { constructor( @Inject(PLATFORM_ID) private platformId: Object, private authService: AuthService, private router: Router ) {} canActivate( route: ActivatedRouteSnapshot, state: RouterStateSnapshot ): boolean | UrlTree { if (this.authService.isLoggedIn()) { return true; } // 服务端环境不弹弹窗,直接返回登录页跳转 if (!isPlatformBrowser(this.platformId)) { return this.router.createUrlTree(['/login'], { queryParams: { retUrl: state.url } }); } // 只有浏览器环境才弹确认框 this.openConfirmationDialog(route); return false; } // 原有openConfirmationDialog、navigation方法逻辑不用改 openConfirmationDialog(route) { Swal.fire({ title: this.title, text: this.text, showCancelButton: true, confirmButtonText: this.okButton, cancelButtonText: this.cancelButton }).then((result) => { if (result.value) { this.navigation(route) } }); } navigation(route) { this.router.navigate(['login'], { queryParams: { retUrl: route.url } }); } }
补充优化点:现在守卫里弹窗是异步逻辑,直接return false再手动navigate虽然能用,最好是把弹窗逻辑转成Observable返回,符合Angular路由守卫的执行规范,避免极端情况下路由流转卡住。
3. 修正Nginx配置,确保Cookie正常传递
很多人部署时漏传Cookie头,导致SSR服务拿不到浏览器带的登录态,在Nginx的反向代理配置里加一行Cookie透传即可:
server { listen 80; server_name 你的域名; root /你的静态文件存储路径; location / { proxy_pass http://127.0.0.1:4000; # 替换成你自己的SSR Node服务监听端口 proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Cookie $http_cookie; # 关键配置:透传浏览器Cookie到SSR服务 proxy_cache_bypass $http_upgrade; } # 静态资源直接走Nginx返回,不用过SSR location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { expires 1y; add_header Cache-Control "public, immutable"; } }
改完配置记得执行nginx -s reload重载配置。
其他可选鉴权方案
如果不想做SSR层的登录态校验,可以选更简单的实现方式:
- 方案1:给所有受canActivate保护的路由配置跳过服务端渲染,直接走客户端渲染,就不存在服务端读不到localStorage的问题,缺点是受保护路由首屏加载稍慢。Angular 12里可以通过在路由定义里加
data: { renderMode: 'client' }配合自定义渲染策略实现。 - 方案2:登录态用HttpOnly Cookie存储,SSR阶段拿到Cookie后直接调用后端接口校验Token有效性,再决定是否放行路由,安全性更高,能避免前端篡改Cookie伪造登录状态。
- 方案3:简化守卫逻辑,去掉弹窗二次确认,未登录时直接跳转到登录页,登录成功后读取retUrl参数跳回原页面,减少守卫里的异步逻辑,兼容性更好。
内容的提问来源于stack exchange,提问作者ganesh
相关产品推荐
相关产品推荐

