Angular 5/6:保护路线(路线保护)而不重定向到错误路线

时间:2018-05-22 23:20:29

标签: angular5 angular-routing angular6 angular-router-guards

我有一点腌渍。 我正在使用Route guard(实现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)。 / p>

问题: 如何向用户显示此特殊错误视图,而不将其重定向到错误路由哪个接缝是我找到的众多来源的首选选项? 如何在不实际加载禁止路由的情况下仍然使用我的RouteGuard ,因为如果我检查FooViewComponent内的访问权限并显示不同的视图,那就有点失败了{首先是{1}}。

理想情况下,我希望我的RouteGuard不仅可以在RouteGuard方法中返回false,还可以使用canActivate()完全替换组件。但我不知道该怎么做,或者事件是否可能。任何替代方案?

这就是我的护卫队现在的样子:

ErrorForbiddenViewComponent

所以我只是阻止路由加载,但我没有重定向。我只将未登录的访问者重定向到登录路由。

推理:

  • 路线应反映某种应用状态 - 访问路线 url应重新创建该状态
  • 要有错误路由(404 Not Found除外)将意味着您的应用程序实际上可以重新创建错误状态。这毫无意义 为什么你会将错误状态保持为你的应用程序的状态?对于 调试目的应该使用日志(控制台或服务器),重新访问 错误页面(即页面刷新)可能会干扰它。
  • 此外,通过重定向到错误路由应用程序应该为用户提供一些错误的见解。对于这个问题,要么需要一些参数 通过url传递或(更糟糕的是)保持错误状态有些错误 服务并在访问错误路径时检索它。
  • 此外,忽略RouteGuard并只加载组件并检查其中的访问权限可能会导致一些额外的依赖关系 无论如何都不会被使用(因为不允许用户),所以 整个懒人装载更加困难。

有没有人为此提供某种解决方案?我也想知道,在Angular 2+出现这么长时间后,没有人遇到这种情况怎么样?每个人都可以重定向吗?

另请注意,虽然我目前正在同步使用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; } } ,但未来可能会有所改变!

4 个答案:

答案 0 :(得分:4)

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

分享我创建的stackblitz poc -

  • 经过身份验证的组件(带有警卫)
  • 登录组件
  • Permission Guard
  • 路由(/auth路由随PermissionGuardService后卫提供)

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

用例是 -

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

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

  

编辑 - 演示   enter image description here

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

干杯!

答案 1 :(得分:2)

您的RouteGuard可以为模态窗口注入您正在使用的任何服务,.canActivate()可以弹出模态而无需重定向,以通知用户而不会干扰应用程序的当前状态。

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

答案 2 :(得分:1)

我最近遇到了同样的问题。最后,我无法使用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>。 / p>

答案 3 :(得分:0)

在查看Tarun Lalwani提供的angular2 example后,在对问题的评论进行深入研究后{I} {}我设法将其应用于我的代码:

指定路线时我不再使用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文档应用了动态组件示例):

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

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