web-dev-qa-db-fra.com

Angular 5/6: protéger la route (garde de la route) sans rediriger vers la route en erreur

J'ai un peu de cornichon . J'utilise Route guard (implémentant l'interface CanActivate) pour vérifier si l'utilisateur est autorisé à accéder à une route particulière:

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

Maintenant, cela fonctionne très bien pour protéger la route '/ protected/foo' de l'activer, mais je voudrais dire à l'utilisateur que la route à laquelle il tente d'accéder est interdite (similaire à 403 Forbidden que vous pouvez obtenir du serveur).

Le problème: Comment puis-je montrer à l'utilisateur cette vue d'erreur spéciale sans le rediriger vers une route d'erreur qui semble être l'option préférée par tant de sources que j'ai trouvées? Comment puis-je continue d’utiliser ma RouteGuard sans charger réellement la route interdite, car si je vérifie l’accès dans ma FooViewComponent et affiche un affichage différent, c’est un peu le point de défaite d’avoir RouteGuard en premier lieu.

Idéalement, j'aimerais que ma RouteGuard retourne non seulement false dans la méthode canActivate(), mais remplace également le composant complètement par say ErrorForbiddenViewComponent. Mais je ne sais pas comment le faire, ou est-ce que cela est possible? Des alternatives?

Voici à quoi ressemble ma garde de route:

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;
    }
}

Donc, j'empêche juste le chargement de la route, mais je ne redirige pas. Je ne redirige que les visiteurs non connectés à la route de connexion.

Raisonnement:

  • Les itinéraires doivent refléter certains états d'application - visiter un itinéraire Url doit recréer cet état
  • Avoir des itinéraires d'erreur (sauf pour 404 Introuvables) signifierait que votre application peut en réalité recréer des états d'erreur. Cela n’a aucun sens. Pourquoi voudriez-vous conserver l’erreur en tant qu’état de votre application? Pour les besoins du débogage .__, il faut utiliser les journaux (console ou serveur), une nouvelle visite de la page d'erreur (C'est-à-dire l'actualisation de la page) pourrait interférer avec cela.
  • De plus, en redirigeant vers l'erreur, l'application de la route devrait fournir des indications d'erreur à l'utilisateur. D'ailleurs, un paramètre devrait être transmis via l'URL ou (bien pire) conserver l'état d'erreur dans un service d'erreur et le récupérer après avoir accédé à la route d'erreur.
  • En outre, ignorer RouteGuard et simplement charger le composant et vérifier l'accès à l'intérieur peut entraîner des dépendances supplémentaires Chargées, qui ne seraient pas utilisées de toute façon (l'utilisateur n'étant pas autorisé), rendant tout le chargement paresseux beaucoup plus difficile.

Quelqu'un at-il une sorte de solution pour cela? Je me demande également comment se fait-il qu'après que Angular 2+ ait été présent pendant si longtemps, personne ne s'était déjà trouvé dans ce genre de situation? Tout le monde est juste d'accord avec la redirection? 

N'oubliez pas non plus que, bien que j'utilise actuellement la FooViewComponent de manière synchrone, cela pourrait changer à l'avenir!

8
Ivan Hušnjak

J'avais une fois travaillé sur le même problème.

Partage de mon stackblitz poc où j'ai créé - 

  • Composant authentifié (avec garde)
  • Composant de connexion 
  • Garde de permission
  • Route (/auth route est fournie avec PermissionGuardService garde)

Le service de garde évalue le type d'utilisateur et traite la redirection/erreur en conséquence.

Les cas d'utilisation sont - 

  • L'utilisateur n'est pas connecté (shows a toast with log in message)
  • L'utilisateur n'est pas admin (shows a toast with unauthorised message)
  • L'utilisateur est admin (show a toast with success messaage)

J'ai enregistré l'utilisateur dans la mémoire de stockage locale.

EDIT - DEMO  enter image description here

Faites-moi savoir si vous avez besoin d'un traitement spécial et je mettrai à jour la base de code.

À votre santé!

3
planet_hunter

Votre RouteGuard peut injecter le service que vous utilisez pour les fenêtres modales et la fonction .canActivate() peut afficher le mode modal sans redirection pour informer l'utilisateur sans perturber l'état actuel de l'application. 

Pour cela, nous utilisons toastr et son wrapper angulaire, car il crée un modeless pop-up qui s'auto-supprime après tant de secondes, sans boutons OK/Annuler. 

2
Ron Newcomb

Après avoir regardé exemple angular2 fourni par Tarun Lalwani dans les commentaires de question et après avoir approfondi l'examen de article du chargeur de composants dynamiques sur la documentation angulaire , j'ai réussi à l'appliquer à mon code:

Je n'utilise plus ma RouteGuard lors de la spécification des itinéraires:

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

Au lieu de cela, j'ai créé RouteGuardComponent spécial et voici comment je l'utilise:

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

C'est le code de 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);
    }

}

En outre, cela nécessite une directive spéciale à définir (je suis sûr que cela peut être fait d'une autre manière, mais je viens d'appliquer cet exemple de composant dynamique à partir de documents angulaires):

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

Ce n’est pas une réponse complète à ma propre question (mais c’est un début), donc si quelqu'un fournit quelque chose de mieux (c’est-à-dire un moyen d’utiliser toujours canActivate et la capacité de charger paresseux), je veillerai à en tenir compte.

0
Ivan Hušnjak