Angular перенаправление на страницу входа

122

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

Я пытаюсь воспроизвести это поведение на 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-адресу.
Амори
1
См. Ответы ниже для простого решения. Все, что является распространенным (перенаправление, если не аутентифицировано), должно иметь простое решение с одним предложением.
Рик О'Ши
7
Примечание. Этот ответ относится к бета-версии или версии кандидата на выпуск Angular 2 и больше не применим для финальной версии Angular 2.
jbandi
1
Теперь есть гораздо лучшее решение, используя Angular Guards
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
У меня есть последующий вопрос. Разве если установка произвольного значения 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()
  }
}

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

Когда пользователь переходит на новый URL-адрес на вашем веб-сайте, вызывается метод активации с текущей инструкцией. Из него вы можете получить URL-адрес и решить, разрешено это или нет. Если не просто перенаправить на страницу входа.

И последнее, что осталось сделать, чтобы он заработал, - это передать его нашему основному компоненту вместо встроенного.

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

Это решение нельзя использовать с @CanActiveдекоратором жизненного цикла, потому что, если переданная ему функция разрешает false, метод активации RouterOutletне будет вызываться.

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

Blacksonic
источник
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 08
объявление копируется из базового встроенного объекта RouterOutlet, чтобы иметь ту же сигнатуру, что и расширенный класс, я бы отключил конкретное правило tslint для этой строки
Blacksonic
Я нашел ссылку на руководство по стилю mgechev (ищите «Предпочитать ввод над декоратором параметров @Attribute»). Изменил строку на _parentRouter: Router, @Input() nameAttr: string,и tslint больше не вызывает ошибку. Также заменен импорт «Атрибутов» на «Ввод» из ядра angular. Надеюсь это поможет.
leovrf 08
1
Проблема с 2.0.0-rc.1, потому что RouterOutlet не экспортируется и нет возможности его расширить
mkuligowski 01
53

Пожалуйста, не перекрывайте выход маршрутизатора! Это кошмар с последней версией роутера (3.0 beta).

Вместо этого используйте интерфейсы 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.

Nilz11
источник
2
Хороший, ответ @Blacksonic отлично работал у меня с устаревшим маршрутизатором. После перехода на новый роутер мне пришлось много реорганизовать. Ваше решение - именно то, что мне нужно!
evandongen
Я не могу заставить canActivate работать в моем компоненте app.component. Я хочу перенаправить пользователя, если он не аутентифицирован. Это версия маршрутизатора, который у меня есть (если мне нужно обновить его, как мне это сделать с помощью командной строки git bash?) У меня есть версия: "@ angular / router": "2.0.0-rc.1"
AngularM
могу ли я использовать тот же класс (AuthGuard) для защиты маршрута другого компонента?
tsiro
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

Thabung
источник
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]    },

]; 
sojan
источник
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
источник