Угловой редирект на страницу входа

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

Я пытаюсь воспроизвести это поведение на Angular. Я наткнулся на декоратор @CanActivate, но это приводит к тому, что компонент вообще не рендерится, не будет перенаправления.

Мой вопрос заключается в следующем:

  • Предоставляет ли Angular способ добиться такого поведения?
  • Если так, то как? Это хорошая практика?
  • Если нет, что было бы лучшим способом обработки авторизации пользователей в Angular?

7 ответов

Решение

Обновление: я опубликовал полный проект 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

Вот обновленный пример использования Angular 4

Маршруты с домашним маршрутом, защищенным 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 перенаправляет на страницу входа, если пользователь не вошел в систему

Обновлено для передачи оригинальной ссылки в параметрах запроса на страницу входа

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

Для полного примера и рабочего демо вы можете проверить этот пост

Использование с конечным маршрутизатором

С введением нового маршрутизатора стало легче охранять маршруты. Вы должны определить охранника, который действует как сервис, и добавить его в маршрут.

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 декоратор жизненного цикла, потому что, если переданная ему функция разрешает ложь, метод активации RouterOutlet не будет звонить.

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

Пожалуйста, не перезаписывайте Router Outlet! Это кошмар с последней версией маршрутизатора (бета-версия 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

После удивительных ответов выше, я также хотел бы 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

Ссылайтесь на этот код, файл 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. 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;      
    }
  }
}
Другие вопросы по тегам