Сайт без бекэнда: аутентификация пользователя в BaaS parse.com через социальные сети

в 2:22, , рубрики: BaaS, Facebook, javascript, nosql, parse.com, twitter, Администрирование баз данных, Веб-разработка, книги, облачный хостинг, социальные сети, хранение данных

Я буду каждое утро развертывать мир, как резиновую ленту на мяче для гольфа, а вечером завертывать обратно. Если очень попросишь — покажу, как это делается.

Р. Брэдбери

Введение

В статье описан Backend-as-a-Service подход к хранению и обработки данных. Рассказаны преимущества и недостатки представителя такого подхода — сервиса parse.com. Коротко представлен сервис аутентификации пользователей через соц. сети uLogin. Основное назначение — показать, как эти два сервиса могут взаимодействовать, чтобы проект не требовал регистрации пользователей по логину и паролю, но в то же время сохранилась возможность авторизации пользователей к действиям над объектами.

О BaaS и parse.com

Parse.com — один из самых популярных провайдеров backend-as-a-service (BaaS). BaaS подход позволяет не поднимать свой сервер для хранения и обработки данных приложения. Это используется в мобильных разработках и в обычном вебе. Parse.com имеет свои SDK под несколько платформ, в том числе серверных. Но я расскажу о javascript.

Возможность работать с базой данных через javascript, не поднимая свой сервер, открывает отличные возможности, например, для Single page application (SPA), которое можно хостить на Github Pages, Bitbucket и многих других бесплатных. Первый вопрос, который у меня возник, когда я услышал про работу с БД из клиентского кода — это разграничение прав доступа, так как ключи общеизвестны. Изучив документацию parse.com, я выяснил, что для этого используется авторизация пользователей. Каждый пользователь имеет свой логин и пароль. SDK имеет методы регистрации нового пользователя по логину и паролю, аутентификации по этим же данным. Можно добавить email, при этом сам parse.com умеет отправлять настраиваемые письма для верификации email.

Пример кода с регистрацией

var user = new Parse.User();
user.set("username", "my name");
user.set("password", "my pass");
user.set("email", "email@example.com");
 
// other fields can be set just like with Parse.Object
user.set("phone", "415-392-0202");
 
user.signUp(null, {
  success: function(user) {
    // Hooray! Let them use the app now.
  },
  error: function(user, error) {
    // Show the error message somewhere and let the user try again.
    alert("Error: " + error.code + " " + error.message);
  }
});

Разграничение прав доступа происходит по ACL, которые можно назначать создаваемым объектам. Например, установить для объекта доступ на публичное чтение, но оставив редактирование только авторизованному пользователю. Дополнительно можно устанавливать ограничения на методы по работе с таблицей. Например, можно разрешить всем запись в таблицу, но запретить чтение всем, кроме определенной группы (в случаях логирования, например). Кроме этого, администратору предоставляется мастер-ключ, с помощью которого можно из клиентского приложения получить полный доступ. Но в SPA такое недопустимо, так как ключ не спрятать. Разумеется, есть админка, где можно редактировать схемы, производить CRUD-операции над всеми данными.
Админка:
image

Постановка проблемы

Аутентификация пользователя только по логину и паролю сейчас может выглядеть грубо. Поэтому parse.com имеет в своём SDK класс Parse.FacebookUtils, с помощью которого можно свзязать аккаунты пользователя с соц. сетью. Но это единственная социальная сеть, которую можно использовать через SDK parse.com в javascript. Есть Twitter, но не для javascript. Что делать, если очень хочется аутентифицировать пользователей через другие соц. сети?

Cloud Code

Parse.com, к счастью, предоставляет не только хранилище данных, но и возможность выполнять некоторый код на их сервере. Фичу они назвали Cloud Code. Код создаваемых функций недоступен для пользователей, а значит это можно использовать при построении подписываемых секретным ключом запросах. Но это не особо-то и пригодилось.

Я начал выбирать сервисы, которые имеют готовые виджеты авторизации пользователей на сайтах, и остановился на uLogin. Пару слов о выборе. Когда-то мне попадался на глаза Логинза, но я остался им не доволен, так как там меня просили вводить логин и пароль даже для входа через Твиттер. Возможно, были какие-то другие, соц. приложения которых просили доступ на написание твитов (omg, бесит). Так вот, uLogin имеет хороший набор соц. сетей, готовый виджет, возврат accessToken в callback-фунции для javascript, а также позволяет создавать свои соц. приложения для аутентификации пользователей.

Так как parse.com просит регистрировать пользователей по логину и паролю, то я пришел к выводу, что эту пару можно получить, используя данные, возвращаемые uLogin. Для этого необходимо создать файл clound/main.js в вашем приложении, в котором нужно определить функцию. Назовём её «getCredentials».

Parse.Cloud.define("getCredentials", function(request, response) {
  var token = request.params.token;
  var userLogin, userPassword;

  response.success({'username': userLogin, 'password': userPassword});
});

Сервис uLogin позволяет настроить список полей, которые он будет возвращать. Но среди обязательных есть:
network – идентификатор соцсети пользователя,
profile – адрес профиля пользователя (ссылка на его страницу в соцсети, если удастся ее получить),
uid – уникальный идентификатор пользователя в рамках соцсети,
identity – глобально уникальный идентификатор пользователя.

