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

如何在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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.14 19:15:50