Как выполнить JWT-аутентификацию с помощью Angular 6 SPA
Опубликовано: 2022-03-11Сегодня мы рассмотрим, как легко интегрировать аутентификацию веб-токена JSON (JWT) в одностраничное приложение (SPA) Angular 6 (или более поздней версии). Давайте начнем с небольшой предыстории.
Что такое веб-токены JSON и зачем их использовать?
Самый простой и лаконичный ответ здесь — они удобны, компактны и безопасны. Рассмотрим эти претензии подробнее:
- Удобно : использование JWT для аутентификации на серверной части после входа в систему требует установки одного HTTP-заголовка, задачу, которую можно легко автоматизировать с помощью функции или подкласса, как мы увидим позже.
- Компактный : токен представляет собой просто строку в кодировке base64, содержащую несколько полей заголовка и полезную нагрузку, если требуется. Общий JWT обычно меньше 200 байт, даже если он подписан.
- Безопасность : хотя это и не требуется, отличная функция безопасности JWT заключается в том, что токены могут быть подписаны с использованием шифрования парой открытых/закрытых ключей RSA или шифрования HMAC с использованием общего секрета. Это гарантирует происхождение и действительность токена.
Все это сводится к тому, что у вас есть безопасный и эффективный способ аутентификации пользователей, а затем проверка вызовов к вашим конечным точкам API без необходимости анализировать какие-либо структуры данных или внедрять собственное шифрование.
Теория приложений
Итак, немного предыстории, и теперь мы можем погрузиться в то, как это будет работать в реальном приложении. В этом примере я собираюсь предположить, что у нас есть сервер Node.js, на котором размещен наш API, и мы разрабатываем список задач SPA с использованием Angular 6. Давайте также поработаем с этой структурой API:
-
/auth
→POST
(отправьте имя пользователя и пароль для аутентификации и получения обратно JWT) -
/todos
→GET
(вернуть список элементов списка дел для пользователя) -
/todos/{id}
→GET
(возврат определенного элемента списка дел) -
/users
→GET
(возвращает список пользователей)
Вскоре мы рассмотрим создание этого простого приложения, а пока давайте сосредоточимся на взаимодействии в теории. У нас есть простая страница входа, где пользователь может ввести свое имя пользователя и пароль. Когда форма отправляется, она отправляет эту информацию в конечную точку /auth
. Затем сервер Node может аутентифицировать пользователя любым подходящим способом (поиск в базе данных, запрос другой веб-службы и т. д.), но в конечном итоге конечная точка должна вернуть JWT.
JWT для этого примера будет содержать несколько зарезервированных утверждений и несколько частных утверждений . Зарезервированные утверждения — это просто рекомендуемые JWT пары ключ-значение, обычно используемые для аутентификации, тогда как частные утверждения — это пары ключ-значение, применимые только к нашему приложению:
Зарезервированные претензии
-
iss
: Эмитент этого токена. Обычно полное доменное имя сервера, но может быть любым, если клиентское приложение знает, что оно ожидается. -
exp
: Дата и время истечения срока действия этого токена. Это в секундах с полуночи 1 января 1970 года по Гринвичу (время Unix). -
nbf
: Недействительно до временной метки. Используется нечасто, но дает нижнюю границу окна достоверности. Тот же формат, что иexp
.
Частные претензии
-
uid
: ID вошедшего в систему пользователя. -
role
: Роль, назначенная вошедшему в систему пользователю.
Наша информация будет закодирована в base64 и подписана с использованием HMAC с общим ключом todo-app-super-shared-secret
. Ниже приведен пример того, как выглядит JWT:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ0b2RvYXBpIiwibmJmIjoxNDk4MTE3NjQyLCJleHAiOjE0OTgxMjEyNDIsInVpZCI6MSwicm9sZSI6ImFkbWluIn0.ZDz_1vcIlnZz64nSM28yA1s-4c_iw3Z2ZtP-SgcYRPQ
Эта строка — все, что нам нужно, чтобы убедиться, что у нас есть действительный логин, чтобы знать, какой пользователь подключен, и даже знать, какие роли у пользователя.
Большинство библиотек и приложений сохраняют этот JWT в localStorage
или sessionStorage
для облегчения поиска, но это обычная практика. Что вы будете делать с токеном, зависит только от вас, если вы можете предоставить его для будущих вызовов API.
Теперь всякий раз, когда SPA хочет сделать вызов любой из защищенных конечных точек API, ему просто нужно отправить токен в HTTP-заголовке Authorization
.
Authorization: Bearer {JWT Token}
Примечание : еще раз, это просто обычная практика. JWT не предписывает какой-либо конкретный метод отправки себя на сервер. Вы также можете добавить его к URL-адресу или отправить в файле cookie.
Как только сервер получает JWT, он может его декодировать, обеспечить согласованность с помощью общего секрета HMAC и проверить срок действия с помощью полей exp
и nbf
. Он также может использовать поле iss
, чтобы убедиться, что он является исходной стороной, выпустившей этот JWT.
Как только сервер будет удовлетворен достоверностью токена, можно будет использовать информацию, хранящуюся внутри JWT. Например, включенный нами uid
дает нам идентификатор пользователя, делающего запрос. Для этого конкретного примера мы также включили поле role
, которое позволяет нам принимать решения о том, должен ли пользователь иметь доступ к определенной конечной точке или нет. (Доверяете ли вы этой информации или, скорее, хотите выполнить поиск в базе данных, зависит от требуемого уровня безопасности.)
function getTodos(jwtString) { var token = JWTDecode(jwtstring); if( Date.now() < token.nbf*1000) { throw new Error('Token not yet valid'); } if( Date.now() > token.exp*1000) { throw new Error('Token has expired'); } if( token.iss != 'todoapi') { throw new Error('Token not issued here'); } var userID = token.uid; var todos = loadUserTodosFromDB(userID); return JSON.stringify(todos); }
Давайте создадим простое приложение Todo
Чтобы продолжить, вам потребуется установить последнюю версию Node.js (6.x или новее), npm (3.x или новее) и angular-cli. Если вам нужно установить Node.js, в который входит npm, следуйте инструкциям здесь. После этого angular-cli
можно установить с помощью npm
(или yarn
, если вы его установили):
# installation using npm npm install -g @angular/cli # installation using yarn yarn global add @angular/cli
Я не буду вдаваться в детали шаблона Angular 6, который мы будем здесь использовать, но для следующего шага я создал репозиторий Github для хранения небольшого приложения todo, чтобы проиллюстрировать простоту добавления JWT-аутентификации в ваше приложение. Просто клонируйте его, используя следующее:
git clone https://github.com/sschocke/angular-jwt-todo.git cd angular-jwt-todo git checkout pre-jwt
Команда git checkout pre-jwt
переключается на именованный выпуск, в котором JWT не реализован.
Внутри должно быть две папки с именами server
и client
. Сервер — это сервер Node API, на котором будет размещаться наш базовый API. Клиент — это наше приложение Angular 6.
Сервер Node API
Для начала установите зависимости и запустите сервер API.
cd server # installation using npm npm install # or installation using yarn yarn node app.js
Вы должны иметь возможность перейти по этим ссылкам и получить представление данных в формате JSON. На данный момент, пока у нас нет аутентификации, мы жестко закодировали конечную точку /todos
для возврата задач для userID=1
:
- http://localhost:4000: тестовая страница, чтобы узнать, работает ли сервер Node.
- http://localhost:4000/api/users: вернуть список пользователей в системе.
- http://localhost:4000/api/todos: вернуть список задач для
userID=1
Угловое приложение
Чтобы начать работу с клиентским приложением, нам также необходимо установить зависимости и запустить сервер разработки.
cd client # using npm npm install npm start # using yarn yarn yarn start
Примечание . В зависимости от скорости вашей линии загрузка всех зависимостей может занять некоторое время.
Если все идет хорошо, теперь вы должны увидеть что-то вроде этого при переходе на http://localhost:4200:
Добавление аутентификации через JWT
Чтобы добавить поддержку аутентификации JWT, мы воспользуемся некоторыми доступными стандартными библиотеками, которые сделают ее проще. Вы, конечно, можете отказаться от этих удобств и реализовать все самостоятельно, но это не входит в наши задачи.
Во-первых, давайте установим библиотеку на стороне клиента. Он разработан и поддерживается Auth0, библиотекой, позволяющей добавить облачную аутентификацию на веб-сайт. Использование самой библиотеки не требует использования их услуг.
cd client # installation using npm npm install @auth0/angular-jwt # installation using yarn yarn add @auth0/angular-jwt
Мы перейдем к коду через секунду, но пока мы этим занимаемся, давайте также настроим серверную часть. Мы будем использовать библиотеки body-parser
, jsonwebtoken
и express-jwt
, чтобы Node понимал тела JSON POST и JWT.
cd server # installation using npm npm install body-parser jsonwebtoken express-jwt # installation using yarn yarn add body-parser jsonwebtoken express-jwt
Конечная точка API для аутентификации
Во-первых, нам нужен способ аутентификации пользователей перед выдачей им токена. Для нашей простой демонстрации мы просто настроим фиксированную конечную точку аутентификации с жестко заданным именем пользователя и паролем. Это может быть как просто, так и сложно, в зависимости от требований вашего приложения. Важно отправить обратно JWT.
В server/app.js
добавьте запись под другими require
строками следующим образом:
const bodyParser = require('body-parser'); const jwt = require('jsonwebtoken'); const expressJwt = require('express-jwt');
А также следующее:
app.use(bodyParser.json()); app.post('/api/auth', function(req, res) { const body = req.body; const user = USERS.find(user => user.username == body.username); if(!user || body.password != 'todo') return res.sendStatus(401); var token = jwt.sign({userID: user.id}, 'todo-app-super-shared-secret', {expiresIn: '2h'}); res.send({token}); });
В основном это базовый код JavaScript. Мы получаем тело JSON, которое было передано в конечную точку /auth
, находим пользователя, совпадающего с этим именем пользователя, проверяем, что у нас есть пользователь и пароль, и возвращаем ошибку 401 Unauthorized
HTTP, если нет.
Важной частью является генерация токена, и мы разберем ее по трем параметрам. Синтаксис sign
следующий: jwt.sign(payload, secretOrPrivateKey, [options, callback])
, где:
-
payload
— это объектный литерал пар ключ-значение, который вы хотели бы закодировать в своем токене. Затем эта информация может быть декодирована из токена любым, у кого есть ключ дешифрования. В нашем примере мы кодируемuser.id
, чтобы, когда мы снова получим токен на серверной части для аутентификации, мы знали, с каким пользователем мы имеем дело. -
secretOrPrivateKey
— это либо общий секретный ключ шифрования HMAC — это то, что мы использовали в нашем приложении для простоты, либо закрытый ключ шифрования RSA/ECDSA. -
options
представляет множество параметров, которые можно передать кодировщику в виде пар ключ-значение. Обычно мы, по крайней мере, указываемexpiresIn
(становится зарезервированным утверждениемexp
) иissuer
(зарезервированное утверждениеiss
), чтобы токен не был действителен вечно, и сервер мог проверить, действительно ли он изначально выдал токен. -
callback
— это функция, которую нужно вызвать после завершения кодирования, если кто-то хочет обрабатывать кодирование токена асинхронно.
(Вы также можете прочитать более подробную информацию о options
и о том, как использовать криптографию с открытым ключом вместо общего секретного ключа.)
Интеграция JWT с Angular 6
Чтобы заставить Angular 6 работать с нашим JWT, достаточно просто использовать angular-jwt
. Просто добавьте в client/src/app/app.modules.ts
:
import { JwtModule } from '@auth0/angular-jwt'; // ... export function tokenGetter() { return localStorage.getItem('access_token'); } @NgModule({ // ... imports: [ BrowserModule, AppRoutingModule, HttpClientModule, FormsModule, // Add this import here JwtModule.forRoot({ config: { tokenGetter: tokenGetter, whitelistedDomains: ['localhost:4000'], blacklistedRoutes: ['localhost:4000/api/auth'] } }) ], // ... }
Это, в принципе, все, что требуется. Конечно, у нас есть еще немного кода для начальной аутентификации, но библиотека angular-jwt
позаботится об отправке токена вместе с каждым HTTP-запросом.

