Статьи

Аутентификация пользователя с помощью стека MEAN

В этой статье мы рассмотрим управление аутентификацией пользователей в стеке MEAN. Мы будем использовать наиболее распространенную архитектуру MEAN, включающую одностраничное приложение Angular с использованием REST API, созданного с использованием Node, Express и MongoDB.

Когда мы думаем об аутентификации пользователей, нам необходимо решить следующие вопросы:

  1. позвольте пользователю зарегистрироваться
  2. сохранить свои данные, но никогда напрямую не хранить свой пароль
  3. пусть возвращающийся пользователь залогинится
  4. поддерживать сеанс зарегистрированного пользователя между посещениями страницы
  5. есть страницы, которые могут видеть только зарегистрированные пользователи
  6. изменить вывод на экран в зависимости от статуса входа (например, кнопка «Войти» или «Мой профиль»).

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

Средство проверки подлинности стека MEAN

Так как же выглядит аутентификация в стеке MEAN?

Все еще поддерживая это на высоком уровне, эти компоненты потока:

  • пользовательские данные хранятся в MongoDB, хэши паролей
  • Функции CRUD встроены в Express API — Создать (зарегистрироваться), Читать (войти, получить профиль), Обновить, Удалить
  • приложение Angular вызывает API и обрабатывает ответы
  • Express API генерирует JSON Web Token (JWT, произносится «Jot») при регистрации или входе в систему и передает его в приложение Angular.
  • приложение Angular хранит JWT, чтобы поддерживать сеанс пользователя
  • Приложение Angular проверяет достоверность JWT при отображении защищенных видов
  • приложение Angular передает JWT обратно в Express при вызове защищенных маршрутов API.

JWT предпочтительнее файлов cookie для поддержания состояния сеанса в браузере. Файлы cookie лучше поддерживают состояние при использовании приложения на стороне сервера.

Пример приложения

Код для этой статьи доступен на GitHub . Для запуска приложения вам необходимо установить Node.js вместе с MongoDB. (Инструкции по установке см. В официальной документации Mongo — Windows, Linux, macOS ).

Угловое приложение

Для простоты примера из этой статьи мы начнем с приложения Angular с четырьмя страницами:

  1. домашняя страница
  2. страница регистрации
  3. страница авторизации
  4. страница профиля

Страницы довольно простые и выглядят так:

Скриншоты приложения

Страница профиля будет доступна только аутентифицированным пользователям. Все файлы для приложения Angular находятся в папке внутри приложения Angular CLI, которая называется /client .

Мы будем использовать Angular CLI для сборки и запуска локального сервера. Если вы не знакомы с Angular CLI, обратитесь к Руководству по Angular 2: создайте приложение CRUD с Angular CLI, чтобы начать работу.

REST API

Мы также начнем со скелета REST API, созданного с использованием Node, Express и MongoDB, с использованием Mongoose для управления схемами. Этот API имеет три маршрута:

  1. /api/register (POST) — для обработки регистрации новых пользователей
  2. /api/login (POST) — для обработки входа в систему возвращающихся пользователей
  3. /api/profile/USERID (GET) — чтобы вернуть данные профиля, если указан USERID .

Код для API хранится в другой папке внутри приложения Express, которая называется api . Это содержит маршруты, контроллеры и модель, и организовано так:

Снимок экрана: структура папок API

В этой начальной точке каждый из контроллеров просто отвечает подтверждением, например так:

 module.exports.register = function(req, res) { console.log("Registering user: " + req.body.email); res.status(200); res.json({ "message" : "User registered: " + req.body.email }); }; 

Хорошо, давайте продолжим с кодом, начиная с базы данных.

Создание схемы данных MongoDB с помощью Mongoose

Существует простая пользовательская схема, определенная в /api/models/users.js . Он определяет потребность в адресе электронной почты, имени, хэше и соли. Хеш и соль будут использоваться вместо сохранения пароля. Адрес email установлен как уникальный, поскольку мы будем использовать его для учетных данных для входа. Вот схема:

 var userSchema = new mongoose.Schema({ email: { type: String, unique: true, required: true }, name: { type: String, required: true }, hash: String, salt: String }); 

