Redirection angulaire vers la page de connexion

122

Je viens du monde Asp.Net MVC où les utilisateurs essayant d'accéder à une page qu'ils ne sont pas autorisés sont automatiquement redirigés vers la page de connexion.

J'essaye de reproduire ce comportement sur Angular. Je suis tombé sur le décorateur @CanActivate, mais il en résulte que le composant ne se rend pas du tout, pas de redirection.

Ma question est la suivante:

  • Angular fournit-il un moyen d'atteindre ce comportement?
  • Si c'est le cas, comment? Est-ce une bonne pratique?
  • Sinon, quelle serait la meilleure pratique pour gérer l'autorisation des utilisateurs dans Angular?
Amaury
la source
J'ai ajouté une directive réelle qui montre comment faire les choses d'authentification, si vous voulez regarder.
Michael Oryl
Cette réponse peut être utile: stackoverflow.com/a/59008239/7059557
AmirReza-Farahlagha

Réponses:

86

Mise à jour: J'ai publié un projet squelette complet Angular 2 avec intégration OAuth2 sur Github qui montre la directive mentionnée ci-dessous en action.

Une façon d'y parvenir serait d'utiliser un fichier directive. Contrairement à Angular 2 components, qui sont essentiellement de nouvelles balises HTML (avec code associé) que vous insérez dans votre page, une directive attributive est un attribut que vous insérez dans une balise qui provoque un certain comportement. Documents ici .

La présence de votre attribut personnalisé provoque des choses pour le composant (ou élément HTML) dans lequel vous avez placé la directive. Considérez cette directive que j'utilise pour mon application Angular2 / OAuth2 actuelle:

import {Directive, OnDestroy} from 'angular2/core';
import {AuthService} from '../services/auth.service';
import {ROUTER_DIRECTIVES, Router, Location} from "angular2/router";

@Directive({
    selector: '[protected]'
})
export class ProtectedDirective implements OnDestroy {
    private sub:any = null;

    constructor(private authService:AuthService, private router:Router, private location:Location) {
        if (!authService.isAuthenticated()) {
            this.location.replaceState('/'); // clears browser history so they can't navigate with back button
            this.router.navigate(['PublicPage']);
        }

        this.sub = this.authService.subscribe((val) => {
            if (!val.authenticated) {
                this.location.replaceState('/'); // clears browser history so they can't navigate with back button
                this.router.navigate(['LoggedoutPage']); // tells them they've been logged out (somehow)
            }
        });
    }

    ngOnDestroy() {
        if (this.sub != null) {
            this.sub.unsubscribe();
        }
    }
}

Cela utilise un service d'authentification que j'ai écrit pour déterminer si l'utilisateur est déjà connecté ou non et s'abonne également à l'événement d'authentification afin qu'il puisse expulser un utilisateur s'il se déconnecte ou expire.

Vous pourriez faire la même chose. Vous créeriez une directive comme la mienne qui vérifie la présence d'un cookie nécessaire ou d'autres informations d'état indiquant que l'utilisateur est authentifié. S'ils n'ont pas les indicateurs que vous recherchez, redirigez l'utilisateur vers votre page publique principale (comme je le fais) ou votre serveur OAuth2 (ou autre). Vous mettriez cet attribut de directive sur n'importe quel composant qui doit être protégé. Dans ce cas, il pourrait être appelé protectedcomme dans la directive que j'ai collée ci-dessus.

<members-only-info [protected]></members-only-info>

Ensuite, vous voudrez naviguer / rediriger l'utilisateur vers une vue de connexion dans votre application et y gérer l'authentification. Vous devrez changer l'itinéraire actuel pour celui que vous vouliez faire. Donc, dans ce cas, vous utiliserez l'injection de dépendances pour obtenir un objet Router dans la constructor()fonction de votre directive, puis vous utiliseriez la navigate()méthode pour envoyer l'utilisateur à votre page de connexion (comme dans mon exemple ci-dessus).

Cela suppose que vous ayez une série de routes quelque part contrôlant une <router-outlet>balise qui ressemble à ceci, peut-être:

@RouteConfig([
    {path: '/loggedout', name: 'LoggedoutPage', component: LoggedoutPageComponent, useAsDefault: true},
    {path: '/public', name: 'PublicPage', component: PublicPageComponent},
    {path: '/protected', name: 'ProtectedPage', component: ProtectedPageComponent}
])

Si, à la place, vous deviez rediriger l'utilisateur vers une URL externe , telle que votre serveur OAuth2, votre directive ferait quelque chose comme ceci:

