Angular и Django: аутентификация с помощью JWT
Интересует тема, как вызывать функции API Angular 6 и HttpClient? В этом учебном пособии будут показаны некоторые методы построения приложения для микро-блогов, использующего Angular 6 и Django Rest Framework (DRF). В процессе мы узнаем следующее:
- Как сделать бэкэнд приложение с помощью Django и API Django Rest Framework
- Создание простого одностраничного приложения Angular 6, которое может запрашивать API
- Аутентификация пользователей с помощью JSON Web Tokens (JWT)
Готовы? Давайте начнем!
Примечание
Если вы новичек в Django, то можете посмотреть как правильно создавать проект Django и проектировать модели. А также ознакомиться с основами Django Rest Framework.
Также необходимы минимальные знания о вирутальном окружении Python и использовании pip для установки библиотек.
Используемые технологии и версии ПО
- Angular 6.1
- RxJS 6.0
- Angular CLI v6.x
- Django 2.1
- Django Rest Framework (DRF)
- Python 3.5 или выше.
- Node 8.x или выше
Django и приложения
Django здесь используется в качестве бекэнда нашего разделенного приложения. Он обслуживает запросы API и отдает HTML для Angular на фронтэнде.
Мы уделим больше внимания DRF и его моделям, сериализаторам, видам (viewsets) в продолжении руководства. А пока что создадим простой проект Django с DRF и DRF JWT и установим все это.
pip install Django pip install djangorestframework djangorestframework-jwt
Здесь мы:
- установили Django
- установили Django Rest Framework
- установили Django Rest Framework JWT — дополнение к DRF, которое предоставляет аутентификацию используя JSON Web Tokens.
Создаем сам проект:
django-admin.py startproject angular_django_example
После создания проекта Django мы получаем базовый файл с настройками проекта. Для активации DRF и его JWT расширения, нужно добавить их в список приложений проекта и настроить.
angular_django_example/settings.py:
INSTALLED_APPS = [ ... 'rest_framework', ] REST_FRAMEWORK = { 'DEFAULT_PERMISSION_CLASSES': ( 'rest_framework.permissions.IsAuthenticatedOrReadOnly', ), 'DEFAULT_AUTHENTICATION_CLASSES': ( 'rest_framework_jwt.authentication.JSONWebTokenAuthentication', 'rest_framework.authentication.SessionAuthentication', 'rest_framework.authentication.BasicAuthentication', ), }
По умолчанию DRF использует Basic и Session аутентификацию. Параметр DEFAULT_AUTHENTICATION_CLASSES позволяет добавить еще один механизм утентификации: JWT.
Как это все работает?
Basic Auth: логин и пароль пользователя передается при каждом вызове API. Это минимальный уровень безопасности и данные пользователя видны в URL.
Session Auth: требует от пользователя авторизоваться используя возможности проекта перед использованием API. Он более безопасен, чем Basic Auth, но он не очень удобен при использовании одностраничного приложения фреймворка Angular.
JSON Web Tokens: промышленный стандарт для генерации токена, который может передаваться в заголовках HTTP при каждом запросе, аутентифицируя пользователя. Этот механизм мы как раз и будем использовать в проекте.
В дополнение к настройкам самого DRF, JWT также имеет свои настройки конфигурации. Мы обновим два параметра в нашем приложении:
JWT_AUTH = { 'JWT_ALLOW_REFRESH': True, 'JWT_EXPIRATION_DELTA': datetime.timedelta(seconds=3600), }
Токен JWT имеет определенное время жизни, после окончания которого становится невалидным. По умолчанию это 5 минут, но мы увеличим его до 1 часа (параметр JWT_EXPIRATION_DELTA
), а параметр JWT_ALLOW_REFRESH
включает возможность обновления времени жизни токена при каждом запросе.
URL
Кроме дополнений в настройках, нам нужно добавить несколько ссылок для нашего API.
angular_django_example/urls.py:
from rest_framework_jwt.views import obtain_jwt_token, refresh_jwt_token urlpatterns = [ ... other patterns here ... path(r'api-token-auth/', obtain_jwt_token), path(r'api-token-refresh/', refresh_jwt_token), ]
Здесь мы указываем ссылки для аутентификации и обновления токена.
Приложение микроблога
После настройки создаем приложение Django и приложение Angular в нем. В терминале выполним команду python manage.py startapp microblog
, которая создаст новое приложение Microblog с основными файлами: views.py, models.py.
Теперь сделаем простое представление и шаблон, которые будут обрабатывать серверную часть одностраничного сайта.
microblog/views.py:
from django.shortcuts import render def index(request, path=''): """ Главная страница. Контейнер для одностраничного плижения """ return render(request, 'index.html')
В соответствие с базовыми примерами Django будем использовать два файла шблонов: base.html, содержащий общий код страницы, и index.html — представляющий содержимое основной страницы.
microblog/templates/base.html:
{% load staticfiles %} <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Angular, Django Rest Framework и JWT токен</title> <base href="/"> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css" integrity="sha384-WskhaSGFgHYWDcbwN70/dfYBj47jz9qbsMId/iRN3ewGhXQFZCSftd1LZCfmhktB" crossorigin="anonymous"> </head> <body> <div class="container"> {% block heading %} <h1>Angular, Django Rest Framework и JWT демо</h1> {% endblock %} {% block content %}{% endblock %} </div> </body> </html>
microblog/templates/index.html:
{% extends "base.html" %} {% load staticfiles %} {% block content %} <p>This is a mini-blog application using a back-end built on Django 2.0 and Django Rest Framework. It illustrates how to create and send JSON Web Token authentication headers with the HTTP requests.</p> <app-root>Loading the app...</app-root> <script type="text/javascript" src="{% static 'front-end/runtime.js' %}"></script> <script type="text/javascript" src="{% static 'front-end/polyfills.js' %}"></script> <script type="text/javascript" src="{% static 'front-end/styles.js' %}"></script> <script type="text/javascript" src="{% static 'front-end/vendor.js' %}"></script> <script type="text/javascript" src="{% static 'front-end/main.js' %}"></script> {% endblock %}
Приложение Angular
Для установки Angular в проекте Django нам нужно лишь расположить исходный код TypeScript внутри нашего приложения Microblog.
cd microblog ng new front-end
Эта команда создает начальное приложение Angular в каталоге microblog/front-end. Нас интересуют следующие объекты:
- app — модули Angular, компонентны и службы
- angular.json — конфигурация Angular CLI
- dist — здесь Angular CLI разместит скомпилированные файлы. Мы поменяем это расположение для совместимости с Django.
Здесь у нас появляется первый конфликт между подходами к структуре приложений Django и Angular. Используемый в Django подход к размещению статических ресурсов не будет искать их в microblog/front-end/dist, где размещены собранные JavaScript ресурсы. А ищет их в каждом приложении в каталоге static. Поэтому поменяем конфигурацию в angular.json.
microblog/front-end/angular.json:
{ ... "projects": { "ng-demo": { ... "architect": { "build": { ... "options": { "outputPath": "../static/front-end", <-- меняем размещение статики ...
Теперь, когда мы будем запускать ng build
ресурсы будут размещаться по правильному пути.
Содержимое нашего приложения Angular
Оно содержит следующее:
- microblog/front-end/src/app/app.component.html — шаблон с формой авторизации
- microblog/front-end/src/app/app.component.ts — основной компонент
- microblog/front-end/src/app/user.service.ts — служба, управляющая запросы аутентификации
Модуль Angular
Начнем разработку приложения с настройки файла модуля:
microblog/front-end/src/app/app.module.ts:
import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { HttpClientModule } from '@angular/common/http'; // add this import { FormsModule } from '@angular/forms'; // add this import { AppComponent } from './app.component'; import { UserService } from './user.service'; // add this @NgModule({ declarations: [AppComponent], imports: [BrowserModule, FormsModule, HttpClientModule], // add this providers: [UserService], // add this bootstrap: [AppComponent] }) export class AppModule { }
Он очень похож на модуль Angular по умолчанию. Мы только добавили встроенные HttpClientModule и FormsModule и наш UserService.
Наше приложение очень простое и содержит только один компонент.
microblog/front-end/src/app/app.component.ts:
import {Component, OnInit} from '@angular/core'; import {UserService} from './user.service'; import {throwError} from 'rxjs'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'] }) export class AppComponent implements OnInit { /** * An object representing the user for the login form */ public user: any; constructor(private _userService: UserService) { } ngOnInit() { this.user = { username: '', password: '' }; } login() { this._userService.login({'username': this.user.username, 'password': this.user.password}); } refreshToken() { this._userService.refreshToken(); } logout() { this._userService.logout(); } }
Шаблон содержит форму авторизации и сообщение, что пользователь уже авторизован.
microblog/front-end/src/app/app.component.html:
<h2>Log In</h2> <div class="row" *ngIf="!_userService.token"> <div class="col-sm-4"> <label>Username:</label><br /> <input type="text" name="login-username" [(ngModel)]="user.username"> <span *ngFor="let error of _userService.errors.username"><br /> {{ error }}</span></div> <div class="col-sm-4"> <label>Password:</label><br /> <input type="password" name="login-password" [(ngModel)]="user.password"> <span *ngFor="let error of _userService.errors.password"><br /> {{ error }}</span> </div> <div class="col-sm-4"> <button (click)="login()" class="btn btn-primary">Log In</button </div> <div class="col-sm-12"> <span *ngFor="let error of _userService.errors.non_field_errors">{{ error }}<br /></span> </div> </div> <div class="row" *ngIf="_userService.token"> <div class="col-sm-12">You are logged in as {{ _userService.username }}.<br /> Token Expires: {{ _userService.token_expires }}<br /> <button (click)="refreshToken()" class="btn btn-primary">Refresh Token</button> <button (click)="logout()" class="btn btn-primary">Log Out</button> </div> </div>
Когда пользователь впервые попадает на страницу, он увидит форму авторизации. После успешного прохождения авторизации, он увидит приветственное сообщение и логин. В целях демонстрации также выведем время истечения жизни токена авторизации JWT.
Теперь перейдем к обработке вызовов API авторизации и управления токенами JWT.
microblog/front-end/src/app/user.service.ts:
import {Injectable} from '@angular/core'; import {HttpClient, HttpHeaders} from '@angular/common/http'; @Injectable() export class UserService { private httpOptions: any; // текущий JWT токен public token: string; // время окончания жизни токена public token_expires: Date; // логин пользователя public username: string; // сообщения об ошибках авторизации public errors: any = []; constructor(private http: HttpClient) { this.httpOptions = { headers: new HttpHeaders({'Content-Type': 'application/json'}) }; } // используем http.post() для получения токена public login(user) { this.http.post('/api-token-auth/', JSON.stringify(user), this.httpOptions).subscribe( data => { this.updateData(data['token']); }, err => { this.errors = err['error']; } ); } // обновление JWT токена public refreshToken() { this.http.post('/api-token-refresh/', JSON.stringify({token: this.token}), this.httpOptions).subscribe( data => { this.updateData(data['token']); }, err => { this.errors = err['error']; } ); } public logout() { this.token = null; this.token_expires = null; this.username = null; } private updateData(token) { this.token = token; this.errors = []; // декодирование токена для получения логина и времени жизни токена const token_parts = this.token.split(/\./); const token_decoded = JSON.parse(window.atob(token_parts[1])); this.token_expires = new Date(token_decoded.exp * 1000); this.username = token_decoded.username; } }
Помните, когда мы устанавливали djangorestframework-jwt, то добавляли два URL в urls.py: "/api-token-auth" и "/api-token-refresh"? Здесь мы используем модуль HttpClient из Angular для отправки POST запроса по этим ссылкам.
Метод login() из UserService отправляет логин и пароль по адресу "/api-token-auth" и получает токен в ответ. Если авторизация не прошла, то мы получим сообщение об ошибке(this.errors), которое будет показано пользователю.
Метод refreshToken() из UserService отправляет токен по адресу "/api-token-refresh" и получает новый токен для пользователя с новым сроком жизни.
Получение времени жизни из JWT токена
Итак, что содержится в самом JWT токене? Мы получим примерно такой токен:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwib3JpZ19pYXQiOjE1MjgwNjg4NDEsInVzZXJfaWQiOjEsImVtYWlsIjoidGVzdEBleGFtcGxlLmNvbSIsImV4cCI6MTUyODA3MjQ0MX0.oxqzt-d2l5Bl473KwObsUetlKS2uOMXn7vqcHcSX5Gg
Он выглядит как случайный набор знаков, но, на самом деле, содержит серию строк base64. Полезные данные находятся между первой и второй точками в формате JSON.
Разобъем строку по точкам и вытащим данные:
const token_parts = this.token.split(/\./); const token_decoded = JSON.parse(window.atob(token_parts[1])); console.log(token_decoded); // output: // { // "orig_iat": 1528071221, // "exp": 1528074821, // "username": "user1", // "email": "user1@example.com", // "user_id": 2 // }
Здесь мы видим логин, email и id из БД, а также время окончания токена в формате Unix timestamp. Преобразовать его в формат даты JavaScipt можно так: this.token_expires = new Date(token_decoded.exp * 1000);
и уже показать пользователю в удобном формате.
Использование токенов в подзапросах API
Перейдем к следующему этапу разработки микроблога. Модели Django и свой API мы обсудим в следующей статье, а пока посмотрим код.
microblog/front-end/app/blog_post.service.ts:
import {Injectable} from '@angular/core'; import {HttpClient, HttpHeaders} from '@angular/common/http'; import {UserService} from './user.service'; @Injectable() export class BlogPostService { constructor(private http: HttpClient, private _userService: UserService) { } // отправка POST запроса для создания нового сообщения в блоге create(post, token) { let httpOptions = { headers: new HttpHeaders({ 'Content-Type': 'application/json', 'Authorization': 'JWT ' + this._userService.token }) }; return this.http.post('/api/posts', JSON.stringify(post), httpOptions); } }
Обратите внимание как мы отправляем JWT токен в заголовке Authorization. Таким образом мы будем "узнавать" пользователя при каждом запросе и позволим Django знать, кто сделал запрос.
Часть 2: Angular и Django: создание приложения микро-блога
Оригинал: https://www.metaltoad.com/blog/angular-api-calls-django-authentication-jwt
Вернуться на верх