Возможно, вы недавно слышали о теме, которая крутится в сообществе Angular: фасады в NgRx. Я подумал, что было бы неплохо написать статью, чтобы обсудить этот вопрос. Давайте узнаем, что такое фасады, аргументы за и против и как их реализовать. (Вы можете найти пример кода в этом репозитории.)

Загляните в Блог Auth0 🔐 и найдите все, что вам нужно знать об инфраструктуре идентификации, управлении доступом, SSO, аутентификации JWT и последних новостях в области безопасности. 👉 AUTH0 БЛОГ 👈

Что такое фасады?

Я сделал этот снимок фасада Букингемского дворца, когда был в Лондоне в ноябре этого года. Несмотря на то, что я знаю, глядя на него, что это Букингемский дворец, я понятия не имею, что внутри - и эти охранники следят за тем, чтобы я ничего не узнал.

В коде термин фасад относится к паттерну фасада, который является структурным паттерном проектирования из известной книги Паттерны проектирования (обычно называемой Бандой четырех в отношении авторов). Точно так же, как фасад здания скрывает от прохожих то, что находится внутри, фасад в коде скрывает сложность базовых сервисов, открывая только определенные методы. Вы часто слышите, как это сокрытие сложности называется добавлением уровня абстракции. абстракция - это более простая и общая модель чего-то, что предоставляет только то, что нужно знать его потребителю.

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

Вот как можно изобразить узор фасада на схеме:

"Источник"

Здесь вы можете увидеть, что клиенты ничего не знают о других услугах. Они просто вызывают doSomething(), и фасад обрабатывает работу со службами за кулисами.

Фасады в NgRx

В последнее время было много дискуссий о том, следует ли использовать фасад с NgRx, чтобы скрыть такие биты, как хранилище, действия и селекторы. Это было вызвано статьей Томаса Бурлесона под названием Фасады NgRx +: лучшее государственное управление. Фасад - это просто сервис Angular, который обрабатывает любое взаимодействие с магазином. Когда компоненту необходимо отправить действие или получить результат селектора, он вместо этого вызовет соответствующие методы в службе фасада.

Эта диаграмма иллюстрирует взаимосвязь между компонентом, фасадом и остальной частью NgRx:

Давайте рассмотрим пример, чтобы лучше понять это. Вот BooksPageComponent из примера кода моего Руководства по аутентификации NgRx (я спрятал код внутри декоратора Component, чтобы его было легче читать):

// src/app/books/components/books-page.component.ts import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; import { select, Store } from '@ngrx/store'; import { Observable } from 'rxjs'; import * as BooksPageActions from '../actions/books-page.actions'; import { Book } from '../models/book'; import * as fromBooks from '../reducers'; import { Logout } from '@app/auth/actions/auth.actions'; @Component({ /* ...hidden for readability */ }) export class BooksPageComponent implements OnInit { books$: Observable<Book[]>; constructor(private store: Store<fromBooks.State>) { this.books$ = store.pipe(select(fromBooks.getAllBooks)); } ngOnInit() { this.store.dispatch(new BooksPageActions.Load()); } logout() { this.store.dispatch(new Logout()); } }

Этот компонент импортирует хранилище, некоторые действия и некоторые редукторы. Он отправляет действие Load во время ngOnInit и использует селектор для книг в конструкторе. Он также отправляет действие, когда пользователь выходит из системы.

Если бы вместо этого мы использовали фасад в компоненте, класс выглядел бы примерно так (для краткости я опущу импорт и декоратор):

// src/app/books/components/books-page.component.ts // above remains the same except for the imports export class BooksPageComponent implements OnInit { books$: Observable<Book[]>; constructor(private booksFacade: BooksFacade) { this.books$ = this.booksFacade.allBooks$; } ngOnInit() { this.booksFacade.loadBooks(); } logout() { this.booksFacade.logout(); } }

Мы заменили селектор на наблюдаемое в службе booksFacade. Мы также заменили обе отправки действий вызовами методов службы booksFacade.

Фасад будет выглядеть так (опять же без импорта для краткости):

// imports above @Injectable() export class BooksFacade { allBooks$: Observable<Book[]>; constructor(private store: Store<fromBooks.State>) { this.allBooks$ = store.pipe(select(fromBooks.getAllBooks)); } getBooks() { this.store.dispatch(new BooksPageActions.Load()); } logout() { this.store.dispatch(new Logout()); } }

Глядя на код компонента изолированно, вы и не подозреваете, что это приложение Angular использует NgRx для управления состоянием. Так это хорошо или плохо? Давайте поговорим о некоторых плюсах и минусах этого подхода.

