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

Вернуться на верх