Почему мы, воины PHP, должны вообще заботиться о Passbook от Apple? Ну, во-первых, потому что Apple сделала эту технологию открытой (ну, вроде…), во-вторых, потому что она может использоваться вне устройств iOS, и в-третьих, потому что она включает в себя много известных и любимых технологий, таких как JSON и RESTful API. Я также добавил бы, что это очень интересная технология, но это мое личное мнение.
В этой статье я покажу вам, как я создал пример веб-приложения, которое создает и распространяет пропуски в форме «членской карты PHPMaster». Это не полнофункциональный продукт, но хорошая база для более серьезного использования в реальных условиях. Вы можете скачать демо-код для статьи с GitHub.
Обратите внимание: чтобы выполнить код этой статьи, вы (или ваш клиент) должны быть зарегистрированы в одной из платных программ для разработчиков Apple iOS, которая позволяет вам создавать необходимые сертификаты для подписи ваших пропусков. По этой причине я также сделал онлайн-приложение для вас.
Что такое пропуск и как его использовать?
Пропуск — это документ с цифровой подписью, такой как билет, членский билет или купон, который можно распространять по электронной почте или через Интернет. Его можно открыть с помощью приложения Passbook на iOS или с помощью приложения, такого как PassWallet на Android. В iOS пропуск также можно связать с сопутствующим приложением, например с приложением Eventbrite , которое позволяет вам покупать билеты и добавлять их в Passbook вашего устройства, но это не обязательно.
Спецификации Apple описывают четыре основных типа проходов:
- Посадочные талоны
- Билеты на мероприятия
- Розничные купоны
- Магазин карт
Есть также универсальный тип прохода, который охватывает другие цели, и этот тип прохода я использую здесь.
Каждый проход содержит различные биты данных. Некоторые, такие как имя участника и штрих-код, отображаются визуально на «лицевой стороне» пропуска. Другие данные расположены на «обороте» прохода и запускаются значком «i». Проходы также могут содержать некоторую скрытую информацию, такую как время и местоположение, которые могут использоваться для отображения соответствующих уведомлений.
На странице разработчиков Apple Passbook вы можете найти официальные руководства и некоторые примеры кода.
Концепция приложения: членская карта PHPMaster
Пример приложения имеет две основные функции:
- Создание и хранение пропусков
- Реализация API регистрации / отмены регистрации веб-службы
Основная форма запрашивает у пользователя полное имя, адрес электронной почты и дополнительную избранную функцию. Изображение участника выбирается с использованием Gravatar API . Код бэкэнда затем сохраняет предоставленные данные в базу данных, генерирует пропуск и отправляет копию на электронную почту подписчика.
Пропуск содержит URL-адрес и коды доступа для веб-службы, поэтому, когда подписчик добавляет пропуск в свою коллекцию, приложение Passbook вызывает предварительно определенный URL-адрес регистрации, который должен быть реализован для проверки и сохранения предоставленных данных для последующего использования.
Структура приложения
Поскольку ядро этой статьи состоит в понимании кода, связанного с передачей, я начну с базы хорошо известных компонентов с открытым исходным кодом, доступных на GitHub и использующих Composer.
Приложение построено с использованием Slim Framework, поскольку оно легкое и хорошо подходит как для разработки RESTful, так и для приложений с веб-интерфейсом. Используется стандартный шаблонный движок Slim для PHP, а также DateTimeLogWriter
из пакета Slim Extras.
Слой данных и модели покрыт Idiorm и Paris . Idiorm заботится о работе с базами данных, а Paris предоставляет простой и понятный интерфейс модели. Это позволяет нам работать с объектом Pass
и Subscriber
и вызывать, например, $pass->pack()
для генерации пакета pass.
Пройти определение
Первый шаг — определить проход; как это будет выглядеть и какие данные будут содержать?
Имея эту информацию под рукой, мы можем создать каталог шаблонов пропусков, содержащий основные ресурсы:
шаблоны / проходит / PHPMaster.raw / logo.png icon.png pass.json
Каждое из изображений может иметь соответствующую ImageName @2x.png
которую можно использовать на устройствах ImageName @2x.png
, если таковые имеются. Логотип отображается в левом верхнем углу прохода, а значок отображается в окне push-уведомлений и почтовом приложении iOS. pass.json
содержит данные для пропуска.
В pass.json
teamIdentifier
назначается нам Apple, когда мы подписываемся на Программу разработчика. passTypeIdentifier
— это уникальный идентификатор обратного домена для нашего типа пропуска, например, pass.com.phpmaster.membership-test (требуется префикс «.pass»). Поле serialNumber
должно быть уникальным для каждого прохода, поэтому хорошим значением для использования является первичный ключ из таблицы базы данных, в которой хранятся проходы.
generic
ключ указывает используемый тип прохода и содержит конкретные поля для прохода. У каждого типа прохода есть набор primaryFields
, secondaryFields
primaryFields
и auxiliaryFields
primaryFields
, отображаемых в разных местах на лицевой стороне; он также может иметь несколько backFields
отображаемых на обратной стороне. Каждое поле имеет метку (отображается публично), ключ (для внутренней ссылки) и значение.
webServiceURL
и authenticationToken
определяются позже в приложении и заполняются базовым URL-адресом и учетными данными доступа, используемыми устройством для регистрации на нашем сервере.
Сертификаты
Следующим шагом является получение сертификата, используемого для генерации пропусков. Панель инициализации Apple Developer используется для запроса сертификатов, и в вики PassKit есть хороший видеоурок . Вам нужен определенный сертификат для каждого типа пропуска; общее имя сертификата должно соответствовать passTypeIdentifier
прохода.
Подпись каждого пропуска должна также включать публичный промежуточный сертификат Apple WWDR .
Каталог и расположение файлов
Вот как выглядит макет приложения:
Каталог bin
содержит утилиту командной строки signpass.php
. Эта программа является портом PHP консольных утилит Apple, написанных на Objective-C и Ruby. Его можно использовать для проверки прохода или для его создания из заданного каталога. Он использует библиотеку PassSigner, которую я подробно опишу позже.
Каталог config
содержит файлы конфигурации. Сделайте копию образца и назовите ее в соответствии с вашей средой (stage, Dev, Prod и т. Д.) Чтобы определить пользовательскую среду, создайте текстовый файл с именем .mode
внутри корневого каталога, содержащий желаемое имя вашей среды. Массив $config['db']
в файле конфигурации используется Idiorm, а массив $config['app']
передается непосредственно конструктору приложения Slim.
Каталог data
содержит файлы журнала, сгенерированные проходы, файл дампа SQL и каталог Certificate
который должен содержать общедоступный сертификат Apple и сертификат для вашего прохода, оба в формате PEM. Сертификаты распространяются в формате CER или P12 и могут быть преобразованы с помощью утилиты openssl следующим образом:
openssl pkcs12 -в данных / Certificate / PHPMaster.p12 -в данных / Certificate / PHPMaster.pem
Каталог templates
содержит файлы templates
внешнего интерфейса. Есть две страницы, home ( main.php
) и страница результатов ( pass.php
), которые совместно используют элементы header и footer. Форма подписки имеет свой собственный файл ( form.php
).
Подкаталог pass содержит шаблоны для каждого типа passes
. Шаблоны каждого прохода организованы в каталоги с именем PassTypeOrName .raw
содержащие необходимые файлы.
Пользовательский код библиотеки находится в lib/MyPassbook
. Внутри этого каталога есть:
- библиотека PassSigner, отвечающая за подписание и проверку пропусков
- классы моделей, построенные на базовых объектах
Subscriber
,Pass
иDevice
DevicePass
— это промежуточный класс, необходимый Парижу, который управляет отношениями «многие ко многим» между устройствами и проходами; как определено в спецификациях Passbook, каждое устройство может содержать много проходов, и копия каждого прохода может храниться на многих устройствах.
public
каталог приложения содержит файлы контроллера. index.php
является основным контроллером. Контроллер включает в себя include/bootstrap.php
который отвечает за запуск приложения, загрузку файла конфигурации, настройку некоторых параметров по умолчанию, подключение ORM к базе данных и запуск средства записи журнала. В частности, скрипт также устанавливает некоторый код в ловушку события slim.before, используемую для захвата специального HTTP-заголовка Authentication.
Файл install.php
— это простой и быстрый скрипт, который сбрасывает приложение; вероятно, нет необходимости говорить, что его нужно удалить после того, как вы все настроите!
Также должен быть файл .htaccess
который перенаправляет все нефайловые URI в index.php
и добавляет пользовательский тип MIME для наших проходов:
Приложение AddType / vnd.apple.pkpass pkpass
Это позволяет Safari открыть проход с помощью утилиты PassViewer.
Написание заявки
Наиболее важными компонентами приложения являются класс PassSigner
и фронт-контроллер index.php
. Paris дает нам возможность использовать классы моделей, чтобы базовая логика базы данных была прозрачной и управлялась Idiorm. Создать класс модели так же просто, как:
<?php class Subscriber extends Model { }
Необходимо создать три класса: Pass
, Device
и Subscriber
.
Основные методы, такие как create()
, find_many()
, delete()
и т. Д., find_many()
от базового класса. Мы можем изменить основные свойства (например, связанную таблицу базы данных или идентификатор столбца), усовершенствовать методы по умолчанию и добавить пользовательские методы по мере необходимости.
Я добавил три метода в класс Subscriber
:
-
pass()
— реализует отношение один-к-одному с объектомPass
и возвращает этот объект. -
create()
— переопределяет методcreate()
по умолчанию, позволяющий передавать массив данных. -
createPass()
— создать объектPass
связанный с текущим подписчиком.
И это шесть методов, которые я добавил в класс Pass
:
-
subscriber()
— реализовать отношение один-к-одному с объектомSubscriber
и вернуть этот объект. -
devices()
— определяет отношение «многие ко многим» с объектомDevice
. У него есть соответствующий методpasses()
в классеDevice
. -
create()
— аналогично методуcreate()
классаSubscriber
. -
delete()
— переопределить метод по умолчанию; удалить связанные объекты «Devices
и «Subscriber
. -
filename()
— вычислить и вернуть имя файла для передачи на диск. -
pack()
— сохранить пароль на диск с помощью библиотеки PassSigner.
Библиотека PassSigner
PassSigner
предоставляет два основных открытых метода для управления PassSigner
:
-
signPass()
— взять каталог, содержащий данные для одного прохода и данные сертификата, и создать сжатый файл PKPASS. -
verifyPassSignature()
берет файл PKPASS и проверяет его структуру и подпись.
Есть также два служебных метода rcopy()
и rrmdir()
для рекурсивного копирования и удаления каталогов, а также некоторые внутренние защищенные методы. Библиотека широко использует функции проверки исключений, и два важных метода проверяют наличие сертификата Apple, который должен быть определен константой APPLE_CERTIFICATE
с APPLE_CERTIFICATE
к сертификату.
Чтобы подписать проход, файл с именем manifest.json
должен быть создан с использованием содержимого каталога прохода. Этот файл должен быть подписан вашим сертификатом и содержать публичный сертификат Apple. Данные подписи сохраняются в файле с именем signature
.
Допустим, у нас есть каталог, содержащий данные о пропусках:
Данные / проходит / PassForUser001.raw info.png logo.png thumbnail.png pass.json
Первым шагом является рекурсивное копирование всего содержимого во временный каталог (например, /tmp/PassForUser001
). Убедитесь, что нет «ненужных» файлов, которые не должны быть включены, например, надоедливые файлы .DS_Store
если вы используете OSX. Если какой-либо из этих файлов включен в заархивированный проход, но отсутствует в манифесте, этот пропуск недействителен.
Затем создайте ассоциативный массив для хранения данных манифеста. Для каждого файла в каталоге проходов есть элемент, относительный путь — это ключ элемента, а значение — хеш файла SHA1. Массив кодируется и сохраняется как manifest.json
в каталоге pass.
<?php $manifestData = array(); $files = new DirectoryIterator($passPath); foreach ($files as $file) { if ($file->isDot() || $file->getFilename() == ".DS_Store") { continue; } $key = trim(str_replace($passPath, "", $file->getPathName()), "/"); $manifestData[$key] = sha1_file($file->getPathName()); }
Теперь для подписи … в основном, мы подписываем файл manifest.json
используя наш сертификат и пароль, включая сертификат Apple WWDR. Двоичный вывод записывается в signature
. Однако здесь есть проблема: бинарный параметр для функций openssl_*
PHP openssl_*
и генерирует текстовую подпись. Я обошел это, вызвав вместо этого команду оболочки из PHP:
<?php $signCommand = sprintf( "openssl smime -binary -sign -certfile %s -signer %s -inkey %s -in %s -out %s -outform DER -passin pass:%s", APPLE_CERTIFICATE, $certPath, $certPath, $manifestPath, $signaturePath, $certPassword ); $signResult = false; $signOut = array(); // needed but unused exec($signCommand, $signOut, $signResult); unset($signOut);
Если опция $zip
включена (по умолчанию она включена), проход упаковывается с использованием класса ZipArchive
.
Чтобы проверить передачу, verifyPassSignature()
извлекает данный проход во временный каталог, а затем вызывает внутренние методы validateManifest()
и validateSignature()
. Оба метода должны возвращать true.
<?php foreach ($files as $file) { ... // Check that each file is present in the manifest if (!array_key_exists((string)$file, $manifestData)) { throw new Exception("No entry in manifest for file '{$file}'"), 506); } // Check SHA1 hash for each file $hash = sha1_file($file->getPathname()); if ($hash == false || $hash != $manifestData[(string)$file]) { throw new Exception( sprintf( "For file %s, manifest's listed SHA1 hash %s doesn't match computed hash, %s", $file, $manifestData[(string)$file], $hash ), 507 ); } }
Манифест действителен, если каталог проходов содержит только файлы, объявленные в манифесте, сам манифест и файл подписи. Если файл отсутствует или файлов нет в списке, значит, пароль недействителен. Кроме того, хэш SHA1 для каждого файла должен совпадать с хешем, сохраненным в манифесте.
Подпись также проверяется с помощью openssl
в три этапа. Сначала мы проверяем содержимое манифеста с его подписью. Опция -noverify
указывает OpenSSL пропустить проверку сертификата подписавшего.
$verifyCommand = sprintf( 'openssl smime -verify -binary -inform DER -in %s -content %s -noverify 2> /dev/null', $signaturePath, $manifestPath );
Затем сертификаты извлекаются из подписи:
<?php $exportCommand = sprintf( 'openssl smime -pk7out -in %s -content %s -inform DER -outform PEM', $signaturePath, $manifestPath );
Вывод команды передается во внутренний parseCertificates()
который я не буду здесь обсуждать. Он использует сочетание OpenSSL и регулярных выражений для извлечения сертификатов из заданных данных в массив ассоциативных массивов, который используется для цикла проверки сертификатов:
<?php $certs = self::parseCertificates($pemData); $foundWWDRCert = false; for ($i = 0; $i < count($certs); $i++) { $commonName = $certs[$i]['subject']['CN']; if ($commonName == APPLE_CERTIFICATE_CN) { $foundWWDRCert = true; } }
Контроллер index.php
отвечает за предоставление как веб-приложения с графическим интерфейсом, так и RESTful API. Он состоит из двух URI: домашней страницы через GET и той же страницы через POST. Первая страница отображает только пустую форму, но вторая более интересна.
Чтобы убедиться, что каждый адрес электронной почты уникален, он спрашивает Париж, который в свою очередь проверяет базу данных:
<?php $subscriber = Model::factory('MyPassbookSubscriber') ->where_equal('email', $memberMail) ->find_one(); if ($subscriber !== false) { $errors['membermail'] = sprintf("The email address '%s' is not available", $memberMail); }
URL-адрес подписчика выбирается из Gravatar:
<?php if (empty($errors['membermail'])) { $memberThumbnail = "https://www.gravatar.com/avatar/" . md5(strtolower(trim($memberMail))) . "?s=60"; }
И теперь он может создать нового подписчика:
<?php try { $subscriber = Model::factory('MyPassbookSubscriber')->create( array( 'name' => $memberName, 'email' => $memberMail, 'created' => date('Ymd H:i:s', $memberSubscription), 'function' => $memberFavFunction, 'picture' => $memberThumbnail ) ); $subscriber->save(); } catch (Exception $e) { $errors['subscriber'] = "Error creating subscriber profile"; }
Файл шаблона JSON прохода загружается в $data
и все заполнители заполняются реальными данными пользователя и приложения:
<?php $data['serialNumber'] = $subscriber->id; $data['webServiceURL'] = sprintf('https://%s/%s/', $env['SERVER_NAME'], $app->request()->getRootUri()); $data['authenticationToken'] = md5($subscriber->id); $data['barcode']['message'] = $subscriber->id; $data['generic']['primaryFields'][0]['value'] = $subscriber->name; $data['generic']['secondaryFields'][0]['value'] = date('Y', $memberSubscription); $data['generic']['auxiliaryFields'][0]['value'] = $subscriber->function . '()'; $data['generic']['backFields'][0]['value'] = $subscriber->id; $data['generic']['backFields'][1]['value'] = $subscriber->created; $data['generic']['backFields'][2]['value'] = $subscriber->email;
Используя методы пользовательской модели, новый $pass
создается с помощью $subscriber->createPass()
и, если все правильно, $pass
может упаковать себя.
<?php if ($pass = $subscriber->createPass($app->config('passes.passType'), $data)) { $pass->pack( $app->config('passes.path'), $app->config('passes.store'), $app->config('passes.certfile'), $app->config('passes.certpass') ); ... } else { $errors['pass'] = 'Unable to create pass'; }
-<?php if ($pass = $subscriber->createPass($app->config('passes.passType'), $data)) { $pass->pack( $app->config('passes.path'), $app->config('passes.store'), $app->config('passes.certfile'), $app->config('passes.certpass') ); ... } else { $errors['pass'] = 'Unable to create pass'; }
Метод pack()
прост: он создает временную копию каталога шаблонов, добавляет специфичные для прохода файлы и вызывает PassSigner::signPass()
в рабочем каталоге. Вновь созданный проход затем отправляется по электронной почте, а pass.php
шаблона pass.php
представляется пользователю по ссылке для скачивания.
Собираюсь отдыхать
Поскольку URL-адрес веб-службы встроен в проход, спецификации Apple говорят, что мы должны реализовать как минимум две конечные точки для регистрации и отмены регистрации устройств. URI всегда / v1 / devices /: deviceId / registrations /: passTypeId /: serialNo и вызывается через POST для регистрации и DELETE для отмены регистрации.
Идентификатор устройства, идентификатор типа пропуска и серийный номер пропуска принимаются из URI, а токен аутентификации находится в заголовке авторизации. Авторизационный токен должен присутствовать в заголовке и быть связан с передачей для успешного завершения, а ответ 401 должен быть возвращен в случае неавторизованного доступа.
В процессе регистрации Push-токен отправляется как тело запроса в формате JSON: {"pushToken": <pushToken> }
. Процесс регистрации создает новый объект Device
в базе данных и заполняет предоставленные поля Device ID и Push Token. Затем создается объект DevicePass
для хранения отношений между устройством и проходом.
Если нам нужно обновить проход, мы используем идентификатор устройства и push-токен, чтобы отправить запрос push-уведомления на Apple Push-сервер (и, очевидно, также необходимо реализовать механизмы обновления).
URI незарегистрированного получает все те же данные, кроме Push-токена. Все ссылки для данного прохода удаляются, включая устройства и связи.
Вывод
Мы многое рассмотрели в этой статье, и еще есть что исследовать об управлении Pass. Не стесняйтесь начинать с библиотеки PassSigner и строить поверх нее. Вы можете легко создавать всевозможные приложения Pass для себя или своих клиентов. Удачного кодирования!
Изображение через Fotolia