Там почти нет приложений без аутентификации и закрытых зон, верно? В этой статье я объясню вам, как обрабатывать ограниченные маршруты в Angular с помощью Guards и Http-запросов, в реальной жизни.
Аутентификация
Вся логика аутентификации будет храниться в отдельном модуле под названием AuthModule.
. Начнем с его создания.
$ ng generate module auth
Эта команда создает папку с именем auth
в каталоге app
и содержит файл TypeScript с именем auth.module.ts
.
Нам понадобится сервис аутентификации, давайте создадим его. Через эту службу наше приложение будет связываться с нашей базой данных (или файлом в данном случае), чтобы завершить процесс входа в систему и сообщить нам статус входа.
$ ng generate service auth/auth --flat
Флаг --flat
указывает, что новый каталог не будет создан
Это будет содержимое auth.service.ts
файла.
// auth.service.ts import { Injectable } from '@angular/core'; @Injectable({ providedIn: 'root' }) export class AuthService { constructor() { } }
Пришло время создать новый метод под названием login
, который принимает email
и password
в качестве параметров, отправляет POST HTTP
запрос на сервер и получает ответ о том, существует ли пользователь или нет. Эта информация будет храниться в BehaviorSubject
.
После этого мы создадим метод с именем isLoggedIn
, который отправляет GET HTTP
запрос на сервер и получает ответ о том, аутентифицирован ли пользователь или нет. Позже в этом посте я объясню, зачем нам это нужно.
// auth.service.ts ... import { HttpClient, HttpHeaders} from "@angular/common/http"; import { Observable, BehaviorSubject } from "rxjs"; ... isLogged: BehaviorSubject<boolean>; constructor(private http: HttpClient){} ... login(email: String, password: String): Observable<boolean> { var body = {}; body['email'] = email; body['password'] = password; const headers = new HttpHeaders({'Content-Type': 'application/json'}); return this.http.post(environment.LOGIN_URL, body, {headers: headers} ).pipe( map((response: any) => { this.isLogged.next(response); return response; } )); } isLoggedIn() { return this.http.get(environment.IS_LOGGEDIN_URL, {withCredentials: true}).pipe( map( (response: any) => { this.isLogged.next(response); return response; } )); } ...
На терминале выполните следующую команду, чтобы создать новый компонент.
$ ng generate component auth/login --flat --module auth
Параметр --module auth
указывает, в каком модуле будет объявлен новый компонент
На этом этапе мы будем использовать Реактивные формы для создания нашей формы входа и установки необходимых валидаторов.
// login.component.ts import { Component, OnInit, OnDestroy } from "@angular/core"; import { FormGroup, FormControl, Validators } from "@angular/forms"; import { Router } from "@angular/router"; import { Subscription } from 'rxjs'; import { AuthService } from "./auth.service"; @Component({ selector: 'app-login', templateUrl: './login.component.html' }) export class LoginComponent implements OnInit, OnDestroy{ loginForm: FormGroup; subscription: Subscription; constructor(private authService: AuthService, private router: Router) {} onSubmit() { this.subscription = this.authService.login( this.myForm.value.email, this.loginForm.value.password) .subscribe( (response: any) => { this.router.navigate(['/new']); } ); } ngOnInit() { this.loginForm = new FormGroup({ email: new FormControl(null, [ Validators.required, Validators.pattern("[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?\.)+[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?") ]), password: new FormControl(null, Validators.required) }); } ngOnDestroy() { if(this.subscription) this.subscription.unsubscribe(); } }
Как вы можете видеть выше, в методе onSubmit
мы обрабатываем событие нажатия кнопки, чтобы получить информацию о пользователе и продолжить процесс входа в систему. Если пользователь успешно входит в систему, он будет перенаправлен на /new
маршрут.
Чтобы завершить процесс входа в систему, нам нужно написать шаблон, о котором я упоминал выше. Файл шаблона login.component.html
будет содержать очень простую структуру HTML: форму с двумя входами (адрес электронной почты и пароль) и кнопку. Никаких наворотов! (но вы можете добавить любой стиль, какой хотите)
<!-- login.component.html --> <form [formGroup]="loginForm" (ngSubmit)="onSubmit()"> <div> <label for="email">Email</label> <input type="email" id="email" formControlName="email"> </div> <div> <label for="password">Password</label> <input type="password" id="password" formControlName="password"> </div> <button type="submit" [disabled]="!loginForm.valid">Log me in</button> </form>
Защита маршрутов
Во-первых, мы предполагаем, что компонент, который мы хотим защитить, называется ProtectedComponent
и принадлежит AppModule
Файл app-routing.module.ts
содержит маршруты приложения и их разрешения.
// app-routing.module.ts import { Routes, RouterModule } from "@angular/router"; import { AppComponent } from './app.component'; import { HomeComponent } from './home.component'; import { ProtectedComponent } from './home.component'; import { LoginComponent } from './auth/login.component'; import { LoggedInGuard } from "./auth/logged-in.guard"; import { GuestGuard } from "./auth/guest.guard"; export const APP_ROUTES: Routes = [ { path: '', component: AppComponent, children: [ { path: '', component: HomeComponent }, { path: 'login', component: LoginComponent, canActivate: [GuestGuard] }, { path: 'protected', component: ProtectedComponent, canActivate: [LoggedInGuard] } ]} ]; export const routing = RouterModule.forRoot(APP_ROUTES);
Как мы видим выше, /
маршрут доступен для всех, /login
маршрут доступен для гостей и /protected
маршрут доступен для авторизованных пользователей.
С первых же слов я добавил термин Стражи , но что они на самом деле означают?
По определению, Защиты - это интерфейсы, которые можно реализовать по-разному, но они возвращают либо
Promise<boolean>
, либо anObservable<boolean>
, либоboolean
. В Angular v.7.1 вместо этого может быть возвращенUrlTree
, который указывает новое состояние маршрутизатора, которое должно быть активировано.
На практике, как следует из названия, они позволяют нам защищать доступ к определенному маршруту.
Доступны четыре типа охранников:
1. CanActivate
: решает, можно ли активировать маршрут
2. CanActivateChild
: решает, можно ли активировать дочерние маршруты в маршруте
3. CanDeactivate
: решает если маршрут можно деактивировать
4. CanLoad
: решает, могут ли быть загружены дочерние маршруты в маршруте
В этом примере нам нужно реализовать два охранника:
GuestGuard
: возвращает, является ли пользователь гостем или нетLoggedinGuard
: возвращает, вошел ли пользователь в систему или нет
Я начинаю с GuestGuard
. В этом случае мы хотим активировать маршрут, если пользователь гость. В противном случае мы хотим перенаправить его / ее на защищенный маршрут.
// guest.guard.ts import { Injectable } from '@angular/core'; import { CanActivate, Router } from '@angular/router'; import { Observable } from 'rxjs'; import { AuthService } from './auth.service'; @Injectable() export class GuestGuard implements CanActivate { constructor(private authService: AuthService, private router: Router) {} canActivate(): Observable<boolean> | Promise<boolean> | boolean { return this.authService.isLogged.pipe(map(logged => { if(logged) { this.router.navigate(['/protected']); return false; } return true; }) ) } }
В LoggedInGuard
мы хотим активировать маршрут, если пользователь вошел в систему. В противном случае мы хотим перенаправить его / ее на маршрут входа.
// logged-in.guard.ts import { Injectable } from '@angular/core'; import { CanActivate, Router, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; import { Observable } from 'rxjs'; import { AuthService } from './auth.service'; @Injectable() export class LoggedInGuard implements CanActivate { constructor(private authService: AuthService, private router: Router) {} canActivate(): Observable<boolean> | Promise<boolean> | boolean { return this.authService.isLogged.pipe(map(logged => { if(!logged) { this.router.navigate(['login']); return false; } return true; }) ) } }
Теперь мы должны предоставить этих охранников в AppModule
.
// app.module.ts @NgModule({ ... providers: [..., GuestGuard, LoggedInGuard] ... })
Окончательная конфигурация
Нам нужно сделать еще две вещи, чтобы он работал идеально:
- Создайте
AppService
, содержащий методinitiallizeApp
- Импортировать
AuthModule
вAppModule
- Запустить
initiallizeApp
вAPP_INITIALIZER
// app.service.ts import { Injectable, Injector } from '@angular/core'; import { AuthService } from './auth/auth.service'; @Injectable() export class AppService { constructor(private injector: Injector) {} initializeApp(): Promise<any> { return new Promise(((resolve, reject) => { this.injector.get(AuthService).isLoggedIn() .toPromise() .then(res => { resolve(); }) })) } }
В начале поста я упомянул, что нам по какой-то причине нужен метод isLoggedIn
.
Давайте подумаем об этом случае: пользователь входит в систему и успешно перенаправляется на защищенный маршрут. И в этот момент он решает перезагрузить страницу. Когда страница загружается снова, аутентификационная информация, которую мы сохранили в BehaviorSubject
, больше не существует. Итак, нам нужно снова спросить наш сервер о статусе аутентификации нашего пользователя, чтобы восстановить эту информацию.
И когда наступит подходящий момент для вызова этого метода? Конечно, при инициализации нашего приложения! Прежде чем что-либо еще загружается.
Как мы можем это сделать?
Существует функция APP_INITIALIZER
, которая делает именно то, что мы хотим.
По определению
APP_INITIALIZER
- это функция, которая будет выполняться при инициализации приложения.
Она импортируется из@angular/core
// app.module.ts import { ..., APP_INITIALIZER } from '@angular/core'; import { AppService } from './app.service'; ... export function app_init(appService: AppService) { return () => appService.initializeApp(); } @NgModule({ ... imports: [ ..., AuthModule ], providers: [ ..., AppService, { provide: APP_INITIALIZER, useFactory: app_init, deps: [AppService], multi: true } ] ... }) ...
Может быть несколько
APP_INITIALIZER
функций. Они выполняются одновременно, и когда все они завершаются, процесс инициализации завершается.
Остается только собрать и запустить ваше приложение!
Заключение
Надеюсь, вы смогли увидеть преимущества использования Route Guards для защиты доступности маршрутов вашего приложения.
Спасибо за чтение!