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
Вернуться на верх