86.91
Rating
ABBYY
Решения для интеллектуальной обработки информации

ABBYYOnline — взгляд архитектора

ABBYY corporate blog
Сегодня мы поговорим о распределенных, гетерогенных и местами довольно серьезно и разнообразно нагруженных интернет приложениях на примере портала http://www.abbyyonline.com. Я постараюсь не углубляться в технические подробности, имеющие отношение к конкретной платформе, и, хотя основная часть описанного приложения реализована на ASP.Net MVC, возможно, данный материал заинтересует всех, имеющих отношение к веб-разработке, вне зависимости от используемого инструментария.

Каждый уважающий себя разработчик, любящий свою профессию и получающий от нее истинное удовольствие, приступая к очередному проекту, стремится написать идеальное приложение – оптимальное, с прекрасной расширяемой архитектурой и красивым кодом… И всегда этому мешают две фундаментальные проблемы:
  1. Несовершенство окружающего нас мира
  2. Требования бизнеса

В проекте AbbyyOnline также пришлось сталкиваться с ними на каждом шагу. В данном случае несовершенство мира заключается в том, что AbbyyOnline (или AOL по внутреннему названию) представляет собой Web приложение, а в нашем несовершенном мире такие приложения обременены отсутствием состояния (то есть они являются настолько stateless, насколько это возможно), ограничены возможностями сети (латентность, пропускная способность), вынуждены приспосабливаться под серпентарий браузеров и обречены на реализацию посредством целого зоопарка технологий, от клиентских скриптов и, упаси байт, флеша до серверного кода и очередного диалекта SQL-я.

Бизнес не менее жесток… По задумке маркетинга AOL должен быть лицом всей компании на просторах интернета, чтобы все услуги, которые ABBYY оказывает онлайн, были представлены на этом портале.

У ABBYY довольно четкое позиционирование – это языки, распознавание и все, что с этим связано, то есть, обработка текстов в том или ином виде. Естественно, захотелось, чтобы и онлайн услуги были оказаны единым фронтом. Разумно? Безусловно, но что это означает на практике? Как минимум, нужна единая база пользователей, должна быть реализована Single SignOn аутентификация, то бишь, если пользователь залогинился на одном сервисе, то и остальные должны узнавать его в лицо без лишних вопросов, должны быть единые элементы дизайна и логики, и все такое…

Все бы ничего, но каждый онлайн сервис компании – это отдельное и довольно серьезное приложение. И мало того, что приложения эти размещены на разных серверах (причем некоторые приложения физически находятся не на одном сервере и требуют для себя довольно серьезной инфраструктуры), так еще и сервера эти, по разным причинам, находятся на разных площадках. Не говоря уже о том, так исторически сложилось, что оные сервера оборудованы разными операционными системами (есть Windows сервера и FreeBSD, если интересуют подробности). Вообщем, по настоящему гетерогенная и распределенная система – и на этом фоне такой мелочью, как разные доменные имена для разных сайтов портала, можно уже пренебречь.

Общая структура

При создании такого рода порталов довольно много данных и страниц являются общими и было бы логично вынести все это в некий центральный сайт, являющийся связующим звеном. В нашем случае в задачу центрального сайта входит аутентификация, работа с общими страницами и работа с разделяемыми ресурсами. Этот центральный сайт оборудован WebService API, посредством которого остальные сайты, в задачи которых входит оказывать сервисные услуги, могут работать с общей инфраструктурой портала, например, получать полную информацию о текущем пользователе или помещать товар в общую корзину интернет-магазина. Сервисные сайты ничего не знают друг о друге, они подозревают только о существовании основного сайта портала и умеют работать с вышеупомянутым API.

Single SignOn и кросс-доменная аутентификация

