## Please edit system and help pages ONLY in the master wiki! ## For more information, please see MoinMoin:MoinDev/Translation. ##master-page:HelpOnAuthentication/ExternalCookie ##master-date:2012-02-07T19:27:21Z #acl -All:write Default #format wiki #language ru = Аутентификация c использованием внешней cookie = <> == Требования == Авторизация с использованием внешней сессионной информации, передаваемой посредством cookie, может являться предпочтительной при тесной интеграции экземпляра вики МойнМойн с некоторым веб-приложением. В этом случае можно использовать вики для поддержки документации или отслеживания задач, равно как и ссылкаться на страницы вики со страниц веб-приложения. Также, вики-страницы могут включать специфические макрокоманды, использующие данные из приложения. В случае наличия механизма аутентификации в интегрируемом веб-приложении, необходимость авторизации как в приложении, так и на вики может раздражать как пользователей, котрым приходиться аутентифицироваться дважды (в приложении и на вики), так и администраторов, которым приходится поддерживать две независимых базы пользователей. Для аутентификации пользователя в МойнМойн посредством внешней cookie, веб-приложение, производящее аутентификацию, должно уметь (или быть можифицировано таким способом, чтобы) создавать cookie при каждой успешной аутентификации, равно как и удалять cookie при завершении сессии. Для предотвращения неавторизованной генерации сессионной cookie третьей стороной, необходима возможность аутентифицировать cookie, передаваемые МойнМойн. Одним из способов добиться этого является хранение хэшей cookie в базе данных, файле или ином защищённом хранилище (недоступном третьей стороне), доступном экземпляру МойнМойн. == Стратегия реализации == Код вики должен быть изменён в двух местах: в `wikiconfig.py` должен быть добавлен новый класс !ExternalCookie, который будет использоваться для аутентификации пользователя. При аутентификации в веб-приложении будет фактически выполнена аутентификация на вики. При завершении сессии в приложении также перестаёт быть доступна аутентификация на вики. Ряд переменных добавляется в конфигурацию экземпляра вики для изменения страницы «<>: <>» и для автоматического создания учётных записей при необходимости. Темы экземпляра вики могут быть модифицированы для изменения метода `username` класса `Theme`. Этот метод генерирует ссылки «<>» и «<>» в области навигации страницы вики. Эти ссылки должны быть модифицированы для ссылания на страницы аутентификации и завершения сессии в веб-приложении. Также необходимо добавить в веб-приложение механизмы создания/удаления cookie и (опционально) создания/удаления записей о сессиях в хранилище, в случае, если они отсутствуют. Если пользователи имеют доступ к вики без аутентификации, хорошей практикой является добавление перенаправления на referrer (страницу, с которой был осуществлён переход) страницы аутентификации в случае успешной аутентификации (аналогично тому, как ведёт себя процесс аутентификации в МойнМойн). == Нереализованные варианты == Синхронизация возможных времён неактивности сессии между приложением и экземпляром не является предпочтительной. Если сессия пользователя инвалидировалась после часа отсутствия активности, аутентификационная cookie может позволять редактировать и сохранять страницы на вики, только если веб-приложение не удаляет записи о просроченных сессиях из общего хранилища. Если же пользователь с просроченной сессией аутентифицируется вновь, то будет создана новая запись в общем хранилище и сгенерирована новая cookie [[ПомощьПоАутентификации|MoinAuth]] — последующие операции на вики будут использовать новую cookie. Приведённый в качестве примера вариант файла `wikiconfig.py` содержит пример того, как просроченные записи могут удаляться из таблицы !MySQL после истечения их срока жизни при выполнении операции на вики. В то же время, при развёртывании большинства экземпляров вики скорее всего будет принято решение удалять неактуальные записи в хранилище силами веб-приложения. Альтернативой является использование общего хранилища для шифрования cookie в веб-приложении и расшифровке её в методе `external_auth`. Если реализуется данный вариант, добавление временной метки к cookie приведёт к инвалидации просроченных cookie при проверки их времени жизни. Без этой проверки, любая сгенерированная cookie будет являться валидной сколь угодно долгий период времени до тех пор, пока не изменится метод шифрования. Тесты с !exPyCrypto показали, что время, затрачиваемое на расшифровку больше, чем проверка с использованием базы !MySQL: в случае использования базы данных время валидации cookie занимало обычно 0 секунд и не превышало значения в 0,02 секунды. Ещё одним способом фильтровать украденные cookie является добавление пользовательского IP-адреса к cookie и сравнение его с IP-адресом, с которого происходит запрос к МойнМойн. Но данный способ не является надёжным ввиду динамического назначения адресов некоторыми ISP и, как следствие необходимости постоянной повторной аутентификации, равно как использование reverse proxy/NAT приводит к тому, что множество пользователей приходят с одного адреса. == Класс ExternalCookie == Первым шагом является переопределение метода `external_auth` класса `Config` путём добавления примерно следующего кода в конфигурацию вики: Код, представленный ниже, предназначен для МойнМойн версии 1.9. * Откройте для редактирования файл `wikiconfig.py` (в случае отдельного экземпляра вики) или `farmconfig.py` (в случае использования [[КакНастраивать#multiwiki|вики-фермы]]) * Найдите строку, содержащую `class Config(DefaultConfig):` или `class FarmConfig(DefaultConfig):` * Замените данную строку кодом, представленным ниже * При использовании `farmconfig.py` найдите и закомментируйте/раскомментируйте соответствуеющие объявления «`class`» {{{#!python # +++++++++++++++ начало примера external_cookie # Данный пример кода может быть полезен при реализации аутентификации с # использованием внешней cookie (созданной внешней программой, не МойнМойн) # c МойнМойн. См. места, помеченные +++, для изменения согласно потребностям. # Данный код необходимсо скопировать в файл farmconfig.py или wikiconfig.py, # заменяя имеющуюся строку # # class Config(DefaultConfig): # или # class FarmConfig(DefaultConfig): from MoinMoin.config.multiconfig import DefaultConfig from MoinMoin.auth import BaseAuth # Данная функция добавлена в случае, если понадобится журналирование действий # во время тестирования import time def writeLog(*args): '''Write an entry in a log file with a timestamp and all of the args.''' s = time.strftime('%Y-%m-%d %H:%M:%S ',time.localtime()) for a in args: s = u'%s %s;' % (s,a) log = open('/somelogfile', 'a') # +++ расположение файла с журналом событий log.write('\n' + s + '\n') log.close() return # Представленные ниже два метода являются примером того, как можно # аутентифицировать по cookie другого приложения. import MySQLdb def verifySession(sidHash): # +++ для использования данного метода необходимо # раскомментировать его вызов ниже в # ExternalCookie. """Return True if sidHash value exists (meaning user is currently logged on), false otherwise. If you are not a MySQL user, find another way to store this information. ActiveSession is a two-column table containing a hashed cookie value (sidHash) plus a date-time stamp (tStamp). Your other application must add an entry to this table each time a user logs on and delete the entry when the user logs off. """ db = MySQLdb.connect(db='mydb',user='myid',passwd='mypw') #+++ пользователь должен иметь доступ на чтение c = db.cursor() q = 'select sidHash from ActiveSession where sidHash="%s"' % sidHash result = c.execute(q) c.close() if result == 1: return True return False def verifySessionPlus(sidHash,timeout=3600*4): # +++ для использования данного # метода необходимо # раскомментировать его # вызов ниже в # ExternalCookie. """Return True if sidHash value exists (meaning user is currently logged on), false otherwise. This version of verifySession deletes entries inactive for more than 4 hours and updates the tStamp field with each moin transaction. If performance is important, find another way to delete inactive sessions. """ db = MySQLdb.connect(db='mydb',user='myid',passwd='mypw') # +++ пользователь должен иметь доступ на запись c = db.cursor() q = 'delete from ActiveSession where tStamp<"%s"' % int(time.time() - timeout) # удаление неактивных записей result = c.execute(q) q = 'update ActiveSession set tStamp=%s where sidHash="%s"' % (int(time.time()),sidHash) result = c.execute(q) c.close() if result == 1: return True return False class ExternalCookie(BaseAuth): name = 'external_cookie' # +++ Следующие две строки могут быть полезны в случае, если # переопределяется метод username в используемых на вики темах. # В случае, если они закомментированы, страницы вики не будут содержать # ссылок на аутентификацию или завершение сессии. login_inputs = ['username', 'password'] # +++ необходимо для показа ссылки # на страницу аутентификации # в области навигации страниц # logout_possible = True # +++ необходимо для показа ссылки на завершение # сессии в области навигации вики-страниц. def __init__(self, autocreate=False): self.autocreate = autocreate BaseAuth.__init__(self) def request(self, request, user_obj, **kw): """Return (user-obj,False) if user is authenticated, else return (None,True). """ # login = kw.get('login') # +++ пример не использует данную переменную; # предполагается, что аутентификация # выполняется во внешнем приложении # user_obj = kw.get('user_obj') # +++ пример не использует данную переменную # username = kw.get('name') # +++ пример не использует данную переменную # logout = kw.get('logout') # +++ пример не использует данную переменную; # предполагается, что завершение сессии # выполняется во внешнем приложении import Cookie user = None # пользователь не аутентифицирован try_next = True # если значение равно True, МойнМойн попытается # воспользоваться следующем способом аутентификации # в списке доступных способов аутентификации. otherAppCookie = "MoinAuth" # +++ имя пользователя, почтовый адрес, псевдоним # пользователя и идентификатор сессии # разделяются символом "#" try: cookie = Cookie.SimpleCookie(request.cookies) # появилось в МойнМойн 1.9 except Cookie.CookieError: cookie = None # игнорировать невалидные cookie if cookie and otherAppCookie in cookie: # наличие данного cookie означает, # что аутентификация пользователя # уже выполнена во внешнем приложении import urllib if sys.version_info[:2] > (2, 5): cookievalue = cookie[otherAppCookie].value # МойнМойн 1.9 и Python 2.6 else: cookievalue = cookie[otherAppCookie] # МойнМойн 1.9 и Python 2.5 # writeLog('cookievalue',cookievalue) # +++ декодирование и обработка значения cookie - необходимо отредактировать # сообразно потребностям. #~ cookievalue = urllib.unquote(cookievalue) # значение cookie является urlencoded, # необходимо раскодировать его (МойнМойн 1.9) #~ cookievalue = cookievalue.decode('utf-8') # декодирование кодировки cookie в Unicode (МойнМойн 1.9) cookievalue = cookievalue[1: -1] # удаления кавычек в начале и в конце (МойнМойн 1.9) # writeLog(u'значение cookie',cookievalue) cookievalues = cookievalue.split('#') # cookie имеет вид имя_пользователя#почтовый_адрес#псевдоним#идентификатор_сессии email = aliasname = sessionid = '' try: # извлечение полей из cookie внешнего приложения auth_username = cookievalues[0] # имя пользователя на вики email = cookievalues[1] # почтовый адрес необходим пользователю для # изменения и сохранения пользовательских предпочтений aliasname = cookievalues[2] # псевдноим актуален только в случае, # если имя пользователя неудобно для # использования на вики sessionid = cookievalues[3] # опциональный идентификатор сессии # внешнего приложения - уникальная # временная метка или случайное число except IndexError: pass # псевдоним и идентификатор сессии не нужны, кроме случая, # если раскомментированы соответствующие строки ниже # writeLog(u'имя пользователя',auth_username) # writeLog(u'почтовый адрес',email) # writeLog(u'псевдоним',aliasname) # writeLog(u'идентификатор сессии',sessionid) # +++ так как кто угодно может сгенерировать cookie, далее представлена # проверка, что cookie создана именно внешним приложением if auth_username: import hashlib # +++ данная библиотека появилась в Python 2.5+; # см. http://code.krypto.org/python/hashlib # для версий Python 2.3, 2.4 sidHash = hashlib.md5(cookievalue).hexdigest() # МойнМойн 1.9 # writeLog('sidHash',sidHash) sidOK = verifySession(sidHash) # проверка, что пользователь аутентифицирован # во внешнем приложении # +++ или же использовать verifySessionPlus if not sidOK: auth_username = None # writeLog(u'имя пользователя после verifySession',auth_username) if auth_username: # пользователь аутентифицирован, необходимо создать объект, описывающий пользователя МойнМойн from MoinMoin.user import User # передача auth_username в конструктор User означает, что аутентификация уже выполнена. user = User(request, name=auth_username, auth_username=auth_username, auth_method=self.name) changed = False if email != user.email: # обновлялся ли почтовый адрес? user.email = email ; changed = True # если да, необходимо обновить профиль # if aliasname != user.aliasname: # +++ обновлялся ли псевдоним? # user.aliasname = aliasname ; # changed = True # если да, необходимо обновить профиль if user: user.create_or_update(changed) if user and user.valid: try_next = False # есть валидный пользователь; список доступных # методов аутентификации нет нужды более обрабатывать # writeLog(str(user), try_next) return user, try_next from MoinMoin.config import multiconfig, url_prefix_static class Config(multiconfig.DefaultConfig): # class FarmConfig(multiconfig.DefaultConfig): auth = [ExternalCookie(autocreate=True)] # аутентификация по внешней cookie является # единственным способом аутентификации # параметр autocreate появился в версии 1.8.0 # +++ Ниже представлены рекомендуемые изменения в форме пользовательских предпочтений # в случае, если external_cookie является единственным способом аутентификации user_form_disable = ['name', 'aliasname', 'email',] # показывать, но не давать изменять user_form_remove = ['password', 'password2', 'css_url', 'logout', 'create', 'account_sendmail','jid'] # удалить полностью #~ user_autocreate = True # +++ МойнМойн будет создавать учётный записи автоматически при их отсутствии cookie_lifetime = (12, 12) # (анонимная сессия, аутентифицированная сессия) по умолчанию равно (0,12) в МойнМойн 1.9 # +++++++++++++++ конец примера реализации external_cookie, далее следует остальная конфигурация экземпляра вики }}} == Начальное тестирование == В случае наличие Firefox с расширением Web Developer, начальное тестирование можно осуществить довольно быстро. Достаточно аутентифицироваться в веб-приложении и кликнуть в Tools — Web Developer — Cookies — View Cookie Information. Скорее всего, можно будет обнаружить как минимум одну cookie, которая выглядит как идентификатор сессии, созданный веб-приложением во время аутентификации, обычно она содержит большое случайное число и, вероятно, временную метку. Далее, необходимо открыть Tools — Web Developer — Cookies — Add Cookie. Дать cookie имя «!MoinAuth» (с сохранением регистра). Указать в качестве значения «имя_пользователя#почтовый_адрес» без пробелов и с использованием «#» в качестве разделителя. Если веб-приложение и экземпляр вики находятся на одном поддомене, задать FQDN тем же, что использует веб-приложение (если нет, не указывать поддомен в доменном имени для того, чтобы cookie была доступна со всех поддоменов домена). Установить путь cookie в «/». Установить переключатель «Session Cookie» и сохранить cookie. Далее, необходимо открыть новую вкладку и перейти на страницу вики. Вики должна показывать, что пользователь аутентифицирован. Далее можно проверить, что можно редактировать и сохранять вики-страницы и редактировать пользовательские предпочтения. == Изменения в веб-приложении == Приведённый в качестве примера метод `external_cookie` ожидает, что cookie будет содержать следующую информацию, разделённую символом «#»: * Идентификатор пользователя на вики — необходим всегда * Почтовый адрес пользователя — необходим, если необходимо предоставить пользователю возмодность обновлять и сохранять свои предпочтения на вики. * Псевдоним пользователя — обычно в нём нет необходимости, используется для переопределения идентификаторов пользователей, которые не являются [[ВикиИмя|ВикиИменами]]. * Уникальный идентификатор сессии — большое случайное число или временная метка и случайное число. Веб-приложение должно сохранять cookie с именем «MoinAuth» и путём «/». Обычно устанавливать путь в «/» не рекомендуется, так как это несёт с собой определённые риски по безопасности, так как оно позволяет приложениям под этим путём читать содержимое cookie. В данном случае это именно то, что необходимо — MoinMoin должен иметь возможность прочитать cookie, установленные другим приложением. Как было показано ранее, можно легко создать cookie с необходимой информацией. Для предотвращения подобных случаев МойнМойн должен проверять валидность cookie путём поиска соответствующей записи в защищённом хранилище, созданном веб-приложением. По возможности необходимо избежать создания дополнительных проблем с боезопасностью посредством создания связи имён пользователей и идентификаторов сессий или даже идентификаторов сессий, которые могут быть получены и использованы. Лучшим решением является хэширование cookie «MoinAuth» и сохранение результата в защищённом хранилище. Добавление временной метки к каждой записи позволит удалять просровенные записи в дальнейшем. В примере файла `wikiconfig.py` присутствует неиспользуемый код (метод `verifySession`), который выполняет хэширование содержимого cookie и проверку наличия результата в таблице MySQL. Кроме того, имеется альтернативный код (метод `verifySessionPlus`), который удаляет из таблицы записи старше 4 часов, проверяет хешированную cookie и обновляет временную метку. Лучшим способом в данном случае является модификация вбе-приложения для удаления устаревших записей из таблицы, возможно, при каждой аутентификации пользователя. Как только модификация веб-приложения будет начата, практически всё тестирование можно выполнять посредством расширения Firefox Web Developer. Перед аутентификацией в приложении cookie !MoinAuth должна отсутствовать, после успешной аутентификации она должна появляться, и исчезать при завершении сессии. Кроме того, защищённое хранилище должно пополняться хэшированными значениями cookie после аутентификации и очищаться от более неактуальных во время завершения сессии. Доступ к страницам вики при наличии аутентифицированной сессии должен приводить к создании cookie «MOIN_SESSION». == Модификация тем == Следующим шагом являетсямодификация ссылок на выполнение аутентификации и завершения сессии в навигационной области вики-страниц. Если пользователи вики могут использовать только одну тему, необходимо модифицировать код ниже для указания на страницы аутентификации/завершения сессии в приложении. Если же пользователи могут выбирать тему, возможно, проще всего будет модифицировать непосредственно файл `MoinMoin/theme/__init__.py`. Если аутентификация происходит строго перед доступом к вики-страницам, эта модификация, равно как и следующая, могут не выполняться. Если закомментировать переменные `login_inputs` и `logout_possible` в начале класса `ExternalCookie`, вики-страницы не будут содержать ссылки на аутентификацию и завершение сессии. Код, представленный ниже, предназначен для МойнМойн версии 1.9. {{{#!python def username(self, d): """ Assemble the username / userprefs link @param d: parameter dictionary @rtype: unicode @return: username html """ request = self.request _ = request.getText userlinks = [] # Добавить ссылку на домашнюю страницы пользователя для # зарегистрированных пользователей. Нет нужды проверять её # существование, пользователь может создать её. if request.user.valid and request.user.name: interwiki = wikiutil.getInterwikiHomePage(request) name = request.user.name aliasname = request.user.aliasname if not aliasname: aliasname = name title = "%s @ %s" % (aliasname, interwiki[0]) # ссылка на (внешнюю) домашнюю страницу пользователя homelink = (request.formatter.interwikilink(1, title=title, id="userhome", generated=True, *interwiki) + request.formatter.text(name) + request.formatter.interwikilink(0, title=title, id="userhome", *interwiki)) userlinks.append(homelink) # ссылка на действие userprefs if 'userprefs' not in self.request.cfg.actions_excluded: userlinks.append(d['page'].link_to(request, text=_('Settings'), querystr={'action': 'userprefs'}, id='userprefs', rel='nofollow')) if request.user.valid: if request.user.auth_method in request.cfg.auth_can_logout: userlinks.append('%s' % _('Logout', formatted=False)) # +++ страница завершения сессии во внешнем приложении else: query = {'action': 'login'} # Специальная ссылка напрямую на аутентификацию если методы аутентификации не требуют ввода. if request.cfg.auth_login_inputs == ['special_no_input']: query['login'] = '1' if request.cfg.auth_have_login: userlinks.append('%s' % _("Login", formatted=False)) # +++ страница аутентификации во внешнем приложении userlinks = [u'
  • %s
  • ' % link for link in userlinks] html = u'
      %s
    ' % ''.join(userlinks) return html }}} == Модификация страницы аутентификации веб-приложения == На последнем этапе можно также рассмотреть необходимость модификации страницы аутентификации для пользователей вики, решивших перейти на страницу аутентификации со страницы вики. Большинство веб-серверов передают приложению referrer запроса, содержащий страницы, с которой был осуществлён запрос данной. Можно рповерить его значение для определения, яляется ли referrer страницей вики и, если да, сохранить URL и перенаправить на него в случае успешной аутентификации.