Почему взаимодействие двух охранников приводит к бесконечной петле перенаправления в Angular 18?

Вопрос или проблема

У меня есть маршруты “https://stackoverflow.com/” и ‘/auth’. Логика заключается в том, что пользователи, не вошедшие в систему, могут получить доступ ТОЛЬКО к странице /auth, в то время как вошедшие в систему пользователи НЕ МОГУТ получить к ней доступ.

Итак, я создал два охранника и по какой-то причине получаю бесконечный цикл между ними.

Я настроил маршруты следующим образом:

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,
    },

Мои охранники:

role.guard.ts:

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

    canActivate(route: ActivatedRouteSnapshot): Observable<boolean> {
        console.log('Сработал RoleGuard');
        const allowedRoles = route.data['allowedRoleIDs'] as number[];
    
        return this.authService.user$.pipe(
          map(user => {
            console.log('пользователь', user);
            if (user === null) {
              console.log('Перенаправление на /auth, так как пользователь равен null');
              this.router.navigate(['/auth']);
              return false;
            }
    
            if (user && !allowedRoles.includes(user.roleID)) {
              console.log('Перенаправление на /404');
              this.router.navigate(['/404']);
              return false;
            }
    
            return true;
          })
        );
    }
}`     

onlyUnauthorized.guard.ts:

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

    canActivate(): Observable<boolean> {
        console.log('Сработал OnlyUnauthorizedGuard');
        return this.authService.user$.pipe(
          take(1),
          map(user => {
            console.log('пользователь ', user);
    
            if (user) {
              console.log('Пользователь вошел в систему, перенаправление на /');
              this.router.navigate(["https://stackoverflow.com/"]);
              return false;
            }
    
            console.log('Разрешено');
            return true;
          })
        );
      }
    }
`    

Как я получаю данные пользователя:

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] Внутренняя ошибка сервера: Страница /auth/getUser не отобразилась за 30 секунд.

Я не вижу запроса к /auth/getUser на вкладке сети в браузере.

Есть такие логи в Docker при доступе к странице /auth (вкладка консоли в браузере не показывает логов) :

`
2024-10-28 00:26:06 Сработал RoleGuard
2024-10-28 00:26:06 пользователь null
2024-10-28 00:26:06 Перенаправление на /auth, так как пользователь равен null
2024-10-28 00:26:06 Сработал OnlyUnauthorizedGuard
2024-10-28 00:26:06 пользователь null
2024-10-28 00:26:06 Разрешено
2024-10-28 00:26:06 Сработал RoleGuard
...`

Кажется, что по какой-то причине, даже если OnlyUnauthorizedGuard возвращает true, происходит перенаправление на /.

Если пользователь равен null и вы получаете доступ к корневому пути /, то RoleGuard перенаправляет на /auth, а затем, когда это разрешено, пытается перенаправить обратно на / снова, что вызывает бесконечный цикл.

Настройте охранников, чтобы предотвратить появление цикла. Используйте take(1) в обоих охранниках, чтобы убедиться, что они корректно завершают цепочку обсервации, затем убедитесь, что пользователь загружен, прежде чем срабатывать охранники. Рассмотрите возможность инициализации состояния пользователя как undefined и обработки логики, когда оно разрешено в null или объект пользователя, и убедитесь, что вы не используете навигацию непосредственно внутри охранников. Вместо этого возвращайте дерево URL.

RoleGuard

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

    canActivate(route: ActivatedRouteSnapshot): Observable<boolean> | UrlTree {
        console.log('Сработал RoleGuard');
        const allowedRoles = route.data['allowedRoleIDs'] as number[];
    
        return this.authService.user$.pipe(
          take(1),
          map(user => {
            console.log('пользователь', user);
            if (user === null) {
              return this.router.createUrlTree(['/auth']);
            }

            if (user && !allowedRoles.includes(user.roleID)) {
              return this.router.createUrlTree(['/404']);
            }

            return true;
          })
        );
    }
}

