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

Angular 18中两个路由守卫交互引发无限重定向循环问题

路由守卫无限重定向循环问题排查与解决

配置了/和/auth两条路由,规则为未登录用户仅能访问/auth页面,已登录用户无法访问该页面。实现两个路由守卫后出现无限重定向循环,同时后端接口/auth/getUser无请求触发且超时。


现有代码配置

路由配置(app.routes.ts)

{
    path: 'auth',
    canActivate: [OnlyUnauthorizedGuard],
    children: [
        { path: '', component: AuthComponent, pathMatch: 'full' },
        { path: 'login', component: LoginComponent },
        { path: 'register', component: RegisterComponent },
    ]
},
{
    path: '',
    data: {
        allowedRoleIDs: [UserRole.Customer, UserRole.Owner, UserRole.Admin, UserRole.Root]
    },
    canActivate: [RoleGuard],
    component: MainComponent,
},

RoleGuard 守卫(role.guard.ts)

export class RoleGuard implements CanActivate { 
    constructor(private authService: AuthService, private router: Router) { }  

    canActivate(route: ActivatedRouteSnapshot): Observable<boolean> {
        console.log('Triggered RoleGuard');
        const allowedRoles = route.data['allowedRoleIDs'] as number[];
    
        return this.authService.user$.pipe(
          map(user => {
            console.log('user', user);
            if (user === null) {
              console.log('Redirecting to /auth since user is null');
              this.router.navigate(['/auth']);
              return false;
            }
    
            if (user && !allowedRoles.includes(user.roleID)) {
              console.log('Redirecting to /404');
              this.router.navigate(['/404']);
              return false;
            }
    
            return true;
          })
        );
    }
}

OnlyUnauthorizedGuard 守卫(onlyUnauthorized.guard.ts)

export class OnlyUnauthorizedGuard implements CanActivate { 
    constructor(private authService: AuthService, private router: Router) {}

    canActivate(): Observable<boolean> {
        console.log('Triggered OnlyUnauthorizedGuard');
        return this.authService.user$.pipe(
          take(1),
          map(user => {
            console.log('user ', user);
    
            if (user) {
              console.log('User is logged in so redirecting to /');
              this.router.navigate(['/']);
              return false;
            }
    
            console.log('Allowed');
            return true;
          })
        );
      }
    }

AuthService 用户数据服务(auth.service.ts)

export class AuthService { 
  private _userSubject = new BehaviorSubject<User | null>(null); 
  user$ = this._userSubject.asObservable();  
  constructor(
    private http: HttpClient
  ) {
    this.loadUser();
  }

  private loadUser() {
    this._loadingSubject.next(true);
    this.http.get<User>('/auth/getUser').pipe(
      tap(user => {
        this._userSubject.next(user)
        console.log(user)
      }),
      catchError(() => {
        this._userSubject.next(null);
        return of(null);
      }),
      finalize(() => this._loadingSubject.next(false))
    ).subscribe();
  }

  get user() {
    return this._userSubject.value;
  }
}

异常现象

  • 浏览器报错:PM [vite] Internal server error: Page /auth/getUser did not render in 30 seconds.,网络面板无/auth/getUser请求
  • Docker日志循环打印:
2024-10-28 00:26:06 Triggered RoleGuard
2024-10-28 00:26:06 user null
2024-10-28 00:26:06 Redirecting to /auth since user is null
2024-10-28 00:26:06 Triggered OnlyUnauthorizedGuard
2024-10-28 00:26:06 user  null
2024-10-28 00:26:06 Allowed
2024-10-28 00:26:06 Triggered RoleGuard
...

问题根源

  1. 守卫重复触发:RoleGuard未添加take(1),会持续监听user$的变化,每次用户状态更新都会重新执行守卫逻辑,引发重复重定向。
  2. 初始值误判:_userSubject初始值为null,页面加载时守卫先于后端接口请求完成执行,直接判定未登录重定向,后续接口返回结果后又触发守卫,形成循环。
  3. 接口请求被拦截:前端路由/auth与后端接口路径冲突,导致/auth/getUser被前端路由拦截,无法转发到后端。

