Website-Suche

javascript - AngularFirebaseAuth: Server-API direkt nach Firebase-Authentifizierung aufrufen?


Meine Auth basiert auf 2 Dingen:

  • Firebase-Authentifizierung (E-Mail/Passwort)
  • Rufe eine Server-API auf, um die vollständige Kundenentität aus der BDD und von der FirebaseID abzurufen (der Benutzer muss existieren). Ein Benutzer wird „authentifiziert“ sein, wenn diese beiden Bedingungen erfüllt sind.

Ich habe auch Auth-Guards, die auf einer isAuthenticated() basieren, die ein Observable zurückgibt (weil bei einem Seiten-Refresh der Guard warten muss, bis die Authentifizierung abgeschlossen ist, bevor der Benutzer irgendwohin umgeleitet wird).

Problem: Ich kann keinen Weg finden, das mit dem ganzen Async- und RxJS-Chaos/Hölle zum Laufen zu bringen. Momentan funktioniert es, aber jedes Mal, wenn isAuthenticated aufgerufen wird, wird die serverAPI-Authentifizierung jedes Mal aufgerufen... Wie kann ich das so refaktorisieren, dass der Server nur einmal aufgerufen wird und trotzdem alles mit Async/Reload weiterhin funktioniert?

AuthService :

export class AuthService {
    public userRole: UserBoRole;
    public authState$: Observable<firebase.User>;

    constructor(
        private afAuth: AngularFireAuth,
        private snackBar: SnackBarService,
        private translate: TranslateService,
        private router: Router,
        private grpcService: GrpcService
    ) {
        this.authState$ = this.afAuth.authState.pipe(
            take(1),
            mergeMap(user => {
                if (!user) {
                    return of(user);
                }

                // User is successfully logged in,
                // now we need to check if he has a correct role to access our app
                // if an error occured, consider our user has not logged in, so we return null
                return this.checkProfile().pipe(
                    take(1),
                    map(() => {
                        this.test = true;
                        return user;
                    }),
                    catchError(err => {
                        console.error(err);
                        return of(null);
                    })
                );
            })
        );

        // Subscribing to auth state change. (useless here because access logic is handled by the AuthGuard)
        this.authState$.subscribe(user => {
            console.log('authState$ changed :', user ? user.toJSON() : 'not logged in');
        });
    }

    checkProfile() {
        return this.callAuthApi().pipe(
            map((customer) => {
                if (!customer || customer.hasRole() === "anonymous") {
                    return Promise.reject(new Error(AuthService.AUTH_ERROR_ROLE));
                }
                this.userRole = customer.getRole();
            })
        );
    }

    isAuthenticated(): Observable<boolean> {
        return this.authState$.pipe(map(authState => !!authState));
    }
}

AuthGuard :

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

    check(): Observable<boolean> {
        return this.authService.isAuthenticated().pipe(
            catchError(err => {
                // notifying UI of the error
                this.authService.handleAuthError(err);

                // signout user
                this.authService.signOut();

                // if an error occured, consider our user has not logged in
                return of(false);
            }),
            tap(isAuthenticated => {
                if (!isAuthenticated) {    
                    // redirecting to login
                    this.router.navigate(['login']);
                }
            })
        );
    }

    canActivateChild(): Observable<boolean> {
        return this.check();
    }

    canActivate(): Observable<boolean> {
        return this.check();
    }
}

Danke

Antwort #1

Sie können Ihre Funktion checkProfile() so ändern, dass sie ein Observable zurückgibt, anstatt ein Observable von einer HTTP-Anfrage oder einem Promise im Fehlerfall. Zuerst prüfen Sie, ob der Benutzer bereits authentifiziert ist (ich habe angenommen, dass userRole in Ordnung ist, da Sie es nach dem Aufruf an das Backend speichern), und wenn ja, geben Sie ein neu erstelltes Observable zurück, ohne das Backend aufzurufen. Andernfalls führen Sie eine Anfrage aus und geben Ihr Observable basierend auf dem Ergebnis des HTTP-Aufrufs zurück. Mit folgendem Beispiel führen Sie den Aufruf nur einmal aus:

checkProfile() {
  return new Observable((observer) => {
    if (this.userRole) {
      observer.next();
      observer.complete();
    } else {
      this.callAuthApi().pipe(
          map((customer) => {
            if (!customer || customer.hasRole() === "anonymous") {
              observer.error(new Error(AuthService.AUTH_ERROR_ROLE));
              observer.complete();
            }
            this.userRole = customer.getRole();
            observer.next();
            observer.complete();
          })
      );
    }
  });
}

Antwort #2

Haha, ReactiveX ist nicht einfach. Es hat eine ziemlich steile Lernkurve. Aber es ist wirklich mächtig.

1. Server nur einmal aufrufen

Du kannst shareReplay verwenden.

Um zu verstehen, wie shareReplay funktioniert, sehen Sie sich hier https://ng-rxjs-share-replay.stackblitz.io an

//shareReplay example
ngOnInit() {    
    const tods$ = this.getTodos();
    tods$.subscribe(console.log);// 1st sub
    tods$.subscribe(console.log);// 2st sub
}

getTodos(): Observable<Todo[]> {
return this.http.get<Todo[]>(this.url)
  .pipe(
    tap(() => console.log('Request')),
    shareReplay(1) // compare with comment and uncomment
  );
}  

Ausgabe mit shareReplay

Request
[Object, Object, Object]
[Object, Object, Object]

Ausgabe ohne shareReplay

Request
[Object, Object, Object]
Request
[Object, Object, Object]

Sie können shareReplay in Ihrem Auth-Service-Code verwenden.

//auth.services.ts
import { shareReplay } from 'rxjs/operators';
...

this.user$ = this.afAuth.authState.pipe(
    tap(user => {
        console.log('login user$ here', user)
    }),
    switchMap(user => {
        if (user) {
            //do something
            return this.db.object(`users/${user.uid}`).valueChanges();
        } else {
            return of(null);
        }
    }),
    shareReplay(1)  //**** this will prevent unnecessary request****
);

2. async und await toPromise()

//auth.service.ts
...
getUser() {
    return this.user$.pipe(first()).toPromise();
}

//auth.guard.ts
...
async canActivate(next: ActivatedRouteSnapshot
  , state: RouterStateSnapshot
): Promise<boolean> {

  const user = await this.auth.getUser();
  //TODO your API code or other conditional authentication here

  if (!user) {
    this.router.navigate(['/login']);
  }
  return !!user;    
}

Ich hoffe, das wird dir helfen.

Verwandte Artikel