Кутове переадресація на сторінку входу


122

Я родом із світу MVC Asp.Net, де користувачі, які намагаються отримати доступ до сторінки, на яку вони не мають права, автоматично переспрямовуються на сторінку входу.

Я намагаюся відтворити цю поведінку на Angular. Я натрапив на декоратор @CanActivate, але це призводить до того, що компонент взагалі не відображає, не перенаправляє.

Моє запитання таке:

  • Чи надає Angular спосіб досягнення такої поведінки?
  • Якщо так, то як? Це хороша практика?
  • Якщо ні, то яка найкраща практика для роботи з авторизацією користувачів у Angular?

Я додав фактичну директиву, яка показує, як робити автентичні речі, якщо ви хочете подивитися.
Майкл Оріл

Ця відповідь може корисним: stackoverflow.com/a/59008239/7059557
AmirReza-Farahlagha

Відповіді:


86

Оновлення: Я опублікував повний скелетний проект Angular 2 з інтеграцією OAuth2 в Github, який показує дію вказану нижче директиву.

Один із способів зробити це через використання а directive. На відміну від Angular 2 components, які є принципово новими тегами HTML (із асоційованим кодом), які ви вставляєте на свою сторінку, атрибутивна директива - це атрибут, який ви поміщаєте в тег, який спричиняє певну поведінку. Документи тут .

Наявність вашого спеціального атрибута призводить до того, що відбувається з компонентом (або елементом HTML), у який ви розмістили директиву. Розгляньте цю директиву, яку я використовую для свого поточного додатка 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();
        }
    }
}

Це використовує службу аутентифікації, яку я написав, щоб визначити, чи користувач уже ввійшов у систему, а також підписався на подію аутентифікації, щоб він міг вигнати користувача, якщо він або вона вийде або вимкнеться.

Ви могли зробити те саме. Ви б створили таку директиву, як моя, яка перевіряє наявність необхідного файлу cookie або іншої інформації про стан, яка вказує на те, що користувач має автентифікацію. Якщо у вас немає тих прапорів, які ви шукаєте, перенаправляйте користувача на вашу головну загальнодоступну сторінку (як я) або на ваш сервер OAuth2 (або що завгодно). Ви покладете цей атрибут директиви на будь-який компонент, який потрібно захистити. У цьому випадку це може називатися protectedяк в директиві, яку я вставив вище.

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

Тоді ви хочете перейти / перенаправити користувача до перегляду входу у вашій програмі та обробити там автентифікацію. Вам доведеться змінити поточний маршрут на той, який ви хотіли це зробити. Тож у такому випадку ви використовуєте ін'єкцію залежності, щоб отримати об’єкт Router у функції вашої директиви, constructor()а потім скористатися navigate()методом для відправки користувача на вашу сторінку входу (як у моєму прикладі вище).

Це передбачає, що у вас є серія маршрутів, десь десь керується <router-outlet>тегом, який виглядає приблизно так:

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

Якщо замість цього вам потрібно було переспрямувати користувача на зовнішню URL-адресу, наприклад, на ваш сервер OAuth2, ви отримаєте директиву зробити щось на зразок наступного:

window.location.href="https://myserver.com/oauth2/authorize?redirect_uri=http://myAppServer.com/myAngular2App/callback&response_type=code&client_id=clientId&scope=my_scope

4
Це працює! Дякую! Тут я також знайшов інший метод - github.com/auth0/angular2-authentication-sample/blob/master/src/… Я не можу сказати, який метод кращий, але, можливо, хтось теж вважає його корисним.
Сергій

3
Дякую ! Я також додав новий маршрут, що містить параметр / protected /: returnUrl, returnUrl являє собою location.path (), перехоплений при ngOnInit директиви. Це дозволяє орієнтуватися на користувача після входу до початково запропонованого URL-адреси.
Amaury

1
Див. Відповіді нижче для простого рішення. Все, що є цим загальним (перенаправлення, якщо воно не має автентичності), повинно мати просте рішення з відповіддю на одне речення.
Rick O'Shea

7
Примітка. Ця відповідь стосується бета-версії або версії-кандидата на випуск Angular 2 і більше не застосовується для фіналу Angular 2.
jbandi

1
Зараз є набагато краще рішення цього питання з використанням кутових гвардійців
mwilson

116

Ось оновлений приклад використання Angular 4 (також сумісний з Angular 5 - 8)

Маршрути з домашнім маршрутом, захищені 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 перенаправляє на сторінку входу, якщо користувач не ввійшов у систему

Оновлено для передачі оригінального URL-адреси в параметрі запитів на сторінку входу

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

Для повного прикладу та робочої демонстрації ви можете ознайомитися з цією публікацією