Первая проблема – это единая база пользователей и Single SignOn аутентификация – в чем здесь сложность? Как мы уже выяснили, одна из особенностей веб приложений – это отсутствие состояния. На практике это означает, что каждый запрос на сервер из браузера не зависит от предыдущего и каждый раз сервер должен гадать «а кто же к нам пришел?». Традиционно эта задача решается посредством записи специальной аутентификационной куки, которую браузер передает обратно серверу при каждом запросе до тех пор, пока срок действия этой куки не истечет. Но дело в том, что из соображений безопасности серверная часть сайта может прочитать только ту куку, которая записана по тому же домену второго уровня, например, сайт, развернутый по адресу http://finereader.abbyyonline.com сможет прочитать куку, которую записал сайт развернутый на http://www.abbyyonline.com, а вот сайт http://finereaderonline.com такую куку уже не прочитает ни при каких обстоятельствах. Однако по условиям нашей задачи разные части портала могут находится на разных доменах, таким образом, нам нужен некий механизм кросс-доменной аутентификации.

В принципе, известно два основных способа решения этой задачи – на редиректах (примерно как это было в .Net Passport, а затем в OpenID, LiveID, etc.) и яваскриптом, как это делает Google или яндекс в своих сервисах. Схема на редиректах работает примерно следующим образом:

При обращении к любой странице любого сайта портала нужно выяснить, знает портал этого пользователя или нет. Почему к любой странице, а не только к защищенной? Потому что ник-нейм, ссылку на профиль и часть другой персональной информации необходимо показывать на всех страницах, чтобы пользователь знал, что мы его помним и рады ему :)

Для достижения этой цели выполняется следующая последовательность действий.
  1. Сайт пытается прочитать аутентификационную куку своего собственного производства. Если это удается, то здесь процесс аутентификации и заканчивается, так как, если такая кука есть, значит, пользователь здесь не в первый раз и мы его успешно опознали на этом сайте.
  2. Сайт пытается прочитать специальную куку, говорящую о том, что процесс аутентификации проводился, но не завершился успехом. Если удалось это, то здесь тоже можно заканчивать – мы знаем, что данного пользователя мы не знаем.
  3. Если предыдущие пункты успехом не увенчались, то…
    1. Сайт создает специальный уникальный токен
    2. В базе данных, в нарошной табличке, создается запись которая, помимо всего прочего, включает в себя токен и url страницы, которая затребовала аутентификацию.
    3. Сайт пишет специальную куку, которая говорит, что пока мы этого пользователя не знаем, но запрос на аутентификацию уже отправили. Роль этой куки вполне может исполнить обычная аутентификационная кука с признаком, что пользователь аноним – в этом случае отпадает необходимость в пункте номер 2 .
    4. Сайт делает редирект запроса на специальный url центрального сервера, передавая в строке запроса все тот же токен.

  4. Получив такой запрос центральный сервер…
    1. Пытается прочитать у пользователя свою аутентификационную куку.
    2. Находит в базе данных, в той самой нарошной табличке по токену переданному в строке запроса нужную запись и вписывает туда результат операции из предыдущего пункта.
    3. Центральный сервер производит редирект обратно на сайт запросивший аутентификацию, на специальный url все с тем же токеном в строке запроса

  5. Сайт, получив запрос на этот специальный url, который должен быть у каждого сайта портала…
    1. Пытается прочитать куку про то, что производился процесс аутентификации (мы ее писали в пункте 3с.) и если не удалось, значит, клиент не поддерживает запись куков, а без куков мы работать не умеем, поэтому нужно отправить пользователя на нарошную страницу, которая доходчиво доведет до его сознания этот прискорбный факт.
    2. Лезет в базу, все в ту же таблицу и по токену считывает результат аутентификации центральным сервером.
    3. Если аутентификация центальным сервером была успешна, то записывает свою аутентификационную куку, чтобы в следующий раз пользователя зря не гонять.
    4. Делает редирект обратно на страницу, которая инициировала процесс аутентификации.


