web-dev-qa-db-ja.com

Angular 5/6:エラールートにリダイレクトせずにルートを保護(ルートガード)

ピクルスが少しあります。ルートガード(CanActivateインターフェイスを実装)を使用して、ユーザーに特定のルートへのアクセスが許可されているかどうかを確認しています。

_const routes: Routes = [
    {
        path: '',
        component: DashboardViewComponent
    },
    {
        path: 'login',
        component: LoginViewComponent
    },
    {
        path: 'protected/foo',
        component: FooViewComponent,
        data: {allowAccessTo: ['Administrator']},
        canActivate: [RouteGuard]
    },
    {
        path: '**',
        component: ErrorNotFoundViewComponent
    }
];
_

これで、 '/ protected/foo'ルートがアクティブにならないように保護できますが、ユーザーにアクセスしようとしているルートが禁止されていることを伝えたいと思います(403 Forbiddenに似ています)。

問題:エラールートにリダイレクトせずに、この特別なエラービューをユーザーに表示するにはどうすればよいですか私が見つけた多くのソースが推奨するオプションはどれですか? そして、どうすればRouteGuardを使用できますか?実際に禁止されたルートをロードせずに、FooViewComponent内のアクセスを確認し、異なるビューを表示すると、最初にRouteGuardを持ちます。

理想的には、RouteGuardcanActivate()メソッドでfalseを返すだけでなく、コンポーネントをErrorForbiddenViewComponentに完全に置き換えたいと思います。しかし、私はそれを行う方法がわからない、またはそれがイベント可能です。代替案はありますか?

これが私のルートガードの外観です。

_import {Injectable} from '@angular/core';
import {Router, CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot} from '@angular/router';
import {AuthService} from '../services/auth.service';

@Injectable()
export class RouteGuard implements CanActivate {

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

    canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
        const { auth, router } = this;
        const { allowAccessTo } = next.data;
        const identity = auth.getIdentity();
        if (
            identity &&
            allowAccessTo.indexOf(identity.role)
        ) {
            // all good, proceed with activating route
            return true;
        }
        if (identity) {
            // TODO show ErrorForbiddenViewComponent instead of redirecting
            console.log('403 Forbidden >>', next);
        }
        else { 
            // not logged in: redirect to login page with the return url
            const [returnUrl, returnQueryParams] = state.url.split('?');
            console.log('401 Unauthorised >>', returnUrl, returnQueryParams, next);
            router.navigate(['/login'], {queryParams: {returnUrl, returnQueryParams}});
        }
        return false;
    }
}
_

そのため、ルートの読み込みを防止しているだけですが、リダイレクトしていません。ログに記録されていない訪問者のみをログインルートにリダイレクトします。

推論:

  • ルートはアプリケーションの特定の状態を反映する必要があります-ルートURLにアクセスすると、その状態が再作成されます
  • エラールートがある場合(404 Not Foundを除く)は、アプリケーションが実際にエラー状態を再作成できることを意味します。これはなぜエラー状態をアプリケーションの状態として保持するのかという意味ではありませんか?デバッグのためにログ(コンソールまたはサーバー)を使用する必要があります。エラーページ(ページの更新など)を再度参照すると、それが妨げられる場合があります。
  • また、エラールートアプリにリダイレクトすることにより、ユーザーにエラーの洞察を提供する必要があります。そのため、URLを介してパラメーターを渡すか、(さらに悪いことに)エラーサービスでエラー状態を維持し、エラールートにアクセスしたときにそれを取得する必要があります。
  • また、RouteGuardを無視し、コンポーネントをロードしてその中のアクセスをチェックするだけで、(ユーザーが許可されていないので)使用されない追加の依存関係がロードされる可能性があり、遅延ロード全体が非常に難しくなります。

誰かがこれに対して何らかの解決策を持っていますか?また、Angular 2+が長い間存在していなかったので、これまで誰もこのような状況はなかったのでしょうか?リダイレクトで大丈夫ですか?

