В этой статье мы добавим аутентификацию в наше приложение Angular и узнаем, как мы можем защитить разделы от нашего приложения от несанкционированного доступа.
Эта статья является частью 5 Учебника SitePoint Angular 2+ о том, как создать приложение CRUD с помощью Angular CLI .
- Часть 0 — Ultimate Angular CLI Справочное руководство
- Часть 1. Подготовка и запуск нашей первой версии приложения Todo
- Часть 2. Создание отдельных компонентов для отображения списка задач и одной задачи
- Часть 3. Обновление сервиса Todo для связи с REST API
- Часть 4. Использование углового маршрутизатора для разрешения данных
- Часть 5. Добавление аутентификации для защиты частного контента.
- Часть 6 — Как обновить Angular Projects до последней версии.
В первой части мы узнали, как запустить наше приложение Todo и развернуть его на страницах GitHub. Это работало просто отлично, но, к сожалению, все приложение было собрано в один компонент.
Во второй части мы рассмотрели более модульную архитектуру компонентов и узнали, как разбить этот отдельный компонент на структурированное дерево более мелких компонентов, которые легче понять, использовать повторно и поддерживать.
В третьей части мы обновили наше приложение для связи с бэкэндом REST API, используя RxJS и HTTP-сервис Angular.
В части 4 мы представили Angular Router и узнали, как маршрутизатор обновляет наше приложение при изменении URL браузера и как мы можем использовать маршрутизатор для разрешения данных из нашего внутреннего API.
Не волнуйся! Вам не нужно следовать части 1, 2, 3 или 4 этого урока, чтобы пять имели смысл. Вы можете просто взять копию нашего репо , проверить код из части 4 и использовать его в качестве отправной точки. Это объясняется более подробно ниже.
Вверх и работает
Убедитесь, что у вас установлена последняя версия Angular CLI. Если вы этого не сделаете, вы можете установить его с помощью следующей команды:
npm install -g @angular/cli@latest
Если вам нужно удалить предыдущую версию Angular CLI, вы можете запустить это:
npm uninstall -g @angular/cli angular-cli npm cache clean npm install -g @angular/cli@latest
После этого вам понадобится копия кода из части 4. Она доступна по адресу https://github.com/sitepoint-editors/angular-todo-app . Каждая статья в этой серии имеет соответствующий тег в репозитории, чтобы вы могли переключаться между различными состояниями приложения.
Код, который мы закончили в части 4 и с которого мы начнем в этой статье, помечен как часть 4 . Код, которым мы заканчиваем эту статью, помечен как часть-5 .
Вы можете думать о тегах как псевдоним определенного идентификатора коммита. Вы можете переключаться между ними, используя git checkout
. Вы можете прочитать больше об этом здесь .
Итак, чтобы приступить к работе (с установленной последней версией Angular CLI), мы должны сделать это:
git clone [email protected]:sitepoint-editors/angular-todo-app.git cd angular-todo-app git checkout part-4 npm install ng serve
Затем посетите http: // localhost: 4200 / . Если все хорошо, вы должны увидеть работающее приложение Todo.
План атаки
В этой статье мы будем:
- настроить бэкэнд для аутентификации
- добавить метод входа в существующий
ApiService
- настроить службу аутентификации для обработки логики аутентификации
- настроить сервис сеанса для хранения данных сеанса
- создать
SignInComponent
для отображения формы входа - настроить защиту маршрута для защиты частей нашего приложения от несанкционированного доступа.
К концу этой статьи вы поймете:
- разница между куки и токенами
- Как создать
AuthService
для реализации логики аутентификации - как создать
SessionService
для хранения данных сеанса - как создать форму для входа с помощью угловой формы
- как создать охрану маршрута, чтобы предотвратить несанкционированный доступ к частям вашего приложения
- как отправить токен пользователя в качестве заголовка авторизации в HTTP-запросе к вашему API
- почему вы никогда не должны отправлять токен своего пользователя третьему лицу.
Наше приложение будет выглядеть так:
Итак, начнем!
Стратегия аутентификации
Серверные веб-приложения обычно обрабатывают пользовательские сеансы на сервере. Они хранят информацию о сеансе на сервере и отправляют идентификатор сеанса в браузер через cookie. Браузер сохраняет куки и автоматически отправляет их на сервер при каждом запросе. Затем сервер извлекает идентификатор сеанса из файла cookie и ищет соответствующие сведения о сеансе из своего внутреннего хранилища (память, база данных и т. Д.). Детали сеанса остаются на сервере и недоступны на клиенте.
Напротив, клиентские веб-приложения, такие как приложения Angular, обычно управляют пользовательскими сеансами на клиенте. Данные сеанса хранятся на клиенте и при необходимости отправляются на сервер. Стандартным способом хранения сеансов в клиенте являются веб-токены JSON , также называемые токенами JWT. Если вы не знакомы с тем, как работают токены, ознакомьтесь с этой простой метафорой, чтобы легко понять и запомнить, как работает аутентификация на основе токенов, и вы никогда больше этого не забудете.
Если вы хотите получить более глубокое понимание файлов cookie и токенов, обязательно ознакомьтесь с докладом Филиппа де Райка о файлах cookie и токенах: парадоксальный выбор .
Из-за популярности веб-токенов JSON в современной экосистеме мы будем использовать стратегию аутентификации на основе JWT.
Настройка бэкэнда
Прежде чем мы сможем добавить аутентификацию к нашему Angular-приложению, нам необходим сервер для аутентификации.
В предыдущих частях этой серии мы использовали json-сервер для обслуживания внутренних данных на db.json
файла db.json
в корне нашего проекта.
К счастью, json-сервер также может быть загружен как модуль узла , что позволяет нам добавлять собственные обработчики запросов.
Начнем с установки модуля npm body-parser, который нам понадобится для анализа JSON в наших HTTP-запросах:
$ npm install --save body-parser
Далее мы создаем новый файл json-server.js
в корне нашего проекта:
const jsonServer = require('json-server'); const server = jsonServer.create(); const router = jsonServer.router('db.json'); const middlewares = jsonServer.defaults(); const bodyParser = require('body-parser'); // Sample JWT token for demo purposes const jwtToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiU2l0ZVBvaW50IFJ' + 'lYWRlciJ9.sS4aPcmnYfm3PQlTtH14az9CGjWkjnsDyG_1ats4yYg'; // Use default middlewares (CORS, static, etc) server.use(middlewares); // Make sure JSON bodies are parsed correctly server.use(bodyParser.json()); // Handle sign-in requests server.post('/sign-in', (req, res) => { const username = req.body.username; const password = req.body.password; if(username === 'demo' && password === 'demo') { res.json({ name: 'SitePoint Reader', token: jwtToken }); } res.send(422, 'Invalid username and password'); }); // Protect other routes server.use((req, res, next) => { if (isAuthorized(req)) { console.log('Access granted'); next(); } else { console.log('Access denied, invalid JWT'); res.sendStatus(401); } }); // API routes server.use(router); // Start server server.listen(3000, () => { console.log('JSON Server is running'); }); // Check whether request is allowed function isAuthorized(req) { let bearer = req.get('Authorization'); if (bearer === 'Bearer ' + jwtToken) { return true; } return false; }
программноеconst jsonServer = require('json-server'); const server = jsonServer.create(); const router = jsonServer.router('db.json'); const middlewares = jsonServer.defaults(); const bodyParser = require('body-parser'); // Sample JWT token for demo purposes const jwtToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiU2l0ZVBvaW50IFJ' + 'lYWRlciJ9.sS4aPcmnYfm3PQlTtH14az9CGjWkjnsDyG_1ats4yYg'; // Use default middlewares (CORS, static, etc) server.use(middlewares); // Make sure JSON bodies are parsed correctly server.use(bodyParser.json()); // Handle sign-in requests server.post('/sign-in', (req, res) => { const username = req.body.username; const password = req.body.password; if(username === 'demo' && password === 'demo') { res.json({ name: 'SitePoint Reader', token: jwtToken }); } res.send(422, 'Invalid username and password'); }); // Protect other routes server.use((req, res, next) => { if (isAuthorized(req)) { console.log('Access granted'); next(); } else { console.log('Access denied, invalid JWT'); res.sendStatus(401); } }); // API routes server.use(router); // Start server server.listen(3000, () => { console.log('JSON Server is running'); }); // Check whether request is allowed function isAuthorized(req) { let bearer = req.get('Authorization'); if (bearer === 'Bearer ' + jwtToken) { return true; } return false; }
Эта статья не предназначена для обучения на json-сервере, но давайте быстро посмотрим, что происходит.
Сначала мы импортируем все оборудование json-сервера:
const jsonServer = require('json-server'); const server = jsonServer.create(); const router = jsonServer.router('db.json'); const middlewares = jsonServer.defaults(); const bodyParser = require('body-parser');
В реальном приложении мы динамически генерировали бы токен JWT при аутентификации пользователя, но для демонстрации мы статически определяем токен JWT:
// Sample JWT token for demo purposes const jwtToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiU2l0ZVBvaW50IFJ' + 'lYWRlciJ9.sS4aPcmnYfm3PQlTtH14az9CGjWkjnsDyG_1ats4yYg';
Далее мы настраиваем json-сервер для запуска собственного промежуточного ПО по умолчанию:
// Use default middlewares (CORS, static, etc) server.use(middlewares);
и правильно проанализировать входящие запросы JSON:
// Make sure JSON bodies are parsed correctly server.use(bodyParser.json());
Промежуточное программное обеспечение Json-сервера по умолчанию — это функции обработчика запросов, которые работают со статическими файлами, CORS и т. Д. Для получения более подробной информации ознакомьтесь с документацией .
Затем мы определяем обработчик запросов для запросов на вход:
// Handle sign-in requests server.post('/sign-in', (req, res) => { const username = req.body.username; const password = req.body.password; if(username === 'demo' && password === 'demo') { res.json({ name: 'SitePoint Reader', token: jwtToken }); } res.send(422, 'Invalid username and password'); });
Мы просим json-сервер прослушивать HTTP-запросы POST при /sign-in
. Если запрос содержит поле имени пользователя со значением demo
и поле пароля со значением demo
, мы возвращаем объект с токеном JWT. Если нет, мы отправляем ответ HTTP 422, чтобы указать, что имя пользователя и пароль неверны.
Кроме того, мы также указываем json-server авторизовать все остальные запросы:
// Protect other routes server.use((req, res, next) => { if (isAuthorized(req)) { console.log('Access granted'); next(); } else { console.log('Access denied, invalid JWT'); res.sendStatus(401); } }); // Check whether request is allowed function isAuthorized(req) { let bearer = req.get('Authorization'); if (bearer === 'Bearer ' + jwtToken) { return true; } return false; }
Если HTTP-запрос клиента содержит заголовок авторизации с токеном JWT, мы предоставляем доступ. Если нет, мы отказываем в доступе и отправляем ответ HTTP 401.
Наконец, мы сообщаем json-серверу загрузить маршруты API из db.json
и запустить сервер:
// API routes server.use(router); // Start server server.listen(3000, () => { console.log('JSON Server is running'); });
Чтобы запустить наш новый сервер, мы запускаем:
$ node json-server.js
Для нашего удобства давайте обновим скрипт json-server
в package.json
:
"json-server": "node json-server.js"
Теперь мы можем запустить:
$ npm run json-server > todo-app@0.0.0 json-server /Users/jvandemo/Projects/sitepoint-editors/angular-todo-app > node json-server.js JSON Server is running
И вуаля, у нас есть собственный API-сервер с запущенной аутентификацией.
Время копать в угловую сторону.
Добавление логики аутентификации в наш сервис API
Теперь, когда у нас есть конечная точка API для аутентификации, давайте добавим новый метод в наш ApiService
для выполнения запроса аутентификации:
@Injectable() export class ApiService { constructor( private http: Http ) { } public signIn(username: string, password: string) { return this.http .post(API_URL + '/sign-in', { username, password }) .map(response => response.json()) .catch(this.handleError); } // ... }
При вызове метод signIn()
выполняет HTTP-запрос POST к нашей новой конечной точке API /sign-in
, включая имя пользователя и пароль в теле запроса.
Если вы не знакомы со встроенным HTTP-сервисом Angular, обязательно прочитайте Часть 3 — Обновите сервис Todo для взаимодействия с REST API .
Создание службы сеанса
Теперь, когда у нас есть метод API для аутентификации на нашем сервере, нам нужен механизм для хранения данных сеанса, которые мы получаем от API, а именно name
и token
.
Поскольку данные будут уникальными для всего нашего приложения, мы будем хранить их в сервисе SessionService
.
Итак, давайте сгенерируем наш новый SessionService:
$ ng generate service session --module app.module.ts create src/app/session.service.spec.ts create src/app/session.service.ts update src/app/app.module.ts
Часть --module app.module.ts
указывает Angular CLI автоматически регистрировать наш новый сервис в качестве поставщика в AppModule
чтобы нам не приходилось регистрировать его вручную. Регистрация службы в качестве поставщика необходима для того, чтобы инжектор угловой зависимости мог создавать его при необходимости. Если вы не знакомы с системой внедрения зависимостей Angular, обязательно ознакомьтесь с официальной документацией .
Откройте src/app/session.service.ts
и добавьте следующий код:
import { Injectable } from '@angular/core'; @Injectable() export class SessionService { public accessToken: string; public name: string; constructor() { } public destroy(): void { this.accessToken = null; this.name = null; } }
У нас все очень просто. Мы определяем свойство для хранения токена доступа API пользователя и свойство для хранения имени пользователя.
Мы также добавляем метод destroy()
для сброса всех данных в случае, если мы хотим выйти из системы текущего пользователя.
Обратите внимание, что SessionService
не знает никакой логики аутентификации. Он отвечает только за хранение данных сеанса.
Мы создадим отдельный AuthService
для реализации реальной логики аутентификации.
Создание службы аутентификации
Помещение логики аутентификации в отдельный сервис способствует хорошему разделению проблем между процессом аутентификации и хранением данных сеанса.
Это гарантирует, что нам не нужно менять SessionService
если изменяется поток аутентификации, и позволяет легко имитировать данные сеанса в модульных тестах.
Итак, давайте создадим сервис под названием AuthService
:
$ ng generate service auth --module app.module.ts create src/app/auth.service.spec.ts create src/app/auth.service.ts update src/app/app.module.ts
Откройте src/app/auth.service.ts
и добавьте следующий код:
import { Injectable } from '@angular/core'; import { SessionService } from './session.service'; @Injectable() export class AuthService { constructor( private session: SessionService, ) { } public isSignedIn() { return !!this.session.accessToken; } public doSignOut() { this.session.destroy(); } public doSignIn(accessToken: string, name: string) { if ((!accessToken) || (!name)) { return; } this.session.accessToken = accessToken; this.session.name = name; } }
Мы SessionService
и добавляем несколько методов:
-
isSignedIn()
: возвращает,isSignedIn()
ли пользователь в систему -
doSignOut()
: выходит из системы, очищая данные сеанса -
doSignIn()
:doSignIn()
пользователя, сохраняя данные сеанса.
Опять же, обратите внимание, как логика аутентификации определяется в AuthService
, тогда как SessionService
используется для хранения фактических данных сеанса.
Теперь, когда у нас есть служба аутентификации, давайте создадим страницу входа с формой аутентификации.
Создание страницы входа
Давайте создадим SignInComponent
используя Angular CLI:
$ ng generate component sign-in create src/app/sign-in/sign-in.component.css create src/app/sign-in/sign-in.component.html create src/app/sign-in/sign-in.component.spec.ts create src/app/sign-in/sign-in.component.ts update src/app/app.module.ts
Наша форма входа будет реактивной формой Angular , поэтому мы должны импортировать ReactiveFormsModule
в наш модуль приложения в src/app/app.module.ts
:
// ... import { ReactiveFormsModule } from '@angular/forms'; @NgModule({ declarations: [ // ... ], imports: [ // ... ReactiveFormsModule ], providers: [ // ... ], bootstrap: [AppComponent] }) export class AppModule { }
Затем мы добавляем наш код TypeScript в src/app/sign-in/sign-in.component.ts
:
import { Component, OnInit } from '@angular/core'; import { ApiService } from '../api.service'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { AuthService } from '../auth.service'; import { Router } from '@angular/router'; @Component({ selector: 'app-sign-in', templateUrl: './sign-in.component.html', styleUrls: ['./sign-in.component.css'] }) export class SignInComponent implements OnInit { public frm: FormGroup; public isBusy = false; public hasFailed = false; public showInputErrors = false; constructor( private api: ApiService, private auth: AuthService, private fb: FormBuilder, private router: Router ) { this.frm = fb.group({ username: ['', Validators.required], password: ['', Validators.required] }); } ngOnInit() { } public doSignIn() { // Make sure form values are valid if (this.frm.invalid) { this.showInputErrors = true; return; } // Reset status this.isBusy = true; this.hasFailed = false; // Grab values from form const username = this.frm.get('username').value; const password = this.frm.get('password').value; // Submit request to API this.api .signIn(username, password) .subscribe( (response) => { this.auth.doSignIn( response.token, response.name ); this.router.navigate(['todos']); }, (error) => { this.isBusy = false; this.hasFailed = true; } ); } }
Сначала мы создаем реактивную форму в конструкторе:
this.frm = fb.group({ username: ['', Validators.required], password: ['', Validators.required] });
Мы определяем реактивную форму как группу форм, которая содержит два элемента управления формы — один для имени пользователя и один для пароля. Оба элемента управления имеют значение по умолчанию пустой строки ''
, и оба элемента управления требуют значения.
Если вы не знакомы с реактивными формами, обязательно ознакомьтесь с официальной документацией на веб-сайте Angular .
Далее мы определяем метод doSignIn()
:
public doSignIn() { // Make sure form values are valid if (this.frm.invalid) { this.showInputErrors = true; return; } // Reset status this.isBusy = true; this.hasFailed = false; // Grab values from form const username = this.frm.get('username').value; const password = this.frm.get('password').value; // Submit request to API this.api .signIn(username, password) .subscribe( (response) => { this.auth.doSignIn( response.token, response.name ); this.router.navigate(['todos']); }, (error) => { this.isBusy = false; this.hasFailed = true; } ); }
Сначала мы проверяем, находится ли форма в действительном состоянии. В конструкторе мы сконфигурировали элементы управления формы имени username
и password
с помощью встроенного в Angular Validators.required
валидатора. Это помечает оба элемента управления как обязательные и приводит к тому, что форма находится в недопустимом состоянии, как только один из элементов управления формы имеет пустое значение.
Если форма находится в недопустимом состоянии, мы showInputErrors
и возвращаемся без вызова API.
Если форма находится в допустимом состоянии ( username
и password
имеют значение), мы устанавливаем для isBusy
значение true и вызываем метод signIn()
нашего ApiService
. Мы будем использовать переменную isBusy
чтобы отключить кнопку входа в представлении, пока выполняется вызов API.
Если вызов API завершается успешно, мы вызываем метод doSignIn()
AuthService
с token
и name
из ответа API и направляем пользователя к маршруту todos
.
Если вызов API завершается неудачно, мы помечаем isBusy
как false
а hasFailed
как true
поэтому мы можем снова включить кнопку входа и показать сообщение об ошибке в представлении.
Теперь, когда у нас есть контроллер нашего компонента, давайте добавим соответствующий шаблон представления в src/app/sign-in/sign-in.component.ts
:
<div class="sign-in-wrapper"> <form [formGroup]="frm"> <h1>Todos</h1> <!-- Username input --> <input type="text" formControlName="username" placeholder="Your username"> <!-- Username validation message --> <div class="input-errors" *ngIf="(frm.get('username').invalid && frm.get('username').touched) || showInputErrors" > <div *ngIf="frm.get('username').hasError('required')"> Please enter your username </div> </div> <!-- Password input --> <input type="password" formControlName="password" placeholder="Your password"> <!-- Password validation message --> <div class="input-errors" *ngIf="(frm.get('password').invalid && frm.get('password').touched) || showInputErrors" > <div *ngIf="frm.get('password').hasError('required')"> Please enter your password </div> </div> <!-- Sign-in error message --> <div class="sign-in-error" *ngIf="hasFailed"> Invalid username and password. </div> <!-- Sing-in button --> <button (click)="doSignIn()" [disabled]="isBusy"> <ng-template [ngIf]="!isBusy">Sign in</ng-template> <ng-template [ngIf]="isBusy">Signing in, please wait...</ng-template> </button> <!-- Tip --> <p class="tip">You can sign in with username "demo" and password "demo".</p> </form> </div>
Прежде всего, мы определяем элемент формы и связываем его с нашей реактивной формой в контроллере, используя [formGroup]="frm"
.
Внутри формы мы добавляем элемент ввода для имени пользователя и привязываем его к соответствующему formControlName="username"
управления формы, используя formControlName="username"
.
Затем мы добавляем ошибку проверки, чтобы отобразить, является ли имя пользователя недействительным. Обратите внимание, как мы можем использовать удобные свойства (предоставляемые Angular), такие как valid
, invalid
, pristine
, dirty
, untouched
и touched
чтобы сузить условия, в которых мы хотим показать сообщение проверки. Здесь мы хотим отобразить ошибку проверки, когда имя пользователя неверно и пользователь коснулся ввода. Кроме того, мы также хотим отобразить ошибку проверки, когда пользователь нажимает кнопку «Войти», а ввод не имеет значения.
Мы повторяем тот же шаблон для ввода пароля и добавляем общее сообщение об ошибке для отображения в случае, если имя пользователя и пароль не являются действительными учетными данными.
Наконец, мы добавляем кнопку отправки:
<button (click)="doSignIn()" [disabled]="isBusy"> <ng-template [ngIf]="!isBusy">Sign in</ng-template> <ng-template [ngIf]="isBusy">Signing in, please wait...</ng-template> </button>
Когда пользователь нажимает кнопку и выполняется вызов API, мы отключаем кнопку с помощью [disabled]="isBusy"
и изменяем ее текст таким образом, чтобы у пользователя была визуальная индикация того, что процесс входа занят.
Теперь, когда у нас есть страница входа, давайте перенастроим наши маршруты в `src/app/app-routing.module.ts
:
import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { SignInComponent } from './sign-in/sign-in.component'; import { PageNotFoundComponent } from './page-not-found/page-not-found.component'; import { TodosComponent } from './todos/todos.component'; import { TodosResolver } from './todos.resolver'; const routes: Routes = [ { path: '', redirectTo: 'sign-in', pathMatch: 'full' }, { path: 'sign-in', component: SignInComponent }, { path: 'todos', component: TodosComponent, resolve: { todos: TodosResolver } }, { path: '**', component: PageNotFoundComponent } ]; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule], providers: [ TodosResolver ] }) export class AppRoutingModule { }
Мы определяем новый маршрут sign-in
:
{ path: 'sign-in', component: SignInComponent }
и перенаправьте URL-адрес по умолчанию на наш новый маршрут входа:
{ path: '', redirectTo: 'sign-in', pathMatch: 'full' }
чтобы пользователь автоматически перенаправлялся на страницу входа при загрузке нашего приложения.
Если вы запускаете:
$ ng serve
и перейдите в браузере по http://localhost:4200
, вы должны увидеть:
Пока что мы уже многое рассмотрели:
- настроить наш бэкэнд
- добавили метод в наш ApiService для входа
- создал AuthService для нашей логики аутентификации
- создал SessionService для хранения наших данных сеанса
- создал SignInComponent для входа пользователей.
Однако, если мы выполним вход с демонстрацией имени пользователя и демо пароля, API вернет ошибку 401 при запросе элементов todo:
Кроме того, Angular по-прежнему позволяет нам переходить в браузере по http://localhost:4200/todos
, даже если мы не вошли в систему.
Чтобы исправить обе проблемы, мы теперь:
- защитить личную область нашего приложения от несанкционированного доступа пользователями, которые не вошли в систему
- отправьте токен пользователя с запросами API, требующими аутентификации.
Давайте начнем с защиты личной области нашего приложения.
Защита личного пространства нашего приложения от несанкционированного доступа
В части 4 мы уже узнали, как использовать Angular Router для разрешения данных. В этом разделе мы рассмотрим охрану маршрута, особенность Angular Router, которая позволяет нам управлять навигацией по маршруту.
По сути, защита маршрута — это функция, которая возвращает либо true
чтобы указать, что маршрутизация разрешена, либо false
чтобы указать, что маршрутизация не разрешена. Охранник также может вернуть Обещание или Наблюдаемое, которое оценивается как истинное или ложное значение. В этом случае маршрутизатор будет ожидать завершения выполнения Promise или Observable.
Есть 4 типа охранников маршрута :
-
CanLoad
: определяет, можно ли загружать модуль сCanLoad
загрузкой -
CanActivate
: определяет, можно ли активировать маршрут приCanActivate
пользователя к маршруту. -
CanActivateChild
: определяет, можно ли активировать маршрут приCanActivateChild
пользователя к одному из его дочерних элементов. -
CanDeactivate
: определяет, можно ли деактивировать маршрут.
В нашем приложении мы хотим убедиться, что пользователь вошел в систему при переходе к маршруту todos
. Таким образом, CanActivate
подходит.
Давайте создадим нашу защиту в новом файле с именем src/app/can-activate-todos.guard.ts
:
import { Injectable } from '@angular/core'; import { AuthService } from './auth.service'; import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from '@angular/router'; import { Observable } from 'rxjs/Observable'; @Injectable() export class CanActivateTodosGuard implements CanActivate { constructor( private auth: AuthService, private router: Router ) { } public canActivate( route: ActivatedRouteSnapshot, state: RouterStateSnapshot ): Observable<boolean> | Promise<boolean> | boolean { if (!this.auth.isSignedIn()) { this.router.navigate(['/sign-in']); return false; } return true; } }
Поскольку наша защита — CanActivate
, ей необходимо реализовать интерфейс CanActivate
, предоставляемый @angular/router
.
Интерфейс CanActivate
требует, чтобы наша защита реализовала метод canActivate()
:
public canActivate( route: ActivatedRouteSnapshot, state: RouterStateSnapshot ): Observable<boolean> | Promise<boolean> | boolean { if (!this.auth.isSignedIn()) { this.router.navigate(['/sign-in']); return false; } return true; }
Метод canActivate()
получает активированный моментальный снимок маршрута и моментальный снимок состояния маршрутизатора в качестве аргументов на тот случай, если они нам понадобятся, чтобы принять умное решение, хотим ли мы разрешить навигацию.
В нашем примере логика очень проста. Если пользователь не вошел в систему, мы даем указание Angular router направить пользователя на страницу входа и прекратить дальнейшую навигацию.
Напротив, если пользователь вошел в систему, мы возвращаем true
позволяя пользователю перейти к запрошенному маршруту.
Теперь, когда мы создали средство защиты маршрутов, мы должны указать маршрутизатору Angular использовать его.
Давайте добавим нашу конфигурацию маршрутизации в src/app/app-routing.module.ts
:
import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { SignInComponent } from './sign-in/sign-in.component'; import { PageNotFoundComponent } from './page-not-found/page-not-found.component'; import { TodosComponent } from './todos/todos.component'; import { CanActivateTodosGuard } from './can-activate-todos.guard'; import { TodosResolver } from './todos.resolver'; const routes: Routes = [ { path: '', redirectTo: 'sign-in', pathMatch: 'full' }, { path: 'sign-in', component: SignInComponent }, { path: 'todos', component: TodosComponent, canActivate: [ CanActivateTodosGuard ], resolve: { todos: TodosResolver } }, { path: '**', component: PageNotFoundComponent } ]; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule], providers: [ CanActivateTodosGuard, TodosResolver ] }) export class AppRoutingModule { }
Мы говорим маршрутизатору Angular использовать нашу canActivate
для маршрута todos
, добавив свойство canActivate
к маршруту:
{ path: 'todos', component: TodosComponent, canActivate: [ CanActivateTodosGuard ], resolve: { todos: TodosResolver } }
Свойство canActivate
принимает массив элементов CanActivate
поэтому вы можете легко зарегистрировать несколько CanActivate
защиты, если это требуется вашему приложению.
Наконец, нам нужно добавить CanActivateTodosGuard
в качестве провайдера, чтобы инжектор зависимостей Angular мог создать его экземпляр, когда маршрутизатор запросит его:
@NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule], providers: [ CanActivateTodosGuard, TodosResolver ] }) export class AppRoutingModule { }
С нашей защитой маршрута наше приложение теперь перенаправляет пользователя на страницу входа, когда он не вошел в систему, и пытается перейти непосредственно к маршруту todos
.
Напротив, когда пользователь вошел в систему, навигация к маршруту todos
разрешена.
Как это мило!
Отправка токена пользователя с помощью запросов API
Пока что наш зарегистрированный пользователь может получить доступ к маршруту todos
, но API по-прежнему отказывается возвращать любые данные todo, потому что мы не отправляем токен пользователя в API.
Итак, давайте откроем src/app/api.service.ts
и скажем Angular отправлять токен нашего пользователя в заголовках нашего HTTP-запроса при необходимости:
import { Injectable } from '@angular/core'; import { Http, Headers, RequestOptions, Response } from '@angular/http'; import { environment } from 'environments/environment'; import { Todo } from './todo'; import { Observable } from 'rxjs/Observable'; import 'rxjs/add/operator/map'; import 'rxjs/add/operator/catch'; import 'rxjs/add/observable/throw'; import { SessionService } from 'app/session.service'; const API_URL = environment.apiUrl; @Injectable() export class ApiService { constructor( private http: Http, private session: SessionService ) { } public signIn(username: string, password: string) { return this.http .post(API_URL + '/sign-in', { username, password }) .map(response => response.json()) .catch(this.handleError); } public getAllTodos(): Observable<Todo[]> { const options = this.getRequestOptions(); return this.http .get(API_URL + '/todos', options) .map(response => { const todos = response.json(); return todos.map((todo) => new Todo(todo)); }) .catch(this.handleError); } public createTodo(todo: Todo): Observable<Todo> { const options = this.getRequestOptions(); return this.http .post(API_URL + '/todos', todo, options) .map(response => { return new Todo(response.json()); }) .catch(this.handleError); } public getTodoById(todoId: number): Observable<Todo> { const options = this.getRequestOptions(); return this.http .get(API_URL + '/todos/' + todoId, options) .map(response => { return new Todo(response.json()); }) .catch(this.handleError); } public updateTodo(todo: Todo): Observable<Todo> { const options = this.getRequestOptions(); return this.http .put(API_URL + '/todos/' + todo.id, todo, options) .map(response => { return new Todo(response.json()); }) .catch(this.handleError); } public deleteTodoById(todoId: number): Observable<null> { const options = this.getRequestOptions(); return this.http .delete(API_URL + '/todos/' + todoId, options) .map(response => null) .catch(this.handleError); } private handleError(error: Response | any) { console.error('ApiService::handleError', error); return Observable.throw(error); } private getRequestOptions() { const headers = new Headers({ 'Authorization': 'Bearer ' + this.session.accessToken }); return new RequestOptions({ headers }); } }
Сначала мы определяем удобный метод для создания наших опций запроса:
private getRequestOptions() { const headers = new Headers({ 'Authorization': 'Bearer ' + this.session.accessToken }); return new RequestOptions({ headers }); }
Затем мы обновляем все методы, которые взаимодействуют с конечной точкой API, требующей аутентификации:
public getAllTodos(): Observable<Todo[]> { const options = this.getRequestOptions(); return this.http .get(API_URL + '/todos', options) .map(response => { const todos = response.json(); return todos.map((todo) => new Todo(todo)); }) .catch(this.handleError); } public createTodo(todo: Todo): Observable<Todo> { const options = this.getRequestOptions(); return this.http .post(API_URL + '/todos', todo, options) .map(response => { return new Todo(response.json()); }) .catch(this.handleError); } public getTodoById(todoId: number): Observable<Todo> { const options = this.getRequestOptions(); return this.http .get(API_URL + '/todos/' + todoId, options) .map(response => { return new Todo(response.json()); }) .catch(this.handleError); } public updateTodo(todo: Todo): Observable<Todo> { const options = this.getRequestOptions(); return this.http .put(API_URL + '/todos/' + todo.id, todo, options) .map(response => { return new Todo(response.json()); }) .catch(this.handleError); } public deleteTodoById(todoId: number): Observable<null> { const options = this.getRequestOptions(); return this.http .delete(API_URL + '/todos/' + todoId, options) .map(response => null) .catch(this.handleError); }
Мы создаем параметры запроса с помощью нашего вспомогательного помощника и передаем эти параметры в качестве второго аргумента в нашем вызове http.
ВНИМАНИЕ: Будьте очень осторожны!
Всегда убедитесь, что вы отправляете токен только доверенному API. Не просто слепо отправляйте токен с каждым исходящим HTTP-запросом.
Например: если ваше приложение взаимодействует со сторонним API, и вы случайно отправили токен своего пользователя этому стороннему API, третья сторона может использовать токен для входа в систему, чтобы запросить ваш API от имени вашего пользователя. Поэтому будьте очень осторожны и отправляйте токен только доверенным сторонам и только с запросами, которые этого требуют.
Чтобы узнать больше об аспектах безопасности аутентификации на основе токенов, обязательно ознакомьтесь с докладом Филиппа Де Райка о файлах cookie и токенах: парадоксальный выбор .
Если вы перейдете в браузер по http://localhost:4200
, вы сможете войти в систему с использованием демо- имени пользователя и пароля.
Добавление кнопки выхода в наш TodosComponent
Для полноты давайте добавим кнопку выхода из нашего списка задач.
Давайте откроем src/app/todos/todos.component.ts
и добавим метод doSignOut()
:
import { Component, OnInit } from '@angular/core'; import { TodoDataService } from '../todo-data.service'; import { Todo } from '../todo'; import { ActivatedRoute, Router } from '@angular/router'; import { AuthService } from '../auth.service'; @Component({ selector: 'app-todos', templateUrl: './todos.component.html', styleUrls: ['./todos.component.css'] }) export class TodosComponent implements OnInit { todos: Todo[] = []; constructor( private todoDataService: TodoDataService, private route: ActivatedRoute, private auth: AuthService, private router: Router ) { } // ... doSignOut() { this.auth.doSignOut(); this.router.navigate(['/sign-in']); } }
Сначала мы импортируем AuthService
и Router
.
Затем мы определяем метод doSignOut()
который выходит из системы и возвращает пользователя обратно на страницу входа.
Теперь, когда у нас есть логика, давайте добавим кнопку к нашему представлению в src/app/todos/todos.component.html
:
<!-- Todos --> <section class="todoapp"> <app-todo-list-header (add)="onAddTodo($event)" ></app-todo-list-header> <app-todo-list [todos]="todos" (toggleComplete)="onToggleTodoComplete($event)" (remove)="onRemoveTodo($event)" ></app-todo-list> <app-todo-list-footer [todos]="todos" ></app-todo-list-footer> </section> <!-- Sign out button --> <button (click)="doSignOut()">Sign out</button>
Если вы обновите свой браузер и войдите снова, вы должны увидеть:
Нажатие на кнопку выхода вызывает метод doSignOut()
в контроллере компонента, возвращая вас обратно на страницу входа.
Кроме того, если вы выходите из системы и пытаетесь перейти в браузере напрямую по http://localhost:4200/todos
, http://localhost:4200/todos
маршрутизации обнаруживает, что вы не вошли в систему, и отправляет вас на страницу входа.
Как это мило!
Мы многое рассказали в этой серии Angular, поэтому давайте вспомним то, что мы узнали.
Резюме
В первой статье мы узнали, как:
- инициализировать наше приложение Todo с помощью Angular CLI
- создать класс
Todo
для представления отдельных задач - создать сервис
TodoDataService
для создания, обновления и удаления задач - используйте компонент
AppComponent
для отображения пользовательского интерфейса - разверните наше приложение на страницах GitHub.
Во второй статье мы реорганизовали AppComponent
чтобы делегировать большую часть его работы:
-
TodoListComponent
для отображения списка задач -
TodoListItemComponent
для отображения одного todo -
TodoListHeaderComponent
для создания новой задачи -
TodoListFooterComponent
чтобы показать, сколькоTodoListFooterComponent
.
В третьей статье мы узнали, как:
- создать макет REST API
- сохранить URL API в качестве переменной среды
- создать
ApiService
для связи с REST API - обновите
TodoDataService
чтобы использовать новыйApiService
- обновить
AppComponent
для обработки асинхронных вызовов API - создайте
ApiMockService
чтобы избежать реальных HTTP-вызовов при выполнении модульных тестов.
В четвертой статье мы узнали:
- почему приложение может нуждаться в маршрутизации
- что такое роутер JavaScript
- что такое Angular Router, как он работает и что он может сделать для вас
- как настроить Angular router и настроить маршруты для нашего приложения
- как сказать Angular роутер, где разместить компоненты в DOM
- как изящно обрабатывать неизвестные URL
- как использовать распознаватель, чтобы позволить Angular router разрешать данные.
В этой пятой статье мы узнали:
- разница между куки и токенами
- Как создать
AuthService
для реализации логики аутентификации - как создать
SessionService
для хранения данных сеанса - как создать форму для входа с помощью угловой формы
- как создать охрану маршрута, чтобы предотвратить несанкционированный доступ к частям вашего приложения
- как отправить токен пользователя в качестве заголовка авторизации в HTTP-запросе к вашему API
- почему вы никогда не должны отправлять токен своего пользователя третьему лицу.
Не стесняйтесь сообщить нам в комментариях ниже, если вы смогли заставить его работать или если у вас есть какие-либо вопросы.
Весь код из этой статьи доступен по адресу https://github.com/sitepoint-editors/angular-todo-app/tree/part-5 .
Хорошего вам!
Вызов
В своем текущем состоянии данные сеанса теряются, когда браузер обновляет страницу.
Можете ли вы выяснить, что необходимо для сохранения данных сеанса в браузере sessionStorage или localStorage?
Дайте нам знать, что вы придумали в комментариях ниже.
Удачи!!