среда, 13 июня 2012 г.

Rails: Один пользователь - одна сессия

Поступила мне тут новая задача, опишу user-case:
  1. Пользователь входит в систему на компьютере «A»
  2. Пользователь оставляет сессию открытой и отходит от компьютера
  3. Пользователь входит в систему на компьютере «B»
  4. На компьютере «A» сессия пользователя закрывается не дожидаясь каких то действия со стороны пользователя.
Первым делом решено было начать хранить сессии в memcashed, для этого в "config/initializers/session_store.rb" пишем следующее:

OmgCashier::Application.config.session_store :mem_cache_store, key: '_your_app_session'

Обратите внимание, если раньше вы хранили сессии в куках, то лучше изменить namespace иначе у залогиненных пользователей на production будут проблемы, а так мы просто всех разлогинем.

Дальше я сразу настроил хранение кеша так же в memcashed, для этого в "config/application.rb" добавил параметр:

config.cache_store = :mem_cache_store

Да, именно в "config/application.rb", так как мне нужен механизм работы с memcashed во всех окружениях. Дальше увидите почему.

Подготовительная часть закончена, пора приступать к основному действию, оно у нас будет происходить в "app/controllers/application_controller.rb":

before_filter :one_session_per_user!

protected

def one_session_per_user!
  # проверяем что пользователь уже вошел
  return unless current_user
  # получаем ключ сессии пользователя
  current_session_key = request.session_options[:id]
  # и namespace под которым сессии хранятся в memcashed
  session_namespace = request.session_options[:namespace]
  # смотрим есть ли у этого пользователия еще какие то сессии
  # тут я как раз использую механизм работы с кешем описанный выше, namespace я выбрал «user:»
  if cash_session_key = Rails.cache.read("user:#{current_user.id}")
    # если сессия в memcashed уже есть и это сессия с этого компа то все нормально
    return if cash_session_key == current_session_key
    # если сессия есть и она не с этого компа то удаляем ее
    Rails.cache.delete("#{session_namespace}:#{cash_session_key}")
    # и оповешаем другой компьютер что сессию нужно закрыть
    Pusher['my-channel'].trigger('should_logout', {:session_key => Digest::MD5.hexdigest('secret' + cash_session_key)})
  end
  # если сессии в кеше нет совсем то запишем ее туда
  Rails.cache.write("user:#{current_user.id}", current_session_key)
end

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

Осталось только подписаться на клиентской стороне на этот самый Pusher и по приходу нового сообщения редиректить пользователя, для этого у нас будет такой javascript код в шаблоне:

// получаем из приложения ключ сессии
var session_key = <%= Digest::MD5.hexdigest('secret' + request.session_options[:id]) %>
// подкючаемся к Pusher и подписываемся на канал
var pusher = new Pusher('Replace with your app key');
var channel = pusher.subscribe('my-channel');

// определяем новое событие
channel.bind('should_logout', function(data) {
  // если получен наш ключ сессии то редиректим
  if (session_key == data.session_key)
    window.location.href = '/users/logout';
  else
    console.log(data);
});

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

Комментариев нет:

Отправить комментарий