14

我有点咸菜。我正在使用路由保护(实现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首先会失败。

理想情况下,我希望我RouteGuard不仅在canActivate()方法中返回 false,而且还希望用 say 完全替换组件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同步,但将来可能会改变!

4

4 回答 4

4

我曾经研究过类似的问题。

分享我创建的stackblitz poc -

  • 认证组件(带保护)
  • 登录组件
  • 权限守卫
  • 路线(/auth路线设有PermissionGuardService警卫)

守卫正在评估用户类型并相应地处理重定向/错误。

用例是 -

  • 用户未登录 ( shows a toast with log in message)
  • 用户不是管理员 ( shows a toast with unauthorised message)
  • 用户是管理员 ( show a toast with success messaage)

我已将用户存储在本地存储中。

编辑 - 演示 在此处输入图像描述

如果您需要对其进行特殊处理,请告诉我,我将更新代码库。

干杯!

于 2018-06-01T20:30:15.207 回答
3

在查看了 Tarun Lalwani 在问题评论中提供的angular2 示例并深入研究了Angular 文档上的动态组件加载器文章之后,我设法将其应用于我的代码:

RouteGuard在指定路线时我不再使用 my :

{
     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 文档中的动态组件示例):

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

这不是我自己问题的完整答案(但它是一个开始),所以如果有人提供更好的东西(即一种仍然使用的方法canActivate和延迟加载的能力),我会确保考虑到这一点。

于 2018-05-30T15:10:53.860 回答
2

我最近遇到了同样的问题。最后,我无法使用CanActivateguard 来做到这一点,所以我在包含<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>如果访问被拒绝,则替换为错误消息。

于 2019-09-27T04:26:29.617 回答
2

您的 RouteGuard 可以注入您用于模态窗口的任何服务,并且.canActivate()可以在不重定向的情况下弹出模态以通知用户而不会干扰应用程序的当前状态。

我们为此使用 toastr 及其角度包装器,因为它创建了一个无模式的弹出窗口,在这么多秒后自动关闭,不需要 OK/Cancel 按钮。

于 2018-06-01T23:18:27.937 回答