Angular przekierowanie do strony logowania

122

Pochodzę ze świata Asp.Net MVC, w którym użytkownicy próbujący uzyskać dostęp do nieautoryzowanej strony są automatycznie przekierowywani na stronę logowania.

Próbuję odtworzyć to zachowanie w Angular. Natknąłem się na dekorator @CanActivate, ale powoduje to, że komponent w ogóle nie renderuje się, nie ma przekierowania.

Moje pytanie jest następujące:

  • Czy Angular zapewnia sposób na osiągnięcie takiego zachowania?
  • Jeśli tak to jak? Czy to dobra praktyka?
  • Jeśli nie, jaka byłaby najlepsza praktyka w zakresie obsługi autoryzacji użytkowników w Angular?
Amaury
źródło
Dodałem aktualną dyrektywę, która pokazuje, jak wykonać autoryzację, jeśli chcesz wyglądać.
Michael Oryl
Ta odpowiedź może być przydatna: stackoverflow.com/a/59008239/7059557
AmirReza-Farahlagha

Odpowiedzi:

86

Aktualizacja: Opublikowałem pełny projekt szkieletu Angular 2 z integracją OAuth2 na Github, który pokazuje w akcji wspomnianą poniżej dyrektywę.

Jednym ze sposobów byłoby użycie pliku directive. W przeciwieństwie do Angular 2 components, które są w zasadzie nowymi tagami HTML (z powiązanym kodem), które wstawiasz do swojej strony, dyrektywa atrybucyjna to atrybut, który umieszczasz w tagu, który powoduje pewne zachowanie. Dokumenty tutaj .

Obecność atrybutu niestandardowego powoduje, że coś dzieje się z komponentem (lub elementem HTML), w którym umieściłeś dyrektywę. Rozważ tę dyrektywę, której używam dla mojej obecnej aplikacji Angular2 / OAuth2:

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

Wykorzystuje to usługę uwierzytelniania, o której pisałem, w celu ustalenia, czy użytkownik jest już zalogowany, czy też nie, a także subskrybuje zdarzenie uwierzytelniania, dzięki czemu może wyrzucić użytkownika, jeśli wyloguje się lub wyloguje.

Możesz zrobić to samo. Utworzyłbyś dyrektywę taką jak moja, która sprawdza obecność niezbędnego pliku cookie lub innych informacji o stanie, które wskazują, że użytkownik jest uwierzytelniony. Jeśli nie mają tych flag, których szukasz, przekieruj użytkownika na swoją główną stronę publiczną (tak jak ja) lub na serwer OAuth2 (lub cokolwiek). Możesz umieścić ten atrybut dyrektywy na dowolnym komponencie, który musi być chroniony. W tym przypadku można by to nazwać protectedjak w dyrektywie, którą wkleiłem powyżej.

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

Następnie chciałbyś nawigować / przekierowywać użytkownika do widoku logowania w swojej aplikacji i tam obsługiwać uwierzytelnianie. Musiałbyś zmienić obecną trasę na tę, którą chcesz to zrobić. Więc w takim przypadku użyłbyś iniekcji zależności, aby pobrać obiekt Routera w funkcji twojej dyrektywy, constructor()a następnie użyj navigate()metody, aby wysłać użytkownika na twoją stronę logowania (jak w moim przykładzie powyżej).

Zakłada się, że masz gdzieś serię tras kontrolujących <router-outlet>tag, który wygląda mniej więcej tak:

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

Jeśli zamiast tego musisz przekierować użytkownika do zewnętrznego adresu URL, takiego jak serwer OAuth2, to Twoja dyrektywa powinna wykonać coś takiego:

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
źródło
4
To działa! dzięki! Znalazłem tu też inną metodę - github.com/auth0/angular2-authentication-sample/blob/master/src/... Nie mogę powiedzieć, która metoda jest lepsza, ale może ktoś też uzna ją za przydatną.
Sergey
3
Dziękuję Ci ! Dodałem również nową trasę zawierającą parametr / protected /: returnUrl, przy czym returnUrl to location.path () przechwycony w ngOnInit dyrektywy. Pozwala to na nawigację użytkownika po zalogowaniu do pierwotnie wyświetlanego adresu URL.
Amaury,
1
Zobacz odpowiedzi poniżej, aby uzyskać proste rozwiązanie. Cokolwiek tak powszechnego (przekierowanie, jeśli nie jest uwierzytelnione) powinno mieć proste rozwiązanie z odpowiedzią w jednym zdaniu.
Rick O'Shea
7
Uwaga: ta odpowiedź dotyczy wersji beta lub kandydującej do wydania Angular 2 i nie ma już zastosowania do finału Angular 2.
jbandi
1
Jest teraz znacznie lepsze rozwiązanie tego problemu, używając Angular Guards
mwilson,
116

Oto zaktualizowany przykład przy użyciu Angular 4 (również kompatybilny z Angular 5-8)

Trasy z trasą do domu chronioną przez 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 przekierowuje do strony logowania, jeśli użytkownik nie jest zalogowany

Zaktualizowano w celu przekazania oryginalnego adresu URL w parametrach zapytania do strony logowania

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

Pełny przykład i działające demo można znaleźć w tym poście