window.location.href="https://myserver.com/oauth2/authorize?redirect_uri=http://myAppServer.com/myAngular2App/callback&response_type=code&client_id=clientId&scope=my_scope
Michael Oryl
la source
4
Ça marche! Merci! J'ai également trouvé une autre méthode ici - github.com/auth0/angular2-authentication-sample/blob/master/src/ ... Je ne peux pas dire quelle méthode est la meilleure, mais peut-être que quelqu'un la trouvera utile aussi.
Sergey
3
Je vous remercie ! J'ai également ajouté une nouvelle route contenant un paramètre / protected /: returnUrl, returnUrl étant le location.path () intercepté à ngOnInit de la directive. Cela permet de naviguer l'utilisateur après la connexion vers l'url invitée à l'origine.
Amaury le
1
Voir les réponses ci-dessous pour une solution simple. Tout ce qui est commun (rediriger si non authentifié) devrait avoir une solution simple avec une seule phrase de réponse.
Rick O'Shea
7
Remarque: Cette réponse concerne une version bêta ou une version candidate d'Angular 2 et n'est plus applicable pour Angular 2 final.
jbandi
1
Il y a une bien meilleure solution à cela maintenant en utilisant des gardes angulaires
mwilson
116

Voici un exemple mis à jour utilisant Angular 4 (également compatible avec Angular 5-8)

Itinéraires avec route d'origine protégée par AuthGuard

import { Routes, RouterModule } from '@angular/router';

import { LoginComponent } from './login/index';
import { HomeComponent } from './home/index';
import { AuthGuard } from './_guards/index';

const appRoutes: Routes = [
    { path: 'login', component: LoginComponent },

    // home route protected by auth guard
    { path: '', component: HomeComponent, canActivate: [AuthGuard] },

    // otherwise redirect to home
    { path: '**', redirectTo: '' }
];

export const routing = RouterModule.forRoot(appRoutes);

AuthGuard redirige vers la page de connexion si l'utilisateur n'est pas connecté

Mise à jour pour transmettre l'URL d'origine des paramètres de requête à la page de connexion

import { Injectable } from '@angular/core';
import { Router, CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';

@Injectable()
export class AuthGuard implements CanActivate {

    constructor(private router: Router) { }

    canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
        if (localStorage.getItem('currentUser')) {
            // logged in so return true
            return true;
        }

        // not logged in so redirect to login page with the return url
        this.router.navigate(['/login'], { queryParams: { returnUrl: state.url }});
        return false;
    }
}

Pour l'exemple complet et la démonstration de travail, vous pouvez consulter cet article

Jason
la source
6
J'ai un suivi Q, n'est-ce pas si la définition d'une valeur arbitraire currentUserdans le localStorageserait toujours en mesure d'accéder à la route protégée? par exemple. localStorage.setItem('currentUser', 'dddddd')?
jsd
2
Cela contournerait la sécurité côté client. Mais cela effacerait également le jeton qui serait nécessaire pour les transactions côté serveur, de sorte qu'aucune donnée utile ne pourrait être extraite de l'application.
Matt Meng
55

Utilisation avec le routeur final

Avec l'introduction du nouveau routeur, il est devenu plus facile de garder les routes. Vous devez définir une garde, qui agit comme un service, et l'ajouter à l'itinéraire.

import { Injectable } from '@angular/core';
import { CanActivate } from '@angular/router';
import { UserService } from '../../auth';

@Injectable()
export class LoggedInGuard implements CanActivate {
  constructor(user: UserService) {
    this._user = user;
  }

  canActivate() {
    return this._user.isLoggedIn();
  }
}

Passez maintenant le LoggedInGuardà la route et ajoutez-le également au providerstableau du module.

import { LoginComponent } from './components/login.component';
import { HomeComponent } from './components/home.component';
import { LoggedInGuard } from './guards/loggedin.guard';

const routes = [
    { path: '', component: HomeComponent, canActivate: [LoggedInGuard] },
    { path: 'login', component: LoginComponent },
];

La déclaration du module:

@NgModule({
  declarations: [AppComponent, HomeComponent, LoginComponent]
  imports: [HttpModule, BrowserModule, RouterModule.forRoot(routes)],
  providers: [UserService, LoggedInGuard],
  bootstrap: [AppComponent]
})
class AppModule {}

Article de blog détaillé sur son fonctionnement avec la version finale: https://medium.com/@blacksonic86/angular-2-authentication-revisited-611bf7373bf9

Utilisation avec le routeur obsolète

Une solution plus robuste consiste à étendre RouterOutletet lors de l'activation d'une route, vérifiez si l'utilisateur est connecté. De cette façon, vous n'avez pas à copier et coller votre directive dans chaque composant. De plus, la redirection basée sur un sous-composant peut être trompeuse.

@Directive({
  selector: 'router-outlet'
})
export class LoggedInRouterOutlet extends RouterOutlet {
  publicRoutes: Array;
  private parentRouter: Router;
  private userService: UserService;

