На Хабре уже не раз и не два публиковались статьи о необходимости разрешить пользователю войти через Google/Twitter/Facebook и т.д. Собственно говоря, прогрессивное человечество давно решило, что требовать от пользователя придумывать логины и пароли — вчерашний день. В данной статье я хочу обсудить возникающие проблемы и способы их решения.
На данный момент я являюсь одним из разработчиков в команде энтузиастов, решивших написать эдакий информационно-справочный портал с элементами социальной сети для сообщества php-фреймворка Kohana. Аутентификация необходима для написания комментария/отзыва, участия в голосованиях и рейтингах и т.д. Просто прикрутить вход через OpenID/OAuth несложно, интересности появляются когда начинаешь задумываться — что дальше?
Наверное стоит заранее предупредить, что в принципе речь пойдет о теоретических изысканиях. Как только у нас будет готов описанный механизм, который будет не стыдно показать, мы его покажем.
Набросаем примерные таблицы базы данных, которые позволят нам хранить максимально возможный объем информации, которую мы сможем вытянуть с пользователя.
users
Тут все понятно, это основная таблица для хранения пользователя.
auth_data
Эта таблица для работы с внешними аккаунтами пользователей. Например, если у юзера A будет аккаунт на Гугле и еще один в Твиттере, то мы должны для одной записи в users предусмотреть две в auth_data — и чтобы ничего не потерялось!
Связи
Естественно, аккаунты надо привязывать к конкретному пользователю, поэтому в таблице auth_data появляется внешний ключ
Как ни странно, в таблицу users мы тоже добавляем внешний ключ
А теперь — собственно механизм аутентификации. По шагам.
Уф, быстренько описал рутину, давайте уже посмотрим различные случаи, требующие дополнительных усилий от программиста.
Частенько становится сложным вспомнить, под каким аккаунтом ранее заходил на сайт. Да и зачем, пускай сайт запоминает, это ему больше надо. Поэтому после успешного логина кидаем куку с токеном, дабы позволить пользователю в следующий раз залогиниться автоматом.
Это небезопасно, скажете Вы, и вероятно будете правы. Поэтому при генерации токена сохраняем также различную информацию о клиенте (IP, User-Agent и т.д. — выбор есть). Если пользователь явно выходит с сайта (через кнопку «выйти»), куку убиваем. Ну и при автологине потребуем подтверждение, если пользователю захочется изменить какие-то критичные данные (поменять мыло или логин). Для крайних случаев можно нарисовать чекбокс «не запоминать меня».
Естественно, в случае с OAuth для использования API сервиса (скажем, послать твит в свой аккаунт) необходим рабочий токен, поэтому надо либо не использовать «запоминание» вообще, либо, опять же, в нужный момент отправить к провайдеру для подтверждения.
Даже если пользователь разлогинился иэмигрировал в Албанию долго не появлялся, надо ему помочь с аутентификацией. Давайте вернемся в момент, когда пользователь залогинился. Выше я писал про специальную куку. Получается, что ее одной мало — кидаем вторую. С длительным сроком действия. Внутри храним идентификатор аккаунта, через который входили.
Вернулись обратно. И вот тут-то нам эта долгоиграющая кука поможет. На ее основе мы подсказываем пользователю что-то вроде «ты ж через Гугл заходил, дружище!». Естественно, если он предпочтет другой аккаунт, мы куку поменяем на новую. В конце концов, мы всего лишь сайт, нам не жалко.
Ага, под другим аккаунтом, говоришь! Значит будем дублировать пользователей? Нет, спасибо. С этим надо бороться, и мы приложим к этому все силы.
В первую очередь смотрим на ту самую куку-долгожителя. Она должна нам подсказать, что не все чисто, и потенциально мы создаем второй аккаунт. Поэтому спрашиваем пользователя, отдельный ли это аккаунт или все-таки существующий. Если отдельный, то после предварительной зачистки куков и прочей информации создаем новенькую запись с нулевым пробегом.
А вот если пользователь утверждает, что это все еще он, просто почему-то хочет войти непривычным для нас способом (все привычные есть у нас в auth_data, мы ведь помним об этом), то придется делать проверку. Навскидку вижу несколько вариантов — предложить залогиниться под оригинальным аккаунтом (т.е. под primary — вот еще одна причина в его указании), либо отправить код подтверждения на почту (если она в профиле пользователя указана). Формально, мы можем не требовать именно вход под primary, достаточно любого из уже подтвержденных — как захотите.
Если подтвердить свои права пользователь не смог, то удаляем свежесозданный аккаунт — нечего нам врать.
Действительно, ведь адрес электронной почты можно смело использовать как идентификатор пользователя. Поэтому даже если кука не найдена, но провайдер вернул нам знакомый
Зачем ждать, пока пользователь перепутает свои аккаунты? Пускай сам укажет, что у него там еще в загашнике есть. В профиле (только под primary-аккаунтом?) предусматриваем возможность добавить еще аккаунты, которые можно считать «своими». И не придется смущать человека непонятными вопросами.
Вы знали, что Github поддерживает OAuth v2? А он такой, он может. Для нашего проекта этот сервис особенный, можно сказать любимый. Как только пользователь входит через Github, мы сразу начинаем дополнительное облизывание.
Естественно, подводных камней будет еще много. Хотелось бы заранее обсудить описанные алгоритмы и возможные косяки/упущения, причем как со стороны посетителя сайта (насколько такая схема удобна, есть предложения, а может Вас постоянно что-то напрягает на подобных сайтах?), так и с точки зрения разработчиков (что реализовывали у себя, где могут быть косяки). Ведь в комментариях к статьям на Хабре зачастую намного больше полезной информации, чем в самой статье.
Небольшое предисловие
На данный момент я являюсь одним из разработчиков в команде энтузиастов, решивших написать эдакий информационно-справочный портал с элементами социальной сети для сообщества php-фреймворка Kohana. Аутентификация необходима для написания комментария/отзыва, участия в голосованиях и рейтингах и т.д. Просто прикрутить вход через OpenID/OAuth несложно, интересности появляются когда начинаешь задумываться — что дальше?
Наверное стоит заранее предупредить, что в принципе речь пойдет о теоретических изысканиях. Как только у нас будет готов описанный механизм, который будет не стыдно показать, мы его покажем.
Аутентификация
База данных
Набросаем примерные таблицы базы данных, которые позволят нам хранить максимально возможный объем информации, которую мы сможем вытянуть с пользователя.
users
Тут все понятно, это основная таблица для хранения пользователя.
id
— идентификатор пользователя. Автоинкремент. Этот идентификатор мы планируем использовать для связи с другими объектами сайта.username
— видимое на сайте имя пользователя (Display Name). Изначально мы генерируем его на основании OpenID или данных OAuth, но обязательно надо дать возможность его сменить.email
— адрес электронной почты. Изначально пытаемся выдернуть из используемого внешнего аккаунта. Не всегда это возможно, поэтому поле необязательное.
auth_data
Эта таблица для работы с внешними аккаунтами пользователей. Например, если у юзера A будет аккаунт на Гугле и еще один в Твиттере, то мы должны для одной записи в users предусмотреть две в auth_data — и чтобы ничего не потерялось!
id
— в общем, все понятно. Синтетический автоинкрементый ключ.service_name
— имя провайдера, использованного для входа. Должно состоять из механизма аутентификации и собственно названия сервиса. Например, если мы вошли через OpenID под своим аккаунтом livejournal, то получим 'openid.livejournal', а 'oauth2.github' означает наш гитхабовский аккаунт, доступный нам через OAuth v2.service_id
— идентификатор пользовательского аккаунта на вышеупомянутом сервисе. Т.е. логин. В паре сservice_name
мы получаем уникальный индекс, больше на сайте таких пользователей быть не должно.email
— опять мыло! Его пользователь поменять не может, и мы можем его использовать для своихгрязныхцелей, например для поиска дублирующих пользователей или генерации человеческого логина.some_data
— сюда скидываем все остальное (в сериализованном виде), полученное от провайдера. А вдруг пригодится? Конечно, поле можно и выкинуть, оно дляскопидомовзапасливых и прозорливых хозяев ;)
Связи
Естественно, аккаунты надо привязывать к конкретному пользователю, поэтому в таблице auth_data появляется внешний ключ
user_id
. Как ни странно, в таблицу users мы тоже добавляем внешний ключ
auth_id
, чтобы привязать пользователя к какому-то аккаунту. Зачем? Таким образом мы хотим выделить некий главный (primary) аккаунт. Например, чтобы автоматом перенести какие-то данные из него в профиль пользователя, или для реализации автологина. Решение может показаться спорным, но мы в данный момент считаем его обоснованным и полезным. Впустите меня, изверги!
А теперь — собственно механизм аутентификации. По шагам.
- Кликнули по кнопке «войти через», ввели свой OpenID или просто нажали на картинку провайдера. Подтвердили/доказали провайдеру, что Вы — это Вы.
- После того, как провайдер отправил Вас обратно на сайт, необходимо проверить наличие данного пользователя в БД. А вдруг Вы уже заходили? Для этого используем упомянутую выше комбинацию
service_id
+service_name
. Если аккаунт найден, то все просто — извлекаем пользователя поuser_id
и вперед. - Но нет, аккаунт ранее не попадал в поле зрения нашего сайта, поэтому давайте быстренько его зарегистрируем, пока он не передумал.
В auth_data заносим все, что отдал провайдер. На основании этих данных пытаемся сгенерироватьusername
пользователя. Сохраняем нового пользователя, указываем использованный аккаунт как primary. Ура, у нас есть новый пользователь!
Ну и что тут обсуждать-то?
Уф, быстренько описал рутину, давайте уже посмотрим различные случаи, требующие дополнительных усилий от программиста.
Запомните меня!
Частенько становится сложным вспомнить, под каким аккаунтом ранее заходил на сайт. Да и зачем, пускай сайт запоминает, это ему больше надо. Поэтому после успешного логина кидаем куку с токеном, дабы позволить пользователю в следующий раз залогиниться автоматом.
Это небезопасно, скажете Вы, и вероятно будете правы. Поэтому при генерации токена сохраняем также различную информацию о клиенте (IP, User-Agent и т.д. — выбор есть). Если пользователь явно выходит с сайта (через кнопку «выйти»), куку убиваем. Ну и при автологине потребуем подтверждение, если пользователю захочется изменить какие-то критичные данные (поменять мыло или логин). Для крайних случаев можно нарисовать чекбокс «не запоминать меня».
Естественно, в случае с OAuth для использования API сервиса (скажем, послать твит в свой аккаунт) необходим рабочий токен, поэтому надо либо не использовать «запоминание» вообще, либо, опять же, в нужный момент отправить к провайдеру для подтверждения.
Запомнили? Нет? Вспоминайте!
Даже если пользователь разлогинился и
Вернулись обратно. И вот тут-то нам эта долгоиграющая кука поможет. На ее основе мы подсказываем пользователю что-то вроде «ты ж через Гугл заходил, дружище!». Естественно, если он предпочтет другой аккаунт, мы куку поменяем на новую. В конце концов, мы всего лишь сайт, нам не жалко.
Вход с разных аккаунтов
Ага, под другим аккаунтом, говоришь! Значит будем дублировать пользователей? Нет, спасибо. С этим надо бороться, и мы приложим к этому все силы.
В первую очередь смотрим на ту самую куку-долгожителя. Она должна нам подсказать, что не все чисто, и потенциально мы создаем второй аккаунт. Поэтому спрашиваем пользователя, отдельный ли это аккаунт или все-таки существующий. Если отдельный, то после предварительной зачистки куков и прочей информации создаем новенькую запись с нулевым пробегом.
А вот если пользователь утверждает, что это все еще он, просто почему-то хочет войти непривычным для нас способом (все привычные есть у нас в auth_data, мы ведь помним об этом), то придется делать проверку. Навскидку вижу несколько вариантов — предложить залогиниться под оригинальным аккаунтом (т.е. под primary — вот еще одна причина в его указании), либо отправить код подтверждения на почту (если она в профиле пользователя указана). Формально, мы можем не требовать именно вход под primary, достаточно любого из уже подтвержденных — как захотите.
Если подтвердить свои права пользователь не смог, то удаляем свежесозданный аккаунт — нечего нам врать.
Почту смотри!
Действительно, ведь адрес электронной почты можно смело использовать как идентификатор пользователя. Поэтому даже если кука не найдена, но провайдер вернул нам знакомый
email
— предлагаем все тот же диалог с выбором пути (слияние аккаунтов либо новый). Эх, жаль, что далеко не все аккаунты отдают мыло… Флаг тебе в руки!
Зачем ждать, пока пользователь перепутает свои аккаунты? Пускай сам укажет, что у него там еще в загашнике есть. В профиле (только под primary-аккаунтом?) предусматриваем возможность добавить еще аккаунты, которые можно считать «своими». И не придется смущать человека непонятными вопросами.
Про Github
Вы знали, что Github поддерживает OAuth v2? А он такой, он может. Для нашего проекта этот сервис особенный, можно сказать любимый. Как только пользователь входит через Github, мы сразу начинаем дополнительное облизывание.
- Я не говорил об этом, но в таблице users мы еще и поле
developer_id
храним. Это ключ для связи с таблицей developers (привет тебе, Кэп!), в ней мы храним разработчиков, которых нам сдал Github через свой API. Дело в том, что мы собираем разработанные модули для Kohana (это одна из основных задач проекта), так что очень полезно знать, что кто-то из посетителей является автором некоторых модулей. - Никогда не бывает лишним собрать дополнительную информацию от пользователя. А если пользователь — программист, то эта информация становится еще полезнее (кхе-кхе). После успешной регистрации напоминаем Гитхабберу, что у нас есть N модулей его авторства, и он может подредактировать какие-то данные (совместимость, версии, описание и т.д.). Нет, так нет. Сами справимся. Но все равно будем отправлять разработчику на почту уведомления о добавлении новых модулей за его авторством (ладно-ладно, опцию отключения уведомлений тоже сделаем).
О чем это я?
Естественно, подводных камней будет еще много. Хотелось бы заранее обсудить описанные алгоритмы и возможные косяки/упущения, причем как со стороны посетителя сайта (насколько такая схема удобна, есть предложения, а может Вас постоянно что-то напрягает на подобных сайтах?), так и с точки зрения разработчиков (что реализовывали у себя, где могут быть косяки). Ведь в комментариях к статьям на Хабре зачастую намного больше полезной информации, чем в самой статье.