При реализации аутентификации в AOL мы пошли вышеописанным путем, и в результате получилась довольно неплохая и компактная библиотечка для кросс-доменной аутентификации, которая отлично себя зарекомендовала в течение довольно длительного периода эксплуатации. При этом, если домен второго уровня у сайтов общий, как это происходит сейчас, то никаких ненужных редиректов не производится и вся аутентификация проходит прозрачно. Те сайты, которые работают на FreeBSD, вынуждены реализовывать клиентскую часть самостоятельно, но это довольно тривиальная задача. Вот еще несколько комментариев:
  • Общее хранилище для сессий не обязательно должно быть полноценной базой данных, можно использовать и стандартный Session Store, и хоть свой собственный WCF сервис с набором соответствующих интерфейсов, который держит все в памяти. В нашем случае сайты портала обмениваются данными с центральным сервисом посредством вышеупомянутого WebService API, а в качестве внутреннего хранилища используется база, так как она и так есть, и нет смысла плодить лишние сущности, занимаясь их поддержкой, но в целом хранилище заменяется легким движением руки...
  • Если, как в нашем случае, все сайты портала заранее известны, то можно применить следующую оптимизацию (сделать аутентификацию не ленивой, а жадной, пользуясь языком разработчиков =) ): вместо того, чтобы делать редирект за аутентификацией на центральный сервер каждый раз как пользователь пришел на новый сайт, можно сразу после того, как пользователь ввел правильную пару логин/пароль, пооткрывать в IFrame все сайты портала, чтобы редиректы произошли один раз сразу на все сайты (собственно в этом случае можно обойтись вообще без редиректов, передав в запросе токен и взяв нужную информацию из базы). Но в общем случае это менее надежно и, если что-то пойдет не так, то один или несколько сайтов пользователя видеть не будут, тогда как с остальными все будет в порядке, и как эту ситуацию разрулить с минимальным ущербом – не очень ясно.
  • Данная схема плоха тем, что не очень дружественна к поисковикам. Эти ребята терпеть не могут редиректов, а уж если какой анонимный краулер сказал, что куки поддерживает, а сам их не хранит, то получается совсем грустно. По этой причине следующая версия нашей библиотеки, скорее всего, будет по умолчанию работать по схеме с яваскриптом, она более дружественна к таким сценариям. Хотя в этом случае пользователи без яваскрипта окажутся пострадавшей стороной.

Создание общего дизайна.

Еще одна проблема, возникающая при создании большего, красивого, распределенного и гетерогенного портала, – это общие элементы дизайна и функционал по их отрисовке. Над каждой частью портала (отдельным сайтом), работает своя команда разработчиков и, как уже говорилось, развернуты эти сайты на разных серверах, сервера физически находятся в разных местах и даже операционные системы там разные. При этом нужно стараться выдержать общий стиль, да еще таким образом, чтобы при необходимости поменять его было достаточно просто и эта замена не требовала бы больших усилий со стороны всех команд, а в идеале, вообще обходилась без их участия. Таким образом, есть необходимость в создании некоторой библиотеки контролов отвечающих за UI, которую могли бы использовать все участники проекта, без излишних усилий со своей стороны.

Стандартные ASP.Net контролы и MVC шаблоны (aka Partial View) не очень удачно справляются с этой задачей, во-первых, их неудобно расшаривать между разными командами в разных проектах, и во-вторых, они совершенно бесполезны для ребят на FreeBSD. После некоторых раздумий, выстроилась следующая схема…

Ядром каждого общего контрола является связка xml/xsl. Локализованный xml такого контрола достается с центрального сервера посредством все того же API, что позволяет в некоторых случаях строить этот xml динамически на основании данных, доступных центральному серверу, например, для отрисовки различного рода меню. Xsl для получения итогового html-я располагается в ресурсах специальной сборки, относящейся к общим библиотекам, и в этой же сборке находится код по извлечению xml-я с центрального сервера, обработке его xsl-ем и правильному кешированию результата. Практически всегда для использования контрола на дочернем сайте достаточно вызвать метод с соответствующими параметрами и контрол будет отрисован, в редких случаях может понадобиться вклиниться в процесс отрисовки и немного его поправить под какую-нибудь специфичную задачу, что тоже не составляет проблем.

