Разделение внешних и внутренних систем стало популярным архитектурным решением. Такой подход не только повышает масштабируемость, но и способствует четкому разделению обязанностей, делая проекты более управляемыми и эффективными. Однако создание рабочей среды разработки может оказаться сложной задачей, особенно при настройке аутентификации. Цель этой статьи — прояснить процесс интеграции интерфейса Quasar/VueJS с серверной частью Django, уделяя особое внимание настройке бесшовной системы аутентификации на основе сеанса.
Если вы хотите пропустить эту статью и просто получить стартовый проект, вы можете найти его здесь: https://github.com/bklik/quasar-django-starter.
Настройка среды
Начнем с настройки среды для нашего проекта. Нам понадобится папка проекта, виртуальная среда Python, проект внешнего интерфейса Quasar и внутренний проект Django.
# Create an navigate to our project folder mkdir vue-django-session cd vue-django-session # Create a python virtual environment and source it python -m venv .venv source .venv/bin/activate # Windows: .venv/Scripts/activate # Install Django and setup a backend project pip install django django-admin startproject backend # Set up a quasar fontend project # - Quasar CLI # - Project folder: frontend # - Vue 3 # - Typescript # - Webpack # - <script setup> # - SCSS # - ESLint/Pinia/Axios # - Prettier npm init quasar
Создание серверной части
Наша среда разработки Django по умолчанию работает на порту 8000, но наша среда разработки Quasar/VueJS по умолчанию работает на порту 8080. Это приведет к ошибкам совместного использования ресурсов между источниками (CORS), поскольку наш интерфейс пытается взаимодействовать с нашим сервером. Нам нужно изменить некоторые настройки, чтобы это заработало.
Сначала нам понадобятся еще несколько требований к проекту.
# Django REST Framework for delivering our API pip install djangorestframework # CORS headers for allowing us to accept requests from other ports pip install django-cors-headers cd backend/ python manage.py migrate python manage.py createsuperuser
Примените следующие изменения настроек.
# backend/backend/settings.py import os ... INSTALLED_APPS = [ ... # Requirements 'djangorestframework', 'django-cors-headers' ] MIDDLEWARE = [ ... 'corsheaders.middleware.CorsMiddleware', 'django.middleware.common.CommonMiddleware', ... ] REST_FRAMEWORK = { 'DEFAULT_AUTHENTICATION_CLASSES': [ 'rest_framework.authentication.SessionAuthentication', ], 'DEFAULT_PERMISSION_CLASSES': [ 'rest_framework.permissions.IsAuthenticated', ], } # This will remove these settings when served by Apache during production if not os.environ.get('WSGI_APPLICATION'): # Allow all origins (not recommended for production) CORS_ALLOW_CREDENTIALS = True # Or allow specific origins CORS_ALLOWED_ORIGINS = [ "http://localhost:8080", ] CSRF_TRUSTED_ORIGINS = [ "http://localhost:8080", ] # Name of token in header CSRF_COOKIE_NAME = "csrftoken" # 20 minutes in seconds SESSION_COOKIE_AGE = 1200 # Resets the cookie are after each request SESSION_SAVE_EVERY_REQUEST = True ...
Создайте приложение для управления сеансами
В Django нет встроенной функции администрирования сеансов, позволяющей узнать, какие пользователи вошли в систему и когда истечет срок действия их сеанса. Мы создадим небольшое приложение для просмотра активных сессий и управления представлениями для входа в систему и проверки аутентификации.
# from backend/ python manage.py startapp app_auth
Примените следующие изменения файла.
# app_auth/admin.py from django.contrib import admin from django.contrib.auth.models import User from django.contrib.sessions.models import Session from django.urls import reverse from django.utils.html import format_html @admin.register(Session) class SessionAdmin(admin.ModelAdmin): # Ceates an easy way to view/expire current sessions list_display = ('session_key', 'username', 'expire_date', 'expire_session') def username(self, obj): session_data = obj.get_decoded() user_id = session_data.get('_auth_user_id') if user_id: user = User.objects.get(id=user_id) return user.username return None def expire_session(self, obj): return format_html( '<a href="{}" class="button">Expire Session</a>', reverse('expire_session', args=[obj.pk]) ) # app_auth/serializers.py from django.contrib.auth.models import User from rest_framework import serializers from rest_framework.exceptions import ValidationError from rest_framework.serializers import Serializer class UserSerializer(serializers.ModelSerializer): permissions = serializers.SerializerMethodField() def get_permissions(self, obj): return list(obj.get_all_permissions()) class Meta: model = User fields = [ 'id', 'username', 'first_name', 'last_name', 'permissions' ] class LoginSerializer(Serializer): username = serializers.CharField() password = serializers.CharField(write_only=True) def validate(self, data): user = User.objects.filter(username=data['username']).first() if user and user.check_password(data['password']): return user raise ValidationError({'error': 'Invalid username or password.'}) # app_auth/views.py from django.contrib.auth import login from django.contrib.sessions.models import Session from django.http import HttpResponseRedirect from rest_framework import permissions, status from rest_framework.response import Response from rest_framework.views import APIView from .serializers import LoginSerializer, UserSerializer class AuthCheck(APIView): permission_classes = [] def get(self, request): if request.user.is_authenticated: user = UserSerializer(request.user, context={'request': request}) return Response({"isAuthenticated": True, "user": user.data}) else: return Response({"isAuthenticated": False}) class LoginView(APIView): authentication_classes = [] permission_classes = (permissions.AllowAny,) def post(self, request): serializer = LoginSerializer(data=request.data) if serializer.is_valid(): user = serializer.validated_data login(request, user) return Response({"detail": "Login successful."}, status=status.HTTP_200_OK) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) def expire_session_view(request, session_key): try: session = Session.objects.get(session_key=session_key) session.delete() except Session.DoesNotExist: pass return HttpResponseRedirect('/admin/sessions/session/') # app_auth/urls.py from django.urls import path from .views import AuthCheck, LoginView, expire_session_view urlpatterns = [ path('auth-check', AuthCheck.as_view(), name='auth_check'), path('login/', LoginView.as_view(), name='login'), path('expire-session/<str:session_key>/', expire_session_view, name='expire_session'), ] # backend/urls.py from django.contrib import admin from django.urls import path, include urlpatterns = [ path('app_auth/', include('app_auth.urls')), path('api-auth/', include('rest_framework.urls', namespace='rest_framework')), path('admin/', admin.site.urls), ] # bachend/settings.py ... INSTALLED_APPS = [ ... # Apps 'app_auth' ]
Если мы проверим наш прогресс с помощью `python Manage.py runserver`, мы сможем получить доступ к администратору Django по адресу: http://127.0.0.1:8000/admin.
Мы можем войти в систему, используя учетные данные суперпользователя, и просмотреть наш сеанс, нажав «Администратор сеансов». Находясь в системе администратора, создайте нового пользователя, чтобы протестировать представление проверки подлинности.
Мы можем использовать localhost в нашем URL-адресе для входа в систему под другим пользователем: http://localhost:8000/app_auth/auth-check.
Используя ссылку «Войти», мы можем пройти аутентификацию с помощью недавно созданных учетных данных тестового пользователя и просмотреть информацию о пользователе.
Если мы вернемся на вкладку администрирования Django и обновим сеансы, теперь мы должны увидеть сеанс нашего тестового пользователя. Мы можем завершить этот сеанс, в результате чего наш тестовый пользователь выйдет из системы.
Создание внешнего интерфейса
Теперь, когда у нас есть готовая серверная часть, нам нужно создать настройки, компоненты, хранилища и средства связи, необходимые для нашего внешнего интерфейса.
Сначала мы настроим аксиомы. Нам нужны axios, чтобы знать, какой URL-адрес использовать, что он должен включать учетные данные и передавать файл cookie csfrtoken в заголовке при отправке/размещении запросов.
// src/boot/axios.ts ... const api = axios.create({ baseURL: 'http://localhost:8000', withCredentials: true, }); // Helper function to get CSRF token from cookie function getCookie(name: string): string | null { const value = '; ' + document.cookie; const parts = value.split('; ' + name + '='); if (parts.length === 2) return parts.pop()?.split(';').shift() || null; return null; } export default boot(({ app }) => { ... // Set the X-CSRFToken header for all POST/PUT requests api.interceptors.request.use((config) => { if ( config.method?.toLowerCase() === 'post' || config.method?.toLowerCase() === 'put' ) { const csrfToken = getCookie('csrftoken'); if (csrfToken) { config.headers['X-CSRFToken'] = csrfToken; } } return config; }); });
Далее нам нужно хранилище, которое будет отправлять запросы учетных данных пользователя на серверную часть.
// src/stores/useAppAuthStore.ts import { defineStore } from "pinia"; import { api } from "src/boot/axios"; type LoginFormType = { username: string; password: string; }; type UserType = { id: number; username: string; first_name: string; last_name: string; permissions: Array<string>; }; type AuthCheckType = { isAuthenticated: boolean; user?: UserType; }; export const useAppAuthStore = defineStore("app_auth", { state: () => ({ loggedOut: true, error: "" as string, user: {} as UserType, loading: false, authInterval: null as ReturnType<typeof setInterval> | null, }), getters: {}, actions: { async login(credentials: LoginFormType) { this.loading = true; await api .post("/app_auth/login/", credentials) .then(() => { this.authCheck(); }) .catch((error) => { console.error(error.response.data); this.error = error.response.data.error; this.loggedOut = true; this.user = {} as UserType; this.loading = false; }); }, async authCheck() { this.loading = true; await api .get("/app_auth/auth-check") .then((response) => { const data = response.data as AuthCheckType; if (data.isAuthenticated && data.user) { this.user = data.user; this.error = ""; this.loggedOut = false; this.loading = false; if (!this.authInterval) { this.authInterval = setInterval(() => { this.authCheck(); }, 1000 * 60 * 21); // 21 minutes in miliseconds } } else { this.error = ""; this.loggedOut = true; this.loading = false; this.authInterval = null; } }) .catch((error) => { console.error(error.response.data); this.error = error; this.loggedOut = true; this.loading = false; }); }, async logout() { this.loading = true; await api .get("/api-auth/logout/") .then(() => { this.authCheck(); }) .catch((error) => { console.error(error.response.data); this.error = error.response.data.error; this.loading = false; }); }, }, });
Создайте компонент, предоставляющий пользователю форму для входа в систему.
<!-- src/components/app_auth/LoginForm.vue --> <template> <q-dialog v-model="appAuthStore.loggedOut" persistent> <q-card class="login-form"> <q-form @submit="submitLogin()"> <q-card-section> <div class="text-h4">Sign in</div> </q-card-section> <q-card-section class="column q-gutter-md"> <q-input type="text" label="Username" required autofocus v-model="loginModel.username" :rules="[ (val) => (val && val.length > 0) || 'Required field.', ]" ></q-input> <q-input type="password" label="Password" required v-model="loginModel.password" :rules="[ (val) => (val && val.length > 0) || 'Required field.', ]" ></q-input> <div v-if="appAuthStore.error" class="text-negative"> <q-icon :name="mdiAlertCircle" size="sm"></q-icon> {{ appAuthStore.error }} </div> </q-card-section> <q-card-actions align="right" class="text-primary"> <q-btn type="submit" color="primary" :loading="appAuthStore.loading" label="Sign in" :icon="mdiLoginVariant" ></q-btn> </q-card-actions> </q-form> </q-card> </q-dialog> </template> <style lang="scss"> .login-form { width: 100%; & input:required + .q-field__label::after { color: $negative; content: " *"; } } </style> <script lang="ts" setup> import { reactive } from "vue"; import { mdiAlertCircle, mdiLoginVariant } from "@quasar/extras/mdi-v6"; import { useAppAuthStore } from "src/stores/useAppAuthStore"; type LoginFormType = { username: string; password: string; }; const defaultModel = { username: "", password: "", }; const appAuthStore = useAppAuthStore(); const loginModel = reactive<LoginFormType>({ ...defaultModel }); function submitLogin() { appAuthStore.login(loginModel).then(() => { if (appAuthStore.error === "") { Object.assign(loginModel, { ...defaultModel }); } }); } </script>
Quasar начинается с примера компонента, который мы можем повторно использовать для отображения информации о пользователе при успешном входе в систему.
<!-- src/components/ExampleComponent.vue --> <template> <pre>{{ appAuthStore.user }}</pre> <q-btn v-if="!appAuthStore.loggedOut" label="Logout" color="primary" @click="appAuthStore.logout()" ></q-btn> </template> <script setup lang="ts"> import { useAppAuthStore } from "src/stores/useAppAuthStore"; const appAuthStore = useAppAuthStore(); </script>
Поскольку IndexPage использовал exampleComponent, нам нужно удалить весь ненужный код.
<!-- src/pages/IndexPage.vue --> <template> <q-page class="row items-center justify-evenly"> <example-component></example-component> </q-page> </template> <script setup lang="ts"> import ExampleComponent from "components/ExampleComponent.vue"; </script>
Включите компонент LoginForm в MainLayout, чтобы он отображался, когда пользователь не прошел аутентификацию.
<!-- src/layouts/MainLayout.vue --> <template> ... <LoginForm></LoginForm> </template> <script setup lang="ts"> ... import LoginForm from 'src/components/app_auth/LoginForm.vue'; </script>
Наконец, нам нужно проверить, аутентифицирован ли пользователь при каждой навигации по приложению, чтобы узнать, вошли ли они в систему.
// src/router/index.ts export default route(function (/* { store, ssrContext } */) { ... // Before each rount, check if the user is logged in Router.beforeEach(() => { appAuthStore.authCheck(); }); });
Запустить среду разработчика
Теперь у нас должно быть все необходимое для запуска внешнего интерфейса, внутреннего интерфейса и проверки аутентификации.
# Terminal 1 python manage.py runserver # Terminal 2 quasar dev
Ваше приложение должно выглядеть примерно так.
Это все, что вам нужно для начала работы с аутентифицированной средой. Надеюсь, это было полезно.