
Взгляд в будущее
В последнее время всякие социальные сети и вообще сервисы-лидеры интернета по посещаемости и количеству аккаунтов завели очень неплохую, на мой взгляд, привычку — предоставление уникальных OpenID-идентификаторов для пользователей, дабы с их использованием можно было зайти на сторонний сайт. Кроме того, параллельно развивается очень похожая, но все-таки не совсем производная технология OAuth, которая появилась на свет благодаря стараниям создателей небезызвестного Twitter и, цитируя википедию, «позволяет предоставить третьей стороне доступ к защищенным ресурсам пользователя, без необходимости передавать ей (третьей стороне) логин и пароль».
Лично меня такая тенденция очень радует и, более того, я почти уверен, что за подобной технологией будущее. В частности, в будущем обязательно появятся новые мэшапы для агрегирования информации с кучи сайтов (в частности, хочется вспомнить очень хороший, но несправедливо забытый сервис Yahoo Pipes, который так и не смог покорить сердца и умы просто потому, что его время тогда еще не пришло. Возможно, все еще впереди), а именно такой «форм-фактор» требует логина на кучу сервисов сразу.
Петь дифирамбы подобным технологиям можно очень долго, но лично меня, например, всегда напрягали сайты, на которых надо с нуля регистрироваться, чтобы что-нибудь скачать. Ведь все мы неизменно сталкивались с тем, что когда ищешь, где скачать тот или иной материал — он зачастую оказывается на каком-то совершенно левом и непонятном сайте с названием в духе allbooksmusicwarezzz.omg.su, который ко всему прочему еще и регистрацию требует. Да нет, дело не в пиратстве, дело в том, что сайтов со всяким барахлом, сделанных на коленке, уйма. А вот человеческая память на логины-пароли ограничена, и тут уж ничего не сделаешь. Но приятный момент здесь еще и в том, что многие OpenID-провайдеры кроме информации, непосредственно служащей для авторизации, могут по запросу предоставить еще и базовую информацию о пользователе — e-mail, полное имя, предпочтительный язык и т.п. Причем на многих подобных сервисах можно управлять тем, что отдавать, а что сохранить в секрете. Например, разве пользователю не будет приятно, когда он, зайдя на очередной сайт, увидит приветливую надпись «Добро пожаловать, Вася!» на чистом русском языке, да еще и профиль уже готов к употреблению, вместе с аватаром, привычками и кличкой нежно любимого кота?
Делаем дело и работаем работу
Довольно лирики, думаю, кому оно надо как разработчику — он и так все вышенаписанное уже знает, а простым пользователям дальнейший материал вряд ли будет интересен. Еще больше хвалебных речей и рассуждений легко найти в блоге Ивана Сагалаева, а мы давайте попробуем сделать свою систему авторизации через OpenID (например, для блога) на Python, с преферансом и пианистками.
Для своего блога, который сейчас находится в разработке у меня в папочке Projects, я решил вообще отказаться от системы регистрации и авторизации через логин-пароль, а оставить только OpenID. В качестве фреймворка был выбран Pylons, а для прикручивания OpenID к Django-проектам существует и развивается проект с простым и понятным названием django-openid. Для Pylons, в общем-то, тоже существует решение под названием AuthKit, однако с ним у меня отношения как-то не очень сложились, а все, что я нашел в сети — это несколько сниппетов, в которых и пришлось разбираться.
Для начала надо установить модуль python-openid, чтобы обеспечить поддержку технологии, а потом создаем контроллер (обработчик запроса по URL, ближайшая ассоциация — джанговский views.py) и начинаем колдовать.
$ paster controller auth
Сразу оговорюсь, что код рабочий ровно до той степени, которая обеспечивает непосредственно аутентификацию, что делать дальше и как это все оформлять — решать только вам, господа творцы. Начало довольно стандартное:
Copy Source | Copy HTML
- from openid.consumer.consumer import Consumer, SUCCESS, FAILURE, DiscoveryFailure
- from openid.store import filestore
- from openid import sreg
- from datetime import datetime
- from hashlib import md5
-
- class AuthController(BaseController):
- def __before__(self):
- self.openid_session = session.get("openid_session", {}) # проверяем, не существует ли openid-сессии
-
- def index(self):
- return render('/accounts/enter.html')
-
- @rest.dispatch_on(POST="signin_POST") # разделяем GET- и POST-запросы по разным обработчикам для удобства
- def signin(self):
- if c.user: # проверяем, не попытался ли уже залогиненый юзер зайти еще раз
- session['message'] = 'Already signed in.'
- session.save()
- redirect(url(action='index')) # и если да, то не пущаем
- session.clear()
- return render('/index.html')
Теперь подходим к самому интересному:
Copy Source | Copy HTML
- def signin_POST(self):
- problem_msg = 'A problem ocurred comunicating to your OpenID server. Please try again.'
-
- g.openid_store = filestore.FileOpenIDStore('.') # создаем временное хранилище для хранения OpenID-данных, g здесь-массив глобальных переменных Pylons
-
- self.consumer = Consumer(self.openid_session, g.openid_store) # ага, вот и наш клиент
- openid = request.params.get('openid', None) # достаем из запроса строку с OpenID - идентификатором
- ...
Ага, а вот тут немного магии. SReg — это то самое расширение, которое позволяет нам запросить у сервера дополнительные данные о пользователе. Поля, значение которых хотелось бы узнать, перечисляем в списке optional, а дополнительные данные, если что, всегда можно запросить у пользователя потом. Если же какая-то дополнительная информация требуется прямо кровь из носу, то можно запросить ее в required, но если сервер ее не отдаст — будет ошибка.
Copy Source | Copy HTML
- ...
- sreg_request = sreg.SRegRequest(
- #required=['email'],
- optional=['fullname', 'timezone', 'language', 'email', 'nickname']
- )
-
- if openid is None:
- session['message'] = problem_msg
- session.save()
- return render('/index.html')
- ...
Здесь я позволил себе схалявить и написал этот код только для того, чтобы объяснить разницу между простым OpenID и кросс-логином с гугловского аккаунта. Дело в том, что гугл не представляет пользователям OpenID-идентификатора вида vasya_pupkin.google.com, а все куда проще и веселее. URL идентификации у всех пользователей гугла выглядит абсолютно одинаково — www.google.com/accounts/o8/id. Любопытно то, что при запросе на этот URL гугл отдает готовый XRDS (XML-подобного вида документ, возвращаемый сервером по стандарту OpenID 2.0), который уже содержит все необходимое для авторизации, а вам как пользователю присваивается уникальный ID, который и является, по сути, идентификатором OpenID.
Copy Source | Copy HTML
- if openid == 'google':
- openid = 'https://www.google.com/accounts/o8/id'
-
- try:
- authrequest = self.consumer.begin(openid) # панеслася
- except DiscoveryFailure, e: # а вдруг ошибка в адресе или такой провайдер существует только в твоем воображении?
- session['message'] = problem_msg
- session.save()
- return redirect(url(controller='auth', action='signin'))
-
- authrequest.addExtension(sreg_request) # подключаем SReg, дабы извлечь требуемые поля для профиля
-
- redirecturl = authrequest.redirectURL(h.url_for('/', qualified=True),
- return_to=h.url_for(action='verified', qualified=True),
- immediate=False
- ) # после всего, что у нас было с сервером, надо как-то жить дальше
- session['openid_session'] = self.openid_session
- session.save()
- return redirect(url(redirecturl))
Ну, теперь можно поговорить с сервером, останемся ли мы друзьями.
Copy Source | Copy HTML
- ...
- def verified(self):
- problem_msg = 'A problem ocurred comunicating to your OpenID server. Please try again.'
- self.consumer = Consumer(self.openid_session, g.openid_store)
- info = self.consumer.complete(request.params, (h.url_for(controller='auth',
- action='verified',
- qualified=True)))
- if info.status == SUCCESS: # все пучком
-
- sreg_response = sreg.SRegResponse.fromSuccessResponse(info) # извлекаем затребованные в SReg поля
-
- user = User(by_openid=info.identity_url) # ищем юзера по идентификатору в базе
-
- if not user.exist: # а вот тут можно делать что угодно. Например, внести юзера в базу
- newuser = User()
- try:
- email = sreg_response.get('email', u''),
- except:
- email = u''
- newuser.create(
- openid = unicode(info.identity_url),
- email = email,
- password = unicode(md5(info.identity_url).hexdigest()),
- ip = request.environ['REMOTE_ADDR']
- )
-
- session.clear() # мутим сессию
- session['openid'] = info.identity_url
- session.save()
-
- if 'redirected_from' in session:
- red_url = session['redirected_from']
- del(session['redirected_from'])
- session.save()
- return redirect(url(red_url))
- return redirect(url(controller='auth', action='index'))
- else: # факир был пьян
- session['message'] = problem_msg
- session.save()
- return redirect(url(action='signin'))
Вот, собственно, и все. Что делать с полученными данными — засовывать в куки, продолжать регистрацию и просить у пользователя дополнительную информацию — решать только вам. Да, и еще, данный код не работает с OpenID от Yahoo. Если охота по завещанию Козьмы Пруткова позрить в корень — есть информация все в том же блоге Ивана Сагалаева. Буду рад услышать любую критику, уточнения, предложения. Постараюсь в дальнейшем разобраться с OAuth и организовать интересующимся немного кода по кросслогину из твиттера.
За возможность почесать голову шилом, как Мастер Виноградинка, очень благодарю вот эту ссылку и всех товарищей, которые оставили там свои сниппеты.
UPD: Хабраюзер mustangostang раскрывает секреты AX (как получить возвращаемую информацию с гугла), ибо гугл SReg не отдает.