OnlyUnauthorizedGuard

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

    canActivate(): Observable<boolean> | UrlTree {
        console.log('Сработал OnlyUnauthorizedGuard');
        return this.authService.user$.pipe(
          take(1),
          map(user => {
            console.log('пользователь ', user);

            if (user) {
              return this.router.createUrlTree(["https://stackoverflow.com/"]);
            }

            return true;
          })
        );
      }
}

Попробуйте добавить состояние загрузки, чтобы обеспечить обратную связь с пользовательским интерфейсом, пока данные пользователя загружаются.

Ответ или решение

Вопрос, который вы поставили, касается взаимодействия двух охранников (guards) в Angular, что приводит к бесконечному циклу перенаправления. Позвольте подробно разобрать эту проблему с учетом ваших маршрутов и логики охранников.

Описание проблемы

Ваше приложение имеет два охранника: RoleGuard, который разрешает доступ только авторизованным пользователям, и OnlyUnauthorizedGuard, который разрешает доступ только неавторизованным пользователям. Ваша логика определяет, что неавторизованные пользователи должны иметь доступ только к маршруту /auth. Данная ситуация может привести к бесконечному циклу перенаправления при определенных условиях.

Логика охранников

  1. RoleGuard:

    • Проверяет, авторизован ли пользователь. Если user равен null, он перенаправляет на /auth. Если user существует, он проверяет разрешенные роли.
  2. OnlyUnauthorizedGuard:

    • Проверяет, не авторизован ли пользователь. Если user существует, он перенаправляет на https://stackoverflow.com/. Если user равен null, то доступ разрешается.

Анализ

Когда пользователь пытается получить доступ к корневому маршруту (/), RoleGuard срабатывает первым и проверяет, является ли пользователь авторизованным. Поскольку user равен null, он перенаправляет на /auth. Теперь, когда пользователь находится на /auth, срабатывает OnlyUnauthorizedGuard, который проверяет, не авторизован ли пользователь, и, поскольку user по-прежнему равен null, доступ разрешается. Однако, если в этот момент пользователь пытается получить доступ к защищенному маршруту, RoleGuard опять срабатывает, приводя к перенаправлению на /auth, и мы попадаем в бесконечный цикл.

Как избежать бесконечного перенаправления

Чтобы решить проблему, можно внести изменения в каждую из охранников, чтобы предотвратить бесконечные перенаправления и корректно обрабатывать состояния пользователя. Рекомендации по изменению:

  1. Использование take(1): Это обеспечит завершение цепочки наблюдения (observable) и предотвратит повторные вызовы при каждом изменении состояния.

  2. Возврат UrlTree: Вместо выполнения перенаправления внутри охранников, возвращайте UrlTree, что позволяет лучше управлять маршрутами и минимизировать риск создания циклов.

Пример исправленных охранников

RoleGuard

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

    canActivate(route: ActivatedRouteSnapshot): Observable<boolean | UrlTree> {
        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) {
                    return this.router.createUrlTree(['/auth']);
                }
                if (user && !allowedRoles.includes(user.roleID)) {
                    return this.router.createUrlTree(['/404']);
                }
                return true;
            })
        );
    }
}

OnlyUnauthorizedGuard

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

    canActivate(): Observable<boolean | UrlTree> {
        console.log('Triggered OnlyUnauthorizedGuard');
        return this.authService.user$.pipe(
            take(1),  
            map(user => {
                console.log('user', user);
                if (user) {
                    return this.router.createUrlTree(["https://stackoverflow.com/"]);
                }
                return true;
            })
        );
    }
}

Заключение

Внедрив указанные изменения, вы сможете избежать бесконечного цикла перенаправления и улучшить взаимодействие охранников. Также следует следить за состоянием загрузки для пользовательского интерфейса, чтобы пользователи знали, что данные загружаются. Успехов в разработке приложения!

Оцените материал
Добавить комментарий

Капча загружается...