Управление паролем без его сохранения

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

Соль — это строка символов, уникальная для каждого пользователя. Хеш создается путем объединения пароля, предоставленного пользователем и соли, а затем с применением одностороннего шифрования. Поскольку хеш не может быть расшифрован, единственный способ аутентифицировать пользователя — это взять пароль, объединить его с солью и снова зашифровать. Если результат этого соответствует хешу, пароль должен быть правильным.

Для настройки и проверки пароля мы можем использовать методы схемы Mongoose. По сути, это функции, которые вы добавляете в схему. Они оба будут использовать crypto модуль Node.js.

Вверху users.js модели users.js требуется крипто, чтобы мы могли его использовать:

 var crypto = require('crypto'); 

Ничего не требует установки, так как криптография поставляется как часть Node. Сам Crypto имеет несколько методов; нас интересуют randomBytes для создания случайной соли и pbkdf2Sync для создания хеша (о Crypto можно pbkdf2Sync больше в документации по API Node.js ).

Установка пароля

Чтобы сохранить ссылку на пароль, мы можем создать новый метод setPassword в схеме userSchema который принимает параметр пароля. Затем метод будет использовать crypto.randomBytes для установки соли и crypto.pbkdf2Sync для установки хеша:

 userSchema.methods.setPassword = function(password){ this.salt = crypto.randomBytes(16).toString('hex'); this.hash = crypto.pbkdf2Sync(password, this.salt, 1000, 64, 'sha512').toString('hex'); }; 

Мы будем использовать этот метод при создании пользователя. Вместо того, чтобы сохранять пароль в пути к password , мы сможем передать его в функцию setPassword чтобы установить пути salt и hash в пользовательском документе.

Проверка пароля

Проверка пароля — аналогичный процесс, но у нас уже есть соль из модели Mongoose. На этот раз мы просто хотим зашифровать соль и пароль и посмотреть, соответствует ли вывод сохраненному хешу.

Добавьте еще один новый метод в users.js модели users.js , который называется validPassword :

 userSchema.methods.validPassword = function(password) { var hash = crypto.pbkdf2Sync(password, this.salt, 1000, 64, 'sha512').toString('hex'); return this.hash === hash; }; 

Создание веб-токена JSON (JWT)

Еще одна вещь, которую должна сделать модель Mongoose, — это сгенерировать JWT , чтобы API мог отправить его в ответ. Метод Mongoose и здесь идеален, поскольку он означает, что мы можем хранить код в одном месте и вызывать его при необходимости. Нам нужно будет позвонить, когда пользователь регистрируется и когда пользователь входит в систему.

Чтобы создать JWT, мы будем использовать модуль с именем jsonwebtoken который необходимо установить в приложении, поэтому запустите его в командной строке:

 npm install jsonwebtoken --save 

Затем users.js это в users.js модели users.js :

 var jwt = require('jsonwebtoken'); 

Этот модуль предоставляет метод sign который мы можем использовать для создания JWT, просто передавая ему данные, которые мы хотим включить в токен, плюс секрет, который будет использовать алгоритм хеширования. Данные должны быть отправлены как объект JavaScript и включать дату истечения срока действия в свойстве exp .

