Angular 13 路由保护(canActivate)仅适用于第一页加载

Anglar 13 route guard (canActivate) only works on first page load

找到解决方案 - 由于解决方案比问题更短,我将其添加到此处: 问题不在于守卫(即使我通过使用 map / switchMap 运算符而不是新的 Obeservable 来优化守卫),而是我用来在组件中获取用户数据的主题。我在用户服务中用 ReplaySubject 替换了 Subject,因为我在 ngOnInit 挂钩中有一个延迟订阅。

在 Angular 13 应用程序中,我设置了 followwing 路由 canActivate guard 到帐户路由:

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

@Injectable()
export class AuthGuard implements CanActivate {
  constructor(private userService: UserService, private route: Router) {
  }

  isLoggedIn(): Observable<boolean | UrlTree> {
    return this.userService.checkLoginStatus().pipe(
      map((status) => {
        if (status.status === 'logged in') {
          return true;
        }
        this.route.navigate(['/login']);
        return false;
      })
    );
  }

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

但是,这仅在我在浏览器中重新加载页面时有效,但在我通过单击路由器 link 使用应用程序导航到那里时无效。我可以在网络选项卡中看到 checkLoginStatus 请求已完成并获得 'logged in' 作为 return 值,但是页面只是保持空白。 谁能发现我的 canActivate 实现中的错误? 我确保块 subscriber.next(true);被执行了,但我想我在那里做的事情有什么不对?

谢谢!

编辑:

感谢您到目前为止的回答。不幸的是,似乎出了点问题,因为即使我这样实现 canActivate:


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

...这应该导致始终授予对该页面的访问权限,即使该页面仍为空白。 所以我将用户服务添加到此 post:

import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Observable, Subject, throwError } from 'rxjs';
import { catchError, tap } from 'rxjs/operators';
import { User } from '../models/user.model';
@Injectable()
export class UserService {
  private userURL = 'https://phoenix:4001/user';
  public user = new Subject<User | null>();

  constructor(private http: HttpClient) {
    this.checkLoginStatus().subscribe(status => {
      if (status.status === 'logged in') {
        this.completeLogin();
      }
    })
  }

  public logout() {
    this.http
      .get<{ message?: string }>(this.userURL + '/logout', {
        withCredentials: true,
      })
      .pipe(catchError(this.handleNotAuthorized.bind(this)))
      .subscribe((resp) => {
        if (resp?.message === 'logged out') {
          this.user.next(null);
        }
      });
  }

  public login(params: { name: string; password: string }) {
    this.http
      .get<{ message?: string; token?: string }>(`${this.userURL}/login`, {
        withCredentials: true,
        params,
      })
      .pipe(catchError(this.handleNotAuthorized.bind(this)))
      .subscribe((response) => {
        if (response.hasOwnProperty('token')) {
          this.completeLogin();
        }
      });
  }

  public register(params: {
    name: string;
    password: string;
    firstName?: string;
    lastName?: string;
  }) {
    this.http
      .post<{ message?: string; token?: string }>(
        `${this.userURL}/register`,
        params,
        {
          withCredentials: true,
        }
      )
      .pipe(catchError(this.handleNotAuthorized.bind(this)))
      .subscribe((response) => {
        if (response.hasOwnProperty('token')) {
          this.completeLogin();
        }
      });
  }

  checkLoginStatus() {
    return this.http
      .get<{status: string}>(this.userURL + '/check', { withCredentials: true })
      .pipe(catchError(this.handleNotAuthorized.bind(this)));
  }

  private getUser(): Observable<User> {
    return this.http
      .get<User>(this.userURL, { withCredentials: true })
      .pipe(catchError(this.handleNotAuthorized.bind(this)));
  }

  private completeLogin() {
    this.getUser().subscribe((user) => {
      this.user.next(user);
    });
  }

