Содержание


Применение сервиса Passport для упрощения управления пользователями и аутентификации в PHP-приложениях для платформы IBM Cloud, Часть 2

Как добавить в PHP-приложение основанный на ролях доступ и восстановление паролей

Используйте интерфейс Passport API для совершенствования управления пользователями в своем PHP-приложении

Comments

Серия контента:

Этот контент является частью # из серии # статей: Применение сервиса Passport для упрощения управления пользователями и аутентификации в PHP-приложениях для платформы IBM Cloud, Часть 2

Следите за выходом новых статей этой серии.

Этот контент является частью серии:Применение сервиса Passport для упрощения управления пользователями и аутентификации в PHP-приложениях для платформы IBM Cloud, Часть 2

Следите за выходом новых статей этой серии.

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

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

Сервис IBM Cloud Passport обеспечивает быстрое и простое добавление к вашему приложению таких возможностей, как полнофункциональное управление пользователями, аутентификация и основанный на ролях доступ

Что нам потребуется

Ознакомьтесь с разделом "Что нам потребуется" в первой части fэтого цикла, в котором перечислено все необходимое для усвоения данного пособия. Обязательно обратите внимание на требования, изложенные в документах: Inversoft License Agreement (Лицензионное соглашение Inversoft) и IBM Cloud terms of use (Условия использования IBM Cloud).

Опробовать демонстрационную версиюПолучить код в GitHub

Шаг 1. Поддержка специальных атрибутов в записях пользователей

Форма для регистрации пользователя, описанная в первой части данного цикла, имела дело лишь с базовой информацией: от пользователя требовались лишь такие сведения, как имя/фамилия, адрес электронной почты и пароль. Вполне возможно, что при регистрации пользователя в реальных условиях вам потребуются от него дополнительные сведения – например, домашний адрес, номер телефона и даже платежная информация.

Хорошая новость состоит в том, что Passport поддерживает для записей пользователей специальные атрибуты, что позволяет вам запрашивать (и сохранять) любую информацию, которую вы посчитаете необходимой для создания записи пользователя вашего приложения. Эта информация хранится в сервисе Passport вместе с другой обязательной информацией о пользователях. К ней можно обратиться при посредстве интерфейса Passport API.

Чтобы проиллюстрировать, как это все работает, вернитесь к предыдущей версии файла $APP_ROOT/views/users-save.phtml и измените регистрационную форму, включив в нее три дополнительных поля — город, род занятий и номер мобильного телефона:

 ... <form method="post" action="<?php echo $data['router']->pathFor('admin-users-save'); ?>"> <div class="form-group"> <label for="fname">First name</label> <input type="text" class="form-control" id="fname" name="fname"> </div> <div class="form-group"> <label for="lname">Last name</label> <input type="text" class="form-control" id="lname" name="lname"> </div> <div class="form-group"> <label for="email">Email address</label> <input type="text" class="form-control" id="email" name="email"> </div> <div class="form-group"> <label for="password">Password</label> <input type="password" class="form-control" id="password" name="password"> </div> <div class="form-group"> <label for="city">City</label> <input type="text" class="form-control" id="city" name="city"> </div> <div class="form-group"> <label for="occupation">Occupation</label> <input type="text" class="form-control" id="occupation" name="occupation"> </div> <div class="form-group"> <label for="mobilePhone">Mobile phone (with country code)</label> <input type="text" class="form-control" id="mobilePhone" name="mobilePhone"> </div> <div class="form-group"> <button type="submit" name="submit" class="btn btn-default">Save</button> </div> </form> ...

Доработанная форма для регистрации пользователя выглядит следующим образом:

Рисунок 1. Форма для регистрации пользователя
User registration form
User registration form