  constructor(
    _elementRef: ElementRef, _loader: DynamicComponentLoader,
    _parentRouter: Router, @Attribute('name') nameAttr: string,
    userService: UserService
  ) {
    super(_elementRef, _loader, _parentRouter, nameAttr);

    this.parentRouter = _parentRouter;
    this.userService = userService;
    this.publicRoutes = [
      '', 'login', 'signup'
    ];
  }

  activate(instruction: ComponentInstruction) {
    if (this._canActivate(instruction.urlPath)) {
      return super.activate(instruction);
    }

    this.parentRouter.navigate(['Login']);
  }

  _canActivate(url) {
    return this.publicRoutes.indexOf(url) !== -1 || this.userService.isLoggedIn()
  }
}

Le UserServicereprésente l'endroit où réside votre logique métier, que l'utilisateur soit connecté ou non. Vous pouvez l'ajouter facilement avec DI dans le constructeur.

Lorsque l'utilisateur accède à une nouvelle URL sur votre site Web, la méthode d'activation est appelée avec l'instruction actuelle. À partir de là, vous pouvez saisir l'url et décider si elle est autorisée ou non. Sinon, redirigez-vous simplement vers la page de connexion.

Une dernière chose reste à faire pour que cela fonctionne, c'est de le passer à notre composant principal au lieu de celui intégré.

@Component({
  selector: 'app',
  directives: [LoggedInRouterOutlet],
  template: template
})
@RouteConfig(...)
export class AppComponent { }

Cette solution ne peut pas être utilisée avec le @CanActivedécorateur de cycle de vie, car si la fonction qui lui est transmise résout false, la méthode activate duRouterOutlet ne sera pas appelée.

A également écrit un article de blog détaillé à ce sujet: https://medium.com/@blacksonic86/authentication-in-angular-2-958052c64492

Blacksonic
la source
2
A également écrit un article de blog plus détaillé à ce sujet medium.com/@blacksonic86/…
Blacksonic
Bonjour @Blacksonic. Je viens de commencer à creuser dans ng2. J'ai suivi votre suggestion mais j'ai fini par obtenir cette erreur pendant gulp-tslint: Failed to lint <classname>.router-outlet.ts[15,28]. In the constructor of class "LoggedInRouterOutlet", the parameter "nameAttr" uses the @Attribute decorator, which is considered as a bad practice. Please, consider construction of type "@Input() nameAttr: string". Impossible de comprendre quoi changer dans le constructeur ("_parentRounter") pour se débarrasser de ce message. Des pensées?
leovrf
la déclaration est copiée à partir de l'objet intégré sous-jacent RouterOutlet pour avoir la même signature que la classe étendue, je désactiverais la règle tslint spécifique pour cette ligne
Blacksonic
J'ai trouvé une référence sur mgechev style-guide (cherchez "Préférer les entrées sur le décorateur de paramètre @Attribute"). Changé la ligne en _parentRouter: Router, @Input() nameAttr: string,et tslint ne lève plus l'erreur. Également remplacé l'importation "Attribut" vers "Entrée" à partir du noyau angulaire. J'espère que cela t'aides.
leovrf
1
Il y a un problème avec 2.0.0-rc.1 car RouterOutlet n'est pas exporté et il n'y a aucune possibilité de l'étendre
mkuligowski
53

Veuillez ne pas remplacer la prise du routeur! C'est un cauchemar avec la dernière version du routeur (3.0 beta).

Utilisez plutôt les interfaces CanActivate et CanDeactivate et définissez la classe sur canActivate / canDeactivate dans votre définition d'itinéraire.

Comme ça:

{ path: '', component: Component, canActivate: [AuthGuard] },

Classe:

@Injectable()
export class AuthGuard implements CanActivate {

    constructor(protected router: Router, protected authService: AuthService)
    {

    }

    canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> | boolean {

        if (state.url !== '/login' && !this.authService.isAuthenticated()) {
            this.router.navigate(['/login']);
            return false;
        }

        return true;
    }
}

Voir aussi: https://angular.io/docs/ts/latest/guide/router.html#!#can-activate-guard

