← Назад к вопросам

Как сделать кастомную авторизацию в Django?

2.0 Middle🔥 171 комментариев
#Django#Безопасность

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Кастомная авторизация в Django

Джанго имеет гибкую систему аутентификации. Вот как создать свою кастомную реализацию.

1. Встроенная система Django

Сначала поймем встроенную систему:

# models.py
from django.contrib.auth.models import AbstractUser
from django.db import models

class CustomUser(AbstractUser):
    """Кастомная модель пользователя"""
    email = models.EmailField(unique=True)
    phone = models.CharField(max_length=20, blank=True)
    is_verified = models.BooleanField(default=False)
    
    USERNAME_FIELD = 'email'  # Использовать email вместо username
    REQUIRED_FIELDS = ['username']  # Обязательные поля при создании
    
    def __str__(self):
        return self.email

# settings.py
AUTH_USER_MODEL = 'myapp.CustomUser'  # Указать кастомную модель

2. Кастомный Backend для аутентификации

# auth_backends.py
from django.contrib.auth.backends import ModelBackend
from django.contrib.auth import get_user_model
from django.core.exceptions import ValidationError
import re

User = get_user_model()

class EmailBackend(ModelBackend):
    """Аутентификация по email вместо username"""
    
    def authenticate(self, request, username=None, password=None, **kwargs):
        try:
            # Сначала пытаемся найти по email
            user = User.objects.get(email=username)
        except User.DoesNotExist:
            # Потом по username (для совместимости)
            try:
                user = User.objects.get(username=username)
            except User.DoesNotExist:
                return None
        
        # Проверяем пароль
        if user.check_password(password) and self.user_can_authenticate(user):
            return user
        
        return None
    
    def get_user(self, user_id):
        try:
            return User.objects.get(pk=user_id)
        except User.DoesNotExist:
            return None

class PhoneBackend(ModelBackend):
    """Аутентификация по телефону"""
    
    def authenticate(self, request, phone=None, password=None, **kwargs):
        try:
            user = User.objects.get(phone=phone)
        except User.DoesNotExist:
            return None
        
        if user.check_password(password) and self.user_can_authenticate(user):
            return user
        
        return None
    
    def get_user(self, user_id):
        try:
            return User.objects.get(pk=user_id)
        except User.DoesNotExist:
            return None

# settings.py
AUTHENTICATION_BACKENDS = [
    'myapp.auth_backends.EmailBackend',
    'myapp.auth_backends.PhoneBackend',
    'django.contrib.auth.backends.ModelBackend',  # Встроенный
]

3. Использование кастомного backend

# views.py
from django.contrib.auth import authenticate, login
from django.views.decorators.http import require_POST
from django.shortcuts import render, redirect

@require_POST
def login_view(request):
    email = request.POST.get('email')
    password = request.POST.get('password')
    
    # Аутентификация через EmailBackend
    user = authenticate(request, username=email, password=password)
    
    if user is not None:
        login(request, user)  # Создать сессию
        return redirect('dashboard')
    else:
        return render(request, 'login.html', {'error': 'Invalid credentials'})

@require_POST
def login_by_phone(request):
    phone = request.POST.get('phone')
    password = request.POST.get('password')
    
    # Аутентификация через PhoneBackend
    user = authenticate(request, phone=phone, password=password)
    
    if user is not None:
        login(request, user)
        return redirect('dashboard')
    else:
        return render(request, 'login.html', {'error': 'Invalid credentials'})

4. Token-based аутентификация (для API)

# models.py
from django.db import models
from django.contrib.auth import get_user_model
import secrets

User = get_user_model()

class AuthToken(models.Model):
    """Токены для API авторизации"""
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    token = models.CharField(max_length=256, unique=True)
    created_at = models.DateTimeField(auto_now_add=True)
    expires_at = models.DateTimeField()
    is_active = models.BooleanField(default=True)
    
    @staticmethod
    def generate_token():
        return secrets.token_urlsafe(32)
    
    def __str__(self):
        return f"{self.user.email} - {self.token[:20]}..."

# auth_backends.py
from rest_framework.authentication import TokenAuthentication
from rest_framework.exceptions import AuthenticationFailed
from datetime import datetime, timezone

class ExpiringTokenAuthentication(TokenAuthentication):
    """Token авторизация с истечением"""
    keyword = 'Token'
    model = AuthToken
    
    def get_model(self):
        return AuthToken
    
    def authenticate_credentials(self, key):
        """Проверить токен и его срок действия"""
        try:
            token = AuthToken.objects.select_related('user').get(token=key)
        except AuthToken.DoesNotExist:
            raise AuthenticationFailed('Invalid token.')
        
        if not token.user.is_active:
            raise AuthenticationFailed('User inactive.')
        
        if not token.is_active:
            raise AuthenticationFailed('Token inactive.')
        
        # Проверка срока действия
        if datetime.now(timezone.utc) > token.expires_at:
            token.delete()
            raise AuthenticationFailed('Token expired.')
        
        return (token.user, token)