Теперь обновите соответствующий обратный вызов Slim, чтобы осуществить валидацию этой новой входной информации, и добавьте ее в JSON-документ, который отсылается в API-метод /api/user/registration.

 <?php // инициализация Slim-приложения - фрагмент // процессор формы пользователя $app->post('/admin/users/save', function (Request $request, Response $response) { // получить конфигурацию $config = $this->get('settings'); // получить входные значения $params = $request->getParams(); // осуществить валидацию входной информации if (!($fname = filter_var($params['fname'], FILTER_SANITIZE_STRING))) { throw new Exception('ERROR: First name is not a valid string'); } if (!($lname = filter_var($params['lname'], FILTER_SANITIZE_STRING))) { throw new Exception('ERROR: Last name is not a valid string'); } $password = trim(strip_tags($params['password'])); if (strlen($password) < 8) { throw new Exception('ERROR: Password should be at least 8 characters long'); } $email = filter_var($params['email'], FILTER_SANITIZE_EMAIL); if (filter_var($email, FILTER_VALIDATE_EMAIL) === false) { throw new Exception('ERROR: Email address should be in a valid format'); } if (!($city = filter_var($params['city'], FILTER_SANITIZE_STRING))) { throw new Exception('ERROR: City is not a valid string'); } if (!($occupation = filter_var($params['occupation'], FILTER_SANITIZE_STRING))) { throw new Exception('ERROR: Occupation is not a valid string'); } $mobilePhone = filter_var($params['mobilePhone'], FILTER_SANITIZE_NUMBER_INT); if (filter_var($mobilePhone, FILTER_VALIDATE_FLOAT) === false) { throw new Exception('ERROR: Mobile phone is not a valid number'); } // генерировать массив данных пользователя $user = [ 'registration' => [ 'applicationId' => $config['passport_app_id'], ], 'skipVerification' => true, 'user' => [ 'email' => $email, 'firstName' => $fname, 'lastName' => $lname, 'password' => $password, 'mobilePhone' => $mobilePhone, 'data' => [ 'attributes' => [ 'city' => $city, 'occupation' => $occupation, ] ] ] ]; // закодировать данные пользователя в формате JSON // направить запрос POST к Passport API для регистрации и создания пользователя $apiResponse = $this->passport->post('/api/user/registration', [ 'body' => json_encode($user), 'headers' => ['Content-Type' => 'application/json'], ]); // в случае успеха отобразить сообщение об успехе // с идентификатором пользователя ($apiResponse->getStatusCode() == 200) { $json = (string)$apiResponse->getBody(); $body = json_decode($json); $response = $this->view->render($response, 'users-save.phtml', [ 'router' => $this->router, 'user' => $body->user ]); return $response; } }); // другие обратные вызовы

Обратите внимание на дополнительный ключ user.data.attributes key in the JSONв JSON-документе, который отсылается в Passport API; назначение этого ключа – содержать в себе все специальные атрибуты, которые вы хотите хранить для пользователей своего приложения. В данном примере этот ключ хранит город пользователя и его род занятий, однако вы можете добавить и другие атрибуты – в соответствии со своими требованиями. Номер мобильного телефона пользователя хранится отдельно в ключе user.mobilePhone. Это заранее определенный ключ, который уже поддерживается интерфейсом Passport API.

Чтобы увидеть дополнительные запросы в действии, создайте с помощью этой регистрационной формы новую учетную запись и не забудьте ввести требуемую дополнительную информацию. Затем перейдите по URL-адресу фронтенда Passport, войдите в систему со своими полномочиями администратора и в инструментальной панели сервиса Passport просмотрите запись нового пользователя, чтобы убедиться в том, что эта информация была успешно сохранена. Ниже показан пример того, что вы должны увидеть в результате:

Рисунок 2. Запись пользователя со специальными атрибутам
User record with custom attributes
User record with custom attributes

Шаг 2. Реализация возможности изменения профиля пользователя

Обычно приложения разрешают своим зарегистрированным пользователям изменять информацию, хранящуюся в соответствующих профилях пользователей. Хотя в данном случае информация о пользователе хранится в Passport, а не в базе данных приложения, тем не менее, с помощью Passport API ее по-прежнему можно без каких-либо трудностей извлечь и модифицировать.

Простейший способ решения этой задачи состоит в повторном использовании существующей формы для регистрации пользователи и в таком ее модифицировании, чтобы она также служила формой для изменения профиля пользователя. Начните с обработчика обратного вызова для маршрута /admin/users/save. Этот маршрут отображает регистрационную форму и подлежит обновлению с целью принятия опционального идентификатора пользователя в качестве параметра маршрута, как показано ниже:

 <?php // инициализация Slim-приложения - фрагмент // обработчик формы пользователя $app->get('/admin/users/save[/{id}]', function (Request $request, Response $response, $args) { $user = []; if (isset($args['id'])) { // санировать входную информацию if (!($id = filter_var($args['id'], FILTER_SANITIZE_STRING))) { throw new Exception('ERROR: User identifier is not a valid string'); } $apiResponse = $this->passport->get('/api/user/' . $id); if ($apiResponse->getStatusCode() == 200) { $json = (string)$apiResponse->getBody(); $body = json_decode($json); $user = $body->user; } } $response = $this->view->render($response, 'users-save.phtml', [ 'router' => $this->router, 'user' => $user ]); return $response; })->setName('admin-users-save'); // другие обратные вызовы

Если запрос этого маршрута имеет прикрепленный опциональный идентификатор пользователя, обратный вызов выполняет запрос к конечной точке /api/user/USER_ID. Затем эта конечная точка возвращает JSON-документ, содержащий запись соответствующего пользователя (включая специальные атрибуты); эта информация в виде массива передается в скрипт просмотраy.