В проектах на FreeBSD и других альтернативных системах использовать подобного рода контолы также довольно удобно. Конечно, логику по получению xml-я, прикладыванию к нему соответствующего xsl-я и прочие boiler-plate приседания здесь приходится реализовывать самостоятельно, но большая часть проблем отрисовки общих элементов все равно оказывается решена. Во всех случаях Xml и xsl те же самые, и при необходимости централизованно изменить логику отрисовки или наполнение контролов не очень обременительно.

Интернет-магазин.

Еще один аспект жизнедеятельности портала, о котором имеет смысл рассказать, – это интерет-магазин. В перспективе, каждому сайту портала будет что продать и даже не только сайтам может оказаться полезной возможность оказать за небольшую денюшку какую-нибудь онлайн-услугу или продать какой-нибудь дистрибутив. Реализовывать каждому сервису свой магазин – идея малопродуктивная. Конечно, и она имеет свои плюсы, но минусов все-таки больше. Однако, на пути реализации централизованного магазина также есть препятствия и основное – заранее не известно, что придется продавать, и товар может быть самый разнообразный – от услуг до коробок. Да и, как уже упоминалось, продавать товары через общий магазин, в перспективе, может понадобиться не только сайтам, явно входящим в структуру портала.

Удачным решением оказалось разделение «витрины» магазина и собственно процесса оплаты заказа. Очевидно, обобщать витрину большого смысла не имеет – товар может быть самым разнообразным и универсального удобного дизайна подовсе не придумаешь, тем более что большая часть предполагаемого товара не известна на момент создания портала, да и пожелания по дизайну у всех участников забега могут быть самые противоречивые. Поэтому «витрину» магазина, то бишь, каталог товаров с ценой и прочим описанием, каждый сервис, желающий продавать, пусть реализует самостоятельно. А вот процесс помещения товара в корзину и дальнейшей оплаты этой корзины, имеет смыл делать общим для всех – логика там довольно мутная, но универсальная, поэтому ее можно реализовать один раз, и любое усовершенствование, например, подключение нового способа оплаты, автоматически заработает для всех сервисов.

Когда пользователь на сайте нажимает кнопку «поместить в корзину», сайт, прибегнув к помощи уже много раз упомянутого здесь API, сообщает AbbyyOnline, что такой-то пользователь (как мы помним, у нас есть отлично работающая система кроссдоменной аутентификации), хочет поместить в корзину такой-то товар, по такой-то цене и общая часть магазина запоминает все эти данные для дальнейшего формирования заказа.
Далее, перейдя в корзину, пользователь уже попадает на центральный сайт, где и происходит процесс оплаты и прочее взаимодействие с выбранной платежной системой. После того как заказ был успешно оплачен, центральный сайт рассылает всем сервисам, чьи товары были в корзине, уведомление о том, что за такой-то товар пользователь уже расплатился.

Хотя мы и отказались в самом начале от идеи централизованного магазина, общая витрина, где были бы представлены самые популярные товары всех сервисов, все-таки нужна. Это жутко полезная штука, как с точки зрения маркетинга и продаж – кроссбрендинг и все такое, так и с точки зрения дизайна и юзабилити – всегда нужна ссылка на «просто магазин», а не на какой-нибудь его конкретный раздел. Поэтому каждый сервис, имеющий желание представить свои товары на общей витрине, должен реализовать простенький RESTful метод, предоставляющий актуальную информацию для этой самой общей витрины.

Заключение

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

Иван Бодягин
Департамент продуктов для распознавания текстов
Tags:ABBYYabbyyonlineasp.net mvcsignonаутентификацияинтернет
Hubs: ABBYY corporate blog
+12
6.4k 19
Comments 21

Top of the last 24 hours

Information

Founded
Location
Россия
Website
www.abbyy.com
Employees
1,001–5,000 employees
Registered