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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.28 19:15:43