На следующем шаге производится обновление регистрационной формы в файле $APP_ROOT/views/users-save.phtml. Это делается для того, чтобы учесть эту дополнительную информацию и заранее заполнить поля формы имеющимися данными пользователя из переданного массива. Ниже показаны необходимые изменения в форме:

 ... <?php if (!isset($_POST['submit'])): ?> <form method="post" action="<?php echo $data['router']->pathFor('admin-users-save'); ?>"> <input name="id" type="hidden" value="<?php echo (isset($data['user']->id)) ? $data['user']->id : ''; ?>" /> <div class="form-group"> <label for="fname">First name</label> <input type="text" class="form-control" id="fname" name="fname" value="<?php echo (isset($data['user']->firstName)) ? $data['user']->firstName : ''; ?>"> </div> <div class="form-group"> <label for="lname">Last name</label> <input type="text" class="form-control" id="lname" name="lname" value="<?php echo (isset($data['user']->lastName)) ? $data['user']->lastName : ''; ?>"> </div> <div class="form-group"> <label for="email">Email address</label> <input type="text" class="form-control" id="email" name="email" value="<?php echo (isset($data['user']->email)) ? $data['user']->email : ''; ?>"> </div> <div class="form-group"> <label for="password">Password</label> <input type="password" class="form-control" id="password" name="password"> </div> <div class="form-group"> <label for="city">City</label> <input type="text" class="form-control" id="city" name="city" value="<?php echo (isset($data['user']->data->attributes->city)) ? $data['user']->data->attributes->city : ''; ?>"> </div> <div class="form-group"> <label for="occupation">Occupation</label> <input type="text" class="form-control" id="occupation" name="occupation" value="<?php echo (isset($data['user']->data->attributes->occupation)) ? $data['user']->data->attributes->occupation : ''; ?>"> </div> <div class="form-group"> <label for="mobilePhone">Mobile phone (with country code)</label> <input type="text" class="form-control" id="mobilePhone" name="mobilePhone" value="<?php echo (isset($data['user']->mobilePhone)) ? $data['user']->mobilePhone : ''; ?>"> </div> <div class="form-group"> <button type="submit" name="submit" class="btn btn-default">Save </button> </div> </form> <?php else: ?> <div class="alert alert-success"> <strong>Success!</strong> The user with identifier <strong><?php echo $data['user']->id; ?></strong> was successfully created or updated. <a role="button" class="btn btn-primary" href="<?php echo $data['router']->pathFor('admin-users-save'); ?>"> Add another?</a> </div> <?php endif; ?> ...

Обратите внимание, что теперь каждое поле формы включает в себя атрибут value, который автоматически задает в этом поле соответствующее значение из записи пользователя (если таковая существует). В результате форма оказывается заполненной заранее имеющимися данными пользователя.

Теперь, когда пользователь частично или полностью изменил эту информацию и представил ее, процессор формы должен осуществить валидацию представленной информации и обновить существующую запись пользователя в сервисе Passport. Эта задача решается посредством отсылки запроса PUT к конечной точке/api/user/USER_ID (идентификатор пользователя включается как часть сигнатуры конечной точки).

Поскольку валидация входной информации для модифицированного запроса осуществляется почти так же, как для нового запроса на регистрацию, имеет смысл повторно применить существующего обработчика обратных вызовов. Вам просто нужно доработать его таким образом, чтобы он различал операции создания и модифицирования по отсутствию/присутствию идентификатора пользователя в запрашиваемом URL-адресе. Ниже показан измененный обратный вызов, который нужно обновить в файле $APP_ROOT/public/index.php:

 <?php // инициализация Slim-приложения - фрагмент // процессор формы пользователя $app->post('/admin/users/save', function (Request $request, Response $response) { // получить конфигурацию $config = $this->get('settings'); // получить входные значения $params = $request->getParams(); // проверить на наличие идентификатора пользователя // если присутствует, то это модифицирование // если отсутствует, то это создание if ($params['id']) { if (!($id = filter_var($params['id'], FILTER_SANITIZE_STRING))) { throw new Exception('ERROR: User identifier is not a valid string'); } } if (!($fname = filter_var($params['fname'], FILTER_SANITIZE_STRING))) { throw new Exception('ERROR: First name is not a valid string'); } if (!($lname = filter_var($params['lname'], FILTER_SANITIZE_STRING))) { throw new Exception('ERROR: Last name is not a valid string'); } $password = trim(strip_tags($params['password'])); if (empty($id)) { if (strlen($password) < 8) { throw new Exception('ERROR: Password should be at least 8 characters long'); } } else { if (!empty($password) && (strlen($password) < 8)) { throw new Exception('ERROR: Password should be at least 8 characters long'); } } $email = filter_var($params['email'], FILTER_SANITIZE_EMAIL); if (filter_var($email, FILTER_VALIDATE_EMAIL) === false) { throw new Exception('ERROR: Email address should be in a valid format'); } if (!($city = filter_var($params['city'], FILTER_SANITIZE_STRING))) { throw new Exception('ERROR: City is not a valid string'); } if (!($occupation = filter_var($params['occupation'], FILTER_SANITIZE_STRING))) { throw new Exception('ERROR: Occupation is not a valid string'); } $mobilePhone = filter_var($params['mobilePhone'], FILTER_SANITIZE_NUMBER_INT); if (filter_var($mobilePhone, FILTER_VALIDATE_FLOAT) === false) { throw new Exception('ERROR: Mobile phone is not a valid number'); } // генерировать массив данных пользователя $user = [ 'registration' => [ 'applicationId' => $config['passport_app_id'], ], 'skipVerification' => true, 'user' => [ 'email' => $email, 'firstName' => $fname, 'lastName' => $lname, 'mobilePhone' => $mobilePhone, 'data' => [ 'attributes' => [ 'city' => $city, 'occupation' => $occupation, ] ] ] ]; // добавить пароль, если существует // может быть пустым для операций модифицирования пользователя if (!empty($password)) { $user['user']['password'] = $password; } if (empty($id)) { // закодировать данные пользователя в формате JSON // направить запрос POST к Passport API для регистрации и создания пользователя $apiResponse = $this->passport->post('/api/user/registration', [ 'body' => json_encode($user), 'headers' => ['Content-Type' => 'application/json'], ]); } else { // закодировать данные пользователя в формате JSON // направить запрос PUT к Passport API для модифицирования пользователяn $apiResponse = $this->passport->put('/api/user/' . $id, [ 'body' => json_encode($user), 'headers' => ['Content-Type' => 'application/json'], ]); } // в случае успеха отобразить сообщение об успехе // с идентификатором пользователя if ($apiResponse->getStatusCode() == 200) { $json = (string)$apiResponse->getBody(); $body = json_decode($json); $response = $this->view->render($response, 'users-save.phtml', [ 'router' => $this->router, 'user' => $body->user ]); return $response; } }); // другие обратные вызовы