解决方案

1. 修复守卫订阅逻辑,避免重复触发

给RoleGuard添加take(1),确保守卫只执行一次判断:

export class RoleGuard implements CanActivate { 
    constructor(private authService: AuthService, private router: Router) { }  

    canActivate(route: ActivatedRouteSnapshot): Observable<boolean> {
        console.log('Triggered RoleGuard');
        const allowedRoles = route.data['allowedRoleIDs'] as number[];
    
        return this.authService.user$.pipe(
          take(1), // 仅取一次用户状态,避免重复触发
          map(user => {
            console.log('user', user);
            if (user === null) {
              console.log('Redirecting to /auth since user is null');
              this.router.navigate(['/auth']);
              return false;
            }
    
            if (user && !allowedRoles.includes(user.roleID)) {
              console.log('Redirecting to /404');
              this.router.navigate(['/404']);
              return false;
            }
    
            return true;
          })
        );
    }
}

2. 等待用户数据加载完成后再执行守卫判断

在AuthService中添加加载状态,让守卫等待接口请求完成后再判断:

export class AuthService { 
  private _userSubject = new BehaviorSubject<User | null>(null); 
  private _loadingSubject = new BehaviorSubject<boolean>(true);
  user$ = this._userSubject.asObservable();  
  loading$ = this._loadingSubject.asObservable(); // 暴露加载状态

  constructor(private http: HttpClient) {
    this.loadUser();
  }

  private loadUser() {
    this._loadingSubject.next(true);
    this.http.get<User>('/auth/getUser').pipe(
      tap(user => {
        this._userSubject.next(user);
        console.log(user);
      }),
      catchError(() => {
        this._userSubject.next(null);
        return of(null);
      }),
      finalize(() => this._loadingSubject.next(false))
    ).subscribe();
  }

  get user() {
    return this._userSubject.value;
  }
}

修改两个守卫,结合加载状态判断:

RoleGuard 修改:

import { combineLatest } from 'rxjs';
import { filter } from 'rxjs/operators';

export class RoleGuard implements CanActivate { 
    constructor(private authService: AuthService, private router: Router) { }  

    canActivate(route: ActivatedRouteSnapshot): Observable<boolean> {
        console.log('Triggered RoleGuard');
        const allowedRoles = route.data['allowedRoleIDs'] as number[];
    
        return combineLatest([this.authService.user$, this.authService.loading$]).pipe(
          filter(([_, loading]) => !loading), // 等待加载完成
          take(1),
          map(([user]) => {
            console.log('user', user);
            if (user === null) {
              console.log('Redirecting to /auth since user is null');
              this.router.navigate(['/auth']);
              return false;
            }
    
            if (user && !allowedRoles.includes(user.roleID)) {
              console.log('Redirecting to /404');
              this.router.navigate(['/404']);
              return false;
            }
    
            return true;
          })
        );
    }
}

OnlyUnauthorizedGuard 修改:

import { combineLatest } from 'rxjs';
import { filter } from 'rxjs/operators';

export class OnlyUnauthorizedGuard implements CanActivate { 
    constructor(private authService: AuthService, private router: Router) {}

    canActivate(): Observable<boolean> {
        console.log('Triggered OnlyUnauthorizedGuard');
        return combineLatest([this.authService.user$, this.authService.loading$]).pipe(
          filter(([_, loading]) => !loading), // 等待加载完成
          take(1),
          map(([user]) => {
            console.log('user ', user);
    
            if (user) {
              console.log('User is logged in so redirecting to /');
              this.router.navigate(['/']);
              return false;
            }
    
            console.log('Allowed');
            return true;
          })
        );
      }
    }

3. 解决接口路径冲突问题

在vite配置中添加代理规则,确保API请求转发到后端:

// vite.config.ts
export default defineConfig({
  server: {
    proxy: {
      '/auth/getUser': {
        target: 'http://你的后端服务地址',
        changeOrigin: true,
      }
    }
  }
});

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.16 22:40:10