Jason
źródło
6
Mam kontynuację P, czy nie jest, jeśli ustawienie dowolnej wartości currentUserw localStoragenadal byłoby w stanie uzyskać dostęp do chronionej trasy? na przykład. localStorage.setItem('currentUser', 'dddddd')?
jsd
2
Pominąłby zabezpieczenia po stronie klienta. Ale wyczyściłoby to również token, który byłby niezbędny do transakcji po stronie serwera, więc żadne przydatne dane nie mogłyby zostać wyodrębnione z aplikacji.
Matt Meng
55

Użycie z końcowym routerem

Wraz z wprowadzeniem nowego routera ochrona tras stała się łatwiejsza. Musisz zdefiniować strażnika, który działa jako usługa, i dodać go do trasy.

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

Teraz przekaż LoggedInGuardtrasę do trasy, a także dodaj ją do providerstablicy modułu.

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

Deklaracja modułu:

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

Szczegółowy post na blogu o tym, jak to działa z ostateczną wersją: https://medium.com/@blacksonic86/angular-2-authentication-revisited-611bf7373bf9

Użycie z przestarzałym routerem

Bardziej niezawodnym rozwiązaniem jest rozszerzenie RouterOutleti podczas aktywacji sprawdzania trasy, czy użytkownik jest zalogowany. W ten sposób nie musisz kopiować i wklejać dyrektywy do każdego komponentu. Ponadto przekierowania oparte na składniku podrzędnym mogą wprowadzać w błąd.

@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()
  }
}

Plik UserServiceStoi w miejscu, gdzie mieszka twoja logika biznesowa, czy użytkownik jest zalogowany czy nie. Możesz go łatwo dodać za pomocą DI w konstruktorze.

Gdy użytkownik przejdzie do nowego adresu URL w Twojej witrynie, metoda aktywacji jest wywoływana z aktualną instrukcją. Z niego możesz pobrać adres URL i zdecydować, czy jest to dozwolone, czy nie. Jeśli nie, po prostu przekieruj do strony logowania.

Ostatnią rzeczą, która pozostała, aby to zadziałało, jest przekazanie go do naszego głównego komponentu zamiast do wbudowanego.

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

Tego rozwiązania nie można używać z @CanActivedekoratorem cyklu życia, ponieważ jeśli przekazana do niego funkcja rozwiąże wartość false, metoda aktywacji elementuRouterOutlet nie zostanie wywołana.

Napisałem również na ten temat szczegółowy post na blogu: https://medium.com/@blacksonic86/authentication-in-angular-2-958052c64492

Blacksonic
źródło
2
Napisałem również bardziej szczegółowy wpis na blogu na ten temat medium.com/@blacksonic86/ ...
Blacksonic
Witaj @Blacksonic. Właśnie zacząłem zagłębiać się w ng2. Postępowałem zgodnie z twoją sugestią, ale skończyło się na tym, że podczas gulp-tslint otrzymałem ten błąd: 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". Nie mogłem określić, co należy zmienić w konstruktorze („_parentRounter”), aby pozbyć się tego komunikatu. jakieś pomysły?
leovrf
deklaracja jest kopiowana z bazowego wbudowanego obiektu RouterOutlet, aby mieć taką samą sygnaturę jak klasa rozszerzona, wyłączyłbym określoną regułę tslint dla tej linii
Blacksonic
Znalazłem wzmiankę o mgechev stylu przewodnika (poszukaj „Wolę wejść ponad parametru dekoratora @Attribute”). Zmieniono linię na, _parentRouter: Router, @Input() nameAttr: string,a tslint nie powoduje już błędu. Zastąpiono także import „Atrybutów” na „Dane wejściowe” z kątowego rdzenia. Mam nadzieję że to pomoże.
leovrf
1
Wystąpił problem z 2.0.0-rc.1, ponieważ RouterOutlet nie jest eksportowany i nie ma możliwości jego rozszerzenia
mkuligowski
53

Proszę, nie zastępuj Router Outlet! To koszmar z najnowszą wersją routera (3.0 beta).

Zamiast tego użyj interfejsów CanActivate i CanDeactivate i ustaw klasę jako canActivate / canDeactivate w definicji trasy.

Tak:

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

Klasa:

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

Zobacz też: https://angular.io/docs/ts/latest/guide/router.html#!#can-activate-guard

Nilz11
źródło
2
Niezłe, odpowiedź @ Blacksonic działała dla mnie doskonale z przestarzałym routerem. Po uaktualnieniu do nowego routera musiałem dużo refaktoryzować. Twoje rozwiązanie jest właśnie tym, czego potrzebowałem!
evandongen
Nie mogę dostać canActivate do pracy w moim komponencie app.component. Chcę przekierować użytkownika, jeśli nie jest uwierzytelniony. To jest wersja routera, którą mam (jeśli muszę ją zaktualizować, jak to zrobić za pomocą wiersza poleceń git bash?) Posiadam wersję: "@ angular / router": "2.0.0-rc.1"
AngularM
czy mogę użyć tej samej klasy (AuthGuard) do ochrony trasy innego komponentu?
tsiro
4

Idąc za wspaniałymi odpowiedziami powyżej, chciałbym również CanActivateChild: pilnować dziecięcych tras. Można go użyć do dodania guarddo podrzędnych tras pomocnych w przypadkach takich jak listy ACL

Tak to wygląda

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

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 (fragment)

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

To pochodzi z https://angular.io/docs/ts/latest/guide/router.html#!#can-activate-guard

Thabung
źródło
2

Zobacz ten kod, plik 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
źródło
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
źródło