6
У мене є запит Q, чи не встановивши довільне значення currentUserв localStorage, все-таки зможе отримати доступ до захищеного маршруту? напр. localStorage.setItem('currentUser', 'dddddd')?
jsd

2
Це обійде захист на стороні клієнта. Але також було б вияснено маркер, який був би необхідний для транзакцій на стороні сервера, тому ніякі корисні дані не могли бути вилучені з програми.
Метт Менг

55

Використання з кінцевим маршрутизатором

З впровадженням нового маршрутизатора стало легше охороняти маршрути. Ви повинні визначити охоронець, який виконує функції служби, і додати його до маршруту.

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

Тепер перейдіть LoggedInGuardдо маршруту, а також додайте його до providersмасиву модуля.

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

Декларація модуля:

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

Детальна публікація в блозі про те, як це працює з остаточним випуском: https://medium.com/@blacksonic86/angular-2-authentication-revisited-611bf7373bf9

Використання з устареним маршрутизатором

Більш надійним рішенням є розширення RouterOutletта при активації маршруту перевірити, чи користувач увійшов у систему. Таким чином, вам не потрібно копіювати та вставляти директиву до кожного компонента. Плюс перенаправлення на основі підкомпонента може ввести в оману.

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

The UserService місце, де знаходиться Ваша бізнес-логіка, чи користувач увійшов чи ні. Ви можете легко додати його за допомогою DI в конструкторі.

Коли користувач переходить на новий URL на вашому веб-сайті, викликується метод активації за допомогою поточної Інструкції. З нього можна схопити URL-адресу та вирішити, чи дозволено це чи ні. Якщо не просто перенаправлення на сторінку входу.

Останнє, що залишається, щоб він працював, це передати його до нашого основного компонента замість вбудованого.

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

Це рішення не можна використовувати з @CanActiveдекоратором життєвого циклу, оскільки якщо передана йому функція вирішує помилку, метод активаціїRouterOutlet не буде викликаний.

Також написав про це докладний пост у блозі: https://medium.com/@blacksonic86/authentication-in-angular-2-958052c64492


2
Також написав більш детальну публікацію в блозі про це medium.com/@blacksonic86/…
Blacksonic

Привіт @Blacksonic. Щойно почав копатися в ng2. Я дотримувався вашої пропозиції, але в результаті отримав цю помилку під час 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". Не вдалося зрозуміти, що змінити в конструкторі ("_parentRounter"), щоб позбутися цього повідомлення. Будь-які думки?
leovrf

декларація скопійована з базового вбудованого об'єкта RouterOutlet, щоб він був таким же підписом, як розширений клас, я б відключив специфічне правило tslint для цього рядка
Blacksonic

Я знайшов посилання на посібник зі стилів mgechev (шукайте "Віддати перевагу введенням через декоратор параметрів @Attribute"). Змінено рядок на _parentRouter: Router, @Input() nameAttr: string,і tslint більше не призводить до помилки. Також замінено імпорт "Attribute" на "Input" з кутового ядра. Сподіваюся, це допомагає.
leovrf

1
Виникла проблема з 2.0.0-rc.1, оскільки RouterOutlet не експортується, і немає можливості його продовження
mkuligowski

53

Будь ласка, не перекривайте маршрутизатор! Це кошмар з останньою версією маршрутизатора (3.0 бета).

Замість цього використовуйте інтерфейси CanActivate і CanDeactivate та встановіть клас як canActivate / canDeactivate у визначенні маршруту.

Щось схоже на те:

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

Клас:

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

Дивіться також: https://angular.io/docs/ts/latest/guide/router.html#!#can-activate-guard


2
Приємно, відповідь @ Blacksonic працювала для мене чудово із устареним маршрутизатором. Мені довелося багато переробляти після переходу на новий роутер. Ваше рішення - саме те, що мені було потрібно!
evandongen

Я не можу отримати canActivate для роботи в своєму app.component. Я хочу переспрямувати користувача, якщо він не підтверджений автентичністю. Це версія у мене маршрутизатора (Якщо мені потрібно оновити, як це зробити, використовуючи командний рядок git bash?) Версія у мене є: "@ angular / router": "2.0.0-rc.1"
AngularM

Чи можу я використовувати той же клас (AuthGuard) для захисту іншого маршрутного компонента?
циро

4

Дотримуючись дивовижних відповідей вище, я також хотів би CanActivateChild: охороняти дитячі маршрути. Його можна використовувати для додавання guardдо дітей маршрутів, корисних для таких випадків, як ACL

Виходить так

src / app / auth-guard.service.ts (уривок)

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 (уривок)

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

Це взято з https://angular.io/docs/ts/latest/guide/router.html#!#can-activate-guard


2

Перегляньте цей код, файл 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]    },

]; 

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;      
    }
  }
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.