- Функция
tokenGetter()
делает именно то, что говорит, но то, как она реализована, полностью зависит от вас. Мы решили вернуть токен, который мы сохранили вlocalStorage
. Вы, конечно, можете предоставить любой другой метод, который вы хотите, если он возвращает закодированную строку веб-токена JSON . - Опция
whiteListedDomains
существует, поэтому вы можете ограничить домены, на которые отправляется JWT, чтобы общедоступные API также не получали ваш JWT. - Опция
blackListedRoutes
позволяет указать определенные маршруты, которые не должны получать JWT, даже если они находятся в домене из белого списка. Например, конечной точке аутентификации не нужно его получать, потому что в этом нет смысла: маркер обычно равен нулю, когда он все равно вызывается.
Заставить все работать вместе
На данный момент у нас есть способ сгенерировать JWT для данного пользователя, используя конечную точку /auth
в нашем API, и мы сделали сантехнику на Angular для отправки JWT с каждым HTTP-запросом. Отлично, но вы можете заметить, что для пользователя абсолютно ничего не изменилось. И вы были бы правы. Мы по-прежнему можем переходить на каждую страницу нашего приложения и вызывать любую конечную точку API, даже не отправляя JWT. Нехорошо!
Нам нужно обновить наше клиентское приложение, чтобы заботиться о том, кто вошел в систему, а также обновить наш API, чтобы он требовал JWT. Давайте начнем.
Нам понадобится новый компонент Angular для входа в систему. Для краткости я буду максимально простым. Нам также понадобится служба, которая будет обрабатывать все наши требования к аутентификации, и Angular Guard для защиты маршрутов, которые не должны быть доступны до входа в систему. Мы сделаем следующее в контексте клиентского приложения.
cd client ng g component login --spec=false --inline-style ng g service auth --flat --spec=false ng g guard auth --flat --spec=false
Это должно было сгенерировать четыре новых файла в папке client
:
src/app/login/login.component.html src/app/login/login.component.ts src/app/auth.service.ts src/app/auth.guard.ts
Затем нам нужно предоставить службу аутентификации и защиту для нашего приложения. Обновите client/src/app/app.modules.ts
:
import { AuthService } from './auth.service'; import { AuthGuard } from './auth.guard'; // ... providers: [ TodoService, UserService, AuthService, AuthGuard ],
Затем обновите маршрутизацию в client/src/app/app-routing.modules.ts
, чтобы использовать защиту аутентификации и указать маршрут для компонента входа.
// ... import { LoginComponent } from './login/login.component'; import { AuthGuard } from './auth.guard'; const routes: Routes = [ { path: 'todos', component: TodoListComponent, canActivate: [AuthGuard] }, { path: 'users', component: UserListComponent, canActivate: [AuthGuard] }, { path: 'login', component: LoginComponent}, // ...
Наконец, обновите client/src/app/auth.guard.ts
со следующим содержимым:
import { Injectable } from '@angular/core'; import { Router, CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; @Injectable() export class AuthGuard implements CanActivate { constructor(private router: Router) { } canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot) { if (localStorage.getItem('access_token')) { return true; } this.router.navigate(['login']); return false; } }
Для нашего демонстрационного приложения мы просто проверяем наличие JWT в локальном хранилище. В реальных приложениях вы должны декодировать токен и проверять его действительность, срок действия и т. д. Например, вы можете использовать для этого JwtHelperService.
На этом этапе наше приложение Angular теперь всегда будет перенаправлять вас на страницу входа, поскольку у нас нет возможности войти в систему. Давайте исправим это, начав со службы аутентификации в client/src/app/auth.service.ts
:
import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; @Injectable() export class AuthService { constructor(private http: HttpClient) { } login(username: string, password: string): Observable<boolean> { return this.http.post<{token: string}>('/api/auth', {username: username, password: password}) .pipe( map(result => { localStorage.setItem('access_token', result.token); return true; }) ); } logout() { localStorage.removeItem('access_token'); } public get loggedIn(): boolean { return (localStorage.getItem('access_token') !== null); } }
Наша служба аутентификации имеет только две функции: login
и logout
:
-
login
POST
отправляет предоставленноеusername
иpassword
на наш сервер и устанавливаетaccess_token
вlocalStorage
, если он получает его обратно. Для простоты здесь нет обработки ошибок. -
logout
просто очищаетaccess_token
изlocalStorage
, требуя получения нового токена, прежде чем снова можно будет получить доступ к чему-либо. -
loggedIn
— это логическое свойство, которое мы можем быстро использовать, чтобы определить, вошел ли пользователь в систему или нет.
И, наконец, компонент входа в систему. Они не имеют никакого отношения к фактической работе с JWT, поэтому не стесняйтесь копировать и вставлять в client/src/app/login/login.components.html
:
<h4 *ngIf="error">{{error}}</h4> <form (ngSubmit)="submit()"> <div class="form-group col-3"> <label for="username">Username</label> <input type="text" name="username" class="form-control" [(ngModel)]="username" /> </div> <div class="form-group col-3"> <label for="password">Password</label> <input type="password" name="password" class="form-control" [(ngModel)]="password" /> </div> <div class="form-group col-3"> <button class="btn btn-primary" type="submit">Login</button> </div> </form>
И client/src/app/login/login.components.ts
потребуется:
import { Component, OnInit } from '@angular/core'; import { AuthService } from '../auth.service'; import { Router } from '@angular/router'; import { first } from 'rxjs/operators'; @Component({ selector: 'app-login', templateUrl: './login.component.html' }) export class LoginComponent { public username: string; public password: string; public error: string; constructor(private auth: AuthService, private router: Router) { } public submit() { this.auth.login(this.username, this.password) .pipe(first()) .subscribe( result => this.router.navigate(['todos']), err => this.error = 'Could not authenticate' ); } }
Вуаля, наш пример входа в Angular 6:
На этом этапе мы должны иметь возможность войти в систему (используя jemma
, paul
или sebastian
с паролем todo
) и снова увидеть все экраны. Но наше приложение показывает одни и те же навигационные заголовки и не может выйти из системы независимо от текущего состояния. Давайте исправим это, прежде чем мы перейдем к исправлению нашего API.
В client/src/app/app.component.ts
замените весь файл следующим:
import { Component } from '@angular/core'; import { Router } from '@angular/router'; import { AuthService } from './auth.service'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'] }) export class AppComponent { constructor(private auth: AuthService, private router: Router) { } logout() { this.auth.logout(); this.router.navigate(['login']); } }
А для client/src/app/app.component.html
замените раздел <nav>
следующим:
<nav class="nav nav-pills"> <a class="nav-link" routerLink="todos" routerLinkActive="active" *ngIf="auth.loggedIn">Todo List</a> <a class="nav-link" routerLink="users" routerLinkActive="active" *ngIf="auth.loggedIn">Users</a> <a class="nav-link" routerLink="login" routerLinkActive="active" *ngIf="!auth.loggedIn">Login</a> <a class="nav-link" (click)="logout()" href="#" *ngIf="auth.loggedIn">Logout</a> </nav>
Мы сделали нашу навигацию контекстно-зависимой, поэтому она должна отображать только определенные элементы в зависимости от того, вошел ли пользователь в систему или нет. auth.loggedIn
можно, конечно, использовать везде, где вы можете импортировать службу аутентификации.
Защита API
Вы можете подумать, что это здорово… Все работает чудесно . Но попробуйте войти под всеми тремя разными именами пользователей, и вы кое-что заметите: все они возвращают один и тот же список задач. Если мы посмотрим на наш сервер API, мы увидим, что у каждого пользователя есть свой собственный список элементов, так что же случилось?
Помните, когда мы начинали, мы запрограммировали конечную точку API /todos
так, чтобы она всегда возвращала список задач для userID=1
. Это произошло потому, что у нас не было никакого способа узнать, кто в данный момент является зарегистрированным пользователем.
Теперь мы это делаем, поэтому давайте посмотрим, насколько просто защитить наши конечные точки и использовать информацию, закодированную в JWT, для предоставления требуемой идентификации пользователя. Сначала добавьте эту строку в файл server/app.js
прямо под последним app.use()
:
app.use(expressJwt({secret: 'todo-app-super-shared-secret'}).unless({path: ['/api/auth']}));
Мы используем промежуточное ПО express-jwt
, сообщаем ему, что такое общий секрет, и указываем массив путей, для которых не требуется JWT. Вот и все. Не нужно трогать каждую конечную точку, создавать операторы if
повсюду или что-то еще.
Внутренне промежуточное ПО делает несколько предположений. Например, предполагается, что HTTP-заголовок Authorization
соответствует общему шаблону JWT Bearer {token}
. (В библиотеке есть множество опций для настройки того, как она работает, если это не так. См. Использование express-jwt для получения более подробной информации.)
Наша вторая цель — использовать информацию, закодированную JWT, чтобы узнать, кто звонит. И снова на помощь приходит express-jwt
. В рамках чтения токена и его проверки он устанавливает закодированную полезную нагрузку, которую мы отправили в процессе подписания, в переменную req.user
в Express. Затем мы можем использовать его для немедленного доступа к любой из сохраненных нами переменных. В нашем случае мы устанавливаем userID
равным идентификатору аутентифицированного пользователя, и поэтому мы можем использовать его напрямую как req.user.userID
.
Снова обновите server/app.js
и измените конечную точку /todos
следующим образом:
res.send(getTodos(req.user.userID));
Вот и все. Наш API теперь защищен от несанкционированного доступа, и мы можем безопасно определить, кто является нашим аутентифицированным пользователем в любой конечной точке. Наше клиентское приложение также имеет простой процесс аутентификации, и любые HTTP-сервисы, которые мы создаем и которые вызывают нашу конечную точку API, будут автоматически иметь прикрепленный токен аутентификации.
Если вы клонировали репозиторий Github и просто хотите увидеть конечный результат в действии, вы можете проверить код в его окончательной форме, используя:
git checkout with-jwt
Надеюсь, вы нашли это пошаговое руководство полезным для добавления аутентификации JWT в свои собственные приложения Angular. Спасибо за прочтение!