Сравнив этот модифицированный обработчик с его более простой версией, описанной в первой части, вы увидите следующие важные отличияs:

  • Обратный вызов начинает свою работу с проверки наличия идентификатора пользователя в параметрах запроса. Если идентификатор пользователя присутствует, обратный вызов полагает, что имеет место операция модифицирования, а не операция создания нового пользователя. Эта информация влияет на то, как будет осуществляться последующая валидация некоторой введенной информации (в особенности валидация пароля).
  • В прошлой версии обратный вызов проверял представленный в форме пароль, чтобы убедиться в том, что тот имеет длину не менее 8 символов, и инициировал ошибку, если это условие не соблюдалось. Однако при выполнении операций модифицирования пользователь может не менять свой существующий пароль. Таким образом, программный код валидации пароля в обработчике необходимо доработать таким образом, чтобы он не инициировал ошибку, если в ходе этих операций пароль не был представлен. Аналогично, ключ user JSON-документа, сгенерированного после валидации входной информации, конфигурируется таким образом, чтобы ключ password включался не только для операций создания нового пользователя, но и для операций модифицирования, когда в форме представлен новый пароль.
  • В прошлой версии клиент Guzzle отослал бы запрос POST в конечную точку /api/user/registration интерфейса Passport API с документом, закодированным в формате JSON. Теперь этот сегмент кода доработан таким образом, что запрос POST отсылается только для операций создания нового пользователя, а для операций модифицирования вместо этого генерируется запрос PUT к конечной точке /api/user/USER_ID endpoint.

Результатом этих изменения является форма, которую теперь можно использовать для обоих процессов – и для регистрации новых пользователей, и для разрешения существующим пользователям вносить изменения в свои профили. Осталось лишь добавить кнопку команды Edit (редактировать) рядом с каждой записью на странице списка пользователей и связать ее с вышеупомянутым маршрутом (вы можете посмотреть программный код для этого в репозитарии исходного кода).

Ниже показан пример того, как выглядит финальный результат:

Рисунок 3. Форма для регистрации пользователя, заранее заполненная для операции редактирования
User registration form populated for edit operation
User registration form populated for edit operation

Шаг 3. Реализация возможности удаления пользователя

Точно так же, как путем интеграции с Passport API вы можете предложить пользователям приложения интерфейс для редактирования своих профилей, вы также можете позволить администраторам приложения удалять пользователей из системы. Для этого достаточно послать запрос DELETE к конечной точке /api/user/USER_ID интерфейса Passport API и включить в качестве параметра запроса дополнительный аргумент hardDelete. Ниже показан программный код, необходимый для добавления этой функциональности в ваше приложение:

 <?php // инициализация Slim-приложения - фрагмент // обработчик удаления пользователя $app->get('/admin/users/delete/{id}', function (Request $request, Response $response, $args) { // подвергнуть входную информацию санации и валидации if (!($id = filter_var($args['id'], FILTER_SANITIZE_STRING))) { throw new Exception('ERROR: User identifier is not a valid string'); } $apiResponse = $this->passport->delete('/api/user/' . $id , [ 'query' => ['hardDelete' => 'true'] ]); return $response->withHeader('Location', $this->router->pathFor('admin-users-index')); })->setName('admin-users-delete'); // другие обратные вызовы

