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

В этом примере нам нужно реализовать два охранника:

  1. GuestGuard: возвращает, является ли пользователь гостем или нет
  2. 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]
...
})

Окончательная конфигурация

Нам нужно сделать еще две вещи, чтобы он работал идеально:

  1. Создайте AppService, содержащий метод initiallizeApp
  2. Импортировать AuthModule в AppModule
  3. Запустить 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 для защиты доступности маршрутов вашего приложения.

Спасибо за чтение!