また、現在FooViewComponentを同期的に使用していますが、今後変更される可能性があることに注意してください。

10
Ivan Hušnjak

私はかつて同様の問題に取り組んでいました。

stackblitz poc 私が作成した場所の共有-

  • 認証済みコンポーネント(ガード付き)
  • ログインコンポーネント
  • 許可ガード
  • ルート(/authルートにはPermissionGuardServiceガードが提供されます)

ガードはユーザータイプを評価し、それに応じてリダイレクト/エラーを処理しています。

ユースケースは次のとおりです-

  • ユーザーはログインしていません(shows a toast with log in message
  • ユーザーは管理者ではありません(shows a toast with unauthorised message
  • ユーザーは管理者です(show a toast with success messaage

ユーザーをローカルストレージに保存しました。

編集-デモ enter image description here

特別な処理が必要な場合はお知らせください。コードベースを更新します。

乾杯!

4
planet_hunter

RouteGuardは、モーダルウィンドウに使用しているサービスをインジェクトでき​​、.canActivate()はリダイレクトなしでモーダルをポップして、アプリの現在の状態を乱すことなくユーザーに通知できます。

Toastrとそのangular=ラッパーを使用します。これはmodelessのポップアップを作成するためです。 。

2
Ron Newcomb

angular2の例 を質問のコメントでTarun Lalwaniが提供した後、および Angular docs 私のコードにそれを適用することができました:

ルートを指定するときにRouteGuardを使用しなくなりました。

{
     path: 'protected/foo',
     component: FooViewComponent,
     data: {allowAccessTo: ['Administrator']}, // admin only
     canActivate: [RouteGuard]
},

代わりに、特別なRouteGuardComponentを作成しました。これを使用する方法を次に示します。

{
    path: 'protected/foo',
    component: RouteGuardComponent,
    data: {component: FooViewComponent, allowAccessTo: ['Administrator']}
},

これはRouteGuardComponentのコードです:

@Component({
    selector: 'app-route-guard',
    template: '<ng-template route-guard-bind-component></ng-template>
    // note the use of special directive ^^
})
export class RouteGuardComponent implements OnInit {

    @ViewChild(RouteGuardBindComponentDirective)
    bindComponent: RouteGuardBindComponentDirective;
    // ^^ and here we bind to that directive instance in template

    constructor(
        private auth: AuthService,
        private route: ActivatedRoute,
        private componentFactoryResolver: ComponentFactoryResolver
    ) {
    }

    ngOnInit() {
        const {auth, route, componentFactoryResolver, bindComponent} = this;
        const {component, allowAccessTo} = route.snapshot.data;
        const identity = auth.getIdentity();
        const hasAccess = identity && allowAccessTo.indexOf(identity.role);
        const componentFactory = componentFactoryResolver.resolveComponentFactory(
            hasAccess ?
               component : // render component
               ErrorForbiddenViewComponent // render Forbidden view
        );
        // finally use factory to create proper component
        routeGuardBindComponentDirective
            .viewContainerRef
            .createComponent(componentFactory);
    }

}

また、これには特別なディレクティブを定義する必要があります(これは他の方法でも実行できると確信していますが、Angular docs)の動的コンポーネントの例を適用したところです:

@Directive({
    selector: '[route-guard-bind-component]'
})
export class RouteGuardBindComponentDirective {
    constructor(public viewContainerRef: ViewContainerRef) {}
}

それは私自身の質問に対する完全な答えではありませんが(開始点です)、誰かがより良い何かを提供する場合(つまり、canActivateを使用する方法と遅延ロードする能力)、私はそれを取ることを確認しますアカウントに。

1
Ivan Hušnjak

最近、同じ問題に遭遇しました。最終的に、CanActivateガードを使用してこれを行うことができなかったため、<router-outlet>を保持するコンポーネントに認証ロジックを実装しました。

そのテンプレートは次のとおりです。

<div class="content">
  <router-outlet *ngIf="(accessAllowed$ | async) else accessDenied"></router-outlet>
</div>
<ng-template #accessDenied>
  <div class="message">
    <mat-icon>lock</mat-icon>
    <span>Access denied.</span>
  </div>
</ng-template>

そしてそのソースコード:

import { ActivatedRoute, ActivationStart, Router } from '@angular/router';
import { filter, switchMap, take } from 'rxjs/operators';
import { merge, Observable, of } from 'rxjs';
import { Component } from '@angular/core';

@Component({
  selector: 'app-panel-content',
  templateUrl: './content.component.html',
  styleUrls: ['./content.component.scss'],
})
export class PanelContentComponent {

  /**
   * A stream of flags whether access to current route is permitted.
   */
  accessAllowed$: Observable<boolean>;

  constructor(
    permissions: UserPermissionsProviderContract, // A service for accessing user permissions; implementation omitted
    route: ActivatedRoute,
    router: Router,
  ) {
    const streams: Observable<boolean>[] = [];

    /*
    The main purpose of this component is to replace `<router-outlet>` with "Access denied" 
    message, if necessary. Such logic will be universal for all possible route components, and 
    doesn't require any additional components - you will always have at least one component with
    `<router-outlet>`.

    This component contains `<router-outlet>`, which by definition means that all possible authorisable 
    routes are beneath it in the hierarchy.
    This implicates that we cannot listen to `route.data` observable of `ActivatedRoute`, because the route 
    itself in this component will always be the parent route of the one we need to process. 

    So the only real (the least hacky, IMO) solution to access data of child routes is to listen to
    router events.
    However, by the time an instance of this component is constructed, all routing events will have been 
    triggered. This is especially important in case user loads the page on this route.

    To solve that, we can merge two streams, the first one of which will be a single access flag 
    for **activated route**, and the second will be a stream of flags, emitted from router 
    events (e.g. caused by user navigating through app).

    This approach requires that the authorised route is bottom-most in the hierarchy, because otherwise the 
    last value emitted from the stream created from router events will be `true`.
    */

    const deepestChild = this.findDeepestTreeNode(route);
    const currentData = deepestChild.routeConfig.data;

    // `data.authActions` is just an array of strings in my case
    if (currentData && 
        currentData.authActions && 
        Array.isArray(currentData.authActions) && 
        currentData.authActions.length > 0) {

      streams.Push(
        // `hasPermissions(actions: strings[]): Observable<boolean>`
        permissions.hasPermissions(currentData.authActions).pipe(take(1))
      );

    } else {
      // If the route in question doesn't have any authorisation logic, simply allow access
      streams.Push(of(true));
    }

    streams.Push(router.events
      .pipe(
        filter(e => e instanceof ActivationStart),
        switchMap((event: ActivationStart) => {
          const data = event.snapshot.data;

          if (data.authActions && 
            Array.isArray(currentData.authActions) && 
            data.authActions.length > 0) {

            return permissions.hasPermissions(data.authActions);
          }

          return of(true);
        }),
      ));

    this.accessAllowed$ = merge(...streams);
  }

  /**
   * Returns the deepest node in a tree with specified root node, or the first 
   * encountered node if there are several on the lowest level.
   * 
   * @param root The root node.
   */
  findDeepestTreeNode<T extends TreeNodeLike>(root: T): T {
    const findDeepest = (node: T, level = 1): [number, T] => {
      if (node.children && node.children.length > 0) {
        const found = node.children.map(child => findDeepest(child as T, level + 1));
        found.sort((a, b) => a[0] - b[0]);

        return found[0];

      } else {
        return [level, node];
      }
    };

    return findDeepest(root)[1];
  }

}

interface TreeNodeLike {
    children?: TreeNodeLike[];
}

ソースコードのコメントでアプローチを説明しましたが、要するに、ルーターイベントを使用してroute.dataの認証データにアクセスし、アクセスが拒否された場合は<router-outlet>をエラーメッセージに置き換えます。

0
mrnateriver