Теперь вам нужно добавить кнопку команды Delete (удалить) рядом с каждой записью на странице списка пользователей и связать ее с показанным выше маршрутом. В конечном итоге вы должны получить следующий результат:

Рисунок 4. Инструментальная панель пользователей с кнопками Edit и Delete
User dashboard with edit/delete buttons
User dashboard with edit/delete buttons

Шаг 4. Реализация доступа на основе ролей

В первой части этого цикла я показал, как можно защитить доступ к определенным страницам вашего приложения, потребовав от пользователя аутентифицировать себя посредством входа в систему (login). Эта возможность была реализована посредством связующего ПО Slim, которое можно добавить к определенным обработчикам маршрута с целью защиты доступа к соответствующим функциям приложения.

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

Интерфейс Passport API полностью поддерживает реализацию этой разновидности аутентификации на основе ролей в вашем PHP-приложении. Каждая запись пользователя включает в себя ключ roles, содержащий полный набор ролей, присвоенных данному пользователю; ваше приложение может использовать этот ключ при принятии решения о предоставлении или запрете доступа этому пользователю к определенным функциям.

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

  1. На первом шаге нужно сообщить интерфейсу Passport API о ролях, которые вы желаете поддерживать. Перейдите по URL-адресу фронтенда Passport, войдите в систему со своими административными полномочиями, перейдите к опции Manage Roles вашего приложения и добавьте две роли: member-silver (члены со статусом серебряный) и member-gold (члены со статусом золотой). В конечном итоге вы должны получить следующий результат:
    Рисунок 5. Роли пользователей
    User roles
    User roles
  2. На следующем шаге нужно доработать форму регистрации пользователей и обработчика с целью поддержки этих двух ролей (так чтобы пользователю можно было присвоить надлежащую роль в процессе его регистрации). Ниже показано дополнение для формы регистрации пользователя в файле $APP_ROOT/views/users-save.phtml:
     ... <div class="form-group"> <label for="tier">Membership tier</label> <select class="form-control" id="tier" name="tier"> <option value="1" <?php echo !empty($data['user']) && !empty($data['user']->registrations[0]->roles) && ($data['user']->registrations[0]->roles[0] == 'member-gold') ? 'selected="selected"' : ''; ?>>Gold</option> <option value="2" <?php echo !empty($data['user']) && !empty($data['user']->registrations[0]->roles) && ($data['user']->registrations[0]->roles[0] == 'member-silver') ? 'selected="selected"' : ''; ?>>Silver</option> </select> </div> ...

    Показанный далее код добавляет список выбора роли в форму для регистрации пользователя. Все это выглядит следующим образом:
    Рисунок 6. Форма профиля пользователя с селектором ролей
    User profile form with role selector
    User profile form with role selector
  3. Одновременно с этим доработайте процессор формы таким образом, чтобы он осуществлял валидацию и добавлял выбранную роль к JSON-документу, который представляется в Passport API при создании нового пользователя. Выбранная роль добавляется к ключу registration.roles.
     <?php // инициализация Slim-приложения - фрагмент // процессор формы пользователя $app->post('/admin/users/save', function (Request $request, Response $response) { // ... if (!($tier = filter_var($params['tier'], FILTER_SANITIZE_NUMBER_INT))) { throw new Exception('ERROR: Membership tier is not valid'); } if ($tier == 1) { $role = 'member-gold'; } else if ($tier == 2) { $role = 'member-silver'; } // генерировать массив данных пользователя $user = [ 'registration' => [ 'applicationId' => $config['passport_app_id'], 'roles' => [ $role ] ], 'skipVerification' => true, 'user' => [ 'email' => $email, 'firstName' => $fname, 'lastName' => $lname, 'mobilePhone' => $mobilePhone, 'data' => [ 'attributes' => [ 'city' => $city, 'occupation' => $occupation, ] ] ] ]; // ... }); // другие обратные вызовы
  4. На следующем шаге производится добавление определенного программного кода для авторизации на основе ролей (реализованной в виде связующего ПО Slim). Этот код проверяет роль пользователя, только что вошедшего в систему, и сравнивает ее с требованиями к роли со стороны маршрута, к которому производится попытка доступа. Если имеют место несоответствия, доступ к этому маршруту запрещается, а пользователь перенаправляется на страницу входа. Ниже показан код, который нужно добавить в файл $APP_ROOT/public/index.php перед другими обработчиками обратных вызовов:
     <?php // инициализация Slim-приложения - фрагмент // простое ПО связующего уровня для авторизации middleware $authorize = function ($role) { return function($request, $response, $next) use ($role) { if ($_SESSION['user']->registrations[0]->roles[0] != $role) { return $response->withHeader('Location', $this->router->pathFor('login')); } return $next($request, $response); }; }; // другие обратные вызовы
  5. На финальном шаге производится присоединение авторизационного ПО связующего уровня ко всем маршрутам, которые подлежат ограничениям согласно ролям. Чтобы посмотреть, как это работает, создайте два маршрута и соответствующих обработчиков обратных вызовов в файле $APP_ROOT/public/index.php, одного только для членов "gold" и другого только для членов "silver":
     <?php // инициализация Slim-приложения - фрагмент // обработчик страницы, доступ к которой ограничен согласно ролям$app->get('/members/gold', function (Request $request, Response $response) { return $this->view->render($response, 'members-gold.phtml', [ 'router' => $this->router, 'user' => $_SESSION['user'] ]); })->setName('members-gold')->add($authenticate)->add($authorize('member-gold')); // обработчик страницы, доступ к которой ограничен согласно ролям $app->get('/members/silver', function (Request $request, Response $response) { return $this->view->render($response, 'members-silver.phtml', [ 'router' => $this->router, 'user' => $_SESSION['user'] ]); })->setName('members-silver')->add($authenticate)->add($authorize('member-silver')); // другие обратные вызовы

    Обратите внимание, что к маршруту /members/goldприсоединены две функции связующего уровня. Функция $authenticate гарантирует, что этот маршрут доступен только вошедшим в систему пользователям, а функция $authorize middleware дополнительно ограничивает доступ к этому маршруту, разрешая его только вошедшим в систему пользователям с ролью "member-gold". Аналогичный подход применяется для ограничения доступа к маршруту /members/silver – к нему разрешается доступ только вошедшим в систему пользователям с ролью "member-silver". Скрипты просмотра для этих маршрутов можно получить в репозитарии исходного кода для описываемого приложения.

    Чтобы посмотреть это присвоение ролей в действии, создайте в этом приложении двух пользователей и присвойте каждому из них роль "gold" или "silver". Затем после входа в систему от имени каждого из этих пользователей попытайтесь получить доступ к этим двум маршрутам. Пользователь с ролью "gold" должен иметь возможность обращаться к маршруту /members/gold, а доступ к маршруту /members/silver должен быть ему запрещен; для пользователя с ролью "silver" ситуация должна быть зеркальной. Ниже показан пример того, что вы должны увидеть в результате:
    Рисунок 7. Страница с ограничением доступа согласно ролям
    Role-restricted page
    Role-restricted page