Корпус для фасадов

Давайте сначала рассмотрим некоторые плюсы паттерна фасада для NgRx.

Плюс №1: Фасады удобнее для разработчиков.

Один из главных аргументов в пользу использования паттерна фасада - это повышение удобства разработки. NgRx часто критикуют из-за того, что для настройки требуется много повторяющегося кода, а также из-за сложности обслуживания и масштабирования большого количества движущихся частей. Каждая функция требует изменений в состоянии, хранилище, действиях, редукторах, селекторах и эффектах. Добавляя слой абстракции через фасад, мы уменьшаем необходимость прямого взаимодействия с этими частями. Например, новый разработчик, пишущий книги с новым списком функций, может просто ввести BooksFacade и вызвать loadBooks(), не беспокоясь об изучении тонкостей магазина.

Фасад завершает хранилище и позволяет вашему компоненту знать только об одном - фасаде хранилища. Чисто. Вы вводите только одно. Он становится интерфейсом для компонентов и магазина. Кажется, здесь намного чище, чем в отдельном магазине.

- Фрости (@aaronfrost) 29 октября 2018

Pro # 2: узор фасада легче масштабировать, чем простой NgRx.

Второй аргумент в пользу рисунка фасада - насколько легко его масштабировать. Скажем, например, нам нужно было разработать семь новых функций для большого приложения. Некоторые из этих функций, вероятно, будут частично совпадать в том, как они влияют на состояние приложения, а также на данные, которые они потребляют. Используя фасады, мы могли бы избавиться от множества повторяющихся работ:

  • Во-первых, мы определяем минимальное количество изменений состояния, которые нам нужно сделать, на основе уникальных сценариев использования.
  • Затем мы добавим их в нужные места в установочные файлы NgRx для всего приложения (например, изменения состояния приложения или редукторов).
  • Наконец, мы применили методы и селекторы к нашему фасаду.

Затем можно было быстро добавить семь новых функций, позволяя фасаду беспокоиться о лежащих в основе элементах NgRx.

« Использование фасадов в NgRx может повысить продуктивность разработки и упростить масштабирование приложений .»

ПОСМОТРЕТЬ

Дело против фасадов

Это уменьшенное трение при разработке и масштабировании действительно звучит великолепно, но разве все - клумба из роз? Давайте посмотрим на другую сторону аргумента.

Минус №1: фасады нарушают непрямое отношение NgRx.

В первый раз, когда я увидел фасады, используемые с NgRx, я сразу же получил ответ: «Подождите, мы просто потратили все это время на настройку действий, редукторов и эффектов NgRx, но теперь мы скрываем все это с помощью службы? ” Оказывается, мое чутье - один из главных аргументов против использования фасадов.

Хотя NgRx действительно критикуют за наличие большого количества движущихся частей, каждая из этих частей была разработана для выполнения определенной функции и связи с другими частями определенным образом. По своей сути NgRx похож на систему обмена сообщениями. Когда пользователь нажимает кнопку «Загрузить книги», компонент отправляет сообщение (действие), в котором говорится: «Эй, загрузите несколько книг!» Эффект слышит это сообщение, получает данные с сервера и отправляет другое сообщение в качестве действия: «Книги загружены!» Редуктор слышит это сообщение и обновляет состояние приложения на «Книги загружены».

Мы называем это косвенным обращением. Косвенность - это когда часть вашего приложения за что-то отвечает, а другие части просто общаются с ней посредством обмена сообщениями. Ни редуктор, ни эффекты не знают о нажатой кнопке. Точно так же редуктор ничего не знает о том, откуда пришли данные или адрес конечной точки API.

Когда вы используете фасады в NgRx, вы обходите эту конструкцию (по крайней мере, на практике). Теперь фасад знает об отправке действий и доступе к селекторам. При этом разработчик, работающий над приложением, больше ничего не знает об этом косвенном обращении. Просто теперь можно позвонить в новую службу.

Шаблон Redux требует высокой стоимости кода для достижения косвенного обращения. Обращение и устранение косвенного обращения с фасадами заставляет меня задуматься, почему вы вообще платите стоимость Redux. Почему бы тогда не сделать что-нибудь вроде Акиты?

- Майк Райан (@MikeRyanDev) 29 октября 2018 г.

Минус 2: Фасады могут привести к повторному использованию действий.

Поскольку косвенное обращение NgRx обходится и скрывается от разработчиков, возникает соблазн повторно использовать действия. Это второй серьезный недостаток фасадного рисунка.

