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

Angular路由守卫如何等待API加载权限数据后再执行?

Angular路由守卫刷新页面权限校验失败问题

我给Angular路由配置了路由守卫,希望在守卫返回true/false前先从API加载侧边栏菜单数据。正常跳转时功能正常,但刷新页面时,守卫会因为DropdownUtils的初始值是空对象直接返回false,没等API加载完成。

相关代码

app-routing文件

{
    path: '',
    component: BaseLayoutComponent,
    runGuardsAndResolvers: 'always',
    children:[
        { 
            path: 'main' ,
            resolve:{ menu: MenuResolver },
            loadChildren: () => import('./main/main.module').then(m => m.MainModule),
            canActivate: [AuthGuard]
        },
    ]
},

main-routing文件

{
    path: '', 
    component: MainComponent,
    children: [
        {
            path: 'user',
            loadChildren: () => import('./users-management/users-management.module').then(m => m.UsersManagementModule),
            canActivate: [MainGuard],
        },
    ]
} 
export class MenuResolver implements Resolve<any> {

constructor(private dropDownUtils: DropDownUtils) {}

resolve(): Observable<any> {
    return this.dropDownUtils.updatePermission();
}
}
export class DropDownUtils {
protected permissionsSubject = new BehaviorSubject<any>({});
public permissions$: Observable<any> = this.permissionsSubject.asObservable();
currentUrl: any;

constructor(private menuService: MenuService, private router: Router) {
    this.router.events.pipe(
      filter(event => event instanceof NavigationEnd)
    ).subscribe(() => {
      this.fetchPermissions().subscribe();
    });
}

public updatePermission(): Observable<any> {
    return this.fetchPermissions();
}

private fetchPermissions(): Observable<any> {
    return this.menuService.getUserMenu().pipe(
        tap((data: any) => {
            this.extractPermissions(data.data);
        })
    );
}

private extractPermissions(menu: any[]): void {
    const checkPermissions: any[] = [];
    console.log(this.router.url)
    if (menu.length > 0) {
        menu.forEach(element => {
            const screen = element.screens.find((x: any) => x.url === this.router.url);
            if (screen) {
                checkPermissions.push(screen);
            }
        });
    }

    if (checkPermissions.length > 0) {
        const permissions = {
            is_create: checkPermissions.some(item => item.is_create),
            is_delete: checkPermissions.some(item => item.is_delete),
            is_read: checkPermissions.some(item => item.is_read),
            is_update: checkPermissions.some(item => item.is_update)
        };
        this.permissionsSubject.next(permissions);
    }
}
}

MainGuard的canActivate方法

canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
    console.log('Guard is activated');
    window.scrollTo(0, 0);

    const token = TokenHelper.getAccessToken();
    if (!token) {
        console.log('Token is missing or expired');
        this.router.navigate(['/login'], { queryParams: { returnUrl: state.url } });
        return of(false);
    }

    if (this.jwtService.isTokenExpired(token)) {
        console.log('Token is expired.');
        this.authService.logout();
        return of(false);
    }

    return this.menu.resolve().pipe(
        switchMap(() => {
            console.log('Menu is resolved');
            return this.dropDownUtils.updatePermission().pipe(
                switchMap(() => this.dropDownUtils.permissions$),
                map((data: any) => {
                    console.log('Permissions data received', data);
                    const hasPermission = data.is_create || data.is_delete || data.is_read || data.is_update;
                    if (hasPermission) {
                        console.log('User has at least one required permission:', hasPermission);
                        return true;
                    }
                    console.log('User lacks the necessary permissions');
                    this.router.navigate(['403']);
                    return false;
                }),
                catchError(error => {
                    console.error('Error in permissions pipeline:', error);
                    this.router.navigate(['403']);
                    return of(false);
                })
            );
        }),
        catchError(error => {
            console.error('Error resolving menu:', error);
            this.router.navigate(['403']);
            return of(false);
        })
    );
}

用户组件获取权限的方式

ngOnInit() {
    this.dropdownUtils.permissions$
    .pipe(
        takeUntil(this.destroy$),
        finalize(() => {})
    )
    .subscribe((permissions:any) => {
        this.permission = permissions;
        console.log(this.permission)
        this.loadData(this.currentPage, this.rowsPerPage);
    });
}

问题分析与修复方案

当前实现的问题

刷新页面时,MainGuard里的switchMap(() => this.dropDownUtils.permissions$)会直接取到BehaviorSubject的初始空对象,因为fetchPermissions的API请求还没完成,导致权限校验直接失败返回false。另外,MenuResolver已经调用了updatePermission,但MainGuard里又重复调用一次,属于冗余操作,还可能引发时序问题。

修复步骤

  1. 调整BehaviorSubject初始值
    将初始空对象改为null,区分“未加载”和“无权限”状态:

    protected permissionsSubject = new BehaviorSubject<any>(null);
    
  2. 过滤守卫中的初始未加载状态
    在订阅permissions$时,用filter跳过初始null值,确保只处理API加载后的有效数据:

    return this.dropDownUtils.updatePermission().pipe(
        switchMap(() => this.dropDownUtils.permissions$.pipe(
            filter(data => data !== null)
        )),
        map((data: any) => {
            // 原权限校验逻辑不变
        })
    );
    
  3. 移除重复的权限请求
    利用MenuResolver已加载的菜单数据,避免重复调用updatePermission:

    // 从父路由获取Resolver返回的菜单数据
    const menuData = route.parent?.data['menu'];
    if (menuData) {
        return this.dropDownUtils.permissions$.pipe(
            filter(data => data !== null),
            map(data => {
                // 权限校验逻辑
            })
        );
    }
    
  4. 完善权限发送逻辑
    确保无论是否找到对应路由的权限,都发送明确的权限对象,避免守卫无限等待:

    private extractPermissions(menu: any[]): void {
        const checkPermissions: any[] = [];
        // 原路由匹配逻辑不变...
    
        const permissions = checkPermissions.length > 0 ? {
            is_create: checkPermissions.some(item => item.is_create),
            is_delete: checkPermissions.some(item => item.is_delete),
            is_read: checkPermissions.some(item => item.is_read),
            is_update: checkPermissions.some(item => item.is_update)
        } : {
            is_create: false,
            is_delete: false,
            is_read: false,
            is_update: false
        };
        this.permissionsSubject.next(permissions);
    }
    

是否需要用NgRx替代RxJS?

如果只是解决当前的权限加载时序问题,不需要立刻切换到NgRx,上述RxJS调整已经能解决问题。但如果项目存在多处全局状态共享、复杂数据流管理场景,NgRx可以提供更规范的状态管理方案,让权限这类全局状态的维护更清晰。简单场景下,保持现有RxJS实现足够轻量高效。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.18 21:35:05