Angular2 canActivate () вызывает асинхронную функцию


83

Я пытаюсь использовать защиту маршрутизатора Angular2, чтобы ограничить доступ к некоторым страницам в моем приложении. Я использую аутентификацию Firebase. Для того , чтобы проверить , если пользователь вошел в систему с Firebase, я должен вызов .subscribe()на FirebaseAuthобъект с обратным вызовом. Это код охранника:

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;
        });
    }
}

При переходе на страницу с охранником на консоль выводится либо authenticated, либо not authenticated(после некоторой задержки в ожидании ответа от firebase). Однако навигация никогда не завершается. Кроме того, если я не авторизован, меня перенаправляют на /loginмаршрут. Итак, проблема, с которой я столкнулся, return trueне отображает запрашиваемую страницу для пользователя. Я предполагаю, что это потому, что я использую обратный вызов, но я не могу понять, как это сделать иначе. Есть предположения?


import Observable как это -> import {Observable} из 'rxjs / Observable';
Карлос Плиего

Ответы:


125

canActivateнеобходимо вернуть, Observableчто завершает:

@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
    }
}

Там является не returnхватает , и я использую map()вместо того , subscribe()потому что subscribe()возвращает SubscriptionнеObservable


можешь показать, как использовать этот класс в других компонентах?

Не уверен, что вы имеете в виду. Вы используете это с маршрутами, а не с компонентами. См. Angular.io/docs/ts/latest/guide/router.html#!#guards
Günter Zöchbauer

В моем случае Observable не работает. Я не вижу вывода на консоль. Однако, если я возвращаю логические значения условно (как в документации), консоль регистрируется. Является ли this.auth простым Observable?
cortopy

@cortopy auth- это значение, испускаемое наблюдаемым (может быть просто trueили false). Наблюдаемое выполняется, когда маршрутизатор подписывается на него. Возможно, что-то не хватает в вашей конфигурации.
Günter Zöchbauer

1
@ günter-zöchbauer да, спасибо за это. Я не понимал, что подписываюсь на подписчика. Большое спасибо за ответ! Он отлично работает
Cortopy

27

Вы можете использовать Observableдля обработки части асинхронной логики. Вот код, который я тестирую, например:

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);
      });
    }
  }
}

2
Это действительно хороший ответ, который мне очень помог. Хотя у меня был аналогичный вопрос, но принятый ответ не решил мою проблему. Это сделал
Константин

Собственно, это правильный ответ !!! Хороший способ использовать метод canActivate, вызывающий асинхронную функцию.
danilo


13

Вы можете вернуть true | false как обещание.

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);
      }
    }

     })
   })
  }
}

1
Этот новый объект Promise меня спасает: D Спасибо.
canmustu

Спасибо. Это решение ожидает ответа на вызов API, а затем перенаправляет .. идеально.
Philip Enc

Это похоже на пример явного антипаттерна конструктора Promise ( stackoverflow.com/questions/23803743/… ). В примере кода предполагается, что getAccessRights () уже возвращает Promise, поэтому я бы попробовал напрямую вернуть его return this.authService.getAccessRights().then...и вернуть логический результат без переноса resolve.
rob3c

6

Чтобы развернуть наиболее популярный ответ. API Auth для AngularFire2 несколько изменился. Это новая подпись для достижения 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;
    });
  }
}

Примечание: это довольно наивный тест. Вы можете выполнить консольный журнал экземпляра User, чтобы узнать, хотите ли вы протестировать более подробный аспект пользователя. Но должно, по крайней мере, помочь защитить маршруты от пользователей, которые не вошли в систему.


5

В самой последней версии AngularFire работает следующий код (связанный с лучшим ответом). Обратите внимание на использование метода «трубы».

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;
        }
      })
    );
  }
}


У меня есть еще 1 вызов XHR после isLoggedIn (), и результат XHR используется во втором вызове XHR. Как получить 2-й вызов ajax, который будет принимать 1-й результат? Приведенный вами пример довольно прост, пожалуйста, дайте мне знать, как использовать карту, если у меня тоже есть другой ajax.
Pratik

2

В моем случае мне нужно было обрабатывать другое поведение в зависимости от ошибки статуса ответа. Вот как это работает у меня с 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!')
      })
    );
  }
}

В некоторых случаях это может не сработать, по крайней мере, со nextстороны tapоператора . Удалите его и добавьте старый добрый, mapкак показано ниже:

  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!')
      })
    );
  }

0

Для того, чтобы показать другой способ реализации. Согласно документации и упомянутым в других ответах возвращаемый тип CanActivate также может быть Promise, который разрешается в логическое значение.

Примечание . Показанный пример реализован в Angular 11, но применим к версиям Angular 2+.

Пример:

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);
  }
}


0

используя async await ... вы ждете, пока обещание разрешится

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
  }

Вот мой 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
  }

маршрут

    { path: 'userEvent', component: NpmeUserEvent, canActivate: [AuthGuard], data: {  security: { semester: true } } },
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.