Аутентификация с помощью сессий в Django SPA
В этой статье мы рассмотрим, как аутентифицировать одностраничные приложения (SPA) с помощью сеансовой аутентификации. Для бэкенда мы будем использовать Django, а для фронтенда - React, библиотеку JavaScript, предназначенную для построения пользовательских интерфейсов.
Не стесняйтесь заменить React на другой инструмент, например Angular, Vue или Svelte.
Сессия против аутентификации на основе токенов
Что это такое?
При сеансовой аутентификации генерируется сеанс, а идентификатор хранится в cookie.
После входа в систему сервер проверяет учетные данные. В случае положительного ответа он генерирует сессию, сохраняет ее, а затем отправляет идентификатор сессии обратно браузеру. Браузер сохраняет идентификатор сессии в виде cookie, который отправляется при каждом запросе к серверу.
Авторизация на основе сеанса является государственной. Каждый раз, когда клиент обращается к серверу, сервер должен определить местонахождение сессии в памяти, чтобы привязать идентификатор сессии к соответствующему пользователю.
С другой стороны, аутентификация на основе токенов является относительно новой по сравнению с аутентификацией на основе сеансов. Она получила распространение с появлением SPA и RESTful API.
После входа в систему сервер проверяет учетные данные и, если они верны, создает и отправляет обратно браузеру подписанный токен. В большинстве случаев токен хранится в localStorage. Затем клиент добавляет токен в заголовок при запросе к серверу. Предполагая, что запрос поступил от авторизованного источника, сервер декодирует токен и проверяет его валидность.
Токен - это строка, в которой закодирована информация о пользователе.
Например:
// token header
{
"alg": "HS256",
"typ": "JWT"
}
// token payload
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022
}
Токен может быть проверен и ему можно доверять, поскольку он подписан цифровой подписью с использованием секретного ключа или пары открытый/закрытый ключ. Наиболее распространенным типом токена является JSON Web Token (JWT).
Поскольку токен содержит всю информацию, необходимую серверу для проверки личности пользователя, аутентификация на основе токенов является статической.
Подробнее о сессиях и токенах читайте в статье Session Authentication vs Token Authentication от Stack Exchange.
Уязвимые места в системе безопасности
Как уже говорилось, при сеансовой аутентификации состояние клиента хранится в cookie. В то время как JWT могут храниться в localStorage или в cookie, большинство реализаций аутентификации на основе токенов хранят JWT в localStorage. Оба этих метода сопряжены с потенциальными проблемами безопасности:
Storage Method | Security Vulnerability |
---|---|
Cookie | Cross Site Request Forgery (CSRF) |
localStorage | Cross-Site Scripting (XSS) |
CSRF - это атака на веб-приложение, в ходе которой злоумышленник пытается обманом заставить аутентифицированного пользователя выполнить вредоносное действие. Большинство CSRF-атак направлено на веб-приложения, использующие аутентификацию на основе cookie-файлов, поскольку веб-браузеры хранят все cookie-файлы, связанные с конкретным доменом каждого запроса. Поэтому при выполнении вредоносного запроса злоумышленник может легко воспользоваться сохраненными cookies.
Чтобы узнать больше о CSRF и способах его предотвращения во Flask, ознакомьтесь со статьей Защита от CSRF во Flask.
XSS-атаки - это тип инъекций, при котором вредоносные скрипты внедряются на стороне клиента, как правило, для обхода одноименной политики браузера. Веб-приложения, хранящие токены в localStorage, открыты для XSS-атак. Откройте браузер и перейдите на любой сайт. Откройте консоль в инструментах разработчика и введите JSON.stringify(localStorage)
. Нажмите клавишу Enter. Это должно вывести элементы localStorage в сериализованном виде JSON. Вот так просто скрипт может получить доступ к localStorage.
Подробнее о том, где хранить JWT, читайте в статье "Где хранить JWT - Cookies vs. HTML5 Web Storage".
Настройка аутентификации на основе сеанса
В данном руководстве рассматриваются следующие подходы к объединению Django с фронтенд-библиотекой или фреймворком:
- Разместить фреймворк через шаблоны Django
- Поддерживать фреймворк отдельно от Django на одном домене
- Создать фреймворк отдельно от Django с помощью Django REST Framework на том же домене
- Обеспечить работу фреймворка отдельно от Django на другом домене
Опять же, не стесняйтесь заменить React на фронтенд по вашему выбору - например, Angular, Vue или Svelte.
Фронтенд, обслуживаемый из Django
При таком подходе мы будем обслуживать наше React-приложение непосредственно из Django. Этот подход наиболее прост в настройке.
Backend
Начнем с создания нового каталога для нашего проекта. Внутри каталога мы создадим и активируем новое виртуальное окружение, установим Django и создадим новый проект Django:
$ mkdir django_react_templates && cd django_react_templates
$ python3.11 -m venv env
$ source env/bin/activate
(env)$ pip install django==4.2.3
(env)$ django-admin startproject djangocookieauth .
После этого создайте новое приложение с именем api
:
(env)$ python manage.py startapp api
Зарегистрируйте приложение в djangocookieauth/settings.py под INSTALLED_APPS
:
# djangocookieauth/settings.py
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'api.apps.ApiConfig', # new
]
Наше приложение будет иметь следующие конечные точки API:
/api/login/
позволяет пользователю войти в систему, указав свое имя пользователя и пароль/api/logout/
выводит пользователя из системы/api/session/
проверяет наличие сессии/api/whoami/
получает пользовательские данные для аутентифицированного пользователя
Для представления возьмите полный код здесь и добавьте его в файл api/views.py.
Добавьте файл urls.py в раздел "api" и определите следующие URL-адреса:
# api/urls.py
from django.urls import path
from . import views
urlpatterns = [
path('login/', views.login_view, name='api-login'),
path('logout/', views.logout_view, name='api-logout'),
path('session/', views.session_view, name='api-session'),
path('whoami/', views.whoami_view, name='api-whoami'),
]
Теперь зарегистрируем URL-адреса наших приложений в базовом проекте:
# djangocookieauth/urls.py
from django.contrib import admin
from django.shortcuts import render
from django.urls import path, include # new import
urlpatterns = [
path('admin/', admin.site.urls),
path('api/', include('api.urls')), # new
]
Код для нашего бэкенда теперь более или менее готов. Выполним команду migrate и создадим суперпользователя для тестирования в будущем:
(env)$ python manage.py migrate
(env)$ python manage.py createsuperuser
Наконец, обновите следующие настройки безопасности в файле djangocookieauth/settings.py:
CSRF_COOKIE_SAMESITE = 'Strict'
SESSION_COOKIE_SAMESITE = 'Strict'
CSRF_COOKIE_HTTPONLY = False # False since we will grab it via universal-cookies
SESSION_COOKIE_HTTPONLY = True
# PROD ONLY
# CSRF_COOKIE_SECURE = True
# SESSION_COOKIE_SECURE = True
Примечания:
- Установка
CSRF_COOKIE_SAMESITE
иSESSION_COOKIE_SAMESITE
в значениеTrue
предотвращает отправку cookies и CSRF-токенов из любых внешних запросов. - Установка
CSRF_COOKIE_HTTPONLY
иSESSION_COOKIE_HTTPONLY
в значениеTrue
блокирует доступ клиентского JavaScript к CSRF и сессионным cookies. Мы устанавливаемCSRF_COOKIE_HTTPONLY
в значениеFalse
, так как будем обращаться к cookie с помощью JavaScript.
Если вы работаете на производстве, то вам следует обслуживать ваш сайт по HTTPS и включить опции
CSRF_COOKIE_SECURE
иSESSION_COOKIE_SECURE
, которые позволят отправлять cookies только по HTTPS.
Frontend
Перед началом работы над фронтендом убедитесь, что у вас установлены Node.js и npm (или Yarn).
Мы будем использовать Vite для создания нового проекта React:
$ npm create vite@4.4 frontend
Выберите React в качестве фреймворка с JavaScript в качестве варианта:
✔ Select a framework: › React
✔ Select a variant: › JavaScript
Затем установите зависимости и запустите сервер разработки:
$ cd frontend
$ npm install
$ npm run dev
Это запустит наше приложение на порт 5173. Посетите http://localhost:5173/, чтобы убедиться в его работоспособности.
Для упрощения работы удалите следующие CSS-файлы:
- frontend/src/App.css
- frontend/src/index.css
Далее добавим Bootstrap в frontend/index.html:
<!-- frontend/index.html -->
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React</title>
<!-- new -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.1/dist/css/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous">
<!-- end of new -->
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
Далее мы будем использовать universal-cookie для загрузки cookies в приложение React.
Установите его из папки "frontend":
$ npm install universal-cookie@4.0.4
Возьмите полный код здесь для компонента App
и добавьте его в файл frontend/src/App.jsx.
Это простое фронтенд-приложение с формой, которая обрабатывается с помощью React state. При загрузке страницы вызывается compontentDidMount()
, который получает сессию и устанавливает isAuthenticated
в значение true
или false
.
Мы получили CSRF-токен с помощью universal-cookie
и передали его в качестве заголовка в запросах в виде X-CSRFToken
:
import Cookies from "universal-cookie";
const cookies = new Cookies();
login = (event) => {
event.preventDefault();
fetch("/api/login/", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-CSRFToken": cookies.get("csrftoken"),
},
credentials: "same-origin",
body: JSON.stringify({username: this.state.username, password: this.state.password}),
})
.then(this.isResponseOk)
.then((data) => {
console.log(data);
this.setState({isAuthenticated: true, username: "", password: "", error: ""});
})
.catch((err) => {
console.log(err);
this.setState({error: "Wrong username or password."});
});
}
Обратите внимание, что при каждом запросе мы использовали credentials: same-origin
. Это необходимо, поскольку мы хотим, чтобы браузер передавал cookies при каждом HTTP-запросе, если URL имеет то же происхождение, что и вызывающий скрипт.
Обновление frontend/src/main.jsx:
// frontend/src/main.jsx
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.jsx'
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<App />
</React.StrictMode>,
)
Обслуживание React
Поскольку мы будем обслуживать статические файлы с /static/
URL, добавьте конфигурацию public base в frontend/vite.config.js:
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
base: '/static/' // new
})
Затем создайте фронтенд-приложение:
$ npm run build
Эта команда создаст папку "dist", которую наш бэкенд будет использовать для обслуживания нашего React-приложения.
Далее мы должны сообщить Django, где находится наше приложение React:
# djangocookieauth/settings.py
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [BASE_DIR.joinpath('frontend')], # new
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
...
STATIC_URL = '/static/'
STATICFILES_DIRS = (
BASE_DIR.joinpath('frontend', 'dist'), # new
)
Если вы используете старую версию Django, убедитесь, что импортировали
os
и используете os.path.join вместо joinpath.
Давайте создадим индексное представление для нашего приложения:
# djangocookieauth/urls.py
from django.contrib import admin
from django.shortcuts import render
from django.urls import path, include
# new
def index_view(request):
return render(request, 'dist/index.html')
urlpatterns = [
path('admin/', admin.site.urls),
path('api/', include('api.urls')),
path('', index_view, name='index'), # new
]
Поскольку Django в конечном итоге обслуживает фронтенд, CSRF-куки будут установлены автоматически.
Из корня проекта запустите сервер Django с помощью команды runserver
следующим образом:
(env)$ python manage.py runserver
Откройте браузер и перейдите по адресу http://localhost:8000/. Теперь ваше React-приложение обслуживается через шаблоны Django.
При загрузке устанавливается CSRF-куки, который используется в последующих XHR-запросах. Если пользователь вводит правильное имя пользователя и пароль, то происходит его аутентификация и сохранение куки sessionid
в браузере.
Вы можете протестировать его с суперпользователем, созданным ранее.
Полный код этого подхода можно получить на GitHub: django_react_templates.
Фронтенд обслуживается отдельно (один и тот же домен)
При таком подходе мы будем создавать фронтенд и обслуживать его отдельно от приложения Django на том же домене. Мы будем использовать Docker и Nginx для локального обслуживания обоих приложений на одном домене.
Основное отличие шаблонного подхода от данного заключается в том, что нам придется вручную получать CSRF-токен при загрузке.
Начнем с создания каталога проекта:
$ mkdir django_react_same_origin && cd django_react_same_origin
Backend
Сначала создайте новый каталог "backend" для проекта Django:
$ mkdir backend && cd backend
Далее создаем и активируем новую виртуальную среду, устанавливаем Django и создаем новый проект Django:
$ python3.11 -m venv env
$ source env/bin/activate
(env)$ pip install django==4.2.3
(env)$ django-admin startproject djangocookieauth .
После этого создайте новое приложение с именем api
:
(env)$ python manage.py startapp api
Зарегистрируйте приложение в djangocookieauth/settings.py под INSTALLED_APPS
:
# backend/djangocookieauth/settings.py
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'api.apps.ApiConfig', # new
]
Наше приложение будет иметь следующие конечные точки API:
/api/get_csrf/
сгенерирует CSRF-токен и вернет его в виде JSON- .
/api/login/
позволит пользователю войти в систему, указав имя пользователя и пароль/api/logout/
выводит пользователя из системы/api/session/
проверяет наличие сессии/api/whoami/
получает пользовательские данные для аутентифицированного пользователя
Для представления возьмите полный код здесь и добавьте его в файл backend/api/views.py.
Добавьте файл urls.py в папку "backend/api" и определите следующие URL-адреса, специфичные для конкретного приложения:
# backend/api/urls.py
from django.urls import path
from . import views
urlpatterns = [
path('csrf/', views.get_csrf, name='api-csrf'),
path('login/', views.login_view, name='api-login'),
path('logout/', views.logout_view, name='api-logout'),
path('session/', views.session_view, name='api-session'),
path('whoami/', views.whoami_view, name='api-whoami'),
]
Теперь зарегистрируем URL-адреса наших приложений в базовом проекте:
# backend/djangocookieauth/urls.py
from django.contrib import admin
from django.shortcuts import render
from django.urls import path, include # new import
urlpatterns = [
path('admin/', admin.site.urls),
path('api/', include('api.urls')), # new
]
Изменим некоторые настройки безопасности в файле backend/djangocookieauth/settings.py:
CSRF_COOKIE_SAMESITE = 'Strict'
SESSION_COOKIE_SAMESITE = 'Strict'
CSRF_COOKIE_HTTPONLY = True
SESSION_COOKIE_HTTPONLY = True
CSRF_TRUSTED_ORIGINS = ['http://localhost:81']
# PROD ONLY
# CSRF_COOKIE_SECURE = True
# SESSION_COOKIE_SECURE = True
Примечания:
- Включение
CSRF_COOKIE_SAMESITE
иSESSION_COOKIE_SAMESITE
предотвращает отправку cookies и CSRF-токенов из любых внешних запросов. - Включение
CSRF_COOKIE_HTTPONLY
иSESSION_COOKIE_HTTPONLY
блокирует доступ JavaScript на стороне клиента к CSRF и сессионным cookies.
Если вы работаете на производстве, то вам следует обслуживать ваш сайт по HTTPS и включить опции
CSRF_COOKIE_SECURE
иSESSION_COOKIE_SECURE
, которые позволят отправлять cookies только по HTTPS.
Создайте файл backend/requirements.txt:
Django==4.2.3
Frontend
Перед началом работы над фронтендом убедитесь, что у вас установлены Node.js и npm (или Yarn).
Мы будем использовать Vite для создания нового проекта React:
$ npm create vite@4.4 frontend
Выберите React в качестве фреймворка с JavaScript в качестве варианта:
✔ Select a framework: › React
✔ Select a variant: › JavaScript
Затем установите зависимости и запустите сервер разработки:
$ cd frontend
$ npm install
$ npm run dev
Это запустит наше приложение на порт 5173. Посетите http://localhost:5173/, чтобы убедиться в его работоспособности.
Для упрощения работы удалите следующие CSS-файлы:
- frontend/src/App.css
- frontend/src/index.css
Далее добавим Bootstrap в frontend/index.html:
<!-- frontend/index.html -->
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React</title>
<!-- new -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.1/dist/css/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous">
<!-- end of new -->
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
Возьмите полный код здесь для компонента App
и добавьте его в файл frontend/src/App.jsx.
Это простое фронтенд-приложение с формой, которая обрабатывается с помощью React state. При загрузке страницы вызывается compontentDidMount()
, который выполняет два вызова API:
- Сначала проверяется, аутентифицирован ли пользователь, вызовом
/api/session/
и устанавливаетсяisAuthenticated
в значениеtrue
илиfalse
. - Если пользователь не аутентифицирован, он получает CSRF-токен из
/api/csrf/
и сохраняет его в состоянии.
Обратите внимание, что при каждом запросе мы использовали credentials: same-origin
. Это необходимо, поскольку мы хотим, чтобы браузер передавал cookies при каждом HTTP-запросе, если URL имеет то же происхождение, что и вызывающий скрипт.
Обновление frontend/src/main.jsx:
// frontend/src/main.jsx
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.jsx'
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<App />
</React.StrictMode>,
)
Docker
Далее выполним Dockerize обоих приложений.
Backend
# backend/Dockerfile
# pull official base image
FROM python:3.11.4-slim-buster
# set working directory
WORKDIR /usr/src/app
# set environment variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
# install dependencies
RUN pip install --upgrade pip
COPY ./requirements.txt .
RUN pip install -r requirements.txt
# add app
COPY . .
# start app
CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"]
Frontend
# frontend/Dockerfile
# pull official base image
FROM node:18-slim
# set working directory
WORKDIR /usr/src/app
# add `/usr/src/app/node_modules/.bin` to $PATH
ENV PATH /usr/src/app/node_modules/.bin:$PATH
# install and cache app dependencies
COPY package.json .
COPY package-lock.json .
RUN npm ci
# start app
CMD ["vite", "--host"]
Nginx
Для того чтобы запустить оба приложения на одном домене, добавим контейнер для Nginx, работающий в качестве обратного прокси. Создадим в корне проекта новую папку с именем "nginx".
# nginx/Dockerfile
FROM nginx:latest
COPY ./nginx.conf /etc/nginx/nginx.conf
Добавьте также конфигурационный файл nginx/nginx.conf. Код для него можно найти здесь.
Обратите внимание на следующие два блока location:
# nginx/nginx.conf
location /api {
proxy_pass http://backend;
...
}
location / {
proxy_pass http://frontend;
...
}
Запросы к /
будут перенаправляться на http://frontend (frontend - имя сервиса из файла Docker Compose, который мы добавим в ближайшее время), а запросы к /api
- на http://backend (backend - имя сервиса из файла Docker Compose).
Docker Compose
Создайте в корне проекта файл docker-compose.yml и добавьте в него следующее:
# docker-compose.yml
version: '3.8'
services:
backend:
build: ./backend
volumes:
- ./backend:/usr/src/app
expose:
- 8000
frontend:
stdin_open: true
build: ./frontend
volumes:
- ./frontend:/usr/src/app
- /usr/src/app/node_modules
expose:
- 5173
environment:
- NODE_ENV=development
depends_on:
- backend
reverse_proxy:
build: ./nginx
ports:
- 81:80
depends_on:
- backend
- frontend
Теперь структура вашего проекта должна выглядеть следующим образом:
├── backend
│ ├── Dockerfile
│ ├── api
│ │ ├── __init__.py
│ │ ├── admin.py
│ │ ├── apps.py
│ │ ├── migrations
│ │ │ └── __init__.py
│ │ ├── models.py
│ │ ├── tests.py
│ │ ├── urls.py
│ │ └── views.py
│ ├── djangocookieauth
│ │ ├── __init__.py
│ │ ├── asgi.py
│ │ ├── settings.py
│ │ ├── urls.py
│ │ └── wsgi.py
│ ├── manage.py
│ └── requirements.txt
├── docker-compose.yml
├── frontend
│ ├── .eslintrc.cjs
│ ├── .gitignore
│ ├── Dockerfile
│ ├── README.md
│ ├── index.html
│ ├── package-lock.json
│ ├── package.json
│ ├── public
│ │ └── vite.svg
│ ├── src
│ │ ├── App.jsx
│ │ ├── assets
│ │ │ └── react.svg
│ │ └── main.jsx
│ └── vite.config.js
└── nginx
├── Dockerfile
└── nginx.con
Запуск с помощью Docker
Сборка образов и разгон контейнеров:
$ docker-compose up -d --build
Если вы столкнулись с ошибкой 'Service frontend failed to build', возможно, отсутствует файл package-lock.json. Переместитесь в папку "frontend" и выполните
npm install --package-lock
для его генерации.
Запустите миграцию и создайте суперпользователя:
$ docker-compose exec backend python manage.py makemigrations
$ docker-compose exec backend python manage.py migrate
$ docker-compose exec backend python manage.py createsuperuser
Ваша заявка должна быть доступна по адресу: http://localhost:81. Протестируйте его, войдя в систему с только что созданным суперпользователем.
Полный код этого подхода можно получить на GitHub: django_react_same_origin.
Django DRF + Frontend обслуживаются отдельно (один и тот же домен)
Данный подход более-менее похож на предыдущий - "Frontend обслуживается отдельно (в одном домене)". Есть несколько незначительных отличий, которые перечислены ниже.
При использовании этого подхода необходимо установить djangorestframework
с помощью pip или добавить его в requirements.txt (при сборке с помощью Docker). После установки необходимо зарегистрировать его в разделе INSTALLED_APPS
внутри вашего settings.py.
# djangocookieauth/settings.py
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'api.apps.ApiConfig',
'rest_framework', # new
]
Для включения SessionAuthentication
необходимо добавить в файл settings.py следующее:
# backend/djangocookieauth/settings.py
# Django REST framework
# https://www.django-rest-framework.org/api-guide/settings/
REST_FRAMEWORK = {
'DEFAULT_RENDERER_CLASSES': [
'rest_framework.renderers.JSONRenderer',
],
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.SessionAuthentication',
],
}
Также рекомендуется установить
JSONRenderer
в качествеDEFAULT_RENDERER_CLASSES
, чтобы отключить DRF-навигацию и этот причудливый дисплей.
При создании представлений session и whoami используйте APIView
, импортированные из rest_framework
, и явно задайте authentication_classes
и permission_classes
:
# backend/api/views.py
from django.http import JsonResponse
from rest_framework.authentication import SessionAuthentication, BasicAuthentication
from rest_framework.permissions import IsAuthenticated
from rest_framework.views import APIView
class SessionView(APIView):
authentication_classes = [SessionAuthentication, BasicAuthentication]
permission_classes = [IsAuthenticated]
@staticmethod
def get(request, format=None):
return JsonResponse({'isAuthenticated': True})
class WhoAmIView(APIView):
authentication_classes = [SessionAuthentication, BasicAuthentication]
permission_classes = [IsAuthenticated]
@staticmethod
def get(request, format=None):
return JsonResponse({'username': request.user.username})
А при регистрации URL-адресов зарегистрировать их следующим образом:
# backend/api/urls.py
from django.urls import path
from . import views
urlpatterns = [
path('csrf/', views.get_csrf, name='api-csrf'),
path('login/', views.login_view, name='api-login'),
path('logout/', views.logout_view, name='api-logout'),
path('session/', views.SessionView.as_view(), name='api-session'), # new
path('whoami/', views.WhoAmIView.as_view(), name='api-whoami'), # new
]
Полный код этого подхода можно получить на GitHub: django_react_drf_same_origin.
Фронтенд обслуживается отдельно (кросс-домен)
При таком подходе мы создадим фронтенд и будем обслуживать его отдельно от приложения Django на другом домене. Нам придется немного ослабить защиту, разрешив междоменные запросы из фронтенда с помощью django-cors-headers.
Начнем с создания каталога проекта:
$ mkdir django_react_cross_origin && cd django_react_cross_origin
Backend
Сначала создайте новый каталог "backend" для проекта Django:
$ mkdir backend && cd backend
Далее создаем и активируем новую виртуальную среду, устанавливаем Django и создаем новый проект Django:
$ python3.11 -m venv env
$ source env/bin/activate
(env)$ pip install django==4.2.3
(env)$ django-admin startproject djangocookieauth .
После этого создайте новое приложение с именем api
:
(env)$ python manage.py startapp api
Зарегистрируйте приложение в backend/djangocookieauth/settings.py под INSTALLED_APPS
:
# backend/djangocookieauth/settings.py
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'api.apps.ApiConfig', # new
]
Наше приложение будет иметь следующие конечные точки API:
/api/get_csrf/
сгенерирует CSRF-токен и вернет его в виде JSON- .
/api/login/
позволит пользователю войти в систему, указав имя пользователя и пароль/api/logout/
выводит пользователя из системы/api/session/
проверяет наличие сессии/api/whoami/
получает пользовательские данные для аутентифицированного пользователя
Для представления возьмите полный код здесь и добавьте его в файл backend/api/views.py.
Добавьте файл urls.py в раздел "api" и определите следующие URL-адреса:
# backend/api/urls.py
from django.urls import path
from . import views
urlpatterns = [
path('csrf/', views.get_csrf, name='api-csrf'),
path('login/', views.login_view, name='api-login'),
path('logout/', views.logout_view, name='api-logout'),
path('session/', views.session_view, name='api-session'),
path('whoami/', views.whoami_view, name='api-whoami'),
]
Теперь зарегистрируем URL-адреса наших приложений в базовом проекте:
# backend/djangocookieauth/urls.py
from django.contrib import admin
from django.shortcuts import render
from django.urls import path, include # new import
urlpatterns = [
path('admin/', admin.site.urls),
path('api/', include('api.urls')), # new
]
Теперь нам необходимо ослабить несколько настроек безопасности, чтобы наши запросы прошли. Сначала установим настройки cookie в файле backend/djangocookieauth/settings.py:
CSRF_COOKIE_SAMESITE = 'Lax'
SESSION_COOKIE_SAMESITE = 'Lax'
CSRF_COOKIE_HTTPONLY = True
SESSION_COOKIE_HTTPONLY = True
CSRF_TRUSTED_ORIGINS = ['http://localhost:5173']
# PROD ONLY
# CSRF_COOKIE_SECURE = True
# SESSION_COOKIE_SECURE = True
Примечания:
- Установка
CSRF_COOKIE_SAMESITE
иSESSION_COOKIE_SAMESITE
в значениеLax
позволяет отправлять CSRF-куки во внешних запросах. - Включение
CSRF_COOKIE_HTTPONLY
иSESSION_COOKIE_HTTPONLY
блокирует доступ клиентского JavaScript к CSRF и сессионным кукам.
Если вы работаете на производстве, то вам следует обслуживать ваш сайт по HTTPS и включить опции
CSRF_COOKIE_SECURE
иSESSION_COOKIE_SECURE
, которые позволят отправлять cookies только по HTTPS.
Для того чтобы разрешить кросс-оригинальное сохранение cookie, нам также необходимо изменить некоторые настройки CORS. Для этого мы будем использовать django-cors-headers
. Начнем с его установки с помощью следующей команды:
(env)$ pip install django-cors-headers==4.2.0
Добавьте его к установленным приложениям и добавьте новый класс промежуточного ПО:
# backend/djangocookieauth/settings.py
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'api.apps.ApiConfig',
'corsheaders', # new
]
MIDDLEWARE = [
'corsheaders.middleware.CorsMiddleware', # new
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
Настроить CORS:
# backend/djangocookieauth/settings.py
CORS_ALLOWED_ORIGINS = [
'http://localhost:5173',
'http://127.0.0.1:5173',
]
CORS_EXPOSE_HEADERS = ['Content-Type', 'X-CSRFToken']
CORS_ALLOW_CREDENTIALS = True
Примечания:
- В списке
CORS_ALLOWED_ORIGINS
мы задаем допустимое происхождение. Стоит отметить, что для целей тестирования вместо настройкиCORS_ALLOWED_ORIGINS
можно установить значениеCORS_ALLOW_ALL_ORIGIN
наTrue
, чтобы разрешить любому origin'у делать запросы. Однако не используйте это в производстве. CORS_EXPOSE_HEADERS
- список HTTP-заголовков, которые открываются браузеру.- Установка значения
CORS_ALLOW_CREDENTIALS
вTrue
позволяет отправлять cookies вместе с кросс-оригинальными запросами.
Код для нашего бэкенда теперь более или менее готов. Запустим команду migrate и создадим суперпользователя для тестирования в будущем:
(env)$ python manage.py migrate
(env)$ python manage.py createsuperuser
Frontend
Перед началом работы над фронтендом убедитесь, что у вас установлены Node.js и npm (или Yarn).
Мы будем использовать Vite для создания нового проекта React:
$ npm create vite@4.4 frontend
Выберите React в качестве фреймворка с JavaScript в качестве варианта:
✔ Select a framework: › React
✔ Select a variant: › JavaScript
Затем установите зависимости и запустите сервер разработки:
$ cd frontend
$ npm install
$ npm run dev
Это запустит наше приложение на порт 5173. Посетите http://localhost:5173/, чтобы убедиться в его работоспособности.
Для упрощения работы удалите следующие CSS-файлы:
- frontend/src/App.css
- frontend/src/index.css
Далее добавим Bootstrap в frontend/index.html:
<!-- frontend/index.html -->
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React</title>
<!-- new -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.1/dist/css/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous">
<!-- end of new -->
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
Возьмите полный код здесь для компонента App
и добавьте его в файл frontend/src/App.js.
Это простое фронтенд-приложение с формой, которая обрабатывается с помощью React state. При загрузке страницы вызывается compontentDidMount()
, который выполняет два вызова API:
- Сначала проверяется, аутентифицирован ли пользователь, вызовом
/api/session/
и устанавливаетсяisAuthenticated
в значениеtrue
илиfalse
. - Если пользователь не аутентифицирован, он получает CSRF-токен из
/api/csrf/
и сохраняет его в состоянии.
Обратите внимание, что при каждом запросе мы использовали credentials: include
. Это необходимо, так как мы хотим, чтобы браузер передавал cookies при каждом HTTP-запросе, даже если URL не совпадает по происхождению с вызывающим скриптом. Следует помнить, что для этого мы изменили некоторые настройки CORS в бэкенде.
Обновление frontend/src/main.jsx:
// frontend/src/main.jsx
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.jsx'
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<App />
</React.StrictMode>,
)
Запуск приложения
Переместитесь в папку "backend" и запустите Django, используя:
(env)$ python manage.py runserver
Ваш бэкэнд должен быть доступен по адресу: http://localhost:8000.
Откройте новое окно терминала, перейдите в папку "frontend" и запустите React с помощью npm:
$ npm run dev
Вы должны иметь возможность получить доступ к своему приложению по адресу http://localhost:5173.
Протестируйте свое приложение, войдя в систему с созданным ранее суперпользователем.
Полный код этого подхода можно получить на GitHub: django_react_same_origin
Заключение
В этой статье подробно рассказывается о том, как настроить аутентификацию на основе сеанса для одностраничных приложений с помощью Django и React. Независимо от того, используете ли вы сессионные куки или токены, хорошо использовать куки для аутентификации, когда клиентом является браузер. Хотя предпочтительнее обслуживать оба приложения на одном домене, их можно обслуживать на разных доменах, ослабив настройки междоменной безопасности.
Мы рассмотрели четыре различных подхода к объединению Django с фронтенд-фреймворком для работы с сессионными аутентификаторами:
Approach | Frontend | Backend |
---|---|---|
Frontend served from Django | Grab the CSRF token using universal-cookies and use credentials: "same-origin" in the requests. |
Set CSRF_COOKIE_SAMESITE , SESSION_COOKIE_SAMESITE to "Strict" . Enable SESSION_COOKIE_HTTPONLY and disable CSRF_COOKIE_HTTPONLY . |
Frontend served separately (same domain) | Obtain CSRF token and use credentials: "same-origin" in the fetch request. |
Add a route handler for generating the CSRF token that gets set in the response headers. Set SESSION_COOKIE_HTTPONLY , CSRF_COOKIE_HTTPONLY to True and SESSION_COOKIE_SAMESITE , CSRF_COOKIE_SAMESITE to "Strict" . |
Frontend served separately with DRF (same domain) | Obtain CSRF token and use credentials: "same-origin" in the fetch request. |
Add a route handler for generating the CSRF token that gets set in the response headers. Set SESSION_COOKIE_HTTPONLY , CSRF_COOKIE_HTTPONLY to True and SESSION_COOKIE_SAMESITE , CSRF_COOKIE_SAMESITE to "Strict" . |
Frontend served separately (cross-origin) | Obtain CSRF token and use credentials: "include" in the fetch request. |
Enable CORS and add a route handler for generating the CSRF token that gets set in the response headers. Set SESSION_COOKIE_HTTPONLY , CSRF_COOKIE_HTTPONLY to True and SESSION_COOKIE_SAMESITE , CSRF_COOKIE_SAMESITE to "Lax" . Add the django-cors-headers package and configure the CORS_ALLOWED_ORIGINS , CORS_EXPOSE_HEADERS , and CORS_ALLOW_CREDENTIALS settings. |
Возьмите код из репозитория django-spa-cookie-auth.
Вернуться на верх