7 August 2010

Пишем себе немного OpenID-авторизации

Python
image

Взгляд в будущее


В последнее время всякие социальные сети и вообще сервисы-лидеры интернета по посещаемости и количеству аккаунтов завели очень неплохую, на мой взгляд, привычку — предоставление уникальных 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
  1. from openid.consumer.consumer import Consumer, SUCCESS, FAILURE, DiscoveryFailure
  2. from openid.store import filestore
  3. from openid import sreg
  4. from datetime import datetime
  5. from hashlib import md5
  6. class AuthController(BaseController):
  7. def __before__(self):
  8. self.openid_session = session.get("openid_session", {}) # проверяем, не существует ли openid-сессии
  9. def index(self):
  10. return render('/accounts/enter.html')
  11. @rest.dispatch_on(POST="signin_POST") # разделяем GET- и POST-запросы по разным обработчикам для удобства
  12. def signin(self):
  13. if c.user: # проверяем, не попытался ли уже залогиненый юзер зайти еще раз
  14. session['message'] = 'Already signed in.'
  15. session.save()
  16. redirect(url(action='index')) # и если да, то не пущаем
  17. session.clear()
  18. return render('/index.html')


Теперь подходим к самому интересному:

Copy Source | Copy HTML
  1. def signin_POST(self):
  2. problem_msg = 'A problem ocurred comunicating to your OpenID server. Please try again.'
  3. g.openid_store = filestore.FileOpenIDStore('.') # создаем временное хранилище для хранения OpenID-данных, g здесь-массив глобальных переменных Pylons
  4. self.consumer = Consumer(self.openid_session, g.openid_store) # ага, вот и наш клиент
  5. openid = request.params.get('openid', None) # достаем из запроса строку с OpenID - идентификатором
  6. ...


Ага, а вот тут немного магии. SReg — это то самое расширение, которое позволяет нам запросить у сервера дополнительные данные о пользователе. Поля, значение которых хотелось бы узнать, перечисляем в списке optional, а дополнительные данные, если что, всегда можно запросить у пользователя потом. Если же какая-то дополнительная информация требуется прямо кровь из носу, то можно запросить ее в required, но если сервер ее не отдаст — будет ошибка.

Copy Source | Copy HTML
  1. ...
  2. sreg_request = sreg.SRegRequest(
  3. #required=['email'],
  4. optional=['fullname', 'timezone', 'language', 'email', 'nickname']
  5. )
  6. if openid is None:
  7. session['message'] = problem_msg
  8. session.save()
  9. return render('/index.html')
  10. ...


Здесь я позволил себе схалявить и написал этот код только для того, чтобы объяснить разницу между простым 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
  1. if openid == 'google':
  2. openid = 'https://www.google.com/accounts/o8/id'
  3. try:
  4. authrequest = self.consumer.begin(openid) # панеслася
  5. except DiscoveryFailure, e: # а вдруг ошибка в адресе или такой провайдер существует только в твоем воображении?
  6. session['message'] = problem_msg
  7. session.save()
  8. return redirect(url(controller='auth', action='signin'))
  9. authrequest.addExtension(sreg_request) # подключаем SReg, дабы извлечь требуемые поля для профиля
  10. redirecturl = authrequest.redirectURL(h.url_for('/', qualified=True),
  11. return_to=h.url_for(action='verified', qualified=True),
  12. immediate=False
  13. ) # после всего, что у нас было с сервером, надо как-то жить дальше
  14. session['openid_session'] = self.openid_session
  15. session.save()
  16. return redirect(url(redirecturl))


Ну, теперь можно поговорить с сервером, останемся ли мы друзьями.

Copy Source | Copy HTML
  1. ...
  2. def verified(self):
  3. problem_msg = 'A problem ocurred comunicating to your OpenID server. Please try again.'
  4. self.consumer = Consumer(self.openid_session, g.openid_store)
  5. info = self.consumer.complete(request.params, (h.url_for(controller='auth',
  6. action='verified',
  7. qualified=True)))
  8. if info.status == SUCCESS: # все пучком
  9. sreg_response = sreg.SRegResponse.fromSuccessResponse(info) # извлекаем затребованные в SReg поля
  10. user = User(by_openid=info.identity_url) # ищем юзера по идентификатору в базе
  11. if not user.exist: # а вот тут можно делать что угодно. Например, внести юзера в базу
  12. newuser = User()
  13. try:
  14. email = sreg_response.get('email', u''),
  15. except:
  16. email = u''
  17. newuser.create(
  18. openid = unicode(info.identity_url),
  19. email = email,
  20. password = unicode(md5(info.identity_url).hexdigest()),
  21. ip = request.environ['REMOTE_ADDR']
  22. )
  23. session.clear() # мутим сессию
  24. session['openid'] = info.identity_url
  25. session.save()
  26. if 'redirected_from' in session:
  27. red_url = session['redirected_from']
  28. del(session['redirected_from'])
  29. session.save()
  30. return redirect(url(red_url))
  31. return redirect(url(controller='auth', action='index'))
  32. else: # факир был пьян
  33. session['message'] = problem_msg
  34. session.save()
  35. return redirect(url(action='signin'))


Вот, собственно, и все. Что делать с полученными данными — засовывать в куки, продолжать регистрацию и просить у пользователя дополнительную информацию — решать только вам. Да, и еще, данный код не работает с OpenID от Yahoo. Если охота по завещанию Козьмы Пруткова позрить в корень — есть информация все в том же блоге Ивана Сагалаева. Буду рад услышать любую критику, уточнения, предложения. Постараюсь в дальнейшем разобраться с OAuth и организовать интересующимся немного кода по кросслогину из твиттера.

За возможность почесать голову шилом, как Мастер Виноградинка, очень благодарю вот эту ссылку и всех товарищей, которые оставили там свои сниппеты.

UPD: Хабраюзер mustangostang раскрывает секреты AX (как получить возвращаемую информацию с гугла), ибо гугл SReg не отдает.
Tags:pythonopenidpylonsавторизацияаутентификация
Hubs: Python
+59
7.3k 106
Comments 18
Top of the last 24 hours