Приложения для Tarantool 1.7. Часть 2. OAuth2-авторизация +39


Как построить свое приложение для Tarantool и при этом не городить огород каждый раз, когда требуется сделать, казалось бы, элементарную вещь? Это продолжение цикла статей о том, как создавать свои приложения для Tarantool.


Сегодня мы рассмотрим вопросы сетевого взаимодействия, установки и использования сторонних модулей.



Содержание цикла «Приложения для Tarantool 1.7»


Часть 1. Хранимые процедуры
Часть 2. OAuth2-авторизация
— Часть 3. Тестирование и запуск


Взаимодействие с внешними сервисами


В качестве примера рассмотрим реализацию OAuth2-авторизации через Facebook в приложении tarantool-authman. При OAuth2-авторизации пользователь переходит по ссылке, которая ведет на окно логина в социальной сети. После ввода авторизационных данных и подтверждения разрешений (permisssions) соцсеть переадресует пользователя обратно на сайт с авторизационным кодом в GET-параметре запроса. Сервер должен обменять этот код на токен (либо на пару токенов — access и refresh). С токеном можно получить информацию о пользователе из социальной сети. Подробнее о OAuth2-авторизации написано в этой статье.



Tarantool-приложение возьмет на себя обмен авторизационного кода (code) на токен доступа (token) к информации о пользователе из социальной сети, а также получит по этому токену пользовательские данные. В нашем случае это email, имя и фамилия. Для обмена авторизационного кода на токен доступа нужно отправить запрос к Facebook с кодом, а также параметрами приложения Facebook — client_id и client_secret.


В Tarantool с версии 1.7.4-151 встроен модуль http.client, работающий на базе libcurl. Модуль позволяет принимать и отправлять HTTP-запросы. Воспользуемся этим модулем для реализации OAuth2-авторизации. Для начала создадим вспомогательную функцию для отправки HTTP-запросов в модуле authman/utils/http.lua:


local http = {}
local utils = require('authman.utils.utils')
local curl_http = require('http.client')

-- config — общая конфигурация приложения authman
function http.api(config)
    local api = {}
    -- Конфигурация сетевых запросов
    local timeout = config.request_timeout

    function api.request(method, url, params, param_values)
        local response, body, ok, msg

        if method == 'POST' then
            -- utils.format — функция для подстановки значений в placeholder’ы
            body = utils.format(params, param_values)

            -- Безопасный вызов pcall не прервет исполнение программы при ошибке сети
            ok, msg = pcall(function()
                response = curl_http.post(url, body, {
                        headers = {['Content-Type'] = 'application/x-www-form-urlencoded'},
                        timeout = timeout
                })
            end)
        end
        return response
    end

    return api
end

return http

Стоит обратить внимание на функцию pcall. Она обрабатывает исключения, возникшие при выполнении анонимной функции. В нашем случае необходимо обработать сетевые ошибки, которые генерирует HTTP-клиент. Результат вызова pcall записывается в переменные ok (true/false) и msg (сообщение об ошибке, nil при успехе).


OAuth2-авторизации в приложении


Создадим модель social и напишем метод для получения токена по авторизационному коду get_token(provider, code), а также метод для получения или обновления данных профиля get_profile_info(provider, token, user_tuple). Рассмотрим эти методы:


-- Метод получения токена
function model.get_token(provider, code)
    local response, data, token
    if provider == 'facebook' then
        -- Здесь http — модуль authman/utils/http.lua
        response = http.request(
            'GET',
            'https://graph.facebook.com/v2.8/oauth/access_token',
            '?client_id=${client_id}&redirect_uri=${redirect_uri}&client_secret=${client_secret}&code=${code}',
            {
                -- config — конфигурации проекта, которые передаются в модель при инициализации
                -- Это параметры приложения в социальной сети
                client_id = config[provider].client_id,
                redirect_uri = config[provider].redirect_uri,
                client_secret = config[provider].client_secret,
                code = code,
            }
        )
        if response == nil or response.code ~= 200 then
            return nil
        else
            data = json.decode(response.body)
            return data.access_token
        end
    end
end

-- Метод получения профиля пользователя
function model.get_profile_info(provider, token, user_tuple)
    local response, data
    user_tuple[user.PROFILE] = {}

    if provider == 'facebook' then
        response = http.request(
            'GET',
            'https://graph.facebook.com/me',
            '?access_token=${token}&fields=email,first_name,last_name',
            { token = token }
        )

        if response == nil or response.code ~= 200 then
            return nil
        else
            data = json.decode(response.body)
            user_tuple[user.EMAIL] = data.email
            user_tuple[user.PROFILE][user.PROFILE_FIRST_NAME] = data.first_name
            user_tuple[user.PROFILE][user.PROFILE_LAST_NAME] = data.last_name
            return data.id
        end
    end
end

Теперь добавим метод API приложения, позволяющий создать пользователя или войти под уже существующим через Facebook. Метод будет возвращать пользователя вместе с сессионными данными. Подробнее о том, как формируется и валидируется сессия, можно посмотреть в исходном коде.


