Эта статья была рецензирована Адедайо Адении . Спасибо всем рецензентам SitePoint за то, что сделали контент SitePoint как можно лучше!
Мы живем в интересные времена. Некоторое время назад компания OfferZen анонсировала новую программируемую кредитную карту . Прошло много времени с тех пор, как я был так взволнован, чтобы получить в руки кусок техники. С тех пор мой разум был полон идей.
Итак, я решил написать об одном из них!
Я собираюсь описать процесс создания пользовательской многофакторной аутентификации для всех транзакций. Я не хочу делать обычные (и скучные) SMS или push-уведомления с одноразовым паролем. Я хочу встроить сканер отпечатков пальцев прямо в мой телефон.
В этом уроке мы рассмотрим, как настроить простое приложение для iOS с помощью React Native. Мы также настроим асинхронный HTTP-сервер с подключением к приложению через веб-сокет.
Мы будем следить за этим, добавляя в приложение возможности сканирования отпечатков пальцев и запрашивая эти сканирования отпечатков пальцев с HTTP-сервера. Затем мы создадим конечную точку, через которую GET-запросы смогут запросить сканирование отпечатка пальца и дождаться его появления.
Здесь есть что рассказать, поэтому я решил оставить программирование кредитной карты для другого урока. Этот урок будет полезен сам по себе, но еще лучше вместе со следующим!
Вы можете найти код для этого урока здесь и здесь . Я протестировал его с PHP
7.1
и последней версией Google Chrome.
Что такое React Native?
Если вы некоторое время создавали веб-сайты, вы наверняка слышали название React. Это библиотека конструктора интерфейсов (между прочим). Он вводит много новых и интересных идей в мир фронт-энда. Одним из них является то, что интерфейсы легче создавать, если думать о них по одному отдельному компоненту за раз. Вроде как, как « съедает слона один укус за раз ».
React Native продвигает эти идеи на шаг вперед, предоставляя цепочку сборки для компиляции интерфейсных технологий (таких как HTML, Javascript и CSS) для собственных приложений iOS и Android.
Да, вы можете создать версию этого руководства для Android. К сожалению, у меня есть только время сосредоточиться на iOS, и вам, вероятно, придется самостоятельно искать сторонние библиотеки для Android (особенно для сканирования отпечатков пальцев).
С React Native можно написать код, очень похожий на тот, который вы найдете в веб-проекте, и он будет работать безупречно для большинства пользователей смартфонов. Так почему мы говорим об этом на канале PHP? Как вы увидите, эта платформа настолько удобна, что достаточно скромного знания Javascript для создания чего-то полезного. Нам не нужно знать Java, Objective-C или Swift!
Начало работы с React Native
Я не буду объяснять шаги по установке React Native на каждой платформе. Уже есть отличные документы о том, как это сделать . Для выполнения этого упражнения вам потребуется установить XCode, NodeJS и инструмент командной строкиact react-native
.
Если у вас есть время и / или желание запустить эмулятор Android, достаточно одних и тех же документов .
Документы содержат шаги для создания нового проекта и запуска его в симуляторе. Нет смысла продолжать дальше, если вы не можете запустить новое приложение. Если это так, поговорите со мной в Twitter или в комментариях.
Установка TouchID
Соблазнительно думать, что React Native устанавливает только обобщенный Javascript API для использования, но это не так. Одной из ее лучших функций (на мой взгляд) является поддержка мощного встроенного модуля.
В случае сканирования отпечатков пальцев нет собственного API Javascript. Но есть множество нативных модулей, которые предоставляют один. Я погуглил «отреагировать на отпечаток родного ios», и первое совпадение работает замечательно:
yarn add react-native-touch-id react-native link
Вам нужно будет выйти из симулятора и перезапустить
react-native run-ios
прежде чем новое приложение получит доступ к собственному модулю.
Файл index.ios.js
умолчанию index.ios.js
, но в большинстве случаев достаточно попробовать TouchID. После небольшой уборки все должно выглядеть примерно так:
import React, { Component } from "react"; import { AppRegistry } from "react-native" import TouchID from "react-native-touch-id" class Fingerprints extends Component { componentDidMount() { TouchID.authenticate("Trying TouchID") .then(success => { alert("Success") }) .catch(error => { alert("Failure") }) } render() { return null } } AppRegistry.registerComponent("Fingerprints", () => Fingerprints)
Это из
index.ios.js
, в проекте приложения
Если вы не уверены в общей структуре, возможно, сейчас самое время освежить в деталях разработку React. Есть множество отличных курсов по этому предмету, включая наши .
Помимо шаблона React Native, мы импортируем установленную TouchID
библиотеку TouchID
. Мы добавили метод componentDidMount
, который вызывается автоматически при визуализации этого компонента. Внутри мы добавили вызов TouchID.authenticate
.
По умолчанию это автоматически завершится ошибкой. По умолчанию на симуляторе отсутствуют зарегистрированные отпечатки пальцев. Когда вы открываете приложение с этим новым кодом, вы должны увидеть сообщение об ошибке.
Чтобы изменить это, перейдите в меню «Оборудование» и выберите «Touch ID» → «Переключить состояние регистрации». Как только вы обновите (что вы можете сделать с помощью, + R), вы должны увидеть подсказку для сканирования вашего отпечатка пальца. Поскольку у симулятора нет способа сделать это физически, вернитесь в то же самое меню «Touch ID» и выберите «Matching Touch». Вы должны увидеть сообщение об успехе.
Мне было интересно играть с ним! Вы можете открыть
ios/Fingerprints.xcodeproj
в XCode и запустить его на подключенном iPhone, чтобы увидеть реальные сканы. Просто не забудьте переименоватьFingerprints
в название вашего приложения React Native.
Создание сервера
Само по себе это еще не полезно. Мы можем смоделировать сканирование отпечатков пальцев, но оно происходит автоматически, а не при необходимости. Мы должны создать сервер, чтобы можно было запрашивать отпечатки пальцев.
Мой любимый вид PHP-сервера — асинхронный PHP-сервер. Я уже писал об этом много раз, так что не стесняйтесь проверять более подробные объяснения на эту тему: разработка игр с ReactJS и PHP и процедурно генерируемая игровая среда с ReactJS, PHP и Websockets .
Я не буду вдаваться в подробности того, что происходит, но не стесняйтесь погрузиться в код этого урока, чтобы узнать о вещах, которые я здесь не упоминаю.
Для начала давайте установим Aerys. Наш файл composer.json
может выглядеть примерно так:
{ "scripts": { "dev": "vendor/bin/aerys -d -c loader.php", "prod": "vendor/bin/aerys -c loader.php" }, "require": { "amphp/aerys": "dev-amp_v2", "pre/kitchen-sink": "^0.1.0" }, "autoload": { "psr-4": { "App\\": "app" } }, "config": { "process-timeout": 0 }, "minimum-stability": "dev", "prefer-stable": true }
Это из
composer.json
в проекте сервера
Мы также можем установить заполнитель для запроса сканирования, а также HTTP-маршрут GET, чтобы добраться до него:
namespace App\Action; use Aerys\Request; use Aerys\Response; class ScanAction { public function __invoke(Request $request, Response $response) { $response->end("requesting a scan..."); } }
Это из
app/Action/ScanAction.pre
в проекте сервера
use Aerys\Router; use App\Action\ScanAction; return (Router $router) => { $router->route( "GET", "/scan", new ScanAction ); };
Это из
routes/web.pre
в проекте сервера
И мы можем запустить сервер с помощью файла конфигурации сервера и скрипта загрузчика препроцессора:
$port = 8080; $host = new Aerys\Host(); $host->expose("*", $port); $host->use($router = Aerys\router()); $web = process .."/routes/web.pre"; $web($router); exec("echo 'http://127.0.0.1:{$port}' | pbcopy");
Это из
config.pre
в проекте сервера
return Pre\processAndRequire(__DIR__ . "/config.pre");
Это из
loader.php
в проекте сервера
loader.php
применяет макросы препроцессора и требует config.pre
. config.pre
создает новый асинхронный сервер Aerys и загружает файл маршрутов. Файл маршрутов регистрирует GET-маршрут к классу ScanAction.
Если вам интересно, является ли часть этого синтаксиса стандартным PHP: это не так. Это макросы препроцессора, о которых я упоминал, и они делают такие аккуратные вещи, как конвертирование
.."/routes/web.pre"
в__DIR__ . "/routes/web.pre"
__DIR__ . "/routes/web.pre"
. Просто посмотрите на файлы, такие какconfig.php
иroutes/web.php
чтобы увидеть действительный синтаксис PHP, который генерируется …
Теперь, когда мы переходим на http://127.0.0.1:8080/scan
, мы должны увидеть сообщение «запрос сканирования …». Это хорошо. Давайте добавим веб-сокеты на наш сервер:
namespace App\Socket; use Aerys\Request; use Aerys\Response; use Aerys\Websocket; use Aerys\Websocket\Endpoint; use Aerys\Websocket\Message; class FingerprintSocket implements Websocket { private $endpoint; private $connections = []; public function onStart(Endpoint $endpoint) { $this->endpoint = $endpoint; } public function onHandshake(Request $request, Response $response) { // $origin = $request->getHeader("origin"); // // if ($origin !== "http://127.0.0.1:8080") { // $response->setStatus(403); // $response->end("origin not allowed"); // return null; // } $info = $request->getConnectionInfo(); return $info["client_addr"]; } public function onOpen(int $clientId, $address) { $this->connections[$clientId] = $address; } public function onData(int $clientId, Message $message) { $body = yield $message; yield $this->endpoint->send( $payload, $clientId ); } public function onClose(int $clientId, int $code, string $reason) { unset($this->connections[$clientId]); } public function onStop() { // nothing to see here... } }
сnamespace App\Socket; use Aerys\Request; use Aerys\Response; use Aerys\Websocket; use Aerys\Websocket\Endpoint; use Aerys\Websocket\Message; class FingerprintSocket implements Websocket { private $endpoint; private $connections = []; public function onStart(Endpoint $endpoint) { $this->endpoint = $endpoint; } public function onHandshake(Request $request, Response $response) { // $origin = $request->getHeader("origin"); // // if ($origin !== "http://127.0.0.1:8080") { // $response->setStatus(403); // $response->end("origin not allowed"); // return null; // } $info = $request->getConnectionInfo(); return $info["client_addr"]; } public function onOpen(int $clientId, $address) { $this->connections[$clientId] = $address; } public function onData(int $clientId, Message $message) { $body = yield $message; yield $this->endpoint->send( $payload, $clientId ); } public function onClose(int $clientId, int $code, string $reason) { unset($this->connections[$clientId]); } public function onStop() { // nothing to see here... } }
Это из
app/Socket/FingerprintSocket.pre
в проекте сервера
use Aerys\Router; use App\Action\ScanAction; use App\Socket\FingerprintSocket; return (Router $router) => { $router->route( "GET", "/scan", new ScanAction ); $router->route( "GET", "/ws", Aerys\websocket(new FingerprintSocket) ); };
Это из
routes/web.pre
в проекте сервера
Мы только что зарегистрировали новый маршрут, который будет отвечать на соединения через веб-сокет и возвращать эхо-сообщения. Используя консоль разработчика Chrome, в той же конечной точке /scan
мы можем проверить это:
Подключение к React Native
Поскольку мы можем отправлять сообщения обратно через веб-сокет, вероятно, сейчас самое время попробовать подключиться из нашего приложения. React Native поставляется с поддержкой веб-сокетов, поэтому мы можем добавить аналогичный код в тот же метод componentDidMount
:
componentDidMount() { // TouchID.authenticate("Trying TouchID") // .then(success => { // alert("Success") // }) // .catch(error => { // alert("Failure") // }) const socket = new WebSocket("ws://127.0.0.1:8080/ws") socket.addEventListener("message", e => console.log(e.data)) socket.addEventListener("open", e => socket.send("hi")) }
Это из
index.ios.js
в проекте приложения
Чтобы увидеть эти сообщения консоли, вам нужно включить удаленную отладку.
Как включить удаленную отладку
Нажмите ⌘
+ D
и выберите «Debug Remote JS». Откроется новая вкладка Chrome. Откройте панель инструментов разработчика этой новой вкладки, затем обновите приложение. Это должно привести к появлению «hi» в консоли, так как оно отражается от сервера.
Запрашивая отпечатки пальцев
Давайте настроим собственный код React для прослушивания запросов на сканирование отпечатков пальцев:
componentDidMount() { const socket = new WebSocket("ws://127.0.0.1:8080/ws") socket.addEventListener("message", (e) => { const data = JSON.parse(e.data) if (data.type === "scan") { TouchID.authenticate("Trying TouchID") .then(success => { // alert("Success") socket.send(JSON.stringify({ "type": "success", "data": success, "promise": data.promise })) }) .catch(error => { // alert("Failure") socket.send(JSON.stringify({ "type": "error", "data": error, "promise": data.promise })) }) } }) // socket.addEventListener("open", e => socket.send("hi")) }
Это из
index.ios.js
в проекте приложения
Когда приходит сообщение, мы анализируем его в JSON. Если тип scan
, мы запрашиваем сканирование отпечатка пальца. Независимо от того, успешно ли выполнено сканирование отпечатка пальца, мы отправляем сериализованный объект JSON с подробной информацией о событии.
Нам также необходимо настроить сервер для работы с этим новым видом сообщений:
use Aerys\Router; use App\Action\ScanAction; use App\Socket\FingerprintSocket; $socket = new FingerprintSocket; return (Router $router) => { $router->route( "GET", "/scan", new ScanAction($socket) ); $router->route( "GET", "/ws", Aerys\websocket($socket) ); };
Это из
routes/web.pre
в проекте сервера
Мы предоставляем ссылку на класс сокета для класса действия. Это означает, что мы можем вызывать методы класса сокета в ответ на HTTP-запросы:
namespace App\Action; use Aerys\Request; use Aerys\Response; use App\Socket\FingerprintSocket; class ScanAction { private $socket; public function __construct(FingerprintSocket $socket) { $this->socket = $socket; } public function __invoke(Request $request, Response $response) { try { yield $this->socket->requestScan(); $response->end("success!"); } catch ($e) { $response->end("failure!"); } } }
Это из
app/Action/ScanAction.pre
в проекте сервера
Когда вызывается маршрут /scan
, мы запрашиваем новое сканирование отпечатков пальцев из класса сокетов. Это отложенное действие (потому что приложение должно запрашивать и ждать сканирования), поэтому мы можем ожидать использование обещаний:
private $id = 0; private $promises = []; public async function requestScan() { $deferred = new Deferred; $this->promises["_{$this->id}"] = $deferred; $body = json_encode([ "type" => "scan", "promise" => "_{$this->id}" ]); $this->id += 1; $this->endpoint->broadcast($body); return $deferred->promise(); } public function onData(int $clientId, Message $message) { $body = yield $message; $data = json_decode($body); $promise = $this->promises[$data->promise]; if ($data->type === "success") { $promise->resolve($data->data); } else { $promise->fail( new \Exception($data->data->message) ); } unset($this->promises["_{$this->id}"]); // yield $this->endpoint->send( // $body, $clientId // ); }
работаютprivate $id = 0; private $promises = []; public async function requestScan() { $deferred = new Deferred; $this->promises["_{$this->id}"] = $deferred; $body = json_encode([ "type" => "scan", "promise" => "_{$this->id}" ]); $this->id += 1; $this->endpoint->broadcast($body); return $deferred->promise(); } public function onData(int $clientId, Message $message) { $body = yield $message; $data = json_decode($body); $promise = $this->promises[$data->promise]; if ($data->type === "success") { $promise->resolve($data->data); } else { $promise->fail( new \Exception($data->data->message) ); } unset($this->promises["_{$this->id}"]); // yield $this->endpoint->send( // $body, $clientId // ); }
неprivate $id = 0; private $promises = []; public async function requestScan() { $deferred = new Deferred; $this->promises["_{$this->id}"] = $deferred; $body = json_encode([ "type" => "scan", "promise" => "_{$this->id}" ]); $this->id += 1; $this->endpoint->broadcast($body); return $deferred->promise(); } public function onData(int $clientId, Message $message) { $body = yield $message; $data = json_decode($body); $promise = $this->promises[$data->promise]; if ($data->type === "success") { $promise->resolve($data->data); } else { $promise->fail( new \Exception($data->data->message) ); } unset($this->promises["_{$this->id}"]); // yield $this->endpoint->send( // $body, $clientId // ); }
Это из
app/Socket/FingerprintSocket.pre
в проекте сервера
Метод requestScan
является асинхронным. Он возвращает отложенный объект (который действует как обещание с методами resolve
и fail
). Мы храним ссылку на отложенный объект и передаем его идентификатор (и запрос на сканирование) всем подключенным клиентам веб-сокетов.
Мы возвращаем отложенную ссылку, так что действие будет ждать, пока оно не будет разрешено (благодаря ключевому слову yield
). Затем мы слушаем сообщения об success
или fail
из приложения. Мы находим связанный отложенный объект и разрешаем или ошибаемся соответствующим образом.
На этом этапе оператор yield действия разрешается, и мы можем ответить, успешно или нет. Вот как это все выглядит вместе:
Резюме
Учитывая, как мало JavaScript нам нужно было написать и насколько круто взаимодействие между iOS и асинхронным PHP-сервером, я думаю, что это был огромный успех. Мы создали способ запрашивать сканирование отпечатков пальцев с помощью простого HTTP GET-запроса. Это само по себе огромно, но теперь подумайте, как мы могли бы интегрировать это на этапе авторизации кредитной карты!
Более подробно об этом в дальнейшем — до этого, пожалуйста, дайте нам знать, что вы думаете в разделе комментариев ниже!