20 октября 2013 г. Сессии в API на Yii c возможностью хранения в Redis

Не так давно у меня возникла необходимость написать API на Yii Framework. Одним из функциональных требований в котором является авторизация. Для механизма авторизации я решил использовать сессии.

Вариант реализации самодельных сессий

До этого я видел не мало реализаций API написанных на PHP и ни разу не видел реализации, где использовался бы встроенный в PHP механизм сессий. В том, что мне в основном попадалось, была реализация самодельных сессий. В большинстве случаев это выглядело следующим образом:

api authorization

  1. Клиент отправляет запрос на сервер c данными на авторизацию
  2. В случае успешной авторизации сервер генерирует уникальный идентификатор (рандомный хеш), сохраняет его у себя в хранилище (БД, кеш и т.п.), записывает информацию о принадлежности клиента к данному идентификатору и проставляет время последнего обращения к серверу. После этого отправляет клиенту ответ с этим самым идентификатором
  3. Клиент, получив идентификатор сессии, сохранив его для дальнейших запросов, отправляет запрос на сервер с переданным идентификатором сессии (в качестве параметра или заголовка) для получения данных
  4. Сервер, проверив идентификатор сессии, отдает данные клиенту и обновляет время последнего обращения к серверу с данным идентификатором Взаимодействие между клиентом и сервером в пунктах 3 и 4 может происходить пока не была уничтожена запись о сессии на сервере. В случае уничтожения сессии необходимо снова выполнить пункты 1 и 2 перед дальнейшем выполнением пунктов 3 и 4. Периодически приходится делать проверки идентификаторов сессии на последнее время обращения к серверу и удалять те, у которых превышен срок жизни, если используемое хранилище сессий не умеет уничтожать записи автоматически по заданному времени жизни. В данном способе довольно много действий, которые требуют реализации.

Вариант с использованием стандартных PHP сессий

А что если использовать стандартные PHP сессии? Что нам даст их использование:

  • автоматическая генерация уникального идентификатора сессии
  • доступ к данным хранимых в сессии, а так же управлении ими из любого места приложения
  • использование стандартных PHP функций для работы с сессиями и в том числе оберток над ними. Например, class CHttpSession Yii фреймворка
  • автоматическое восстановление сохраненного ранее окружения. Например, автоматический логин пользователя при получении идентификатора ранее созданной сессии
  • автоматическое удаление сессий, у которых закончилось время жизни Давайте рассмотрим, как работают сессии на основе cookie.

  1. Браузер отправляет запрос на сервер на получение информации по указанному URL
  2. Сервер возвращает ответ с заголовком “Set-Cookie”, который сообщает браузеру, что нужно записать идентификатор сессии в cookie. Пример заголовка “Set-Cookie”:
    Set-Cookie: PHPSESSID=p2799jqivvk8gnruif1lvtv5l5; path=/
    
  3. Браузер, успешно записав идентификатор сессии в cookie, отправляет запрос на получение нового URL, но уже с заголовком “Cookie”:
    Cookie: PHPSESSID=p2799jqivvk8gnruif1lvtv5l5
    
  4. Сервер отдает страницу браузеру

Все последующие запросы от браузера идут с заголовком “Cookie”, в котором содержится информация об идентификаторе сессии. Всё это работает автоматически в браузере в случае, если cookie не отключены. Но что делать, если cookie отключены или в роли клиента будет не браузер? В этом случае всё будет не так просто. Конечно, можно использовать прием и передачу заголовков “Set-Cookie” и “Cookie” на стороне клиента, но давайте рассмотрим вариант, как это можно сделать ниже.

Использование PHP сессий в API

Перед началом использования сессий нужно обратить внимание на параметры в php.ini связанные с сессиями. Особенно обратите внимание на следующие параметры: session.use_cookies, session.use_only_cookies, session.use_trans_sid. Для того что бы начать использовать механизм PHP сессий для API нужно настроить эти параметры следующим образом:

    session.use_cookies = 0
    session.use_only_cookies = 0
    session.use_trans_sid = 1
    session.name = session

Конечно же не обязательно задавать эти настройки напрямую в php.ini, достаточно задать их через php функцию ini_set. Этими настройками мы отключим возможность использования cookie для хранения идентификаторов на стороне клиента, так как подразумевается использование API не только браузером, а и другими приложениями, мобильными устройствами и т.п. Включение параметра session.use_trans_sid даст нам возможность передавать идентификатор сессии в качестве GET или POST параметра. Если вы собираетесь разрабатывать REST API, то передача идентификатора через POST параметр не лучший вариант, так как в REST используются еще методы, такие как PUT и DELETE, при использовании которых передача идентификатора сессии не будет работать. Поэтому лучше передавать идентификатор в качестве GET параметра, который будет работать с любым из методов в REST API. Так же зададим название GET параметра в параметре session.name, который по умолчанию называется PHPSESSID. URL с переданным идентификатором сессии будет выглядеть следующим образом:

https://api.example.com/action?session=l2kkl7c9sm2dfedr767itc9966

Использование PHP сессий в Yii Framework

Теперь давайте рассмотрим, как можно использовать этот механизм в Yii Framework. Для работы с сессиями в Yii предусмотрен класс CHttpSession. Для того что бы использовать его нужно прописать в конфиге в массив components следующие настройки:

'session' => array(
    'autoStart' => true,
    'cookieMode'=>'none',
    'useTransparentSessionID' => true, 
    'sessionName' => 'session',
    'timeout' => 28800,
),

где'cookieMode'=>'none' ставит настройки php.ini в session.use_cookies = 0 и session.use_only_cookies = 0

'useTransparentSessionID' => true ставит php.ini в session.use_trans_sid = 1

Для API с не очень большим количеством обращений этого было бы достаточно, но по умолчанию, сессии хранятся в виде “Plain text” файла на диске, что может стать слабым звеном при интенсивном чтении и записи сессий в высоконагруженных API. В этом случае можно использовать один из вариантов решений:

  • заменить диск на SSD
  • поставить рейд 10 уровня из SSD дисков
  • использовать RAM диск. Например, файловая система Tmpfs в Linux
  • хранение сессий в Memcached (хранение данных в оперативной памяти)
  • хранение сессий в Redis (хранение данных в оперативной памяти)

Хранение сессий в Redis

Я хотел бы остановить свое внимание на Redis, в силу его разнообразных структур хранения данных. Так же хочу отметить не маловажную возможность восстановления данных (сессий в нашем случае) после перезагрузки сервера. Перед тем, как использовать Redis в качестве хранилища сессий, нужно установить Redis сервер и PHP Extension для Redis. Как установить то и другое, можно почитать здесь. После успешной установки появится возможность использовать PHP Session handler из PHP Extension для Redis. Но для того, чтобы использовать PHP Session handler Redis-а, не меняя напрямую php.ini и имея возможность задавать Redis в качестве хранилища сессий в конфиге Yii, мне пришлось немного изменить CHttpSession, унаследовавшись от него и написав свой класс RedisSessionManager.

Теперь конфиг для session компонента будет выглядеть следующим образом:

'session' => array(
    'class' => 'application.components.RedisSessionManager',
    'autoStart' => true,
    'cookieMode'=>'none', 
    'useTransparentSessionID' => true, 
    'sessionName' => 'session',
    'saveHandler'=>'redis',
    'savePath' => 'tcp://localhost:6379?database=10&prefix=session::',
    'timeout' => 28800,
),

Использование сессий для авторизации в API

Теперь можно использовать сессии для авторизации пользователей в API. Сделать это можно следующим образом:

метод login:

public function actionLogin()
{
    $params = $this->getRequestParams();
    $identity=new UserIdentity($params[‘username’],$params['password']);
    if($identity->authenticate()){
        $this->sendResponse(Status::OK, array(
            'session'=>Yii::app()->session->getSessionID(),
            'message'=>'Successful login',
        ));
    }else{
        $this->sendResponse(Status::UNAUTHORIZED, $identity->errorMessage);
    }
}

Что здесь происходит? Сначала мы получаем username и password, которые пришли из запроса. Затем пробуем авторизоваться с помощью этого логина и пароля и в случае успешного входа возвращаем идентификатор сессии для его дальнейшего использования при обращении к другим методам API.

вот так выглядит класс UserIdentity:

class UserIdentity extends CUserIdentity
{
    public function authenticate()
    {
        $account = Yii::app()->account->getByName($this->username);
        $password = Yii::app()->account->hashPassword($this->password);
        if(!$account || $this->username !== $account->username){
            $this->errorCode = self::ERROR_USERNAME_INVALID;
            $this->errorMessage = 'User with username '.$this->username.' not found';
            return false;
        } else if ($password !== $account->password) {
            $this->errorCode = self::ERROR_PASSWORD_INVALID;
            $this->errorMessage = 'Wrong password';
            return false;
        } else {
            $this->errorCode = self::ERROR_NONE;
            Yii::app()->user->login($this);
            Yii::app()->user->setId($account->id);
            Yii::app()->user->setName($account->nickname);
            return true;
        }
    }
}

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

метод logout:

public function actionLogout()
{
    if(Yii::app()->session->destroySession()){
        $this->sendResponse(Status::OK, 'Successful logout');
    }else{
        $this->sendResponse(Status::BAD_REQUEST, 'Logout was not successful');
    }
}

Здесь всё просто. Просто уничтожаем текущую сессию со всем её содержимым.

Так же хочу отметить несколько советов по использованию сессий в API:

  • важно использовать шифрованное соединение для общения между клиентом и сервером, что бы не дать возможность перехватить идентификатор сессии злоумышленнику для его дальнейшего использования. К примеру можно использовать протокол HTTPS
  • можно использовать дополнительные алгоритмы аутентификации сессии на случай, если все же сессия была перехвачена злоумышленником. Например, привязывать сессию к ip пользователя, дополнительно сохраняя ip внутри сессии и проверяя не изменился ли он при очередном обращении. В случае не совпадения сохраненного ранее ip с текущим уничтожать сессию
  • ставьте ограничение на время жизни сессии. Так как это время обновляется автоматически при очередном запросе к api, то я бы поставил его, к примеру, в 2 часа. То есть при не активности пользователя в течении 2 часов сессия уничтожается автоматически. Это уменьшит шанс переполнения хранилища сессий

И напоследок короткое демо видео, как работает авторизация в API, написанном на Yii, c хранением сессий в Redis

Copyright MobiDev

Разработка