Допустим, мы работаем над нашим приложением для книг. У нас есть два места, где пользователь может добавить новую книгу: 1) список книг и 2) страница сведений о книге. Было бы заманчиво добавить к нашему фасаду метод с именем addBook() и использовать его для отправки одного и того же действия в обоих этих экземплярах (что-то вроде [Books] Add Book).

Однако это будет примером плохой гигиены. Когда мы вернемся к этому коду через год или два из-за возникшей ошибки, мы не узнаем, когда мы отлаживаем, откуда взялся [Books] Add Book. Вместо этого нам лучше последовать совету Майка Райана из его выступления на конференции ng-conf 2018 Хорошая гигиена. Для захвата событий лучше использовать действия, а не команды. В нашем примере у нашего booksReducer может быть просто дополнительный регистр:

function booksReducer(state, action){ switch (action.type) { case '[Books List] Add Book': case '[Book Detail] Add Book': return [...state, action.book]; default: return state; } }

При написании действий мы всегда хотим сосредоточиться на ясности, а не на краткости. Хорошие действия - это действия, которые вы можете прочитать через год и сказать, куда они направляются.

Когда дело доходит до паттерна фасада, мы можем смягчить эту проблему, создав dispatch метод в нашем фасаде вместо того, чтобы абстрагироваться от действий. В нашем примере выше вместо общего addBook() метода мы вызываем facadeService.dispatch(new AddBookFromList()) или facadeService.dispatch(new AddBookFromDetail()). Делая это, мы теряем немного абстракции, но это избавит нас от головной боли в будущем, следуя передовым методам создания действий.

Итак, что это такое?

Фасады могут значительно ускорить время разработки в больших приложениях NgRx, что делает разработчиков счастливыми и значительно упрощает масштабирование. С другой стороны, дополнительный уровень абстракции может свести на нет цель косвенного обращения NgRx, вызывая путаницу и низкую гигиену действий. Итак, что побеждает?

Все разработчики в какой-то момент своей карьеры узнают, что увеличение абстракции всегда имеет свою цену. Абстракция торгует прозрачностью ради удобства. Это может вызвать трудности при отладке или обслуживании, но в какой-то момент они возникнут. Вы и ваша команда всегда должны решать, стоит ли этот компромисс и как бороться с любыми недостатками. Некоторые люди в сообществе Angular утверждают, что преимущества паттерна фасада перевешивают стоимость повышенной абстракции, а некоторые утверждают, что нет.

Я считаю, что шаблон фасада определенно имеет место в разработке NgRx, но я хотел бы сделать два предостережения. Если вы собираетесь использовать фасады с NgRx:

  1. Убедитесь, что ваши разработчики понимают шаблон NgRx, как работает его косвенное обращение и почему вы используете фасад, и
  2. Продвигайте хорошую гигиену действий, используя dispatch метод в фасадной службе вместо абстрагирования действий.

Обучая своих товарищей по команде паттерну NgRx и одновременно используя фасад, вы избавите себя от головной боли, когда что-то начнет ломаться. Используя dispatch метод в своем фасаде, вы уменьшите тенденцию к повторному использованию действий за счет сохранения читабельности кода и простоты отладки.

« Несмотря на то, что использование фасадов в NgRx может быть действительно полезным, важно помнить о гигиене действий и не использовать их повторно .»

ПОСМОТРЕТЬ

Реализация фасада

Теперь, когда мы знаем, почему мы можем или не можем захотеть использовать фасад с NgRx, давайте создадим его. Мы собираемся добавить фасад к простому приложению с книгами и использовать его для упрощения компонента, отображающего коллекцию книг. Мы также будем соблюдать правила гигиены, используя dispatch шаблон, описанный выше.

Настроить приложение

Для начала нам нужно убедиться, что Node и npm установлены. Нам также понадобится Angular CLI:

npm install -g @angular/cli

Код этого руководства находится в репозитории блога Auth0. Вот какие команды нам понадобятся:

git clone https://github.com/auth0-blog/ngrx-facades.git cd ngrx-facades npm install git checkout 8e24360

Протестируйте приложение

Теперь у нас должна быть возможность запустить ng serve, перейти к http://localhost:4200 и щелкнуть «Посмотреть мою коллекцию книг».

Добавить фасадный сервис

Поскольку фасад - это просто служба, мы можем сгенерировать наш исходный код с помощью Angular CLI:

ng generate service /books/services/books-facade --spec=false

Теперь у нас будет services папка внутри нашей books папки с файлом с именем books-facade.service.ts. Открыв его, мы увидим следующее:

// src/app/books/services/books-facade.service.ts import { Injectable } from '@angular/core'; @Injectable({ providedIn: 'root' }) export class BooksFacadeService { constructor() { } }

Примечание. Вы можете удалить Service из названия класса. Некоторые люди также удаляют service из имени файла и перемещают этот файл в другое место, более связанное с состоянием, чтобы избежать путаницы со службами HTTP. Я оставляю это на ваше усмотрение и придерживаюсь некоторых простых настроек по умолчанию.

Давайте подумаем о том, что мы изначально хотим, чтобы наш новый фасад делал для нас. Мы собираемся использовать его в BooksPageComponent, о котором мы говорили выше. Напомним, как выглядит этот класс компонента:

// src/app/books/components/books-page.component.ts // Omitting Component decorator and imports for brevity. export class BooksPageComponent implements OnInit { books$: Observable<Book[]>; constructor(private store: Store<fromBooks.State>) { this.books$ = store.pipe(select(fromBooks.getAllBooks)); } ngOnInit() { this.store.dispatch(new BooksPageActions.Load()); } }

Наш компонент делает следующее:

  • Вызывает селектор книг
  • Отправляет действие Load из магазина во время ловушки жизненного цикла ngOnInit.

Давайте заменим их на нашем фасаде. Мы сможем просто скопировать часть этого кода с небольшими изменениями.

Мы знаем, что нам нужно ввести Store в конструктор, который примет State, экспортированный из src/app/books/reducers/books.ts, точно так же, как в BooksPageComponent. Мы также знаем, что нам понадобится dispatch метод, принимающий Action (который нам нужно будет импортировать). Давайте соответствующим образом обновим наш фасад, импортировав то, что нам нужно, и добавив эти вещи:

// src/app/books/services/books-facade.service.ts import { Injectable } from '@angular/core'; import { Store, Action } from '@ngrx/store'; import * as fromBooks from '../reducers'; @Injectable({ providedIn: 'root' }) export class BooksFacadeService { constructor(private store: Store<fromBooks.State>) { } dispatch(action: Action) { this.store.dispatch(action); } }

Обратите внимание, что мы используем тот же импорт и ту же инъекцию магазина, что и в компоненте.

Наконец, давайте инициализируем новый наблюдаемый объект для книг и используем селектор в конструкторе. Мы в основном скопируем то, что есть в компоненте прямо сейчас, но давайте изменим имя наблюдаемого на allBooks$, чтобы быть ясным. Нам также нужно будет импортировать Observable из rxjs, добавить select к нашему импорту из ngrx/store и импортировать модель Book. Готовый код будет выглядеть так:

// src/app/books/services/books-facade.service.ts import { Injectable } from '@angular/core'; import { Store, Action, select } from '@ngrx/store'; import { Observable } from 'rxjs'; import * as fromBooks from '../reducers'; import { Book } from '../models/book'; @Injectable({ providedIn: 'root' }) export class BooksFacadeService { allBooks$: Observable<Book[]>; constructor(private store: Store<fromBooks.State>) { this.allBooks$ = store.pipe(select(fromBooks.getAllBooks)); } dispatch(action: Action) { this.store.dispatch(action); } }

Наш фасад готов! Теперь давайте обновим компонент страницы с книгами.

Обновите страницу книг

Первое, что мы сделаем для обновления BooksPageComponent, это заменим инъекцию магазина на BooksFacadeService:

// src/app/books/components/books-page.component.ts // no changes to above imports import { BooksFacadeService } from '../services/books-facade.service'; @Component({ // hidden, no changes }) export class BooksPageComponent implements OnInit { books$: Observable<Book[]>; constructor(private booksFacade: BooksFacadeService) { this.books$ = store.pipe(select(fromBooks.getAllBooks)); } ngOnInit() { this.store.dispatch(new BooksPageActions.Load()); } }

Конечно, теперь мы будем видеть красные волнистые линии в нашем редакторе под ссылками на магазин. Мы можем исправить это, заменив селектор на booksFacade.allBooks$ и другую ссылку на store внутри ngOnInit на booksFacade. Остальное останется без изменений. После очистки импорта готовый код будет выглядеть следующим образом (опять же, без декоратора, поскольку изменений нет):