# views.py
from rest_framework.decorators import api_view, authentication_classes
from rest_framework.response import Response
from rest_framework.status import HTTP_200_OK, HTTP_401_UNAUTHORIZED
from rest_framework.authtoken.models import Token
from django.contrib.auth import authenticate
from datetime import datetime, timedelta, timezone

@api_view(['POST'])
def login_api(request):
    """Получить токен по email/пароль"""
    email = request.data.get('email')
    password = request.data.get('password')
    
    user = authenticate(request, username=email, password=password)
    
    if user:
        # Удалить старый токен если существует
        AuthToken.objects.filter(user=user).delete()
        
        # Создать новый токен
        token = AuthToken.objects.create(
            user=user,
            token=AuthToken.generate_token(),
            expires_at=datetime.now(timezone.utc) + timedelta(days=30)
        )
        
        return Response({
            'token': token.token,
            'expires_at': token.expires_at.isoformat(),
            'user': {
                'id': user.id,
                'email': user.email,
                'first_name': user.first_name,
            }
        }, status=HTTP_200_OK)
    
    return Response({'error': 'Invalid credentials'}, status=HTTP_401_UNAUTHORIZED)

@api_view(['GET'])
@authentication_classes([ExpiringTokenAuthentication])
def profile_api(request):
    """Получить профиль (требует токена)"""
    user = request.user
    return Response({
        'id': user.id,
        'email': user.email,
        'phone': user.phone,
        'is_verified': user.is_verified,
    })

5. JWT авторизация (более современный способ)

# Установить: pip install djangorestframework-simplejwt

# settings.py
from datetime import timedelta

SIMPLE_JWT = {
    'ACCESS_TOKEN_LIFETIME': timedelta(minutes=15),
    'REFRESH_TOKEN_LIFETIME': timedelta(days=7),
    'ALGORITHM': 'HS256',
    'SIGNING_KEY': SECRET_KEY,
}

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework_simplejwt.authentication.JWTAuthentication',
    )
}

# urls.py
from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView

urlpatterns = [
    path('api/token/', TokenObtainPairView.as_view()),  # Получить токены
    path('api/token/refresh/', TokenRefreshView.as_view()),  # Обновить access token
]

# views.py
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response

@api_view(['GET'])
@permission_classes([IsAuthenticated])
def profile(request):
    """Защищенный endpoint - требует JWT токена"""
    user = request.user
    return Response({
        'id': user.id,
        'email': user.email,
        'first_name': user.first_name,
    })

6. OAuth2 (интеграция с Google, GitHub)

# Установить: pip install python-decouple django-allauth

# settings.py
INSTALLED_APPS = [
    'django.contrib.auth',
    'django.contrib.sites',
    'allauth',
    'allauth.account',
    'allauth.socialaccount',
    'allauth.socialaccount.providers.google',
    'allauth.socialaccount.providers.github',
]

SOCIALACCOUNT_PROVIDERS = {
    'google': {
        'SCOPE': [
            'profile',
            'email',
        ],
        'AUTH_PARAMS': {
            'access_type': 'online',
        },
        'APP': {
            'client_id': os.getenv('GOOGLE_CLIENT_ID'),
            'secret': os.getenv('GOOGLE_CLIENT_SECRET'),
        }
    }
}

# urls.py
from django.urls import path, include

urlpatterns = [
    path('accounts/', include('allauth.urls')),
]

# template
<a href="{% provider_login_url 'google' %}">Sign in with Google</a>

7. 2FA (двухфакторная авторизация)

# models.py
from django_otp.models import Device
from django_otp.plugins.otp_totp.models import StaticDevice, StaticToken
from django_otp.util import random_hex
import pyotp

