Account Manager: аккаунты, токены и все-все-все. Лекция Яндекса
Android предоставляет мощную систему работы с аккаунтами. Наличие Account Manager уже давно помогает Яндексу — за годы разработки приложений и сервисов мы накопили большой опыт, связанный с механизмами авторизации в Android. Узнать об этом опыте можно из лекции разработчика Кирилла Борисова. Заодно вы поймёте, как указанные системы пригодятся вашему приложению и как избежать подводных камней при взаимодействии с ними.
— Я хочу рассказать про одну небольшую подсистему Android. C ней редко кто сталкивается, слышали о ней совсем немногие, но она может пригодиться гораздо большему числу людей. Это Account Manager — страшная вещь, которая заведует аккаунтами, токенами и всем, что с этим связано.
Рассмотрим, что же такое аккаунт в Android в теории, сферический в вакууме. Затем поглядим на Account Manager как таковой, на сам сервис. И посмотрим, какой же был тернистый путь прошёл Яндекс в укрощении этой зверюги.
Представьте: вы обыкновенный разработчик, у вас есть маленькое приложение. Для большего понимания — это просто игра, где надо выстроить три штуки в ряд и все будет хорошо.
А теперь давайте сделаем так, чтобы результаты того, сколько частичек я поставил рядом, попали в интернет. Для этого понадобится какой-то облачный бэкенд, куда мы будем обращаться и выкладывать результаты.
Мы с бэкендом связаны, все хорошо.
В какой-то момент мы понимаем, что бэкенд, который не производит никакой аутентификации пользователя, — не самая полезная штука.
Хорошо, мы добавляем в приложение аутентификацию по OAuth. Все хорошо, но теперь необходимо где-то хранить все эти логины, пароли пользователей, токены, связанные с ними. Поэтому в нашем приложении валяется база с аккаунтами. И неожиданно простая идея похвастать перед друзьями обрастает кучей деталей.
Какие минусы этого подхода? Во-первых, ваши аккаунты, все авторизации, токены, связанные с ними, хранятся в самом приложении. Поэтому когда у пользователя перестанет хватать места на очередное фото очередного котика, он просто откроет список приложений, нажмет «очистить», и неожиданно все ваши достижения и логины пропадут. Печаль.
Во-вторых, необходимо полностью контролировать этот процесс. Знать, где какие токены, как их доставать, как обрабатывать, куда какие запросы кидать в OAuth. А представьте, что у вас больше одного приложения, которое использует один и тот же бэкенд. Каждому приложению придется дублировать эту логику и т. д.
И самая меньшая из ваших проблем — логиниваться и разлогиниваться придется через само приложение. Представьте, что у вас много приложений, не как у Яндекса, но хотя бы штук пять. Пользователь неожиданно захотел передать телефон жене, он разлогинивается в одном приложении, во втором, третьем, четвертом… Неудобно.
Как это улучшить? Можно воспользоваться функцией аккаунтов.
Вы все так или иначе встречались с аккаунтами. Это обыкновенный невзрачный пункт в меню настроек, вы увидите там странные названия — Google, Яндекс, добавить аккаунт. Список может варьироваться от телефона к телефону.
Это группа системных аккаунтов, которые используются различными приложениями, чтобы хранить данные о логинах централизованно: кто куда вошел, какие с этим связаны учетные данные и т. д.
Зачем это вам? Самое главное преимущество — хранение учетных данных происходит вне приложения. Теперь пользователь может удалять приложение, очищать, но если он один раз в нем залогинился, в следующий раз вы об этом будете знать. Это позволяет сильно снизить нагрузку как на ваш бэкенд, которому не придется постоянно обрабатывать «зайти», «выйти» и так далее, а также не заставлять пользователя раз за разом входить в приложение.
Во-вторых, вы можете абстрагировать логику авторизации от вашего приложения, и залогинившись один раз в одном приложении, вы можете сделать так, чтобы во всех приложениях, связанных с ним, эта аутентификационная информация была доступна.
Также появляется возможность управлять аккаунтами централизованно. Если хотите залогинить ВасюПупкин1, можете просто зайти в настройки, аккаунты, нажать удалить — все, как будто вас не было на телефоне.
Самая вкусная возможность — использовать Sync Adapter. Это вещь, ради которой вообще многие связываются с Account Manager.
Эта штука позволяет создавать некий код, который будет запускаться Android периодически, по запросу, по изменению данных в контент-провайдере, и будет позволять обновлять данные в фоне, не запуская для этого само приложение.
Так как может понадобиться какая-то аутентификация, аккаунты в системе также нужны этому Sync Adapter Framework, поэтому в Android они связаны практически намертво. Вы не можете сделать такую штуку по обновлению данных в фоне, если не добавите систему аккаунтов, а без Account Manager это сделать не получится.
Account Manager — обыкновенный системный сервис. Этакая абстрагированная прослойка между вашей бизнес-логикой и логикой авторизации, связанной с бэкендом.
По совместительству он является хранителем системной базы аккаунтов, той самой централизованной ямы, куда сваливаются логины и пароли ваших приложений.
Если вернемся к рассмотренной ранее схеме, что есть бэкенд, OAuth, приложение и так далее, то с добавлением Account Manager она будет выглядеть так.
Здесь все также наши друзья-облачка, есть приложение, но в нашу картину данных добавился прокси-слой — Account Manager. Когда приложение хочет куда-то залогиниться, в свой бэкенд, оно не идет напрямую в облачную систему аутентификации, не обращается к какому-то OAuth, оно идет в систему сервисов Account Manager и спрашивает: залогинь меня, пожалуйста, мне нужен токен, чтобы запрос кинуть.
Account Manager уже, пряча под капотом всю логику авторизации, позволяет приложению не задумываться о том, что же там происходит. Он попросил токен — токен вернулся. Либо не вернулся, что тоже бывает.
Если рассмотреть процесс поближе, выглядит он так. Для краткости здесь не показан слой Account Manager как такового, потому что он является прослойкой между вашим приложением и аутентификатором.
Есть некий getOauthToken, системный вызов, в который мы передаем системный аккаунт, он уходит в Account Manager, к аутентификатору, а тот уже перенаправляет его в нужный OAuth, запрашивает выдачу токена, получает его и отдает приложению.
Приложение в свою очередь присоединяет его к API запросу, кидает к бэкенду, бэкенд снова идет в OAuth, проверяет токен и возвращает ответ от API.
Процесс не сильно отличается от стандартной OAuth авторизации, но теперь вашему приложению не надо об этом думать, а главное — не надо мешать это с бизнес-логикой.
Что же будет, если у нас больше одного приложения? Я уже упоминал, что с помощью одного Account Manager можно сделать аутентификационную логику между несколькими приложениями. Все достаточно просто, если есть Яндекс.Почта, Яндекс.Музыка, то ходить они будут скорее всего в один бэкенд, если у них один и тот же тип аккаунтов, и они могут просто воспользоваться услугами Account Manager, который перекинет их запрос к единому коду, аутентификатору. И то, что он передаст этот токен, он сохранит в базе аккаунтов.
По какому принципу Account Manager понимает, что этому приложению можно давать доступ к этому типу аккаунтов его токеном, а этому нельзя? Все банально и просто: одинаковая подпись. Аккаунты и группа аккаунтов разделяются между приложениями, если у них подпись одинаковая. Если придет абстрактный Facebook, то он не сможет попросить данные Яндексовых аккаунтов, просто потому что Android скажет security exception, извини, твоя подпись не совпадает.
Если углубляться, то это связано с реализацией модели разграничения доступа в Android. Каждое приложение в терминах Linux, на основе которого построен Android, это как бы отдельный пользователь. У них есть отдельный номер UID, у каждого приложения своя папочка владельцев, у которых выставлен этот самый UID. То же самое происходит и с аккаунтами. Каждому типу аккаунтов соответствует некий UID. Приложение, которое заведует этими аккаунтами, в состав которых входит аутентификатор, должно совпадать по подписи со своими клиентами. Если у нас у аутентификатора была бы подпись не Яндекса, то Яндекс.Почта и Яндекс.Музыка не смогли бы получить доступ к аккаунту, даже если бы они знали, как они называются, знали их пароли и т. д.
К слову о Facebook, что произойдет, если в системе появится еще группа аккаунтов?
Ничего интересного, просто добавится еще одно приложение, еще одна группа аккаунтов в системный список, и рядом с иконкой Яндекса появится иконка Facebook. У них будут свои аккаунты, разделение между которыми обеспечивается Account Manager, Яндекс не видит Facebook и не может воспользоваться его аккаунтами, Facebook не может воспользоваться аккаунтами Яндекса. Всё просто.
Здесь стоит упомянуть, насколько это секьюрно. Раньше я хранил это рядом со своим приложением, знал, что это мое, никто это не трогал, а теперь оно хранится где-то там.
На самом деле эти аккаунты хранятся в системной базе, доступ к которой есть только у системных сервисов Android. Единственным нормальным способом получить оттуда данные, не являясь Android или приложением, которое за них отвечает, является получение root на телефоне. Но если у вас root на телефоне, то вам мало чем можно помешать. Вы можете и аккаунты забрать, и трафик снифать и т. д. Будем считать, что пока вы не ворвались в чужой телефон с помощью root, аккаунты достаточно секьюрны, это достаточная гарантия в большинстве случаев.
Аутентификатор — это сложное слово мелькает достаточно часто. Что оно из себя представляет?
Это банальнейший класс, который экстендит банальный абстрактный интерфейс из самого Android, AbstractAccountAuthenticator. Здесь не приведен полный пример этого класса, поскольку он является просто прокси, которая прокидывает запрос, приходящий с системного Account Manager, в свой связующий код.
Как он объявляется? Аутентификатор является частью какого-то приложения на Android, не являясь частью системы. Если на вашем устройстве будет появляться какой-то тип аккаунтов, значит, он будет связан с каким-то приложением. Нельзя просто так создать аккаунты Яндекса на телефоне, где нет ни одного приложения Яндекса. В приложении Яндекса, хотя бы в одном, должен находиться этот аутентификатор, который скажет системе, что привет, я отвечаю за такие типы аккаунтов, пожалуйста, считай меня тут главным.
Как он это говорит?
У нас есть такой XML-код, который описывается в манифесте приложения, которое экспортирует аутентификатор, и по сути, здесь минимально необходимый объем информации. Некий ID вашего типа аккаунта, который может совпадать или не совпадать с вашим application ID. Вы указываете, как показывать ваш аккаунт, например, Яндекс, Facebook. Также вы показываете две иконки, большую и маленькую. Все. Когда система замечает это в манифесте вашего приложения, вы можете отвечать за определённый тип аккаунтов.
А как в вашем приложении этим воспользоваться? Когда ваше приложение кидает запрос к Account Manager, Account Manager идет в ваш аутентификатор, который представляет из себя банальный сервис в вашем приложении.
Как и все сервисы, которые биндятся, вы просто возвращаете объект IBinder, реализующий интерфейс, и продолжается работа.
Связуется это все очень просто. В вашем интерфейсе вы прописываете meta-data, который указывает, что у вас есть некий аутентификатор.xml, и система начинает отвечать за этот тип аккаунтов, и говорить вашему приложению, вашему аутентификатору, что пришел запрос, дай мне токен, добавь аккаунт и т. д.
Давайте рассмотрим интересную ситуацию. Представьте, у вас есть сервис, аутентификатор, он встроен в ваше приложение. И тут в вашей системе внезапно появляется второе приложение, которое реализует этот же тип аккаунтов, там есть этот же аутентификатор. К примеру, вы не знаете, в каком порядке ваше приложение поставит пользователь. Если бы вы знали, что ваш пользователь всегда ставит одно приложение перед другим, вы могли бы реализовать этот код в первом приложении, а во втором вызывать его. Но жить более жестока, пользователь может поставить вначале Яндекс.Карту, потом Яндекс.Музыку, либо наоборот, либо вообще про них забыть и поставить Навигатор. На телефоне должно быть хотя бы одно приложение, реализующее аутентификатор. А их два.
С этим нет проблем. Систем возьмет один из них, как правило, принадлежащий приложению, появившемуся раньше, и будет общаться с ним. Остальные сервисы будут вертеться на фоне, система будет их игнорировать. Если приложение, содержащее Аутентификатор1, будет удалено, система просто прозрачно переключится на второй. И с точки зрения приложения ничего не изменилось, просто попросило токен, а кто его выдал, из какого приложения, его не должно беспокоить.
Это предъявляет требования к тому, чтобы код, который реализуется этим идентификатором, по возможности предоставлял один и тот же интерфейс, содержал одну и ту же логику между разными приложениями.
Казалось бы, причем тут Яндекс?
Необходимость того, чтобы наши пользователи находились в любом приложении Яндекса в своем аккаунте, залогинились в Музыке, потом поставили еще 20, и заставлять логиниться в каждом из них по отдельности жестоко и нечеловечно. Именно поэтому в глубинах нашей компании давно была рождена библиотека с вдохновенным названием Account Manager.
Это принесет нам дальше очень много боли. Сложно объяснять, как взаимодействует ваша библиотека Account Manager с сервисом Account Manager.
Она представляет из себя единое решение для работы с аккаунтами Яндекса. Если приложение Яндекса хочет работать с этими аккаунтами —, а оно может не хотеть, мы не против, это его право, — то должно интегрировать в себя эту библиотеку, чтобы между всеми приложениями не было разночтений о том, как логиниться в систему, как обращаться с аккаунтами. Это очень важное требование, когда ваш зоопарка приложений разрабатывает куча разных людей.
Оно используется почти во всех приложениях Яндекс, как на Android, так и iOS. Оно старается работать хорошо в достаточно нелегких условиях.
Наш первый подход содержал аж 4 версии и 83 подминорные версии, он реализовывал под собой подход «мастер — клиент».
В условиях ограниченного набора памяти, процессорного времени и прочее решили сделать так, что у нас будет отвечать за аккаунты ровно один работающий сервис, а в остальных приложениях в системе сервис аутентификатора как бы будет, но будет приглушен. Системе достаточно неинтересно это все. Она видит, что есть один сервис работающий, она к нему стучится, все хорошо.
Как бы все замечательно, но что же произойдет, если это приложение будет удалено?
Ничего хорошего.
Дальше между всеми приложениями Яндекса на вашем телефоне происходят выборы мастера. Каждое приложение получает список всех остальных приложений на телефоне, начинает его перебирать в поисках приложения с самой высокой версией, и в случае нескольких таких, с самой ранней датой установки. Если оно равно ему самому, то это приложение считает, что оно мастер, включает сервис. Когда оно это делает, система видит это, что появился новый сервис, тут же его подхватывает. Все хорошо.
Что мы не учли при выходе в этот жестокий мир? На достаточно медленных телефонах или на быстрых телефонах с гигантским количеством приложений Яндекса или приложений в принципе, система может не успевать замечать, что появился новый аутентификатор, там какие-то выборы происходит… Система тогда пополняется вопросительными знаками, пожимает плечами и удаляет все аккаунты Яндекса. Эта ситуация случалась часто, и с этим надо было что-то делать.
Мы придумали такую клевую штуку. Добавили каждому аутентификатору контент-провайдер, и под ним базу. Что же происходило? Когда было приложение мастер, когда оно получало информацию, обычно через системный бродкаст, что список аккаунтов изменился, оно шло в этот мастер и забирало себе копию его базы аккаунтов. Как локальный маленький бэкап. И это дало название схеме «мастер — клиент», по сути это мастер-клиентская репликация.
Что происходит в ситуации, когда появляется такой знак вопроса? Аккаунт-менеджер рано или поздно замечает, что в системе вновь появился кто-то, отвечающий за аккаунты Яндекса, в системной базе уже пусто. Оно идет к этому приложению, а то делает вид, что так и надо, все замечательно, просто оно берет теперь аккаунты не из системной базы, а из локального маленького бэкапа. С точки зрения пользователя ничего не произошло, вот такой список аккаунтов.
На деле список аккаунтов между несколькими приложениями иногда разъезжался. Бывало, что в Яндекс.Музыке один список, в Картах — другой, еще в Яндекс что-то там появились аккаунты, о которых вы давно забыли, но они где-то в бэкапе хранились. Это было неприятно и неудобно.
Схема, когда одно приложение стопалось, а другое работало в офлайне, нам сильно мешала. Особенно весело было, когда происходила такая ситуация.
При попытке скачать бэкап от мастер-приложения в клиентское приложение, это не получалось. К примеру, контент-провайдеру запретили доступ к другому контент-провайдеру.
В каких ситуациях это могло происходить?
Мы были очень удивлены, узнав, что наши добрые братья-китайцы в своих прошивках ради общечеловеческого блага реализовали энергосохранение. Добрая прошивка видела, что одно приложение пытается пойти в другое приложение и говорило: «Ты же его пробудишь! Заставишь есть батарейку! Так нельзя!» И втихую дропало запрос на получение данных, интент просто не долетал.
Нигде это не было прописано и документировано. Единственное, что нас навело на подозрение и заставило признать проблему, это когда мы увидели случайно в логе отладочное сообщение, что такой-то интент дропнут из-за правил внутреннего файервола.
Также эти прекрасные люди, бывало, блокировали наши запросы в сеть к бэкенду Яндекса для аутентификации и т. д.
К сожалению, с этим ничего толком поделать не удавалось. Непросто говорить с людьми, не знающими не только твоего, но и английского языка. И растущая доля этих устройств заставляла с ними считаться.
Недостатки такого подхода.
Задержка смены аутентификатора приводит к тому, что система удаляет аккаунты. Мастер в любой момент может быть убит. И автономная работа ведет к рассинхронизации.
Жизнь — боль. Так жизнь нельзя, надо что-то делать. В начале года мы предприняли волевое усилие и сделали второй подход к снаряду.
Начиная с версии 5.0 мы провели масштабнейший рефакторинг внутри библиотеки, изменили схему работы, отказались от автономного режима, а главное — решили, что поддерживаем только эту ветку, и все приложения, которые должны будут работать в будущем, должны идти в ногу с этим будущим, использовать только нашу свежую версию библиотеки, которая умеет, может и хочет.
Что же в ней изменилось? Мы поняли, что у нас все уже сделано за нас. Account Manager, по идее, когда есть n работающих аутентификаторов, когда один из них потух, он бесшовно переключится на второй, аккаунты удалены не будут. Отлично, мы начали это тестировать. Все было хорошо, получалась ситуация, что приложение было удалено, Account Manager переключился, Account Manager«ы сохранились — красота.
Content Provider сделан здесь исключительно из соображений совместимости. Старое приложение пришло за бэкапом, а мы ему слили всю системную базу. Она самая свежая, все хорошо. Но не все так хорошо. Отдали релиз тестировщикам, и неожиданно заметили, что аккаунты начали пропадать.
Как же так, все же работает. Мы открываем разработческие девайсы, проверяем — все работает. Приходим к тестировщикам. Выясняется, что аккаунты пропадают на небольшом подмножестве устройств, которые имеют на борту Android 7.0 и выше.
Чем же нам помогли братья из Google? Начиная с Android 7.0 они решили повысить безопасность Android как такового, очень похвально, и теперь когда аутентификатор удаляется из системы, она просто пожимает плечами, переключается, но перед этим удаляет все аккаунты из системной базы.
Мы долго были озадачены такой ситуацией. Но в целом, это имеет смысл. Почитав исходный код Android, мы поняли, что они борются с ситуацией, когда в системе не осталось никаких аутентификаторов. Система может не знать, что в ней могут быть какие-то аутентификаторы, к примеру, системный сервис не поднялся и прочее. Если она их просто оставляет в системной базе аккаунтов, может прийти левое приложение, которое говорит, что я отвечаю за этот тип аккаунтов, оно мое, и система бы просто отдала этот список аккаунтов. Это плохо и понятно. Но работу нам не облегчило. Поэтому с Android 7.0 наша схема перестала работать.
Утерев слезы, мы вернулись к старой схеме плюс. У нас все еще есть база с бэкапами, которая пополняется из контент провайдера, но теперь работает чуть более понятным и костыльным способом.
Каждый раз, когда в системе изменяется список аккаунтов, каждое приложение идет в системную базу, единый источник правды, и скачивает оттуда аккаунты к себе в локальную базу, на всякий случай.
Если какое-то из этих приложений становится аутентификатором и понимает, что в системе пустой список аккаунтов, и мы седьмой Android и выше, то оно просто втихую их восстанавливает в системную базу. А далее происходит все, как и ранее. Все хорошо, напрямую системная база, никаких бэкапов и прочее.
Стыдно признаться, но это решение работает отлично и решило нашу проблему. Все новое — хорошо забытое старое.
К сожалению, мы все еще не избавлены от проблемы, что одно приложение удалено, а второе остановлено, к примеру, из двух приложений на девайсе, и Account Manager все еще будет удалять наши аккаунты.
В какой ситуации это возможно? К примеру, пользователь зашел в список приложений на устройстве и нажал force stop. Ситуация редкая, но бывает, особенно когда у человека телефон Xiaomi или Meizu. Они тоже в порыве добра и заботы о пользователе продолжают удалять, блокировать и мешать работе приложения в фоне, что мешает не только им жрать батарейку, но и нам обеспечивать радость от жизни.
Мы так ничего и не смогли с этим сделать. Пытаемся понять, что должно произойти. Есть надежда, что кто-то из нас выучит китайский, решит сменить работу, устроится в эти фирмы и поможет нам изнутри, но пока все плохо.
Гулять так гулять, мы решили сделать еще более дерзкий шаг — переписать библиотеку с нуля. Зачем? По ходу борьбы с этими страшными обстоятельствами мы наплодили кодовую базу, которая была занимательной, очень многогранной, обширной и малопонятной.
Поэтому мы решили, что самый лучший код — написанный заново. И это оказалось правильно. Наш код стал более оптимальным. Мы применили весь опыт, накопившийся при разработке предыдущих версий библиотеки. Мы сделали новый интерфейс, более логичную поддержку работы с бэкендом, которая отвечает всем новым технологиям, что там применяются. Наш код стал прямолинейнее и лучше.
А главное, мы заимели собственный SyncAdapter. Теперь ваш телефон узнаёт, если приложения Яндекса работают на устройстве и говорят Android — когда у тебя будет время, запусти периодически мне этот SyncAdapter, идут на бэкенд Яндекса. Предположим, например, что вы сменили аватарку с одного котика на другого. Когда вы решите выбрать пользователя в вашем приложении, вы сможете выбрать аккаунт со свежей фоточкой. Согласитесь, приятно.
Под капотом есть много дополнительной логики, которая обеспечивает большую стабильность работы с аккаунтами — они работают с информацией, помогают делать мир лучше. А главное, наш код теперь работает в отдельном процессе.
Если по ходу у вас возникали вопросы, что все приложения Яндекса просыпаются в своем великолепии и все это работает каждый раз, когда приходит системный бродкаст о том, что приложение удалено, — да, так и было. Но теперь мы работаем в отдельном процессе, и когда приходит системный бродкаст, пробуждается только процесс, содержащий код библиотеки. Он весит ощутимо меньше, чем код основного приложения.
Теперь это выглядит следующим образом. Бизнес-логика в основном потоке, а всю белиберду по общению с Account Manager и менеджменту аккаунтов выполняет отдельный процесс. Если библиотека по какой-то причине упадет — чего быть не может, но все-таки, — то она потащит за собой только свой процесс, а функции приложения затронуты не будут. Это позволило нам добиться большей стабильности. Случаи потери аккаунтов, разлогинов и прочего снизились, они буквально единичные. Мир стал лучше.
Здесь наш тернистый путь пока прервался. Все больше приложений Яндекса переходят на эту новую версию библиотеки. Мы думаем, что с приходом новых, более лучших телефонов, версий Android и так далее все будет хорошо.