import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; import { Observable } from 'rxjs'; import * as BooksPageActions from '../actions/books-page.actions'; import { Book } from '../models/book'; import { BooksFacadeService } from '../services/books-facade.service'; @Component({ // hidden, no changes }) export class BooksPageComponent implements OnInit { books$: Observable<Book[]>; constructor(private booksFacade: BooksFacadeService) { this.books$ = booksFacade.allBooks$; } ngOnInit() { this.booksFacade.dispatch(new BooksPageActions.Load()); } }

И это все! Мы больше не используем магазин в этом компоненте. У нас все еще должна быть возможность запустить ng serve и посмотреть список книг. Чтобы еще раз проверить, что фасад работает, мы можем установить точку останова для метода dispatch в службе. Он должен срабатывать при загрузке книг.

Поскольку это такой простой пример, мы не получили здесь большого преимущества от использования фасада. Однако легко представить себе, насколько это было бы спасением, если бы этот компонент использовал пять селекторов одновременно, а семь других компонентов делали бы то же самое! Теперь вы знаете все, что нужно для масштабирования этого примера. Попробуйте добавить в приложение больше селекторов или действий!

Помните, что вы можете получить доступ к готовому образцу кода здесь.

Вывод

Шаблон фасада может быть чрезвычайно полезен при создании больших приложений Angular, использующих NgRx для управления состоянием. В то же время при использовании этого подхода полезно знать о подводных камнях. Повышенная абстракция может вызвать повышенную непрозрачность, если вы не будете осторожны, а также может привести к некоторым вредным привычкам, когда дело доходит до создания и использования действий. Вы можете избежать этих ловушек, если ваша команда будет хорошо разбираться в шаблоне NgRx, обучая новых разработчиков причинам использования фасадов и используя метод dispatch в вашем фасаде вместо абстрагирования действий. Удачи и удачного кодирования!

Кроме того: аутентифицируйте приложение Angular и API узла с помощью Auth0

Мы можем защитить наши приложения и API, чтобы только прошедшие аутентификацию пользователи могли получить к ним доступ. Давайте посмотрим, как это сделать с помощью приложения Angular и Node API с помощью Auth0. Вы можете клонировать этот образец приложения и API из репозитория angular-auth0-aside на GitHub.

Функции

Образец Angular-приложения и API имеет следующие особенности:

  • Angular-приложение, созданное с помощью Angular CLI и обслуживаемое по адресу http: // localhost: 4200
  • Аутентификация с помощью auth0.js с использованием страницы входа
  • Маршрут API, защищенный сервером узла, http://localhost:3001/api/dragons возвращает данные JSON для GET аутентифицированных запросов
  • Приложение Angular извлекает данные из API после аутентификации пользователя с помощью Auth0
  • Страница профиля требует аутентификации для доступа с использованием средств защиты маршрута
  • Служба аутентификации использует субъекты для предоставления приложению данных аутентификации и профиля.

Подпишитесь на Auth0

Вам понадобится учетная запись Auth0 для управления аутентификацией. Вы можете зарегистрировать бесплатную учетную запись здесь. Затем настройте приложение Auth0 и API, чтобы Auth0 мог взаимодействовать с приложением Angular и API узла.

Настройка приложения Auth0

  1. Перейдите в раздел Панель инструментов Auth0: Приложения и нажмите кнопку + Создать приложение ».
  2. Назовите свое новое приложение и выберите «Одностраничные веб-приложения».
  3. В Настройках вашего нового приложения Auth0 добавьте http://localhost:4200/callback к Разрешенным URL-адресам обратного вызова.
  4. Добавьте http://localhost:4200 как в Разрешенные веб-источники, так и в Разрешенные URL-адреса выхода. Нажмите кнопку «Сохранить изменения».
  5. Если хотите, можете завязать социальные связи. Затем вы можете включить их для своего приложения в параметрах Приложение на вкладке Подключения. В примере, показанном на скриншоте выше, используется база данных имен пользователей и паролей, Facebook, Google и Twitter.

Примечание. Настройте свои собственные ключи социальных сетей и не оставляйте социальные связи для использования ключей разработчика Auth0, иначе вы столкнетесь с проблемами при обновлении токенов.

Настроить API

  1. Перейдите в API на панели инструментов Auth0 и нажмите кнопку Создать API. Введите имя API. В качестве идентификатора укажите URL-адрес конечной точки API. В этом примере это http://localhost:3001/api/. Алгоритм подписи должен быть RS256.
  2. Вы можете ознакомиться с примером Node.js на вкладке Быстрый старт в настройках вашего нового API. Мы реализуем наш Node API таким образом, используя Express, express-jwt и jwks-rsa.

Теперь мы готовы реализовать аутентификацию Auth0 как для нашего клиента Angular, так и для серверного API Node.

Зависимости и настройка

Приложение Angular использует Angular CLI. Убедитесь, что у вас установлен глобальный интерфейс командной строки:

$ npm install -g @angular/cli

После клонирования проекта на GitHub установите зависимости Node как для приложения Angular, так и для сервера Node, выполнив следующие команды в корне папки проекта:

$ npm install $ cd server $ npm install

Node API находится в папке /server в корне нашего примера приложения.

Найдите config.js.example файл и удалите расширение .example из имени файла. Затем откройте файл:

// server/config.js (formerly config.js.example) module.exports = { CLIENT_DOMAIN: '[YOUR_AUTH0_DOMAIN]', // e.g., 'you.auth0.com' AUTH0_AUDIENCE: 'http://localhost:3001/api/' };

Измените значение CLIENT_DOMAIN на свой полный домен Auth0 и установите AUTH0_AUDIENCE для своей аудитории (в этом примере это http://localhost:3001/api/). Маршрут /api/dragons будет защищен express-jwt и jwks-rsa.

Примечание. Чтобы узнать больше о наборе веб-ключей RS256 и JSON, прочтите Навигация по RS256 и JWKS.

Наш API теперь защищен, поэтому давайте убедимся, что наше приложение Angular также может взаимодействовать с Auth0. Для этого активируем src/environments/environment.ts.example файл, удалив .example из расширения файла. Затем откройте файл и измените строки [YOUR_CLIENT_ID] и [YOUR_AUTH0_DOMAIN] на вашу информацию Auth0:

// src/environments/environment.ts (formerly environment.ts.example) ... export const environment = { production: false, auth: { CLIENT_ID: '[YOUR_CLIENT_ID]', CLIENT_DOMAIN: '[YOUR_AUTH0_DOMAIN]', // e.g., 'you.auth0.com' ... } };

Наше приложение и API настроены. Их можно обслужить, запустив ng serve из корневой папки и node server из папки /server. Команда npm start будет запускаться для вас одновременно, используя одновременно.

Теперь, когда запущены Node API и приложение Angular, давайте посмотрим, как реализована аутентификация.

Служба аутентификации

Логика аутентификации во внешнем интерфейсе обрабатывается службой аутентификации AuthService: src/app/auth/auth.service.ts файл. Мы рассмотрим этот код ниже.

// src/app/auth/auth.service.ts import { Injectable } from '@angular/core'; import { BehaviorSubject, Observable, bindNodeCallback, of } from 'rxjs'; import * as auth0 from 'auth0-js'; import { environment } from './../../environments/environment'; import { Router } from '@angular/router'; @Injectable() export class AuthService { // Create Auth0 web auth instance // @TODO: Update environment variables and remove .example // extension in src/environments/environment.ts.example private _Auth0 = new auth0.WebAuth({ clientID: environment.auth.CLIENT_ID, domain: environment.auth.CLIENT_DOMAIN, responseType: 'id_token token', redirectUri: environment.auth.REDIRECT, audience: environment.auth.AUDIENCE, scope: 'openid profile email' }); // Track whether or not to renew token private _authFlag = 'isLoggedIn'; // Create stream for token token$: Observable<string>; // Create stream for user profile data userProfile$ = new BehaviorSubject<any>(null); // Authentication navigation onAuthSuccessUrl = '/'; onAuthFailureUrl = '/'; logoutUrl = environment.auth.LOGOUT_URL; // Create observable of Auth0 parseHash method to gather auth results parseHash$ = bindNodeCallback(this._Auth0.parseHash.bind(this._Auth0)); // Create observable of Auth0 checkSession method to // verify authorization server session and renew tokens checkSession$ = bindNodeCallback(this._Auth0.checkSession.bind(this._Auth0)); constructor(private router: Router) { } login() { this._Auth0.authorize(); } handleLoginCallback() { if (window.location.hash && !this.authenticated) { this.parseHash$().subscribe( authResult => { this._setAuth(authResult); window.location.hash = ''; this.router.navigate([this.onAuthSuccessUrl]); }, err => this._handleError(err) ) } } private _setAuth(authResult) { // Observable of token this.token$ = of(authResult.accessToken); // Emit value for user data subject this.userProfile$.next(authResult.idTokenPayload); // Set flag in local storage stating this app is logged in localStorage.setItem(this._authFlag, JSON.stringify(true)); } get authenticated(): boolean { return JSON.parse(localStorage.getItem(this._authFlag)); } renewAuth() { if (this.authenticated) { this.checkSession$({}).subscribe( authResult => this._setAuth(authResult), err => { localStorage.removeItem(this._authFlag); this.router.navigate([this.onAuthFailureUrl]); } ); } } logout() { // Set authentication status flag in local storage to false localStorage.setItem(this._authFlag, JSON.stringify(false)); // This does a refresh and redirects back to homepage // Make sure you have the logout URL in your Auth0 // Dashboard Application settings in Allowed Logout URLs this._Auth0.logout({ returnTo: this.logoutUrl, clientID: environment.auth.CLIENT_ID }); } private _handleError(err) { if (err.error_description) { console.error(`Error: ${err.error_description}`); } else { console.error(`Error: ${JSON.stringify(err)}`); } } }

Эта служба использует переменные конфигурации аутентификации из environment.ts для создания экземпляра auth0.js WebAuth. Затем создается _authFlag член, который является просто флагом, который мы можем сохранить в локальном хранилище. Он сообщает нам, следует ли пытаться обновить токены с помощью сервера авторизации Auth0 (например, после обновления всей страницы или при последующем возврате в приложение). Все, что он делает, это заявляет: Это интерфейсное приложение думает, что этот пользователь аутентифицирован, а затем позволяет нам применять логику на основе этой оценки и проверять, точна она или нет.

Мы добавим и введем token$ наблюдаемый объект, который предоставит поток строки токена доступа. Это для использования с перехватчиком токенов. Мы не хотим, чтобы наш перехватчик использовал поток, который испускает значение по умолчанию без каких-либо полезных значений. Мы объявим token$ в нашем _setAuth() методе ниже, когда токен доступа станет доступным.

Мы будем использовать RxJS BehaviorSubject, чтобы предоставить поток профиля пользователя, на который вы можете подписаться в любом месте приложения. Мы также сохраним некоторые пути для навигации, чтобы приложение могло легко определять, куда отправлять пользователей, если аутентификация завершилась успешно, не удалось или пользователь вышел из системы.

Следующее, что мы сделаем, это создадим наблюдаемые из auth0.js методов parseHash() (что позволяет нам извлекать данные аутентификации из хэша при входе в систему) и checkSession() (что позволяет нам получать новые токены при наличии у пользователя сеанса связи с сервером авторизации) . Использование наблюдаемых с этими методами позволяет нам легко публиковать события аутентификации и подписываться на них в нашем приложении Angular.

Мы создадим наблюдаемые обратные вызовы из этих двух auth0.js методов, используя RxJS bindNodeCallback. Чтобы сохранить объем this, мы bind() это так:

bindNodeCallback(this._Auth0.parseHash.bind(this._Auth0))

Метод login() авторизует запрос аутентификации с Auth0, используя переменные конфигурации среды. Пользователю будет показана страница входа, и он сможет пройти аутентификацию.

Примечание. Если это первый визит пользователя в наше приложение и наш обратный вызов находится на localhost, им также будет представлен экран согласия, на котором они могут предоставить доступ к нашему API. Клиент первой стороны в домене, отличном от localhost, будет пользоваться большим доверием, поэтому диалоговое окно согласия не будет отображаться в этом случае. Вы можете изменить это, отредактировав Настройки API панели управления Auth0. Найдите переключатель "Разрешить пропуск согласия пользователя".

Мы получим accessToken, expiresIn и idTokenPayload в хэше URL от Auth0 при возврате в наше приложение после аутентификации на странице входа. Метод handleLoginCallback() подписывается на parseHash$() наблюдаемый для потоковой передачи данных аутентификации (_setAuth()) путем создания нашего token$ наблюдаемого и выдачи значения для объекта поведения userProfile$. Таким образом, все подписанные компоненты в приложении информируются об обновлении токена и пользовательских данных. _authFlag также имеет значение true и хранится в локальном хранилище, поэтому, если пользователь вернется в приложение позже, мы можем проверить, запрашивать ли у сервера авторизации новый токен. По сути, флаг служит для того, чтобы сообщить серверу авторизации: Это приложение думает, что этот пользователь аутентифицирован. Если да, дайте мне их данные . Мы проверяем состояние флага в локальном хранилище с помощью метода доступа authenticated.

Примечание. Данные профиля пользователя принимают форму, определенную стандартными утверждениями OpenID.

Метод renewAuth(), если _authFlag равен true, подписывается на checkSession$() наблюдаемое, чтобы спросить сервер авторизации, действительно ли пользователь авторизован (мы можем передавать аргументы этому наблюдаемому, как и функции auth0.js). Если да, будут возвращены свежие данные аутентификации, и мы запустим метод _setAuth(), чтобы обновить необходимые потоки аутентификации в нашем приложении. Если пользователь не авторизован с помощью Auth0, _authFlag удаляется, и пользователь будет перенаправлен на URL-адрес, который мы установили в качестве места сбоя аутентификации.

Затем у нас есть logout() метод, который устанавливает _authFlag в false и выходит из сеанса аутентификации на сервере Auth0. Затем Auth0 logout() method перенаправляет обратно в место, которое мы установили как наш logoutUrl.

Как только AuthService предоставляется в app.module.ts, его методы и свойства можно использовать в любом месте нашего приложения, например, в домашнем компоненте.

Компонент обратного вызова

Компонент обратного вызова - это то место, куда приложение перенаправляется после аутентификации. Этот компонент просто показывает сообщение о загрузке до завершения процесса входа в систему. Он выполняет handleLoginCallback() метод службы аутентификации для анализа хэша и извлечения информации аутентификации.

// src/app/callback/callback.component.ts import { Component, OnInit } from '@angular/core'; import { AuthService } from '../auth/auth.service'; @Component({ selector: 'app-callback', template: `<div>Loading...</div>`, styles: [] }) export class CallbackComponent implements OnInit { constructor(private auth: AuthService) { } ngOnInit() { this.auth.handleLoginCallback(); } }

Выполнение запросов API с проверкой подлинности

Для выполнения аутентифицированных HTTP-запросов необходимо добавить заголовок Authorization с токеном доступа к нашим исходящим запросам. Обратите внимание, что api.service.ts файл этого не делает.

Вместо этого эта функция находится в службе перехватчика HTTP под названием token.interceptor.ts.

// src/app/auth/token.interceptor.ts import { Injectable } from '@angular/core'; import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor } from '@angular/common/http'; import { AuthService } from './auth.service'; import { Observable } from 'rxjs'; import { mergeMap } from 'rxjs/operators'; @Injectable() export class InterceptorService implements HttpInterceptor { constructor(private auth: AuthService) { } intercept( req: HttpRequest<any>, next: HttpHandler ): Observable<HttpEvent<any>> { // @NOTE: If you have some endpoints that are public // and do not need Authorization header, implement logic // here to accommodate that and conditionally let public // requests pass through based on your requirements return this.auth.token$ .pipe( mergeMap(token => { if (token) { const tokenReq = req.clone({ setHeaders: { Authorization: `Bearer ${token}` } }); return next.handle(tokenReq); } }) ); } }

Как упоминалось выше, мы можем вернуть наблюдаемый token$, чтобы получить токен, затем клонировать исходящий HTTP-запрос и прикрепить заголовок Authorization перед отправкой запроса по его пути.

Перехватчик должен быть указан так в app-routing.module.ts файле:

// src/app/app-routing.module.ts ... import { HTTP_INTERCEPTORS } from '@angular/common/http'; import { InterceptorService } from './auth/token.interceptor'; ... @NgModule({ imports: [...], providers: [ ..., { provide: HTTP_INTERCEPTORS, useClass: InterceptorService, multi: true } ], ... }) export class AppRoutingModule {}

Примечание. Мы установили multi в true, потому что можем реализовать несколько перехватчиков, которые будут работать в порядке объявления.

Последние штрихи: охрана маршрута и страница профиля

Компонент страницы профиля может отображать информацию профиля аутентифицированного пользователя. Однако мы хотим, чтобы этот компонент был доступен, только если пользователь вошел в систему.

С аутентифицированным запросом API и входом / выходом, реализованным в компоненте Home, последний штрих - защитить маршрут нашего профиля от несанкционированного доступа. auth.guard.ts route guard может проверять аутентификацию и активировать маршруты условно. Охрана реализована на выбранных нами маршрутах в app-routing.module.ts файле так:

// src/app/app-routing.module.ts ... import { AuthGuard } from './auth/auth.guard'; ... @NgModule({ imports: [ RouterModule.forRoot([ ..., { path: 'profile', component: ProfileComponent, canActivate: [ AuthGuard ] }, ... ]) ], providers: [ AuthGuard, ... ], ... }) export class AppRoutingModule {}

Что нужно сделать: элегантная обработка ошибок

Теперь, когда есть основная функциональность, вы захотите подумать о том, как правильно обрабатывать ошибки и реагировать на них. Для этого потребуется реализовать некоторые функции. На ошибки нужно реагировать, но вам следует подумать, как вы предпочитаете реагировать на них, когда они возникают.

Дополнительные ресурсы

Вот и все! У нас есть аутентифицированный API-интерфейс Node и приложение Angular с входом в систему, выходом из системы, информацией профиля и защищенным маршрутом. Чтобы узнать больше, ознакомьтесь со следующими ресурсами:

Первоначально опубликовано на auth0.com.