Следует отметить, что в данной реализации пользователи, которым не присвоена никакая роль, не смогут обратиться ни к одной странице с ограничением доступа согласно ролям (если разработчик не создал для таких пользователей специального исключения). Именно по этой причине следует определить и реализовать правила контроля доступа на основе ролей в самом начале проекта. Это позволит избежать необходимости написания специальных исключений для пользователей, которым не присвоено никаких ролей.

Шаг 5. Реализация механизма восстановления пароля (этап запроса)

Каждое полезное приложение должно иметь тот или иной способ для преодоления проблемы забытых пользователями паролей. Интерфейс Passport API включает в себя механизм, помогающий быстро реализовать систему восстановления для забытых паролей без необходимости написания большого объема кода и без затрат большого количества времени. Он работает следующим образом:

  1. Пользователь инициирует соответствующий рабочий процесс, нажав на ссылку в приложении.
  2. Приложение запрашивает у пользователя адрес его электронной почты. После получения этой информации приложение посылает запрос к конечной точке /api/user/forgot-password интерфейса Passport API и передает адрес его электронной почты этого пользователя.
  3. Passport API посылает по этому адресу электронное письмо, содержащее верификационную ссылку и уникальный верификационный идентификатор. Эта ссылка ведет пользователя к маршруту внутри приложения.
  4. Пользователь получает вышеуказанное письмо и нажимает на верификационную ссылку.
  5. Приложение запрашивает от пользователя новый пароль. После получения этой информации приложение посылает запрос к конечной точке /api/user/change-password/VERIFICATION_ID интерфейса Passport API и передает верификационный идентификатор и новый пароль.
  6. Passport API верифицирует запрос с помощью верификационного идентификатора. Если имеет место совпадение, API заменяет прежний пароль пользователя представленным значением.

Чтобы реализовать этот процесс, начните с создания формы для запроса пользователем нового пароля ($APP_ROOT/views/password-request.phtml):

 ... <?php if (!isset($_POST['submit'])): ?> <div> <form method="post" action="<?php echo $data['router']->pathFor('password-request'); ?>"> <div class="form-group"> <label for="email">Email address</label> <input type="text" class="form-control" id="email" name="email"> </div> <div class="form-group"> <button type="submit" name="submit" class="btn btn-default">Submit</button> </div> </form> </div> <?php else: ?> <div> <?php if ($data['status'] == 200): ?> <div class="alert alert-success"> <strong>Success!</strong> A verification email has been sent to your email address. Click the link in the email to proceed. </div> <?php elseif ($data['status'] == 404): ?> <div class="alert alert-danger"> <strong>Failure!</strong> No user matching that identifier could be found. </div> <?php else: ?> <div class="alert alert-danger"> <strong>Failure!</strong> Something unexpected happened. </div> <?php endif; ?> </div> <?php endif; ?> ...