-- Метод api в authman/init.lua
function api.social_auth(provider, code)
    local token, social_id, social_tuple
    local user_tuple = {}

    if not (validator.provider(provider) and validator.not_empty_string(code)) then
        return response.error(error.WRONG_PROVIDER)
    end

    -- Получим OAuth2-токен
    token = social.get_token(provider, code, user_tuple)
    if not validator.not_empty_string(token) then
        return response.error(error.WRONG_AUTH_CODE)
    end

    -- Получим информацию о пользователе
    social_id = social.get_profile_info(provider, token, user_tuple)
    if not validator.not_empty_string(social_id) then
        return response.error(error.SOCIAL_AUTH_ERROR)
    end

    user_tuple[user.EMAIL] = utils.lower(user_tuple[user.EMAIL])
    user_tuple[user.IS_ACTIVE] = true
    user_tuple[user.TYPE] = user.SOCIAL_TYPE

    -- Проверим, есть ли в space пользователь с таким же social_id
    social_tuple = social.get_by_social_id(social_id, provider)
    if social_tuple == nil then
        -- Если нет — создадим его
        user_tuple = user.create(user_tuple)
        social_tuple = social.create({
            [social.USER_ID] = user_tuple[user.ID],
            [social.PROVIDER] = provider,
            [social.SOCIAL_ID] = social_id,
            [social.TOKEN] = token
        })
    else
        -- А если есть — обновим информацию профиля
        user_tuple[user.ID] = social_tuple[social.USER_ID]
        user_tuple = user.create_or_update(user_tuple)
        social_tuple = social.update({
            [social.ID] = social_tuple[social.ID],
            [social.USER_ID] = user_tuple[user.ID],
            [social.TOKEN] = token
        })
    end

    -- Создание пользовательской сессии
    local new_session = session.create(
        user_tuple[user.ID], session.SOCIAL_SESSION_TYPE, social_tuple[social.ID]
    )

    return response.ok(user.serialize(user_tuple, {
        session = new_session,
        social = social.serialize(social_tuple),
    }))
end

Как проверить, что метод работает? Для начала необходимо зарегистрировать приложение в Facebook. Сделаем это на странице разработчика Facebook. В созданном приложении нужно добавить продукт «Вход через Facebook» и указать redirect_uri — «Действительные URL-адреса для перенаправления OAuth». Параметр redirect_uri — это урл вашего сайта, куда социальная сеть перенаправит пользователя с параметром code после успешной авторизации в социальной сети. Далее откройте в браузере урл https://www.facebook.com/v2.8/dialog/oauth?client_id=${client_id}&redirect_uri=${redirect_uri}&scope=email, где


• client_id — id вашего приложения в Facebook;
• redirect_uri — урл для редиректа, который вы указали ранее;
• scope — список разрешений (в данном случае только email).


Facebook запросит подтверждение разрешений, после подтверждения переадресует вас с GET-параметром code. Это и есть тот самый авторизационный код, которой принимает метод api.social_auth(). Прежде чем проверять работоспособность кода, создадим конфигурационный файл authman/config/config.lua, в котором укажем настройки приложения Facebook.


return {
    facebook = {
        client_id = 'id from fb application',
        client_secret = 'secret from fb application'',
        redirect_uri='http://redirect_to_your_service',
    }
}

Теперь проверим, что код работает и приложение получает информацию о пользователе из социальной сети:


$ tarantool
version 1.7.4-384-g70898fd
type 'help' for interactive help
tarantool> config = require('config.config')
tarantool> box.cfg({listen = 3331})
tarantool> auth = require('authman').api(config)
tarantool> code = 'auth_code_from_get_param'
tarantool> ok, user = auth.social_auth('facebook', code)
tarantool> user
---
- is_active: true
  social:
    provider: facebook
    social_id: '000000000000001'
  profile: {'first_name': 'Иван', 'last_name': 'Иванов'}
  id: b1e1fe02-47a2-41c6-ac8e-44dae71cde5e
  email: ivanov@mail.ru
  session: ...
...

Установка сторонних модулей


Для решения многих задач хорошо иметь под рукой готовые решения. Например, в версиях Tarantool ниже 1.7.4-151 отправить HTTP-запрос «из коробки» было нельзя. Требовался модуль tarantool-curl. Сейчас этот модуль больше не поддерживается, не рекомендуется его использовать. Однако есть много других полезных модулей, один из них — tarantool-queue — реализация FIFO-очереди.


Есть несколько способов установить tarantool-queue. Первый, самый простой и удобный, появился относительно недавно, в версии Tarantool 1.7.4-294.


$ tarantoolctl rocks install queue

Другие пакеты Tarantool также доступны для установки с использованием менеджера пакетов. Полный список модулей для Tarantool можно найти на странице Rocks.


Второй способ — с помощью менеджера пакетов вашей ОС. Здесь необходимо подключить репозиторий Tarantool, если вы еще не подключили его на этапе установки, а также убедиться, что соответствующий пакет есть в репозитории. Например, для Ubuntu:


$ sudo apt-get install tarantool-queue

Третий способ сложнее, однако позволяет использовать не только приложения для Tarantool, но и готовые модули Lua. Устанавливать модули для Tarantool и Lua удобно с помощью менеджера пакетов LuaRocks. Подробно о нем и доступных модулях можно прочитать в документации. Установим LuaRocks и настроим его для работы с репозиторием Tarantool:


$ sudo apt-get install luarocks

Теперь настроим LuaRocks, чтобы устанавливать не только Lua-пакеты, но и пакеты для Tarantool. Для этого требуется создать файл ~/.luarocks/config.lua со следующим содержимым:


rocks_servers = {
    [[http://luarocks.org/repositories/rocks]],
    [[http://rocks.tarantool.org/]]
}

Установим сам модуль и проверим его работу:


# Установка tarantool-queue
$ sudo luarocks install queue

# Запустим интерактивную консоль и проверим работоспособность модуля:
$ tarantool
version 1.7.3-433-gef900f2
type 'help' for interactive help
tarantool> box.cfg({listen = 3331})
tarantool> queue = require('queue')
tarantool> test_queue = queue.create_tube('test_queue', 'fifo')
tarantool> test_queue:put({'task_1'})
tarantool> test_queue:put({'task_2'})
tarantool> test_queue:take()
---
- [0, 't', ['task_1']]
...

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




К сожалению, не доступен сервер mySQL