如何在angular-oauth2-oidc中实现多AuthGuard并控制执行顺序
Angular OAuth2认证与角色路由拦截的顺序冲突问题
我开发了一个Angular应用,通过angular-oauth2-oidc库实现OAuth2认证拦截页面。需求是通过API从token中获取角色与权限,再用AuthGuard按角色拦截路由,但当前存在顺序冲突问题:角色校验依赖token,但OAuth2认证流程和角色API请求的执行顺序没协调好,导致Guard校验时角色数据还未加载完成。
尝试过创建主Guard来管理多个Guard的调用顺序,但不知道如何手动触发OAuth2流程,也无法重写现有AuthGuard,陷入瓶颈。
相关代码实现
AuthModule
const configAuthZero: AuthConfig = environment.idp; export function storageFactory(): OAuthStorage { return localStorage } @NgModule({ imports: [OAuthModule.forRoot({ resourceServer: { allowedUrls: [environment.adressUrl], sendAccessToken: true } })], providers: [ InitialAuthService, { provide: AuthConfig, useValue: configAuthZero }, { provide: OAuthStorage, useFactory: storageFactory }, { provide: APP_INITIALIZER, useFactory: (initialAuthService: InitialAuthService) => () => initialAuthService.initAuth(), deps: [InitialAuthService], multi: true, }, ], }) export class AuthModule { }
InitialAuthService
@Injectable({ providedIn: "root", }) export class InitialAuthService { private jwtHelper: JwtHelperService = new JwtHelperService(); private isAuthenticatedSubject = new BehaviorSubject<boolean>(false); private isAuthenticated = this.isAuthenticatedSubject.asObservable(); private _decodedAccessToken: any; private _decodedIDToken: any; get decodedAccessToken() { return this._decodedAccessToken; } get decodedIDToken() { return this._decodedIDToken; } get profile() { return this.oauthService.loadUserProfile(); } constructor( private oauthService: OAuthService, private authConfig: AuthConfig, public router: Router, private authService: AuthService, ) { } async initAuth(): Promise<any> { return new Promise<void>((resolveFn, rejectFn) => { this.oauthService.configure(this.authConfig); this.oauthService.setStorage(localStorage); this.oauthService.tokenValidationHandler = new JwksValidationHandler(); this.oauthService.events .pipe(filter((e: any) => e.type === "token_received")) .subscribe(({ type }) => { this.handleNewToken(); }); this.oauthService.events .pipe(filter((e: any) => e.type === "token_expires")) .subscribe(({ type }) => { console.debug("token_expires"); this.logoutSession(); }); this.oauthService.events .pipe(filter((e: any) => e.type === "token_error")) .subscribe(({ type }) => { console.debug("token_error"); this.logoutSession(); }); this.oauthService.events .subscribe(_ => { this.isAuthenticatedSubject.next(this.oauthService.hasValidAccessToken()); }); this.oauthService.loadDiscoveryDocumentAndLogin().then( (isLoggedIn) => { if (isLoggedIn) { resolveFn(); } else { this.oauthService.initImplicitFlow(); rejectFn(); } }, (error: { status: number; }) => { console.log({ error }); if (error.status === 400) { location.reload(); } } ); }); } private handleNewToken() { this._decodedAccessToken = this.jwtHelper.decodeToken( this.oauthService.getAccessToken() ); this._decodedIDToken = this.jwtHelper.decodeToken( this.oauthService.getIdToken() ); this.authService.login(); } logoutSession() { this.oauthService.logOut(); this.authService.logout(); } isLogin() : Observable<boolean> { return this.isAuthenticated; } }
AuthService
@Injectable() export class AuthService { constructor(private userRoleService: UserRoleService) { } private user!: UserRole|null; isAuthorized() { return !!this.user; } hasRole(role: Role) { return this.user!=null && this.isAuthorized() && this.user.id === role; } login() { this.userRoleService.getByUserRoleToken().subscribe({ next: (res) => { this.user = res; }, error: (e) => { console.error(e); }, complete: () => { } }); } logout() { this.user = null; } }
AuthGuard
@Injectable() export class AuthGuard implements CanActivate, CanLoad { constructor( private router: Router, private authService: AuthService ) { } canActivate(route: ActivatedRouteSnapshot): Observable<boolean> | Promise<boolean> | boolean { if (!this.authService.isAuthorized()) { this.router.navigate(['login']); return false; } const roles = route.data['roles'] as Role[]; if (roles && !roles.some(r => this.authService.hasRole(r))) { this.router.navigate(['error', 'not-found']); return false; } return true; } canLoad(route: Route): Observable<boolean> | Promise<boolean> | boolean { if (!this.authService.isAuthorized()) { return false; } const roles = route.data && route.data["roles"] as Role[]; if (roles && !roles.some(r => this.authService.hasRole(r))) { return false; } return true; } }
AppRoutingModule
const routes: Routes = [ { path: 'admin', canLoad: [AuthGuard], canActivate: [AuthGuard], data: { roles: [ "ADMIN", ] }, loadChildren: () => import('./admin/admin-routing.module').then(m => m.AdminRoutingModule), }, { path: 'user', loadChildren: () => import('./auth/user/user.module').then(m => m.UserModule),}, // Always last { path: '**', component: PageNotFoundComponent }, ]; @NgModule({ imports: [RouterModule.forRoot(routes),], exports: [RouterModule], declarations: [ ], providers: [ AuthGuard, AuthService ], }) export class AppRoutingModule { }
解决方案
1. 改造AuthService,让角色加载成为可观察的异步流
当前AuthService.login()仅订阅API但不返回Observable,导致Guard无法等待角色加载完成。修改为返回Observable,并通过BehaviorSubject维护角色状态:
@Injectable() export class AuthService { private userSubject = new BehaviorSubject<UserRole|null>(null); public user$ = this.userSubject.asObservable(); constructor(private userRoleService: UserRoleService) {} isAuthorized(): boolean { return this.userSubject.value !== null; } hasRole(role: Role): boolean { const user = this.userSubject.value; return !!user && user.id === role; } login(): Observable<UserRole> { return this.userRoleService.getByUserRoleToken().pipe( tap(res => this.userSubject.next(res)), catchError(e => { console.error(e); this.logout(); return throwError(() => e); }) ); } logout(): void { this.userSubject.next(null); } }
2. 改造InitialAuthService,确保角色加载完成后再标记认证完成
在handleNewToken中等待AuthService.login()完成,同时新增角色加载状态的Subject:
@Injectable({ providedIn: "root", }) export class InitialAuthService { // 保留原有属性 private jwtHelper: JwtHelperService = new JwtHelperService(); private isAuthenticatedSubject = new BehaviorSubject<boolean>(false); public isAuthenticated$ = this.isAuthenticatedSubject.asObservable(); private _decodedAccessToken: any; private _decodedIDToken: any; // 新增角色加载完成状态 private rolesLoadedSubject = new BehaviorSubject<boolean>(false); public rolesLoaded$ = this.rolesLoadedSubject.asObservable(); // 保留原有getter和构造函数 get decodedAccessToken() { return this._decodedAccessToken; } get decodedIDToken() { return this._decodedIDToken; } get profile() { return this.oauthService.loadUserProfile(); } constructor( private oauthService: OAuthService, private authConfig: AuthConfig, public router: Router, private authService: AuthService, ) {} async initAuth(): Promise<any> { // 保留原有OAuth配置和事件监听逻辑 return new Promise<void>((resolveFn, rejectFn) => { this.oauthService.configure(this.authConfig); this.oauthService.setStorage(localStorage); this.oauthService.tokenValidationHandler = new JwksValidationHandler(); this.oauthService.events .pipe(filter((e: any) => e.type === "token_received")) .subscribe(() => this.handleNewToken()); this.oauthService.events .pipe(filter((e: any) => e.type === "token_expires")) .subscribe(() => this.logoutSession()); this.oauthService.events .pipe(filter((e: any) => e.type === "token_error")) .subscribe(() => this.logoutSession()); this.oauthService.events .subscribe(() => this.isAuthenticatedSubject.next(this.oauthService.hasValidAccessToken())); this.oauthService.loadDiscoveryDocumentAndLogin().then( (isLoggedIn) => { if (!isLoggedIn) { this.oauthService.initImplicitFlow(); rejectFn(); } }, (error) => { console.log({ error }); if (error.status === 400) location.reload(); rejectFn(); } ); }); } private async handleNewToken() { this._decodedAccessToken = this.jwtHelper.decodeToken(this.oauthService.getAccessToken()); this._decodedIDToken = this.jwtHelper.decodeToken(this.oauthService.getIdToken()); try { await this.authService.login().toPromise(); this.rolesLoadedSubject.next(true); } catch (e) { this.rolesLoadedSubject.next(false); this.logoutSession(); } } // 新增:合并认证状态与角色加载状态 get isAuthenticatedWithRoles$(): Observable<boolean> { return combineLatest([this.isAuthenticated$, this.rolesLoaded$]).pipe( map(([isAuth, rolesLoaded]) => isAuth && rolesLoaded) ); } logoutSession() { this.oauthService.logOut(); this.authService.logout(); this.rolesLoadedSubject.next(false); } }
3. 重写AuthGuard,等待认证与角色加载完成后再校验
修改Guard返回Observable,等待InitialAuthService的联合状态,确保token和角色都准备好后再执行校验:
@Injectable() export class AuthGuard implements CanActivate, CanLoad { constructor( private router: Router, private authService: AuthService, private initialAuthService: InitialAuthService ) {} private checkAuthorization(roles: Role[] | undefined): boolean { if (!this.authService.isAuthorized()) { this.router.navigate(['login']); return false; } if (roles && !roles.some(r => this.authService.hasRole(r))) { this.router.navigate(['error', 'not-found']); return false; } return true; } canActivate(route: ActivatedRouteSnapshot): Observable<boolean> { const roles = route.data['roles'] as Role[]; return this.initialAuthService.isAuthenticatedWithRoles$.pipe( filter(isReady => isReady), map(() => this.checkAuthorization(roles)) ); } canLoad(route: Route): Observable<boolean> { const roles = route.data?.['roles'] as Role[]; return this.initialAuthService.isAuthenticatedWithRoles$.pipe( filter(isReady => isReady), map(() => this.checkAuthorization(roles)) ); } }
关键说明
- 核心思路是将同步的角色校验改为异步等待,确保Guard触发时,OAuth2 token已获取且角色API请求完成
- 通过
combineLatest合并认证状态和角色加载状态,只有两者都为true时才进行路由校验 - 避免依赖
APP_INITIALIZER的同步阻塞,改用Guard中的异步等待,更符合Angular路由生命周期
内容的提问来源于stack exchange,提问作者Jack Boch
相关产品推荐
相关产品推荐

