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

Angular 17 SSR模式下页面刷新/路由跳转时登录页闪烁问题求助

问题描述

成功登录后用户跳转至首页,但页面刷新或路由跳转至其他页面时,登录页会短暂闪烁。推测原因是页面先执行服务端渲染(SSR),再进行客户端渲染(CSR):页面刷新时,服务端渲染阶段因无法读取客户端sessionStorage中的token,判定认证状态为false,从而渲染登录页发送至客户端;但客户端初始化后能读取到token,认证状态变为true,随即切换到首页或目标页面,期间路由未发生变化。关闭SSR后闪烁问题消失,现寻求无需关闭SSR即可解决该问题的方案。


相关代码

authGuard.ts

export const AuthGuard: CanActivateFn | CanActivateChildFn = (route, state) => {
  const router: Router = inject(Router);
  return inject(AuthService).checkAuthentication()
  .pipe(
    switchMap((isAuthenticated)=>{
      if (!isAuthenticated) {
        return router.createUrlTree(['auth/sign-in'], {queryParams: {param: state.url}})
      }
      return of(true);
    })
  );
}

app.route.ts

export const routes : Routes = [
  {
    path: '',
    component: LayoutComponent,
    canActivate: [AuthGuard],
    canActivateChild: [AuthGuard],
    children: [
      { path: 'home', component: DashboardComponent },
      { path: 'settings', loadChildren: () => import('./modules/settings/settings.routes') },
      { path: 'users', loadChildren: () => import('./modules/users/users.routes')},
    ]
  },
  {
    path: 'auth',
    loadChildren: ()=> import('./login/login.routes')
  },
  { path: '', pathMatch: 'full', redirectTo: 'home' },
  { path: '**', pathMatch: 'full', redirectTo: 'home' },
];

auth.service.ts

@Injectable({ providedIn: 'root' })
export class AuthService {
 private _storeService = inject(StoreService);

 signIn(credentials: { username: string; password: string }): Observable<any> {
        return this._httpClient.post(AUTH_API + 'User/login', credentials).pipe(
            switchMap((response: any) => {
                this._storeService.saveAccessToken(response.token); // save token on sessionStorage
                this._storeService.saveActiveUser(response.user);
                return of(response);
            }),
        );
    }

 get accessToken(): string {
        return this._storeService.getAccessToken() ?? '';
    }

  checkAuthentication(): Observable<boolean> {
    if ( !this.accessToken ||  AuthUtils.isTokenExpired(this.accessToken))
        {
          return of(false);
        } else { return of(true) }
   }
}

解决方案

闪烁问题的核心是SSR与CSR阶段认证状态不一致,以下是几种适配Angular函数式守卫的可行方案:

1. 利用TransferState同步前后端认证状态

Angular的TransferState可在SSR阶段将服务端获取的认证状态传递到客户端,避免前后端判定结果冲突。

实现步骤:

  • 在AuthService中引入依赖:
import { TransferState, makeStateKey } from '@angular/platform-browser';
import { PLATFORM_ID, inject } from '@angular/core';
import { isPlatformServer } from '@angular/common';

const AUTH_STATE_KEY = makeStateKey<boolean>('isAuthenticated');
  • 修改checkAuthentication方法,适配前后端逻辑:
constructor(private transferState: TransferState) {}

checkAuthentication(): Observable<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 of(isAuthenticated);
  }

  // 服务端逻辑:从cookie读取token并验证(需登录时将token存入cookie)
  if (isPlatformServer(inject(PLATFORM_ID))) {
    const token = this.getServerCookie('access_token'); // 实现服务端读取cookie的方法
    const isAuthenticated = token && !AuthUtils.isTokenExpired(token);
    this.transferState.set(AUTH_STATE_KEY, isAuthenticated);
    return of(isAuthenticated);
  }

  // 客户端正常逻辑
  const isAuthenticated = this.accessToken && !AuthUtils.isTokenExpired(this.accessToken);
  return of(isAuthenticated);
}

// 服务端读取cookie的辅助方法(需在server.ts中注入request对象,或使用Angular Universal的cookie工具)
private getServerCookie(name: string): string | null {
  // 此处需根据你的SSR环境实现,比如从Express的request中读取cookie
  const request = inject(Request);
  return request.cookies[name] || null;
}
  • 登录时同步token到cookie:
    在signIn方法中添加:
document.cookie = `access_token=${response.token}; path=/; secure; SameSite=Strict`;

2. 服务端跳过认证判定,由客户端统一处理

修改AuthGuard,让服务端直接放行,待客户端初始化完成后再执行认证逻辑,避免服务端错误渲染登录页。

import { PLATFORM_ID, inject } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';

export const AuthGuard: CanActivateFn | CanActivateChildFn = (route, state) => {
  const router: Router = inject(Router);
  const authService = inject(AuthService);
  const platformId = inject(PLATFORM_ID);

  if (isPlatformBrowser(platformId)) {
    return authService.checkAuthentication().pipe(
      switchMap((isAuthenticated) => {
        if (!isAuthenticated) {
          return of(router.createUrlTree(['auth/sign-in'], { queryParams: { param: state.url } }));
        }
        return of(true);
      })
    );
  } else {
    // 服务端直接返回true,客户端初始化后会重新验证
    return of(true);
  }
};

注意:此方案服务端会渲染目标页面(即使用户未认证),客户端初始化后会重定向到登录页,适合对SSR权限控制要求不高的场景。

3. 改用Cookie存储token替代sessionStorage

将token存储在HttpOnly Cookie中,让服务端和客户端都能读取,确保前后端认证状态完全一致。

修改AuthService的存储逻辑:

// 保存token
saveAccessToken(token: string): void {
  document.cookie = `access_token=${token}; path=/; secure; SameSite=Strict`;
}

// 获取token
getAccessToken(): string | null {
  const value = `; ${document.cookie}`;
  const parts = value.split(`; access_token=`);
  if (parts.length === 2) return parts.pop()?.split(';').shift() || null;
  return null;
}

同时更新checkAuthentication方法,直接从Cookie读取token验证即可。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.29 20:42:47