Найти - Пользователи
Полная версия: Правильная аутентификация для realtime проектов
Начало » Django » Правильная аутентификация для realtime проектов
1
in
Добрый день. Разбираюсь с новой для себя областью веб сокетов. Выбрал связку tornado+sockjs+django
столкнулся с одной серьезной трудностью, связанной с идентификацией пользователей при обмене сообщений между сервером торнадо и клиентом. Во множестве примеров используется идентификация пользователя спомощью id сессии. Но насколько я понял, sockjs на сервере не принимает никаких куков.

Вот типичный код из туториалов:

def get_session(session_key):
    return _engine.SessionStore(session_key)
def get_user(session):
    class Dummy(object):
        pass
    django_request = Dummy()
    django_request.session = session
    return django.contrib.auth.get_user(django_request)
class Connection (SockJSConnection):
   
   def on_open(self, info):
        """
        Определяем сессию django.
        """
        self.django_session = get_session(info.get_cookie('sessionid').value)
        self.user = get_user(self.django_session)

на строчке :
 self.django_session = get_session(info.get_cookie('sessionid').value)
приложение торнадо умрет потому что не обнаружит никаких куков в обработчике событий

Судя по всему туториалы уже не освсем актуальны

на гитхабе один из ведущих разработчиков sockjs предлагает такой вариант аутентификации пользователей

client side
<script language="javascript">
    var sock = new SockJS('/echo');
    // Lets assume we invented simple protocol, where first word is a command name and second is a payload.
    sock.onconnect = function() {
        sock.send('auth,xxx');
        sock.send('echo,bar');
    };
</script>

server side
class EchoConnection(SockJSConnection):
    def on_open(self, info):
        self.authenticated = False
    def on_message(self, msg):
        pack, data = msg.split(',', 1)
        # Ignore all packets except of 'auth' if user is not yet authenticated
        if not self.authenticated and pack != 'auth':
            return
        if pack == 'auth':
            # Decrypt user_id (or user name - you decide). You might want to add salt to the token as well.
            user_id = des_decrypt(data, secret_key)
            # Validate user_id here by making DB call, etc.
            user = get_user(user_id)
            if user is None and user.is_active:
                self.send('error,Invalid user!')
                return
            self.authenticated = True
        elif pack == 'echo':
            self.send(data)

Но для меня тема альтернативной аутентификации пользователей тема абсолютно новая. До последнего времени доверял это дело движку django. И теперь не совсем понимаю что делать.

Нашел некий Rest Framework, который генерит токены для пользователей, а также некоторые рекомендуют использовать redis для хранения сессий, но я так и не понял каким образом это реально делается. Поделитесь опытом, в какую сторону копать. Я уже нашел альтернативный способ обработки сообщений от сокетов через nodejs + socket.io, но sockjs кажется гораздо более привлекательным, ровно как и торнадо.
in
Напишу, о том как выкрутился. Не знаю может быть это избыточное решение. Покритикуете тогда, если что не так.

Прежде всего мне не нравилось в туториалах то, что есть какие непонятные манипуляции с фековым реквестом, который необходим для вытаскивания айдишника юзера. В сессиях джанго,в свою очередь, айдишник хранится где-то в недрах сессии, практически прозапас, и нет никакого официального апи чтобы вытаскивать ассоциированного с сессией пользователя. Поэтому манипуляции с реквестом, совсем не во всех случаях будут давать айдишник авторизованного пользователя вместо чего будет возвращать айдишник анонима.

Поэтому этот сильно пахнущий код я выбросил на помойку:
def get_user(session):
    class Dummy(object):
        pass
    django_request = Dummy()
    django_request.session = session
    return django.contrib.auth.get_user(django_request)

Для комфортной работы с вебсокетами нужны либо другие сессии, либо дополнительное хранилище, которое увязывает сессию с айдишником авторизованного пользователя. Поскольку я все еще эмоционально привязан к джанго, уходить сильно в сторону я не решаюсь и поэтому искать альтернативные сессии я не стал, но создавать новую модель мне тоже не хотелось и ограничился тем что создал альтернативное хранилище на redis.

Редис просто хранит хеш для пар сессия, айдишник пользователя. Сохраняются эти пары при авторизации. Для этого пришлось заменить джанговское вью, на свое альтернативное.

import redis
strict_redis = redis.StrictRedis(host='localhost', port=6379, db=0, decode_responses=True)
@sensitive_post_parameters()
@csrf_protect
@never_cache
def login(request, template_name='registration/login.html',
          redirect_field_name=REDIRECT_FIELD_NAME,
          authentication_form=AuthenticationForm,
          current_app=None, extra_context=None):
    """
    Displays the login form and handles the login action.
    """
    redirect_to = request.REQUEST.get(redirect_field_name, '')
    if request.method == "POST":
        form = authentication_form(request, data=request.POST)
        if form.is_valid():
            # Ensure the user-originating redirection url is safe.
            if not is_safe_url(url=redirect_to, host=request.get_host()):
                redirect_to = resolve_url(settings.LOGIN_REDIRECT_URL)
            # Okay, security check complete. Log the user in.
            auth_login(request, form.get_user())
            # мой код дополняющий кеширование сессий ассоциирующихся с айди юзеров
            session_key = request.session.session_key
            user_id = str(form.get_user().id)
            strict_redis.hset(ls.CHAT_REDIS_SESSIONS_STORAGE, session_key, user_id)
            return HttpResponseRedirect(redirect_to)
    else:
        form = authentication_form(request)
    current_site = get_current_site(request)
    context = {
        'form': form,
        redirect_field_name: redirect_to,
        'site': current_site,
        'site_name': current_site.name,
    }
    if extra_context is not None:
        context.update(extra_context)
    return TemplateResponse(request, template_name, context,
                            current_app=current_app)

В процессе реализации альтернативного вью всплыл нюанс связанный с тем, что при вызове auth джанго тестирует браузер на наличие включенных куков и поэтому для корректной обработки альтернативной формы в сеттинги необходимо добавить небольшой middleware

MIDDLEWARE_CLASSES = (
   """ другие middlewares"""
    'registration.middleware.cookies_test.TestCookieMiddleware'
)

class TestCookieMiddleware(object):
    def process_view(self, request, view_func, view_args, view_kwargs):
        request.session.set_test_cookie()
        return

Еще стоит обратить внимание на параметр decode_responses в клиенте редиса, иначе он будет заварачивать айдишник в байты.

Вот и все после этого клиент редис спокойно отдаст айдишник по сессии.

def get_user(session_key):
    cached_id = strict_redis.hget(ls.CHAT_REDIS_SESSIONS_STORAGE, session_key)
    user = User.objects.get(id=cached_id)
    return user

Единственное что в коде осталось не слишком красивым, это то что во view клиента работающего с сокетами приходится отдавать повторно сессию, поскольку по умолчанию session_key это http read only кука и прочитать ее яваскриптом не получится. Ну и еще нюанс, авторизация происходит с помощью явного отдельного сообщения на сервер, в теле обработчика, который установил открытие соединения.

ws.onopen = function() {
    ws.send ('auth,' + sessionid)
 };
Grayson
Столкнулся с такой же задачей, пост выше очень помог, спасибо.

Если запускать торнадо как manage.py команду, то таких проблем не возникает.
This is a "lo-fi" version of our main content. To view the full version with more information, formatting and images, please click here.
Powered by DjangoBB