Angular2 canActivate () wywołując funkcję asynchroniczną

83

Próbuję użyć zabezpieczeń routera Angular2, aby ograniczyć dostęp do niektórych stron w mojej aplikacji. Używam uwierzytelniania Firebase. W celu sprawdzenia, czy użytkownik jest zalogowany z Firebase, mam do rozmowy .subscribe()na FirebaseAuthobiekcie z zwrotnego. Oto kod dla strażnika:

import { CanActivate, Router, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
import { AngularFireAuth } from "angularfire2/angularfire2";
import { Injectable } from "@angular/core";
import { Observable } from "rxjs/Rx";

@Injectable()
export class AuthGuard implements CanActivate {

    constructor(private auth: AngularFireAuth, private router: Router) {}

    canActivate(route:ActivatedRouteSnapshot, state:RouterStateSnapshot):Observable<boolean>|boolean {
        this.auth.subscribe((auth) => {
            if (auth) {
                console.log('authenticated');
                return true;
            }
            console.log('not authenticated');
            this.router.navigateByUrl('/login');
            return false;
        });
    }
}

Kiedy przejść do strony, która ma osłonę na nim, albo authenticatedczy not authenticatedjest drukowany w konsoli (po pewnym opóźnieniem czeka na odpowiedź od Firebase). Jednak nawigacja nigdy nie jest zakończona. Jeśli nie jestem zalogowany, następuje przekierowanie na /logintrasę. Tak więc problem, który mam, return truenie wyświetla żądanej strony użytkownikowi. Zakładam, że dzieje się tak, ponieważ używam wywołania zwrotnego, ale nie jestem w stanie dowiedzieć się, jak to zrobić inaczej. jakieś pomysły?

Evan Salter
źródło
import Observable w ten sposób -> import {Observable} from 'rxjs / Observable';
Carlos Pliego

Odpowiedzi:

125

canActivatemusi zwrócić, Observablektóry kończy:

@Injectable()
export class AuthGuard implements CanActivate {

    constructor(private auth: AngularFireAuth, private router: Router) {}

    canActivate(route:ActivatedRouteSnapshot, state:RouterStateSnapshot):Observable<boolean>|boolean {
        return this.auth.map((auth) => {
            if (auth) {
                console.log('authenticated');
                return true;
            }
            console.log('not authenticated');
            this.router.navigateByUrl('/login');
            return false;
        }).first(); // this might not be necessary - ensure `first` is imported if you use it
    }
}

Jest returnbrakuje i używam map()zamiast subscribe()ponieważ subscribe()Zwraca Subscriptionnie jestObservable

Günter Zöchbauer
źródło
czy możesz pokazać, jak używać tej klasy w innych komponentach?
Nie wiem co masz na myśli. Używasz tego z trasami, a nie komponentami. Zobacz angular.io/docs/ts/latest/guide/router.html#!#guards
Günter Zöchbauer
Observable nie działa w moim przypadku. Nie widzę żadnych danych wyjściowych konsoli. Jeśli jednak zwrócę wartości logiczne warunkowo (jak w dokumentacji), konsola zostanie zalogowana. Czy this.auth jest prostym Observable?
cortopy
@cortopy authto wartość emitowana przez obserwowalne (może być po prostu truelub false). Obserwowalne jest wykonywane, gdy router subskrybuje to. Może czegoś brakuje w Twojej konfiguracji.
Günter Zöchbauer
1
@ günter-zöchbauer tak, dzięki za to. Nie zdawałem sobie sprawy, że subskrybuję subskrybenta. Wielkie dzięki za odpowiedź! Działa świetnie
cortopy
27

Możesz użyć Observabledo obsługi asynchronicznej części logicznej. Oto kod, który testuję na przykład:

import { Injectable } from '@angular/core';
import { CanActivate } from '@angular/router';
import { Observable } from 'rxjs/Observable';
import { DetailService } from './detail.service';

@Injectable()
export class DetailGuard implements CanActivate {

  constructor(
    private detailService: DetailService
  ) {}

  public canActivate(): boolean|Observable<boolean> {
    if (this.detailService.tempData) {
      return true;
    } else {
      console.log('loading...');
      return new Observable<boolean>((observer) => {
        setTimeout(() => {
          console.log('done!');
          this.detailService.tempData = [1, 2, 3];
          observer.next(true);
          observer.complete();
        }, 1000 * 5);
      });
    }
  }
}
KobeLuo
źródło
2
To właściwie dobra odpowiedź, która naprawdę mi pomogła. Mimo że miałem podobne pytanie, ale zaakceptowana odpowiedź nie rozwiązała mojego problemu. Ten zrobił
Konstantin
Właściwie to jest właściwa odpowiedź !!! Dobry sposób na użycie metody canActivate wywołującej funkcję asynchroniczną.
danilo
18

canActivatemoże zwrócić, Promisektóry rozwiązuje booleanrównież

paulsouche
źródło
13

Możesz zwrócić true | false jako obietnicę.

import {Injectable} from '@angular/core';
import {ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot} from '@angular/router';
import {Observable} from 'rxjs';
import {AuthService} from "../services/authorization.service";

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

  canActivate(
    next: ActivatedRouteSnapshot,
    state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean {
  return new Promise((resolve, reject) => {
  this.authService.getAccessRights().then((response) => {
    let result = <any>response;
    let url = state.url.substr(1,state.url.length);
    if(url == 'getDepartment'){
      if(result.getDepartment){
        resolve(true);
      } else {
        this.router.navigate(['login']);
        resolve(false);
      }
    }

     })
   })
  }
}
nieuczciwy chłopak
źródło
1
Ten nowy obiekt Promise mnie ratuje: D Dzięki.
canmustu,
Dziękuję Ci. To rozwiązanie czeka, aż wywołanie api odpowie, a następnie przekieruje ... idealne.
Philip Enc
Wygląda to na przykład jawnego antipattern konstruktora Promise ( stackoverflow.com/questions/23803743/… ). Przykładowy kod sugeruje, że getAccessRights () zwraca już Obietnicę, więc spróbuję return this.authService.getAccessRights().then...zwrócić ją bezpośrednio za pomocą i zwrócić wynik boolowski bez zawijania resolve.
rob3c
6

Aby rozwinąć najpopularniejszą odpowiedź. Auth API for AngularFire2 ma pewne zmiany. To jest nowa sygnatura do uzyskania AngularFire2 AuthGuard:

import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { AngularFireAuth } from 'angularfire2/auth';
import { CanActivate, Router, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';

@Injectable()
export class AuthGuardService implements CanActivate {

  constructor(
    private auth: AngularFireAuth,
    private router : Router
  ) {}

  canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot):Observable<boolean>|boolean {
    return this.auth.authState.map(User => {
      return (User) ? true : false;
    });
  }
}

Uwaga: to jest dość naiwny test. Możesz zarejestrować w konsoli instancję użytkownika, aby sprawdzić, czy chcesz przetestować pod kątem bardziej szczegółowego aspektu użytkownika. Powinien jednak przynajmniej pomóc w ochronie tras przed niezalogowanymi użytkownikami.

Rudi Strydom
źródło
5

W najnowszej wersji AngularFire działa następujący kod (powiązany z najlepszą odpowiedzią). Zwróć uwagę na użycie metody „potoku”.

import { Injectable } from '@angular/core';
import {ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot} from '@angular/router';
import {AngularFireAuth} from '@angular/fire/auth';
import {map} from 'rxjs/operators';
import {Observable} from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class AuthGuardService implements CanActivate {

  constructor(private afAuth: AngularFireAuth, private router: Router) {
  }

  canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean {
    return this.afAuth.authState.pipe(
      map(user => {
        if(user) {
          return true;
        } else {
          this.router.navigate(['/login']);
          return false;
        }
      })
    );
  }
}

Ajitesh
źródło
Mam jeszcze 1 wywołanie XHR po isLoggedIn (), a wynik XHR jest używany w drugim wywołaniu XHR. Jak mieć drugie wywołanie Ajax, które zaakceptuje pierwszy wynik? Przykład, który podałeś, jest dość łatwy, czy możesz mi dać znać, jak używać mapy, jeśli mam też inny Ajax.
Pratik
2

W moim przypadku musiałem poradzić sobie z różnymi zachowaniami zależnymi od błędu statusu odpowiedzi. Tak to działa u mnie z RxJS 6+:

@Injectable()
export class AuthGuard implements CanActivate {

  constructor(private auth: AngularFireAuth, private router: Router) {}

  public canActivate(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot
  ): Observable<boolean> | boolean {
    return this.auth.pipe(
      tap({
        next: val => {
          if (val) {
            console.log(val, 'authenticated');
            return of(true); // or if you want Observable replace true with of(true)
          }
          console.log(val, 'acces denied!');
          return of(false); // or if you want Observable replace true with of(true)
        },
        error: error => {
          let redirectRoute: string;
          if (error.status === 401) {
            redirectRoute = '/error/401';
            this.router.navigateByUrl(redirectRoute);
          } else if (error.status === 403) {
            redirectRoute = '/error/403';
            this.router.navigateByUrl(redirectRoute);
          }
        },
        complete: () => console.log('completed!')
      })
    );
  }
}

W niektórych przypadkach może to nie działać, przynajmniej nextczęść tapoperatora . Usuń go i dodaj stary towar, mapjak poniżej:

  public canActivate(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot
  ): Observable<boolean> | boolean {
    return this.auth.pipe(
      map((auth) => {
        if (auth) {
          console.log('authenticated');
          return true;
        }
        console.log('not authenticated');
        this.router.navigateByUrl('/login');
        return false;
      }),
      tap({
        error: error => {
          let redirectRoute: string;
          if (error.status === 401) {
            redirectRoute = '/error/401';
            this.router.navigateByUrl(redirectRoute);
          } else if (error.status === 403) {
            redirectRoute = '/error/403';
            this.router.navigateByUrl(redirectRoute);
          }
        },
        complete: () => console.log('completed!')
      })
    );
  }
mpro
źródło
0

Aby pokazać inny sposób realizacji. Zgodnie z dokumentacją i wspomnianymi w innych odpowiedziach zwracany typ CanActivate może być również Obietnicą, która jest przekształcana na wartość logiczną.

Uwaga : Pokazany przykład jest zaimplementowany w Angular 11, ale ma zastosowanie do wersji Angular 2+.

Przykład:

import {
  Injectable
} from '@angular/core';
import {
  ActivatedRouteSnapshot,
  CanActivate,
  CanActivateChild,
  Router,
  RouterStateSnapshot,
  UrlTree
} from '@angular/router';
import {
  Observable
} from 'rxjs/Observable';
import {
  AuthService
} from './auth.service';

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

  canActivate(
    route: ActivatedRouteSnapshot, state: RouterStateSnapshot
  ): Observable < boolean | UrlTree > | Promise < boolean | UrlTree > | boolean | UrlTree {
    return this.checkAuthentication();
  }

  async checkAuthentication(): Promise < boolean > {
    // Implement your authentication in authService
    const isAuthenticate: boolean = await this.authService.isAuthenticated();
    return isAuthenticate;
  }

  canActivateChild(
    childRoute: ActivatedRouteSnapshot, state: RouterStateSnapshot
  ): Observable < boolean | UrlTree > | Promise < boolean | UrlTree > | boolean | UrlTree {
    return this.canActivate(childRoute, state);
  }
}

