
Там почти нет приложений без аутентификации и закрытых зон, верно? В этой статье я объясню вам, как обрабатывать ограниченные маршруты в 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 для защиты доступности маршрутов вашего приложения.
Спасибо за чтение!