Nilz11
la source
2
Bien, la réponse de @ Blacksonic fonctionnait parfaitement pour moi avec le routeur obsolète. J'ai dû beaucoup refactoriser après la mise à niveau vers le nouveau routeur. Votre solution est exactement ce dont j'avais besoin!
evandongen
Je ne peux pas faire fonctionner canActivate dans mon app.component. Je cherche à rediriger l'utilisateur s'il n'est pas authentifié. Ceci est la version du routeur que j'ai (si j'ai besoin de le mettre à jour, comment puis-je le faire en utilisant la ligne de commande git bash?) Version que j'ai: "@ angular / router": "2.0.0-rc.1"
AngularM
puis-je utiliser la même classe (AuthGuard) pour protéger une autre route de composant?
tsiro
4

En suivant les réponses impressionnantes ci-dessus, je voudrais également CanActivateChild: garder les routes des enfants. Il peut être utilisé pour ajouter guardaux enfants des routes utiles pour des cas tels que les ACL

Ça va comme ça

src / app / auth-guard.service.ts (extrait)

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

@Injectable()
export class AuthGuard implements CanActivate, CanActivateChild {
  constructor(private authService: AuthService, private router:     Router) {}

  canActivate(route: ActivatedRouteSnapshot, state:    RouterStateSnapshot): boolean {
    let url: string = state.url;
    return this.checkLogin(url);
  }

  canActivateChild(route: ActivatedRouteSnapshot, state:  RouterStateSnapshot): boolean {
    return this.canActivate(route, state);
  }

/* . . . */
}

src / app / admin / admin-routing.module.ts (extrait)

const adminRoutes: Routes = [
  {
    path: 'admin',
    component: AdminComponent,
    canActivate: [AuthGuard],
    children: [
      {
        path: '',
        canActivateChild: [AuthGuard],
        children: [
          { path: 'crises', component: ManageCrisesComponent },
          { path: 'heroes', component: ManageHeroesComponent },
          { path: '', component: AdminDashboardComponent }
        ]
      }
    ]
  }
];

@NgModule({
  imports: [
    RouterModule.forChild(adminRoutes)
  ],
  exports: [
    RouterModule
  ]
})
export class AdminRoutingModule {}

Ceci est tiré de https://angular.io/docs/ts/latest/guide/router.html#!#can-activate-guard

Thabung
la source
2

Reportez-vous à ce code, fichier auth.ts

import { CanActivate } from '@angular/router';
import { Injectable } from '@angular/core';
import {  } from 'angular-2-local-storage';
import { Router } from '@angular/router';

@Injectable()
export class AuthGuard implements CanActivate {
constructor(public localStorageService:LocalStorageService, private router: Router){}
canActivate() {
// Imaginary method that is supposed to validate an auth token
// and return a boolean
var logInStatus         =   this.localStorageService.get('logInStatus');
if(logInStatus == 1){
    console.log('****** log in status 1*****')
    return true;
}else{
    console.log('****** log in status not 1 *****')
    this.router.navigate(['/']);
    return false;
}


}

}
// *****And the app.routes.ts file is as follow ******//
      import {  Routes  } from '@angular/router';
      import {  HomePageComponent   } from './home-page/home- page.component';
      import {  WatchComponent  } from './watch/watch.component';
      import {  TeachersPageComponent   } from './teachers-page/teachers-page.component';
      import {  UserDashboardComponent  } from './user-dashboard/user- dashboard.component';
      import {  FormOneComponent    } from './form-one/form-one.component';
      import {  FormTwoComponent    } from './form-two/form-two.component';
      import {  AuthGuard   } from './authguard';
      import {  LoginDetailsComponent } from './login-details/login-details.component';
      import {  TransactionResolver } from './trans.resolver'
      export const routes:Routes    =   [
    { path:'',              component:HomePageComponent                                                 },
    { path:'watch',         component:WatchComponent                                                },
    { path:'teachers',      component:TeachersPageComponent                                         },
    { path:'dashboard',     component:UserDashboardComponent,       canActivate: [AuthGuard],   resolve: { dashboardData:TransactionResolver } },
    { path:'formone',       component:FormOneComponent,                 canActivate: [AuthGuard],   resolve: { dashboardData:TransactionResolver } },
    { path:'formtwo',       component:FormTwoComponent,                 canActivate: [AuthGuard],   resolve: { dashboardData:TransactionResolver } },
    { path:'login-details', component:LoginDetailsComponent,            canActivate: [AuthGuard]    },

]; 
Sojan
la source
1

1. Create a guard as seen below. 2. Install ngx-cookie-service to get cookies returned by external SSO. 3. Create ssoPath in environment.ts (SSO Login redirection). 4. Get the state.url and use encodeURIComponent.

import { Injectable } from '@angular/core';
import { CanActivate, Router, ActivatedRouteSnapshot, RouterStateSnapshot } from 
  '@angular/router';
import { CookieService } from 'ngx-cookie-service';
import { environment } from '../../../environments/environment.prod';

@Injectable()
export class AuthGuardService implements CanActivate {
  private returnUrl: string;
  constructor(private _router: Router, private cookie: CookieService) {}

canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
    if (this.cookie.get('MasterSignOn')) {
      return true;
    } else {
      let uri = window.location.origin + '/#' + state.url;
      this.returnUrl = encodeURIComponent(uri);      
      window.location.href = environment.ssoPath +  this.returnUrl ;   
      return false;      
    }
  }
}
M. Laida
la source