Затем создайте обработчиков обратных вызовов для рендеринга этой формы и для обработки представленного адреса электронной почты:

 <?php // инициализация Slim-приложения - фрагмент // обработчики восстановления пароля (этап запроса) $app->get('/password-request', function (Request $request, Response $response) { return $this->view->render($response, 'password-request.phtml', [ 'router' => $this->router ]); })->setName('password-request'); $app->post('/password-request', function (Request $request, Response $response) { try { // validate input $params = $request->getParams(); $email = filter_var($params['email'], FILTER_SANITIZE_EMAIL); if (filter_var($email, FILTER_VALIDATE_EMAIL) === false) { throw new Exception('ERROR: Email address should be in a valid format'); } // генерировать массив данных $data = [ 'loginId' => $email, 'sendForgotPasswordEmail' => true ]; $apiResponse = $this->passport->post('/api/user/forgot-password', [ 'body' => json_encode($data), 'headers' => ['Content-Type' => 'application/json'], ]); } catch (ClientException $e) { // при наличии исключения Guzzle // если 404, ошибка user not found (пользователь не найден) // обойти обработчик исключения и показать сообщение об ошибке //в случае других ошибок перейти к обработчику исключения обычным образом if ($e->getResponse()->getStatusCode() != 404) { throw new Exception($e->getResponse()); } else { $apiResponse = $e->getResponse(); } } return $this->view->render($response, 'password-request.phtml', [ 'router' => $this->router, 'status' => $apiResponse->getStatusCode() ]); }); // другие обратные вызовы

Форма выглядит следующим образом; к ней можно обратиться по URL-адресу приложения /password-request:

Рисунок 8. Запрос на восстановление пароля и ответ
Password reset request and response
Password reset request and response

Вы можете добавить ссылку на этот URL на странице входа в приложение, как показано ниже:

 <a href="<?php echo $data['router']->pathFor('password-request'); ?>" class="btn btn-default">Forgot password?</a>

Когда эта форма представлена, производится валидация входного адреса электронной почты; в случае его валидности обработчик посылает запрос POST к конечной точке /api/user/forgot-password. Тело этого запроса содержит представленный адрес электронной почты и флаг, дающий интерфейсу Passport API указание послать верификационное электронное письмо по указанному адресу.

Если адресу электронной почты пользователя не удалось найти соответствие в базе данной Passport, то в ответ на запрос выдается ошибка 404. Обработчик ловит эту ошибку и использует ее для генерации сообщения об ошибке. Если соответствие найдено, сервис Passport посылает пользователю электронное письмо, содержащее верификационную ссылку.

Целевой объект этой ссылки можно сконфигурировать в инструментальной панели сервиса Passport. Эта ссылка должна указывать на URL-адрес, контролируемый вашим приложением; Passport автоматически присоединит к ней уникальный верификационный идентификатор. Чтобы определить эту ссылку, перейдите по URL-адресу фронтенда Passport, войдите в систему с помощью своих административных полномочий и перейдите к шаблону Settings → Email Templates → Forgot Password. Обновите эту ссылку в шаблоне, как показано ниже, чтобы домен отражал хост вашего приложения. В URL-адресе ссылки обратите внимание на заполнитель ${user.verificationId}, который представляет уникальный верификационный идентификатор, используемый для защитной проверки перед заданием нового пароля.

Рисунок 9. Шаблон электронного письма для повторного задания пароля
Email template for password reset
Email template for password reset

Шаг 6. Реализация механизма восстановления пароля (этапы верификации и повторной установки)

Когда пользователь получил электронное письмо и нажал на верификационную ссылку, приложение отображает этому пользователю форму для ввода нового пароля.

Ниже показан обработчик обратного вызова для URL-адреса приложения /password-reset, инициализация которого осуществляется, когда пользователь нажимает на верификационную ссылку в электронном письме:

 <?php // инициализация Slim-приложения - фрагмент // обработчики восстановления пароля (этап повторной установки) $app->get('/password-reset[/{id}]', function (Request $request, Response $response, $args) { // подвергнуть входную информацию санации и валидации if (!($id = filter_var($args['id'], FILTER_SANITIZE_STRING))) { throw new Exception('ERROR: Verification string is invalid'); } return $this->view->render($response, 'password-reset.phtml', [ 'router' => $this->router, 'id' => $args['id'] ]); })->setName('password-reset'); // другие обратные вызовы

