Angular перенаправить на страницу входа
Я пришел из мира Asp.Net MVC, где пользователи, пытающиеся получить доступ к странице, на которую они не авторизованы, автоматически перенаправляются на страницу входа.
Я пытаюсь воспроизвести это поведение на Angular. Я пришел через @CanActivate decorator, но это приводит к тому, что компонент не является рендерингом вообще, без перенаправления.
Мой вопрос следующий:
- Предоставляет ли Angular способ достижения такого поведения?
- Если да, то как? Это хорошая практика?
- Если нет, то какая была бы наилучшая практика для обработки авторизации пользователя в Angular?
Ответы
Ответ 1
Обновление: Я опубликовал полный проект скелета 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
как в директиве I, вставленной выше.
<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
Ответ 2
Вот обновленный пример использования 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;
}
}
Для полного примера и рабочей демонстрации вы можете проверить это сообщение
Ответ 3
Использование конечного маршрутизатора
С введением нового маршрутизатора стало легче охранять маршруты. Вы должны определить охрану, которая действует как услуга, и добавить ее в маршрут.
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
Ответ 4
Пожалуйста, не переопределяйте маршрутизатор! Это кошмар с последним выпуском маршрутизатора (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
Ответ 5
Следуя замечательным ответам выше, я также хотел бы 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
Ответ 6
Обратитесь к этому коду,
файл 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] },
];
Ответ 7
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;
}
}
}