Добавление метода generateJwt в userSchema для возврата JWT выглядит следующим образом:

 userSchema.methods.generateJwt = function() { var expiry = new Date(); expiry.setDate(expiry.getDate() + 7); return jwt.sign({ _id: this._id, email: this.email, name: this.name, exp: parseInt(expiry.getTime() / 1000), }, "MY_SECRET"); // DO NOT KEEP YOUR SECRET IN THE CODE! }; 

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

Это все, что нам нужно сделать с базой данных.

Настройте паспорт для обработки экспресс-аутентификации

Passport — это Node-модуль, который упрощает процесс обработки аутентификации в Express. Он предоставляет общий шлюз для работы со многими различными «стратегиями» аутентификации, такими как вход в систему через Facebook, Twitter или Oauth. Стратегия, которую мы будем использовать, называется «локальной», так как она использует имя пользователя и пароль, хранящиеся локально.

Чтобы использовать Passport, сначала установите его и стратегию, сохранив их в package.json :

 npm install passport --save npm install passport-local --save 

Настроить паспорт

В папке api создайте новую config папки и создайте там файл с именем passport.js . Здесь мы определяем стратегию.

Перед определением стратегии этот файл должен требовать Passport, стратегии, Mongoose и модель User :

 var passport = require('passport'); var LocalStrategy = require('passport-local').Strategy; var mongoose = require('mongoose'); var User = mongoose.model('User'); 

Для локальной стратегии нам просто нужно написать запрос Mongoose по модели User . Этот запрос должен найти пользователя с указанным адресом электронной почты, а затем вызвать метод validPassword чтобы проверить, совпадают ли хэши. Довольно просто

Есть только одно любопытство Паспорта, чтобы иметь дело с. Внутренне локальная стратегия для Passport предполагает две части данных, называемые username и password . Однако в качестве уникального идентификатора мы используем email а не username . Это можно настроить в объекте параметров со свойством usernameField в определении стратегии. После этого дело доходит до запроса Mongoose.

Итак, определение стратегии будет выглядеть так:

 passport.use(new LocalStrategy({ usernameField: 'email' }, function(username, password, done) { User.findOne({ email: username }, function (err, user) { if (err) { return done(err); } // Return if user not found in database if (!user) { return done(null, false, { message: 'User not found' }); } // Return if password is wrong if (!user.validPassword(password)) { return done(null, false, { message: 'Password is wrong' }); } // If credentials are correct, return the user object return done(null, user); }); } )); 

Обратите внимание, как validPassword схемы validPassword вызывается непосредственно в user экземпляре.

Теперь просто нужно добавить паспорт в приложение. Поэтому в app.js нам требуется модуль Passport, конфигурация Passport и инициализация Passport в качестве промежуточного программного обеспечения. Размещение всех этих элементов внутри app.js очень важно, так как они должны вписываться в определенную последовательность.

Модуль Passport должен быть обязательным в верхней части файла с другими общими require :

 var express = require('express'); var path = require('path'); var favicon = require('serve-favicon'); var logger = require('morgan'); var cookieParser = require('cookie-parser'); var bodyParser = require('body-parser'); var passport = require('passport'); 

Конфигурация должна потребоваться после того, как требуется модель, так как конфигурация ссылается на модель.

 require('./api/models/db'); require('./api/config/passport'); 

Наконец, Passport следует инициализировать как промежуточное программное обеспечение Express непосредственно перед добавлением маршрутов API, поскольку эти маршруты используются в первый раз.

 app.use(passport.initialize()); app.use('/api', routesApi); 

Теперь у нас есть схема и паспорт. Затем пришло время использовать их в маршрутах и ​​контроллерах API.

Настройте конечные точки API

С помощью API у нас есть две вещи:

  1. сделать контроллеры функциональными
  2. /api/profile маршрут /api/profile чтобы только аутентифицированные пользователи могли получить к нему доступ.

Кодирование регистра и API контроллеров входа

В примере приложения контроллеры регистрации и входа находятся в /api/controllers/authentication.js . Для работы контроллеров файл должен требовать Passport, Mongoose и пользовательскую модель:

 var passport = require('passport'); var mongoose = require('mongoose'); var User = mongoose.model('User'); 

Контроллер API регистра

Контроллер регистра должен сделать следующее:

  1. взять данные из отправленной формы и создать новый экземпляр модели Mongoose
  2. вызовите метод setPassword который мы создали ранее, чтобы добавить соль и хеш к экземпляру
  3. сохранить экземпляр как запись в базе данных
  4. создать JWT
  5. отправьте JWT внутри ответа JSON.

В коде все это выглядит так:

 module.exports.register = function(req, res) { var user = new User(); user.name = req.body.name; user.email = req.body.email; user.setPassword(req.body.password); user.save(function(err) { var token; token = user.generateJwt(); res.status(200); res.json({ "token" : token }); }); }; 

Это использует методы setPassword и generateJwt которые мы создали в определении схемы Mongoose. Посмотрите, как наличие этого кода в схеме делает этот контроллер действительно простым для чтения и понимания.

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

Контроллер API входа

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

Чтобы Passport использовал свою магию и запустил стратегию, определенную в конфигурации, нам нужно вызвать метод authenticate как показано ниже. Этот метод вызывает обратный вызов с тремя возможными параметрами err , user и info . Если user определен, его можно использовать для генерации JWT, который будет возвращен браузеру:

 module.exports.login = function(req, res) { passport.authenticate('local', function(err, user, info){ var token; // If Passport throws/catches an error if (err) { res.status(404).json(err); return; } // If a user is found if(user){ token = user.generateJwt(); res.status(200); res.json({ "token" : token }); } else { // If user is not found res.status(401).json(info); } })(req, res); }; 

Защита маршрута API

Последнее, что нужно сделать в серверной части, — убедиться, что только аутентифицированные пользователи могут получить доступ к маршруту /api/profile . Способ проверки запроса состоит в том, чтобы убедиться, что JWT, отправленный с ним, является подлинным, используя секрет снова. Вот почему вы должны держать это в секрете, а не в коде.

Настройка аутентификации маршрута

Сначала нам нужно установить часть промежуточного программного обеспечения с именем express-jwt :

 npm install express-jwt --save 

Затем мы должны потребовать это и настроить его в файле, где определены маршруты. В примере приложения это /api/routes/index.js . Конфигурация — это случай, когда она сообщает секрет и, при необходимости, имя свойства, которое нужно создать в объекте req который будет содержать JWT. Мы сможем использовать это свойство внутри контроллера, связанного с маршрутом. Имя свойства по умолчанию — user , но это имя экземпляра нашей модели Mongoose User , поэтому мы установим его в payload чтобы избежать путаницы:

 var jwt = require('express-jwt'); var auth = jwt({ secret: 'MY_SECRET', userProperty: 'payload' }); 

Опять же, не держите секрет в коде!

Применение аутентификации маршрута

Чтобы применить это промежуточное программное обеспечение, просто назовите функцию в середине защищаемого маршрута, например:

 router.get('/profile', auth, ctrlProfile.profileRead); 

Если кто-то попытается получить доступ к этому маршруту сейчас без действительного JWT, промежуточное ПО выдаст ошибку. Чтобы убедиться, что наш API работает хорошо, перехватите эту ошибку и верните ответ 401, добавив следующее в раздел обработчиков ошибок основного файла app.js:

 // error handlers // Catch unauthorised errors app.use(function (err, req, res, next) { if (err.name === 'UnauthorizedError') { res.status(401); res.json({"message" : err.name + ": " + err.message}); } }); 

Использование аутентификации маршрута

В этом примере мы только хотим, чтобы люди могли просматривать свои собственные профили, поэтому мы получаем идентификатор пользователя из JWT и используем его в запросе Mongoose.

Контроллер для этого маршрута находится в /api/controllers/profile.js . Все содержимое этого файла выглядит так:

 var mongoose = require('mongoose'); var User = mongoose.model('User'); module.exports.profileRead = function(req, res) { // If no user ID exists in the JWT return a 401 if (!req.payload._id) { res.status(401).json({ "message" : "UnauthorizedError: private profile" }); } else { // Otherwise continue User .findById(req.payload._id) .exec(function(err, user) { res.status(200).json(user); }); } }; 

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

Вот и все для задней части. База данных настроена, у нас есть конечные точки API для регистрации и входа, которые генерируют и возвращают JWT, а также защищенный маршрут. На переднем конце!

Создать службу угловой аутентификации

Большую часть работы переднего плана можно поместить в службу Angular, создавая методы для управления:

  • сохранение JWT в локальном хранилище
  • чтение JWT из локального хранилища
  • удаление JWT из локального хранилища
  • вызов регистра и регистрации конечных точек API
  • проверка, вошел ли пользователь в данный момент
  • получение сведений о вошедшем в систему пользователе из JWT.

Нам нужно будет создать новый сервис под названием AuthenticationService . С помощью CLI это можно сделать, запустив ng generate service authentication и убедившись, что она указана в поставщиках модулей приложения. В примере приложения это находится в файле /client/src/app/authentication.service.ts .

Локальное хранилище: сохранение, чтение и удаление JWT

Чтобы держать пользователя вошедшим между посещениями, мы используем localStorage в браузере для сохранения JWT. Альтернативой является использование sessionStorage , который будет сохранять токен только во время текущего сеанса браузера.

Во-первых, мы хотим создать несколько интерфейсов для обработки типов данных. Это полезно для проверки типов нашего приложения. Профиль возвращает объект, отформатированный как UserDetails , а конечные точки входа и регистрации ожидают TokenPayload во время запроса и возвращают объект TokenResponse :

 export interface UserDetails { _id: string; email: string; name: string; exp: number; iat: number; } interface TokenResponse { token: string; } export interface TokenPayload { email: string; password: string; name?: string; } 

Этот сервис использует сервис HttpClient от Angular для выполнения HTTP-запросов к нашему серверному приложению (которое мы будем использовать в HttpClient время) и сервис Router для программной навигации. Мы должны внедрить их в наш сервисный конструктор.

Затем мы определяем четыре метода, которые взаимодействуют с токеном JWT. Мы реализуем saveToken для обработки хранения токена в localStorage и в свойстве token , метода getToken для извлечения токена из localStorage или из свойства token и функции logout из logout которая удаляет токен JWT из памяти и перенаправляет на домашнюю страницу.

Важно отметить, что этот код не запускается, если вы используете рендеринг на стороне сервера, потому что такие API, как localStorage и window.atob недоступны, и есть подробные сведения о решениях для рендеринга на стороне сервера в документации Angular. ,

 import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Observable } from 'rxjs/Observable'; import { map } from 'rxjs/operators/map'; import { Router } from '@angular/router'; // Interfaces here @Injectable() export class AuthenticationService { private token: string; constructor(private http: HttpClient, private router: Router) {} private saveToken(token: string): void { localStorage.setItem('mean-token', token); this.token = token; } private getToken(): string { if (!this.token) { this.token = localStorage.getItem('mean-token'); } return this.token; } public logout(): void { this.token = ''; window.localStorage.removeItem('mean-token'); this.router.navigateByUrl('/'); } } 

Теперь давайте добавим метод для проверки этого токена — и действительность токена — чтобы выяснить, вошел ли посетитель в систему.

Получение данных из JWT

Когда мы устанавливаем данные для JWT (в методе generateJwt Mongoose), мы включаем дату истечения в свойство exp . Но если вы посмотрите на JWT, это будет случайная строка, как в следующем примере:

 eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJfaWQiOiI1NWQ0MjNjMTUxMzcxMmNkMzE3YTRkYTciLCJlbWFpbCI6InNpbW9uQGZ1bGxzdGFja3RyYWluaW5nLmNvbSIsIm5hbWUiOiJTaW1vbiBIb2xtZXMiLCJleHAiOjE0NDA1NzA5NDUsImlhdCI6MTQzOTk2NjE0NX0.jS50GlmolxLoKrA_24LDKaW3vNaY94Y9EqYAFvsTiLg 

Итак, как вы читаете JWT?

JWT на самом деле состоит из трех отдельных строк, разделенных точкой . , Эти три части:

  1. Заголовок — закодированный объект JSON, содержащий тип и используемый алгоритм хеширования.
  2. Полезная нагрузка — закодированный объект JSON, содержащий данные, реальное тело токена
  3. Подпись — зашифрованный хэш заголовка и полезной нагрузки, использующий «секрет», установленный на сервере.

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

Есть функция atob() которая является родной для современных браузеров и которая будет декодировать строку Base64 следующим образом.

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

В конце этого, функция getUserDetails должна возвращать объект типа UserDetails или null , в зависимости от того, найден действительный токен или нет. В совокупности это выглядит так:

 public getUserDetails(): UserDetails { const token = this.getToken(); let payload; if (token) { payload = token.split('.')[1]; payload = window.atob(payload); return JSON.parse(payload); } else { return null; } } 

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

Проверьте, зарегистрирован ли пользователь

Добавьте новый метод isLoggedIn в сервис. Он использует метод getUserDetails для получения сведений о токене из токена JWT и проверяет, что срок действия еще не прошел:

 public isLoggedIn(): boolean { const user = this.getUserDetails(); if (user) { return user.exp > Date.now() / 1000; } else { return false; } } 

Если токен существует, метод вернется, если пользователь вошел в систему как логическое значение. Теперь мы можем построить наши HTTP-запросы для загрузки данных, используя токен для авторизации.

Структурирование вызовов API

Чтобы упростить выполнение вызовов API, добавьте метод request к AuthenticationService , который способен создавать и возвращать надлежащий HTTP-запрос, наблюдаемый в зависимости от конкретного типа запроса. Это частный метод, поскольку он используется только этим сервисом и существует только для уменьшения дублирования кода. Это будет использовать сервис Angular HttpClient ; не забудьте вставить это в AuthenticationService если его там еще нет:

 private request(method: 'post'|'get', type: 'login'|'register'|'profile', user?: TokenPayload): Observable<any> { let base; if (method === 'post') { base = this.http.post(`/api/${type}`, user); } else { base = this.http.get(`/api/${type}`, { headers: { Authorization: `Bearer ${this.getToken()}` }}); } const request = base.pipe( map((data: TokenResponse) => { if (data.token) { this.saveToken(data.token); } return data; }) ); return request; } 

Для этого требуется оператор map от RxJS, чтобы перехватить и сохранить токен в сервисе, если он возвращен при входе в систему через API или при регистрации. Теперь мы можем реализовать публичные методы для вызова API.

Вызов конечных точек API регистрации и входа

Всего три способа добавить. Нам понадобится интерфейс между приложением Angular и API для вызова конечных точек входа в систему и регистрации и сохранения возвращенного токена или конечной точки профиля для получения сведений о пользователе:

 public register(user: TokenPayload): Observable<any> { return this.request('post', 'register', user); } public login(user: TokenPayload): Observable<any> { return this.request('post', 'login', user); } public profile(): Observable<any> { return this.request('get', 'profile'); } 

Каждый метод возвращает наблюдаемое, которое будет обрабатывать HTTP-запрос для одного из вызовов API, которые нам нужно сделать. Это завершает обслуживание; Теперь связать все вместе в приложении Angular.

Применить аутентификацию к угловому приложению

Мы можем использовать AuthenticationService внутри приложения Angular несколькими способами, чтобы получить опыт, к которому мы стремимся:

  1. подключить реестр и формы входа
  2. обновить навигацию, чтобы отразить статус пользователя
  3. разрешить только зарегистрированным пользователям доступ к /profile маршрута
  4. вызовите защищенный /api/profile API-маршрут.

Подключите регистр и контроллеры входа

Мы начнем с просмотра формы регистрации и входа.

Страница регистрации

HTML- NgModel для формы регистрации уже существует, и к NgModel прикреплены директивы NgModel , все они привязаны к свойствам, установленным в свойстве контроллера credentials . Форма также имеет привязку события (submit) для обработки (submit) . В примере приложения оно находится в /client/src/app/register/register.component.html и выглядит так:

 <form (submit)="register()"> <div class="form-group"> <label for="name">Full name</label> <input type="text" class="form-control" name="name" placeholder="Enter your name" [(ngModel)]="credentials.name"> </div> <div class="form-group"> <label for="email">Email address</label> <input type="email" class="form-control" name="email" placeholder="Enter email" [(ngModel)]="credentials.email"> </div> <div class="form-group"> <label for="password">Password</label> <input type="password" class="form-control" name="password" placeholder="Password" [(ngModel)]="credentials.password"> </div> <button type="submit" class="btn btn-default">Register!</button> </form> 

Первая задача в контроллере — убедиться, что наш AuthenticationService и Router внедрены и доступны через конструктор. Затем, внутри обработчика register для отправки формы, вызовите auth.register , передав ему учетные данные из формы.

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

В примере приложения контроллер находится в /client/src/app/register/register.component.ts и выглядит следующим образом:

 import { Component } from '@angular/core'; import { AuthenticationService, TokenPayload } from '../authentication.service'; import { Router } from '@angular/router'; @Component({ templateUrl: './register.component.html' }) export class RegisterComponent { credentials: TokenPayload = { email: '', name: '', password: '' }; constructor(private auth: AuthenticationService, private router: Router) {} register() { this.auth.register(this.credentials).subscribe(() => { this.router.navigateByUrl('/profile'); }, (err) => { console.error(err); }); } } 

Страница входа

Страница входа очень похожа на страницу регистрации, но в этой форме мы не просим имя, просто адрес электронной почты и пароль. В примере приложения оно находится в /client/src/app/login/login.component.html и выглядит так:

 <form (submit)="login()"> <div class="form-group"> <label for="email">Email address</label> <input type="email" class="form-control" name="email" placeholder="Enter email" [(ngModel)]="credentials.email"> </div> <div class="form-group"> <label for="password">Password</label> <input type="password" class="form-control" name="password" placeholder="Password" [(ngModel)]="credentials.password"> </div> <button type="submit" class="btn btn-default">Sign in!</button> </form> 

Еще раз, у нас есть обработчик отправки формы и атрибуты NgModel для каждого из входных данных. В контроллере нам нужна та же функциональность, что и в контроллере регистра, но на этот раз он называется методом login в систему AuthenticationService .

В примере приложения контроллер находится в /client/src/app/login/login.controller.ts и выглядит следующим образом:

 import { Component } from '@angular/core'; import { AuthenticationService, TokenPayload } from '../authentication.service'; import { Router } from '@angular/router'; @Component({ templateUrl: './login.component.html' }) export class LoginComponent { credentials: TokenPayload = { email: '', password: '' }; constructor(private auth: AuthenticationService, private router: Router) {} login() { this.auth.login(this.credentials).subscribe(() => { this.router.navigateByUrl('/profile'); }, (err) => { console.error(err); }); } } 

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

Изменить содержимое в зависимости от статуса пользователя

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

Сначала мы рассмотрим контроллер компонентов приложения. Мы можем внедрить AuthenticationService в компонент и вызвать его непосредственно в нашем шаблоне. В примере приложения файл находится в /client/src/app/app.component.ts и выглядит следующим образом:

 import { Component } from '@angular/core'; import { AuthenticationService } from './authentication.service'; @Component({ selector: 'app-root', templateUrl: './app.component.html' }) export class AppComponent { constructor(public auth: AuthenticationService) {} } 

Это довольно просто, верно? Теперь в связанном шаблоне мы можем использовать auth.isLoggedIn() чтобы определить, отображать ли ссылку для входа или ссылку на профиль. Чтобы добавить имя пользователя в ссылку на профиль, мы можем получить доступ к свойству name в auth.getUserDetails()?.name . Помните, что это получение данных от JWT. ?. Оператор — это особый способ доступа к свойству объекта, который может быть неопределенным, без выдачи ошибки.

В примере приложения файл находится в /client/src/app/app.component.html и обновленная часть выглядит следующим образом:

 <ul class="nav navbar-nav navbar-right"> <li *ngIf="!auth.isLoggedIn()"><a routerLink="/login">Sign in</a></li> <li *ngIf="auth.isLoggedIn()"><a routerLink="/profile">{{ auth.getUserDetails()?.name }}</a></li> <li *ngIf="auth.isLoggedIn()"><a (click)="auth.logout()">Logout</a></li> </ul> 

Защитить маршрут только для зарегистрированных пользователей

На этом шаге мы увидим, как сделать маршрут доступным только зарегистрированным пользователям, защищая путь /profile .

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

Для этого нам нужно создать службу защиты маршрута, ng generate service auth-guard . Он должен реализовывать интерфейс CanActivate и связанный метод canActivate . Этот метод возвращает логическое значение из метода AuthenticationService.isLoggedIn (в основном проверяет, найден ли токен и остается ли он действительным), и если пользователь недействителен, также перенаправляет их на домашнюю страницу:

 import { Injectable } from '@angular/core'; import { Router, CanActivate } from '@angular/router'; import { AuthenticationService } from './authentication.service'; @Injectable() export class AuthGuardService implements CanActivate { constructor(private auth: AuthenticationService, private router: Router) {} canActivate() { if (!this.auth.isLoggedIn()) { this.router.navigateByUrl('/'); return false; } return true; } } 

Чтобы включить эту защиту, мы должны объявить ее в конфигурации маршрута. Есть свойство canActivate , которое принимает массив служб, которые должны быть вызваны перед активацией маршрута. Убедитесь, что вы также объявляете эти сервисы в NgModule providers App NgModule . Маршруты определены в модуле приложения , который содержит маршруты, как вы видите здесь:

 const routes: Routes = [ { path: '', component: HomeComponent }, { path: 'login', component: LoginComponent }, { path: 'register', component: RegisterComponent }, { path: 'profile', component: ProfileComponent, canActivate: [AuthGuardService] } ]; 

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

Вызовите защищенный маршрут API

Маршрут /api/profile был настроен для проверки наличия JWT в запросе. В противном случае он вернет 401 несанкционированную ошибку.

Чтобы передать токен в API, его необходимо отправить в виде заголовка запроса, называемого Authorization . Следующий фрагмент показывает основную функцию службы данных и формат, необходимый для отправки токена. AuthenticationService уже обрабатывает это, но вы можете найти это в /client/src/app/authentication.service.ts .

 base = this.http.get(`/api/${type}`, { headers: { Authorization: `Bearer ${this.getToken()}` }}); 

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

Чтобы использовать это на странице профиля, нам просто нужно обновить контроллер в /client/src/app/profile/profile.component.ts в примере приложения. Это заполняет свойство details когда API возвращает некоторые данные, которые должны соответствовать интерфейсу UserDetails .

 import { Component } from '@angular/core'; import { AuthenticationService, UserDetails } from '../authentication.service'; @Component({ templateUrl: './profile.component.html' }) export class ProfileComponent { details: UserDetails; constructor(private auth: AuthenticationService) {} ngOnInit() { this.auth.profile().subscribe(user => { this.details = user; }, (err) => { console.error(err); }); } } 

Затем, конечно, это просто случай обновления привязок в представлении ( /client/src/app/profile/profile.component.html ). Опять ?. является оператором безопасности для свойств привязки, которые не существуют при первом рендеринге (поскольку данные должны загружаться первыми).

 <div class="form-horizontal"> <div class="form-group"> <label class="col-sm-3 control-label">Full name</label> <p class="form-control-static">{{ details?.name }}</p> </div> <div class="form-group"> <label class="col-sm-3 control-label">Email</label> <p class="form-control-static">{{ details?.email }}</p> </div> </div> 

И вот последняя страница профиля, когда вы вошли в систему:

Скриншот страницы профиля

Вот как управлять аутентификацией в стеке MEAN, от обеспечения безопасности маршрутов API и управления пользовательскими данными до работы с JWT и защиты маршрутов. Если вы внедрили такую ​​систему аутентификации в одном из ваших собственных приложений и у вас есть какие-либо советы, рекомендации или рекомендации, обязательно поделитесь ими в комментариях ниже!