Этот обработчик обратного вызова ищет верификационный идентификатор, переданный вместе с URL-адресом в качестве параметра маршрута, производит его санирование, а затем осуществляет рендеринг формы, содержащей верификационный индикатор в виде скрытого поля, вместе с полями для ввода нового пароля пользователем. Ниже показан программный код для этой формы, который нужно создать в файле $APP_ROOT/views/password-reset.phtml:

 ... <?php if (!isset($_POST['submit'])): ?> <div> <form method="post" action="<?php echo $data['router']->pathFor('password-reset'); ?>"> <input name="id" type="hidden" value="<?php echo htmlentities($data['id']); ?>" /> <div class="form-group"> <label for="password">Password</label> <input type="password" class="form-control" id="password" name="password"> </div> <div class="form-group"> <label for="password-confirm">Password (again)</label> <input type="password" class="form-control" id="password-confirm" name="password-confirm"> </div> <div class="form-group"> <button type="submit" name="submit" class="btn btn-default">Submit</button> </div> </form> </div> <?php else: ?> <div> <?php if ($data['status'] == 200): ?> <div class="alert alert-success"> <strong>Success!</strong> Your password was changed. </div> <?php elseif ($data['status'] == 404): ?> <div class="alert alert-danger"> <strong>Failure!</strong> Your password could not be changed. </div> <?php else: ?> <div class="alert alert-danger"> <strong>Failure!</strong> Something unexpected happened. </div> <?php endif; ?> </div> <?php endif; ?> ...

Обратите внимание, что форма содержит скрытое поле для верификационного индикатора, который передается в форму как переменная шаблона обработчиком вызовов. Все это выглядит следующим образом:

Рисунок 10. Форма для повторного задания пароля
Password reset form
Password reset form

На финальном шаге осуществляется обработка этой формы и обновление пароля пользователя в базе данных Passport. Обработка формы осуществляется следующим образом:

 <?php // инициализация Slim-приложения - фрагмент // обработчик восстановления пароля $app->post('/password-reset', function (Request $request, Response $response) { try { // осуществить валидацию входной информации $params = $request->getParams(); // подвергнуть входную информацию санации и валидации if (!($id = filter_var($params['id'], FILTER_SANITIZE_STRING))) { throw new Exception('ERROR: Verification string is invalid'); } $password = trim(strip_tags($params['password'])); if (strlen($password) < 8) { throw new Exception('ERROR: Password should be at least 8 characters long'); } $passwordConfirm = trim(strip_tags($params['password-confirm'])); if ($password != $passwordConfirm) { throw new Exception('ERROR: Passwords do not match'); } // генерировать массив данных $data = [ 'password' => $password, ]; $apiResponse = $this->passport->post('/api/user/change-password/' . $id, [ 'body' => json_encode($data), 'headers' => ['Content-Type' => 'application/json'], ]); } catch (ClientException $e) { // при наличии исключения Guzzle // если 404, ошибка user not found (пользователь не найден) // обойти обработчик исключения и показать сообщение об ошибке // в случае других ошибок перейти к обработчику исключения обычным образом if ($e->getResponse()->getStatusCode() != 404) { throw new Exception($e->getResponse()); } else { $apiResponse = $e->getResponse(); } } return $this->view->render($response, 'password-reset.phtml', [ 'router' => $this->router, 'status' => $apiResponse->getStatusCode() ]); }); // другие обратные вызовы

Когда пользователь представляет эту форму со своим новым паролем, процессор формы верифицирует этот пароль на соответствие требованиям, а затем инициирует запрос POST к конечной точке /api/user/change-password/VERIFICATION_ID. Тело этого запроса POST содержит новый пароль пользователя.

В конце работы сервиса Passport интерфейс Passport API проверяет валидность верификационного идентификатора. Если он валиден, API заменяет прежний пароль пользователя новым значением. В зависимости от того, завершилась операция успехом или нет, API возвращает код ошибки 200 или 4xx. Обратный вызов способен перехватить этот код и использовать его для отображения соответствующего сообщения об успехе или неудаче. В случае успеха пользователь сможет войти в приложения с новым паролем.

Заключение

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

Если вы желаете поэкспериментировать с сервисом Passport, который был рассмотрен в этой статье, опробуйте для начала демонстрационное приложение. После этого загрузите программный код из репозитария GitHub и подробнее рассмотрите, как взаимодействуют все составные части. Вы также можете воспользоваться ссылками в разделе "Похожие темы", чтобы узнать больше о различных сервисах и инструментах, использованных в этой статье. Успехов!


Ресурсы для скачивания


Похожие темы


Комментарии

Войдите или зарегистрируйтесь для того чтобы оставлять комментарии или подписаться на них.

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Облачные вычисления, Security
ArticleID=1051282
ArticleTitle=Применение сервиса Passport для упрощения управления пользователями и аутентификации в PHP-приложениях для платформы IBM Cloud, Часть 2: Как добавить в PHP-приложение основанный на ролях доступ и восстановление паролей
publish-date=07242017