使CanActivate等待Subject计算

时间:2019-06-20 12:08:12

标签: angular typescript firebase rxjs google-cloud-firestore

我正在尝试在Angular + Firebase应用程序中实现身份验证保护。

我正在使用Firebase身份验证(FA)和Firebase Cloud Firestore(FCF)的组合来保存用户。因此,仅FA用户是不够的,我需要在FCF中找到同一用户才能考虑该用户已登录。

在注册期间,我将用户保存在FCF中的id下,与FA中该用户的uid相同。

在我的authentication.service中:

    export class AuthenticationService {

      public authenticatedUserSubject: Subject<null | User> = new Subject <null | User>();

      public constructor(
        private readonly angularFireAuth: AngularFireAuth,
        private readonly angularFirestore: AngularFirestore
      ) {    
        this.angularFireAuth.authState.subscribe(    
          (userInfo: null | UserInfo) => {    
            if (userInfo === null) {    
              this.authenticatedUserSubject.next(null);    
            } else {    
              this.angularFirestore.collection<User>('users').doc<User>(userInfo.uid).valueChanges().pipe(take(1)).subscribe(
                (user: undefined | User): void => {   
                  if (typeof user === 'undefined') {
                    this.authenticatedUserSubject.next(null);
                  } else {
                    this.authenticatedUserSubject.next(user);
                  }
                }
              );
            }
          }
        ); 
      }   
    }

根据我的理解,当服务初始化时,这段代码将开始侦听FA用户的更改,随后我将检查FCF用户并更新authenticatedUserSubject

在我的authenticated.guard中:

    public canActivate(activatedRouteSnapshot: ActivatedRouteSnapshot, routerStateSnapshot: RouterStateSnapshot): Observable<boolean> {
        return this.authenticationService.authenticatedUserSubject.pipe(take(1)).pipe(  
          map( 
            (user: null | User): boolean => {
              if (user === null) {
                this.router.navigateByUrl('/sign-in');
                return false;
              } else {
                return true;
              }
            }
          )
        );
      }

问题:

  • 警卫没有从主体那里获得任何价值
  • 如果我使用BehaviourSubject代替初始值为null的Subject,刷新页面将发出null作为默认值,将用户扔到/sign-in

目标:

  • 在刷新/加载应用程序时,应在运行路由防护之前计算authenticatedUserSubject
  • 注销后,警卫应了解状态变化

此外,如果有更好的方法来实现此功能,请告诉我!

2 个答案:

答案 0 :(得分:0)

我想您可以像您所说的那样使用初始值为null的BehaviorSubject,但是您需要采取的防护措施是通过以下方式使用takeWhile

public canActivate(activatedRouteSnapshot: ActivatedRouteSnapshot, routerStateSnapshot: RouterStateSnapshot): Observable<boolean> {

    return this.authenticationService.authenticatedUserSubject.pipe(
    tap((user: User) => {
       if(!user){
          this.router.navigate(['/sign-up']);
       }
    }),
    takeWhile((data) => data === null, true)
    );

  }

您知道方法需要布尔值,因此您可以在管道中的takeWhile之前映射它。

希望这对您有用,因为我遇到了同样的问题,并且奏效了。

答案 1 :(得分:0)

您应该使用BehaviourSubject而不是Subject。主题不向用户发出最后发出的值。只有在订阅后Subject.next()时,Subject的订阅者才会获得价值。 BehaviourSubject并非如此。订阅者将始终获得订阅中最后发出的值。借助此功能,我们可以尝试改进代码,看看问题是否解决:

像这样更改您的服务-

export class AuthenticationService {

    public authenticatedUserSubject: BehaviorSubject<User> = new BehaviorSubject<User>(null);

    public constructor(
      private readonly angularFireAuth: AngularFireAuth,
      private readonly angularFirestore: AngularFirestore
    ) {


        combineLatest(this.angularFireAuth.authState, 
                      this.angularFirestore.collection<User>('users').doc<User>(userInfo.uid).valueChanges()
                     )
                    .pipe(
                        tap(([userFromFireAuth, userFromFirestore]) => {
                            if (!userFromFireAuth) {
                                this.authenticatedUserSubject.next(null); 
                            } else {
                                if (typeof userFromFireAuth === 'undefined') {
                                    this.authenticatedUserSubject.next(null);
                                } else {
                                    this.authenticatedUserSubject.next(userFromFireAuth);
                                }
                            }
                        })
                    ).subscribe();       
    }   
  }

具有此功能将避免链接subscribe()

现在让我们这样重写canActivate:

public canActivate(activatedRouteSnapshot: ActivatedRouteSnapshot, routerStateSnapshot: RouterStateSnapshot): Observable<boolean> {

        return this.authenticationService.authenticatedUserSubject
                   .pipe(
                       //this will wait until user becomes NOT NULL
                       skipWhile(user => !user),
                       tap(user => {
                            if (!user) {
                                //this will be useful when user logged out and trying to reach a page which requires authentication
                                //NOTICE - When page is NOT refreshed
                                this.router.navigate(['/sign-up']);
                            }
                        }),
                        take(1),
                        map(() => true)
                   );                   
      }

让我们知道它是否有效。