identity — это, как правило, тоже url, похожий на profile, поэтому я решил склеивать логин пользователя (здесь username) из network и uid.
Для пароля же нужно что-то более секретное, поэтому я решил в качестве пароля использовать хеш от identity, token, secret.
identity для уникальности, token для секретности, т.к. каждый вход в uLogin будет создавать новый токен, известный только пользователю и самому uLogin, и secret — соль, хранимая в настройках приложения.
Этот подход плох тем, что с каждым новом входом через uLogin, будет новый token, а значит и новый пароль. Поэтому нужно каждый раз сохранять новый пароль пользователя. Однако, parse.com после входа пользователя достаточно долго сохраняет сессию, поэтому аутентифицироваться каждый раз не придется. Готовый Cloud Code.

Решение довольно подозрительное по качеству, но в некоторых случаях вполне работоспособно. Например, если отказаться от входа пользователей по их логинам и паролям. Но даже в этом случае, метод можно доработать и перестать обновлять пароль пользователя после получения токена. Как еще один вариант, это использовать в качестве временного пароля случайную строку вместо хеша (осознание этого пришло чуть позже).

Разумеется, временный пароль выдаётся после подтверждения токена, путём получения данных из uLogin. После этого мы определяем, есть ли у нас пользователь с таким логином. Если нет, то регистрируем. Записываем ему данные из соц. сети (псевдоним, аватар) в хранилище, возвращаем пару логин и пароль.
Взаимодействие с uLogin и c parse.com происходит по https, все данные доступны только самому пользователю.

Аутентификация на клиенте
Нужно совсем немного кода, чтобы получить аутентифицированного в parse.com пользователя.
Подключить виджет uLogin

<script src="//ulogin.ru/js/ulogin.js"></script>
<div id="uLogin" data-ulogin="display=small;fields=first_name,last_name;providers=vkontakte,odnoklassniki,mailru,facebook;hidden=other;redirect_uri=&callback=authCallback"></div>

Добавить код callback-функции.

var user;
window.authCallback = function (token) {
        Parse.Cloud.run('getCredentials', {token: token}, {
            success: function (data) {
                Parse.User.logIn(data.username, data.password, {
                    success: function () {
                        user = Parse.User.current();
                    },
                    error: function (user, error) {
                      //handle it
                    }
                });


            }
        });
    };

Здесь происходит вызов написанной нами ранее серверной функции "getCredentials" в Cloud Code.
При следующем заходе пользователя с помощью var user = Parse.User.current(); можно определить, аутентифицирован пользователь или нет. Если да, то виджет uLogin следует скрыть.

Плюсы и минусы одностраничного приложения с использованием parse.com

Плюсы

  • Нет своего сервера, который нужно поддерживать
  • Бесплатно 20GB для файлов, 20GB для данных, 2TB трафика, 30 запросов в секунду, нет ограничения по количеству пользователей
  • Возможность выполнять js-код на сервере
  • Работа с файлами (создание, получение содержимого, получение url)
  • Неплохая готовая аналитика с графиками, событиями и прочее.
Минусы:

  • Задержка получения данных
  • Нет автоматического резервного копирования (есть кнопка для создания дампа)
  • Нет автоматического очищения неиспользуемых файлов (есть кнопка для этого)
  • Необходимость проектирования с целью уменьшения количества запросов к бэкэнду
  • Зависимость от двух внешних сервисов: parse.com и uLogin (вторую зависимость можно легко заменить на свой сервер)
Замечание:

Возможно написание других клиентов к тем же общедоступным данным. Если клиентская сторона позволяет аутентифицировать пользователя через uLogin, то нет никаких преград, чтобы управлять данными пользователя. Недостаток в том, что лимит на количество запросов общий.

Демо-проект

Посмотрев на imhonet, bookmix, livelib у меня появилась мысль сделать свой сервис для хранения прочитанных книг. Главная цель которого — список книг, без лишних довесов вроде оценок, покупок, обложек. При выборе подобных сервисов, интересует надежность хранения данных, чтобы не вышло так, что проект закроется через год-другой, а данные будут утрачены. Поэтому при разработке своего сервиса я решил использовать внешние бесплатные хранилища данных, то есть BaaS. Это позволит не оплачивать хостинг, не беспокоится о проблемах обновления ПО. Статику планировал держать на Github Pages. Но раз сервис стал зависеть от parse.com, то и хостингом решил пользоваться тоже его. Для SPA решил использовать AngularJS, так как был с ним знаком и знал, что мне от него нужно. Знатоки разделения сущностей заметят странные зависимости между ними, но с каждым проектом на AngularJS я узнаю что-то новое о нём. Теперь знаю, как сделать лучше. Переписывать только из академических целей посчитал лишним.

Некоторые могут сказать, что регистрация пользователей только через соц. сети может повлечь серьезные трудности по восстановлению доступа к проекту в случае утраты доступа к соц. сети. Эту проблему можно было бы решить, настроив uLogin так, чтобы он спрашивал email пользователя (и сам верифицировал его), а затем восстанавливать доступ по email. Но в демо-проекте такого нет, так как список прочитанных книг общедоступен, а значит, в случае серьезных проблем, его можно просто продублировать на другую учётную запись.

Исходный код
Демо

P.S.: Демо — это мой продакшн, выключен не будет. От хабраэффекта возможны ошибки 155 (RequestLimitExceeded) при загрузке данных — это нормально, просто попробуйте позже.

Автор: getId

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js