rishabh sethi
źródło
0

używając async czekaj ... czekasz na rozwiązanie obietnicy

async getCurrentSemester() {
    let boolReturn: boolean = false
    let semester = await this.semesterService.getCurrentSemester().toPromise();
    try {

      if (semester['statusCode'] == 200) {
        boolReturn = true
      } else {
        this.router.navigate(["/error-page"]);
        boolReturn = false
      }
    }
    catch (error) {
      boolReturn = false
      this.router.navigate(["/error-page"]);
    }
    return boolReturn
  }

Oto mój auth gaurd (@angular v7.2)

async canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
    let security: any = null
    if (next.data) {
      security = next.data.security
    }
    let bool1 = false;
    let bool2 = false;
    let bool3 = true;

    if (this.webService.getCookie('token') != null && this.webService.getCookie('token') != '') {
      bool1 = true
    }
    else {
      this.webService.setSession("currentUrl", state.url.split('?')[0]);
      this.webService.setSession("applicationId", state.root.queryParams['applicationId']);
      this.webService.setSession("token", state.root.queryParams['token']);
      this.router.navigate(["/initializing"]);
      bool1 = false
    }
    bool2 = this.getRolesSecurity(next)
    if (security && security.semester) {
      // ----  watch this peace of code
      bool3 = await this.getCurrentSemester()
    }

    console.log('bool3: ', bool3);

    return bool1 && bool2 && bool3
  }

trasa jest

    { path: 'userEvent', component: NpmeUserEvent, canActivate: [AuthGuard], data: {  security: { semester: true } } },
anonimowy
źródło