问题
I am trying to implement canActivate in Angular 2/4 using RxJS observables. I have already read another SO question on this. With the following code, my canActivate
method works only once, when the app launches, but hello
is never printed again when the isLoggedIn
observable triggers new values.
canActivate(): Observable<boolean> {
return this.authService.isLoggedIn().map(isLoggedIn => {
console.log('hello');
if (!isLoggedIn) {
this.router.navigate(['/login']);
}
return isLoggedIn;
}).first();
}
or this version is not working as well:
canActivate(): Observable<boolean> {
return this.authService.isLoggedIn().map(isLoggedIn => {
console.log('hello');
if (isLoggedIn) {
this.router.navigate(['/']);
}
return !isLoggedIn;
});
}
However, it works fine with this code:
canActivate(): Observable<boolean> {
return Observable.create(obs => {
this.authService.isLoggedIn().map(isLoggedIn => {
console.log('hello');
if (isLoggedIn) {
this.router.navigate(['/']);
}
return !isLoggedIn;
}).subscribe(isLoggedIn => obs.next(isLoggedIn));
});
}
What am I doing wrong in the first piece of code ?
EDIT: here is the isLoggedIn
implementation
@LocalStorage(AuthService.JWT_TOKEN_KEY)
private readonly token: string;
private tokenStream: Subject<string>;
public isLoggedIn(): Observable<boolean> {
if (!this.tokenStream) {
this.tokenStream = new BehaviorSubject(this.token);
this.storage.observe(AuthService.JWT_TOKEN_KEY)
.subscribe(token => this.tokenStream.next(token));
}
return this.tokenStream.map(token => {
return token != null
});
}
that uses ngx-webstorage. and RxJS BehaviorSubject.
回答1:
Challenges of AuthService with RxJs
This was one of the things I struggled with when switching from AngularJs's promises to Angular's Observable pattern. You see promises are pull notifications and observables are push notifications. As such, you have to rethink your AuthService so that it uses a push pattern. I kept thinking in terms of pulling even when I wrote working Observables. I couldn't stop thinking in terms of pulling.
With a promise pattern it was easier. When the AuthService was created it would either create a promise that resolved to "not logged in" or it would create an async promise that would "restore logged state". You could then have a method named isLoggedIn()
that would return that promise. That allowed you to easily handle delays between showing user data and when you receive user data.
AuthService as a Push service
Now, we switch to Observables and the verb "is" needs to be changed to "when". Making this small change helps you re-think how things are going to work. So lets rename "isLoggedIn" to "whenLoggedIn()" which will be an Observable that emits data when a user authenticates.
class AuthService {
private logIns: Subject = new Subject<UserData>();
public setUser(user: UserData) {
this.logIns.next(user);
}
public whenLoggedIn(): Observable<UserData> {
return this.logIns;
}
}
// example
AuthService.whenLoggedIn().subscribe(console.log);
AuthService.setUser(new UserData());
When a user passed to setUser
it's emitted to subscribes that a new user has been authenticated.
Problems With Above Approach
The above introduces several problems that need to be fixed.
- subscribing to
whenLoggedIn
will listen for new users forever. The pull stream is never completed. - There is no concept of "current state". The previous
setUser
is lost after being pushed to subscribers. - It only tells you when a user is authenticated. Not if there is no current user.
We can fix some of this by switching from Subject
to BehaviorSubject
.
class AuthService {
private logIns: Subject = new BehaviorSubject<UserData>(null);
public setUser(user: UserData) {
this.logIns.next(user);
}
public whenLoggedIn(): Observable<UserData> {
return this.logIns;
}
}
// example
AuthService.whenLoggedIn().first().subscribe(console.log);
AuthService.setUser(new UserData());
This is much closer to what we want.
Changes
BehaviorSubject
will always emit the last value for each new subscription.whenLoggedIn().first()
was added to subscribe and auto unsubscribe after the first value is received. If we didn't useBehaviorSubject
this would block until someone calledsetUser
which might never happen.
Problems With BehaviorSubject
BehaviorSubject
doesn't work for AuthService and I'll demonstrate with this sample code here.
class AuthService {
private logIns: Subject = new BehaviorSubject<UserData>(null);
public constructor(userSessionToken:string, tokenService: TokenService) {
if(userSessionToken) {
tokenService.create(userSessionToken).subscribe((user:UserData) => {
this.logIns.next(user);
});
}
}
public setUser(user: UserData) {
this.logIns.next(user);
}
public whenLoggedIn(): Observable<UserData> {
return this.logIns;
}
}
Here's how the problem would appear in your code.
// example
let auth = new AuthService("my_token", tokenService);
auth.whenLoggedIn().first().subscribe(console.log);
The above creates a new AuthService with a token to restore the user session, but when it runs the console just prints "null".
This happens because BehaviorSubject
is created with an initial value of null
, and the operation to restore the user session is going to happen later after the HTTP call is complete. AuthService will continue to emit null
until the session is restored, but that's a problem when you want to use route activators.
ReplaySubject Is Better
We want to remember the current user, but not emit anything until we know if there is a user or not. ReplaySubject
is the answer to this problem.
Here's how you would use it.
class AuthService {
private logIns: Subject<UserData> = new ReplaySubject(1);
public constructor(userSessionToken:string, tokenService: TokenService) {
if(userSessionToken) {
tokenService.create(userSessionToken).subscribe((user:UserData) => {
this.logIns.next(user);
}, ()=> {
this.logIns.next(null);
console.error('could not restore session');
});
} else {
this.logIns.next(null);
}
}
public setUser(user: UserData) {
this.logIns.next(user);
}
public whenLoggedIn(): Observable<UserData> {
return this.logIns;
}
}
// example
let auth = new AuthService("my_token", tokenService);
auth.whenLoggedIn().first().subscribe(console.log);
The above will not wait until the first value is emitted by whenLoggedIn
. It will get the first
value and unsubscribe.
ReplaySubject
works because it remembers the 1
items or emits nothing. It's the nothing part that is important. When we use AuthService in canActivate
we want to wait until a user state is known.
CanActivate Example
This now makes it a lot easier to write a user guard that redirects to a login screen or allows the route to change.
class UserGuard implements CanActivate {
public constructor(private auth: AuthService, private router: Router) {
}
public canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
return this.auth.whenLoggedIn()
.first()
.do((user:UserData) => {
if(user === null) {
this.router.navigate('/login');
}
})
.map((user:UserData) => !!user);
}
This will yield an Observable of true or false if there is a user session. It will also block router change until that state is known (i.e. are we fetching data from the server?).
It will also redirect the router to the login screen if there is no user data.
回答2:
This is how I made it
import { Injectable, Inject, Optional } from "@angular/core";
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot } from "@angular/router";
import { Observable } from "rxjs/Rx";
import { AuthService, MyLogin, FacebookLogin, GoogleLogin } from "./auth.service";
@Injectable()
export class AuthGuard implements CanActivate {
constructor() {
}
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> | boolean {
// return this.typeSelector(SigninComponent.loginTypeProperty);
let isLogged: boolean = AuthService.loggedIn;
window.alert('isLogged =' + isLogged);
return isLogged;
}
}
This is in another situation where I have to change the password:
import { Injectable, Inject, Optional } from "@angular/core";
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot } from "@angular/router";
import { Observable } from "rxjs/Rx";
import { NewPasswordAuthService } from "./new-password.auth.service";
import { Router, ActivatedRoute } from '@angular/router';
import { IPwCookie } from './IPwCookie.interface';
@Injectable()
export class NewPasswordGuard implements CanActivate {
constructor(private authService: NewPasswordAuthService, private router: Router, private route: ActivatedRoute,)
{
}
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> | boolean {
console.log('query params' + JSON.stringify(route.params));
let hash:string = route.params.hash;
let userId: number = route.params.userId;
console.log('query params string' + JSON.stringify(route.params.hash));
return this.authService.activate(hash)
.map(
(): boolean => {
console.log('check passed =' + NewPasswordAuthService.isAuthenticated());
let passwordCookie:IPwCookie = {
hash: hash,
userId: userId
};
localStorage.setItem('password_hash', JSON.stringify(passwordCookie));
return NewPasswordAuthService.isAuthenticated();
},
(error) => {console.log(error)}
);
}
}
As far as I see you should try adding a return type to the map function, this was one thing that gave me problems. If you do not add a return type to the map function, in this context it does not work.
So just do .map((): boolean => {} ...)
回答3:
canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
return this.service.callServiceMethod())
.map(result => {
return true;
}).catch(err => {
return Observable.of(false);
});
return Observable.of(true);
}
来源:https://stackoverflow.com/questions/44697159/angular-4-canactivate-observable-not-invoked