  private handleNotAuthorized(error: any) {
    if (error?.error?.message === 'Unauthorized') {
      this.user.next(null);
      return throwError(() => {});
    }
    let errorMessage = '';
    if (error.error instanceof ErrorEvent) {
      // Get client-side error
      errorMessage = error.error.message;
    } else {
      // Get server-side error
      errorMessage = `Error Code: ${error.status}\nMessage: ${error.message}`;
    }
    return throwError(() => {
      return errorMessage;
    });
  }
}

账户组件非常简单:

import { Component, OnInit } from '@angular/core';
import { User } from 'src/app/models/user.model';
import { UserService } from 'src/app/services/user.service';

@Component({
  selector: 'app-account',
  templateUrl: './account.component.html'
})
export class AccountComponent implements OnInit {
  userData: User | null = null;
  constructor(private userService: UserService) { }

  ngOnInit(): void {
    this.userService.user.subscribe(user => {
      if(user) {
        this.userData = user;
      }
    })
  }

}

我的路由器是这样的:

import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { AuthGuard } from './services/auth-guard.service';
import { ContentResolver } from './services/content-resolver.service';
import { ProductResolver } from './services/product-resolver.service';
import { AboutComponent } from './views/about/about.component';
import { AccountComponent } from './views/account/account.component';
import { ContentEngagementComponent } from './views/content-engagement/content-engagement.component';
import { DirectiveComponent } from './views/directive/directive.component';
import { HomeComponent } from './views/home/home.component';
import { LoginComponent } from './views/login/login.component';
import { ProductComponent } from './views/product/product.component';
import { ShopComponent } from './views/shop/shop.component';
import { TeaserComponent } from './views/teaser/teaser.component';
import { ThankYouComponent } from './views/thank-you/thank-you.component';

const routes: Routes = [
  {
    path: '',
    component: HomeComponent,
    pathMatch: 'full',
    resolve: [ContentResolver],
  },
  {
    path: 'shop/:id',
    component: ProductComponent,
    resolve: [ProductResolver],
  },
  {
    path: 'shop',
    component: ShopComponent,
    resolve: [ContentResolver, ProductResolver],
  },
  {
    path: 'about',
    component: AboutComponent,
    resolve: [ContentResolver],
  },
  {
    path: 'login',
    component: LoginComponent,
  },
  {
    path: 'content-engagement',
    component: ContentEngagementComponent,
  },
  {
    path: 'teaser',
    component: TeaserComponent,
  },
  {
    path: 'account',
    component: AccountComponent,
    canActivate: [AuthGuard]
  },
  {
    path: 'thankyou',
    component: ThankYouComponent,
  },
  {
    path: 'directives',
    component: DirectiveComponent,
  },
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule],
})
export class AppRoutingModule {}

一点代码清理。当您已经拥有一个新的可观察对象时,无需创建它,只需管道映射现有的对象并 return 它。根据你的代码,我假设你想将用户发送到 /login 如果他们没有登录试图访问它保护的路由。

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

@Injectable()
export class AuthGuard implements CanActivate {
  constructor(private userService: UserService, private route: Router) {
  }

  isLoggedIn(): Observable<boolean | UrlTree> {
    return this.userService.checkLoginStatus().pipe(
        map(status) => {
          if (status.status === 'logged in') {
            return true;
          } 
            route.navigate(['/login\']);
            return false;
          }
      }));
    });
  }

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

isLogged 方法 returns Observable 在您的代码中没有完成,它必须完成。

 isLoggedIn(): Observable<boolean | UrlTree> {
    return new Observable((subscriber) => {
      this.userService.checkLoginStatus().subscribe((status) => {
        if (status.status === 'logged in') {
          subscriber.next(true);
        } else {
          subscriber.next(this.route.parseUrl('/login'));
        }
        subscriber.complete();
      });
    });
  }

你不是return可观察的 而是一个新的 Observable,尝试这样做。

  isLoggedIn(): Observable<boolean | UrlTree> {
    return this.userService.checkLoginStatus()
        .pipe(
             switchMap((status) => {
                 if (status.status === 'logged in') {
                   return of(true);
                 } else {
                   this.route.parseUrl('/login');
                   return of(false);
                 }
             })
        );
   }