class UserTwoFactor(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    is_enabled = models.BooleanField(default=False)
    secret_key = models.CharField(max_length=32, blank=True)
    backup_codes = models.JSONField(default=list)
    
    def generate_secret(self):
        self.secret_key = pyotp.random_base32()
        return self.secret_key
    
    def verify_token(self, token):
        """Проверить код из Google Authenticator"""
        totp = pyotp.TOTP(self.secret_key)
        return totp.verify(token)
    
    def generate_backup_codes(self):
        """Сгенерировать коды восстановления"""
        codes = [random_hex(8) for _ in range(10)]
        self.backup_codes = codes
        return codes

# views.py
from django.http import JsonResponse
from django.views.decorators.http import require_POST

@require_POST
def enable_2fa(request):
    """Включить двухфакторную авторизацию"""
    user = request.user
    
    # Создать или получить 2FA
    two_fa, created = UserTwoFactor.objects.get_or_create(user=user)
    
    # Сгенерировать секретный ключ
    secret = two_fa.generate_secret()
    
    # Сгенерировать коды восстановления
    backup_codes = two_fa.generate_backup_codes()
    two_fa.save()
    
    # Создать QR код
    totp = pyotp.TOTP(secret)
    qr_uri = totp.provisioning_uri(
        name=user.email,
        issuer_name='MyApp'
    )
    
    return JsonResponse({
        'qr_uri': qr_uri,
        'backup_codes': backup_codes,
        'secret': secret
    })

@require_POST
def verify_2fa(request):
    """Проверить код 2FA при логине"""
    user = request.user
    token = request.POST.get('token')
    
    try:
        two_fa = UserTwoFactor.objects.get(user=user)
        
        # Проверить код
        if two_fa.verify_token(token):
            login(request, user)
            return JsonResponse({'success': True})
        
        # Проверить код восстановления
        if token in two_fa.backup_codes:
            two_fa.backup_codes.remove(token)
            two_fa.save()
            login(request, user)
            return JsonResponse({'success': True, 'warning': 'Backup code used'})
    
    except UserTwoFactor.DoesNotExist:
        pass
    
    return JsonResponse({'error': 'Invalid token'}, status=400)

8. Сравнение подходов

┌──────────────┬──────────┬────────────┬─────────────┬────────────┐
│ Подход       │ Сложность│ Безопасность│ Масштабируемость│ API     │
├──────────────┼──────────┼────────────┼─────────────┼────────────┤
│ Session      │ Низкая   │ Хорошо     │ Среднее     │ Не подходит│
│ Token (REST) │ Средняя  │ Хорошо     │ Хорошее     │ Подходит   │
│ JWT          │ Средняя  │ Отличная   │ Отличное    │ Идеально   │
│ OAuth2       │ Высокая  │ Отличная   │ Отличное    │ Идеально   │
│ 2FA          │ Высокая  │ Отличная   │ Хорошее     │ Опционально│
└──────────────┴──────────┴────────────┴─────────────┴────────────┘

9. Best Practices

# ✅ ХОРОШО: Никогда не логируй пароли
def login_view(request):
    email = request.POST.get('email')
    password = request.POST.get('password')
    # НЕ делай: logger.info(f"Login attempt: {email}, {password}")
    logger.info(f"Login attempt: {email}")  # Только email

# ✅ ХОРОШО: Rate limiting для защиты от brute force
from django_ratelimit.decorators import ratelimit

@ratelimit(key='ip', rate='5/m')  # 5 попыток в минуту
@require_POST
def login_view(request):
    pass

# ✅ ХОРОШО: Хешировать пароли при сохранении
user = User.objects.create_user(
    email='alice@example.com',
    password='secure_password'  # Django автоматически хеширует
)

# ❌ ПЛОХО: Сохранять пароли в plain text
user.password = 'plaintext_password'
user.save()

# ✅ ХОРОШО: Использовать HTTPS для всех логинов
if not request.is_secure():
    return redirect('https://...')

# ✅ ХОРОШО: Set secure cookie flags
SESSION_COOKIE_SECURE = True  # Только HTTPS
SESSION_COOKIE_HTTPONLY = True  # Не доступен из JS
SESSION_COOKIE_SAMESITE = 'Strict'  # CSRF защита

10. Полный пример: Email + Password + JWT

# models.py
from django.contrib.auth.models import AbstractUser

class CustomUser(AbstractUser):
    email = models.EmailField(unique=True)
    USERNAME_FIELD = 'email'

# auth_backends.py
class EmailBackend(ModelBackend):
    def authenticate(self, request, username=None, password=None, **kwargs):
        try:
            user = CustomUser.objects.get(email=username)
        except CustomUser.DoesNotExist:
            return None
        if user.check_password(password):
            return user
        return None

# urls.py
from rest_framework_simplejwt.views import TokenObtainPairView
from . import views

urlpatterns = [
    path('api/login/', views.CustomTokenObtainPairView.as_view()),
    path('api/profile/', views.profile_view),
]

# views.py
from rest_framework_simplejwt.views import TokenObtainPairView

class CustomTokenObtainPairView(TokenObtainPairView):
    """Login с email вместо username"""
    pass

@api_view(['GET'])
@permission_classes([IsAuthenticated])
def profile_view(request):
    user = request.user
    return Response({'email': user.email, 'id': user.id})

Заключение

Для кастомной авторизации в Django:

  1. Session - для традиционных веб-приложений
  2. Token - для простого API
  3. JWT - для современного API (рекомендуется)
  4. OAuth2 - для социальных логинов
  5. 2FA - для повышенной безопасности

Всегда помни о безопасности: HTTPS, rate limiting, хеширование паролей!