Angular2 canActivate()调用异步函数

我正在尝试使用 Angular2路由器保护,以限制访问我的应用程序中的一些网页。我正在使用 Firebase 验证。为了检查用户是否使用 Firebase 登录,我必须使用回调调用 FirebaseAuth对象上的 .subscribe()。这是守卫的密码:

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

当导航到一个包含保护的页面时,会将 authenticatednot authenticated打印到控制台(在等待来自 firebase 的响应的一段延迟之后)。但是,导航永远不会完成。此外,如果我没有登录,我被重定向到 /login路线。因此,我遇到的问题是 return true不能向用户显示请求的页面。我假设这是因为我正在使用回调函数,但是我无法找到其他方法。有什么想法吗?

80086 次浏览

canActivate needs to return an Observable that completes:

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

There is a return missing and I use map() instead of subscribe() because subscribe() returns a Subscription not an Observable

canActivate can return a Promise that resolves a boolean too

You might use Observable to handle the async logic part. Here is the code I test for example:

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

To expand on the most popular answer. The Auth API for AngularFire2 has changes somewhat. This is new signature to achieve a 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;
});
}
}

Note: This is a fairly naive test. You can console log the User instance to see if you would like to test against some more detailed aspect of the user. But should at least help protect routes against user's who are not logged in.

You can return true|false as a promise.

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


})
})
}
}

In the most recent version of AngularFire, the following code works (related to the best answer). Note the usage of "pipe" method.

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

In my case I needed to handle different behavior depends on the response status error. This is how it works for me with 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!')
})
);
}
}

In some cases this might not work, at least the next part of tap operator. Remove it and add old good map like below:

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

In order to show another way of implementation. As per documentation, and mentioned by other answers return type of CanActivate can also be a Promise that resolves to boolean.

Note: The example shown is implemented in Angular 11, but is applicable to Angular 2+ versions.

Example:

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

using async await... you wait for the promise to resolve

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
}

Here is my 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
}

route is

    { path: 'userEvent', component: NpmeUserEvent, canActivate: [AuthGuard], data: {  security: { semester: true } } },