текст перекладу
Фон
Нещодавно я досліджував варіанти автентифікації сокетів у Django Channels. Спочатку це здавалось досить простим — як знайти печиво на кухні бабусі. Згідно з документацією Channels, управління сесіями здійснюється за допомогою функцій login
і logout
, а AuthMiddlewareStack
зберігає сесію користувача до її закінчення (або поки ваша кішка не вирішить сісти на клавіатуру).
Моє завдання полягало в автентифікації користувачів за допомогою OTP-коду, надісланого на їх мобільні телефони. Мій початковий підхід виглядав так:
async def user_login(self, content):
verify_code = self.scope.get("verify_code")
code = content.get("code")
mobile = self.scope.get("MOBILE")
if mobile and verify_code and code == verify_code:
user = await load_user_by_mobile(mobile)
del self.scope["VERIFY_CODE"]
# Log in the user to this session.
await login(self.scope, user)
# Save the session.
await database_sync_to_async(self.scope["session"].save)()
await self.send_json(send_code(200))
else:
await self.send_json(send_code(SystemErrorCodes.InvalidVerifyCode))
Цей підхід успішно автентифікував сокет. Однак, ініціація іншого споживача (наприклад, RequestConsumer
) призводила до того, що self.scope['user']
ставало AnonymousUser
. Крім того, закриття вкладки браузера (у моєму випадку Chrome) призводило до втрати сесії. У браузері не з’являлись cookies — як відкрити банку з печивом і знайти її порожньою. 😢
Дослідження рішень
Пошуки в Інтернеті підказали JWT токени як поширене рішення. Проте я уникнув JWT через проблеми безпеки. Мій проєкт не вимагав рівня безпеки як у Форт-Ноксі, але я хотів більш безпечний підхід — жодних злодіїв у цій банці з печивом! 🍪
Тоді я знайшов Django Channels JWT, який на диво не використовує JWT токени. Натомість він використовує тимчасові одноразові UUID-ключі для автентифікації. Користувачі автентифікуються через HTTP-ендпоінт, генеруючи UUID, який зберігається в кеші на 10 хвилин. Клієнт передає цей ключ як рядок запиту під час з’єднання сокетів, а ключ видаляється після успішної автентифікації. Все досить просто і безпечно — як собака, що ховає свою улюблену кістку.
Цей підхід був простим і безпечним, але йому бракувало постійності. Надихнувшись ним, я розробив своє власне рішення. 😀
Запропонований підхід
Я зрозумів, що метод login
у Django Channels не надає cookies для браузера. Щоб вирішити цю проблему, я використав основний метод login
Django для автентифікації користувачів через HTTP і доставки cookie. Моя реалізація включала двоступеневу форму:
def mobile_login(request):
if request.method == 'POST':
form = LoginMobileForm(request.POST)
if form.is_valid():
mobile = form.cleaned_data['mobile']
code = randint(10000, 99999)
request.session['mobile'] = mobile
request.session['code'] = code
send_otp(mobile, code)
return HttpResponseRedirect(reverse('mobile-verify'))
else:
form = LoginMobileForm()
return render(request, 'mobile_login.html', {'form': form})def verify_login(request):
if request.method == 'POST':
form = LoginVerifyForm(request.POST)
if form.is_valid():
code = form.cleaned_data['code']
if request.session.get('code') == code:
mobile = request.session['mobile']
user = get_user_by_mobile(mobile)
login(request, user)
return redirect('panel')
else:
form = LoginVerifyForm()
return render(request, 'verify_login.html', {'form': form})
Ця реалізація успішно реєструвала користувачів та надсилала cookies до браузера. Однак, AuthMiddlewareStack
в Channels все ще не міг автентифікувати сокет за допомогою цієї сесії. Це була справжня тверда печиво! 😀
Автентифікація на основі UUID
Я вирішив ввести автентифікованого користувача в підключення WebSocket.
текст перекладу
Оскільки моя view, яка називалась panel
, вже була автентифікована, я додав UUID ключ:
def panel(request):
uuid = provide_uuid(request.user)
return render(request, 'panel.html', {'user': request.user, 'uuid': uuid})
def provide_uuid(user: User) -> str:
uuid = str(uuid4())
cache.set(uuid, user.id, 60)
return uuid
В HTML-шаблоні (panel.html
):
const socket = new WebSocket('ws://localhost:8000/ws/panel/?uuid={{ uuid }}');
Кожного разу, коли викликається цей view, генерується новий одноразовий UUID ключ для автентифікації сокета. Потім я реалізував власний middleware для автентифікації на основі UUID:
import logging
from urllib.parse import parse_qsl
from asgiref.sync import sync_to_async
from channels.db import database_sync_to_async
from channels.middleware import BaseMiddleware
from django.contrib.auth import get_user_model
from django.contrib.auth.models import AnonymousUser
from django.core.cache import cache
from django.db import close_old_connectionslogger = logging.getLogger(__name__)User = get_user_model()@database_sync_to_async
def get_user(user_id):
try:
return User.objects.get(id=user_id)
except User.DoesNotExist:
return sync_to_async(AnonymousUser)()class UUIDAuthMiddleware(BaseMiddleware): def __init__(self, inner):
super().__init__(inner) async def auth(self, query_string):
query_params = dict(parse_qsl(query_string))
uuid = query_params.get('uuid')
if uuid in cache:
user_id = cache.get(uuid)
cache.delete(uuid)
return await get_user(user_id)
return AnonymousUser() async def __call__(self, scope, receive, send):
close_old_connections() query_string = scope['query_string'].decode('utf-8')
scope['user'] = await self.auth(query_string) return await super().__call__(scope, receive, send)def UUIDAuthMiddlewareStack(inner):
return UUIDAuthMiddleware(inner)
Висновок
Цей підхід поєднує безпечну обробку cookie в Django з одноразовими UUID ключами, що забезпечує посилене захист від несанкціонованого доступу. Це також гарантує постійну автентифікацію для підключень сокетів за допомогою вбудованих механізмів Django.
Більше жодних злодіїв cookie і зниклих сесій — лише плавне з’єднання для WebSockets! 😀
Джерела
- Пакет Channels: https://pypi.org/project/channels/
- Django Channels JWT: https://pypi.org/project/django-channels-jwt/
Перекладено з: Simple, Secure and Persistence socket authentication in Django-channels