Содержание


Sentry 2 и PHP

Часть 2. Аутентификация и управление доступом для PHP

Comments

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

Этот контент является частью # из серии # статей: Sentry 2 и PHP

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

Этот контент является частью серии:Sentry 2 и PHP

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

В первой части (EN) этой серии вы познакомились с системой Sentry 2, ее установкой и настройкой, а также рассмотрели несколько примеров ее использования для реализации процессов аутентификации в приложениях, написанных на PHP. Кроме всего прочего были продемонстрированы создание регистрационной формы с активацией по электронной почте, реализация процесса восстановления пароля и создание менеджера учетных записей пользователей с поддержкой создания, редактирования и удаления учетных записей.

Данная статья (последняя в серии) посвящена модели полномочий Sentry 2. В ней рассматриваются создание групп, назначение пользователей и их полномочий, а также проверка полномочий для выборочного включения функций приложения. Кроме того, в ней демонстрируется защита приложения с помощью временного блокирования входа и временного отключения пользователей, а также описывается использование Sentry 2 со сторонними сервисами аутентификации, такими как Google и Twitter. Итак, приступим!

Назначение пользовательских полномочий

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

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

Листинг 1. Предоставление пользовательских полномочий
<?php
// установка автозагрузчика
require ('vendor\autoload.php');

// настройка базы данных
$dsn      = 'mysql:dbname=appdata;host=localhost';
$u = 'sentry';
$p = 'g39ejdl';
Cartalyst\Sentry\Facades\Native\Sentry::setupDatabaseResolver(
  new PDO($dsn, $u, $p));

// создание записи о пользователе
try {

  $user = Cartalyst\Sentry\Facades\Native\Sentry::createUser(array(
      'email'    => 'test@example.com',
      'password' => 'guessme',
      'first_name' => 'Test',
      'last_name' => 'User',
      'activated' => true,
      'permissions' => array('read' => 1, 'write' => 1)
  ));
  
} catch (Exception $e) {
  echo $e->getMessage();
}
?>

Листинг 1 показывает, что назначить полномочия учетной записи пользователя очень просто: добавьте ключ 'permissions' к массиву, переданному в методcreateUser(), и укажите коллекцию полномочий. Чтобы назначить или изменить полномочия существующих учетных записей, извлеките соответствующий объектUser, назначьте новые полномочия и сохраните объект в базе данных. Имена полномочий можно настраивать по собственному желанию. Имена полномочий могут иметь следующие значения: разрешено (1), запрещено (-1) и наследуется (0).

В листинге 2 в сценарий создания учетной записи, приведенный в первой части данной серии, добавлена возможность указать полномочия view, add, edit или delete, позволяющие просматривать, добавлять, редактировать и удалять учетные записи других пользователей.

Листинг 2. Интерактивное назначение пользовательских полномочий
<?php if (isset($_POST['submit'])): ?>
<?php
  // установка автозагрузчика
  require ('vendor\autoload.php');

  // настройка базы данных
  $dsn = 'mysql:dbname=appdata;host=localhost';
  $u = 'sentry';
  $p = 'g39ejdl';
  Cartalyst\Sentry\Facades\Native\Sentry::setupDatabaseResolver(new PDO($dsn, $u, $p));
  
  // проверка входных данных и создание записи о пользователе
  try {
    $email = filter_var($_POST['email'], FILTER_SANITIZE_EMAIL);
    $fname = strip_tags($_POST['first_name']);
    $lname = strip_tags($_POST['last_name']);
    $password = strip_tags($_POST['password']);
    
    $user = Cartalyst\Sentry\Facades\Native\Sentry::createUser(array(
        'email'    => $email,
        'password' => $password,
        'first_name' => $fname,
        'last_name' => $lname,
        'permissions' => $_POST['perms'],
        'activated' => true,
    ));

    echo 'User successfully created.';
    
  } catch (Exception $e) {
    echo 'User could not be created. ' . $e->getMessage();
  }
?>
<?php else: ?>
<html>
<head></head>
<body> 
  <h1>Add User</h2>
  <form action="<?php echo htmlentities($_SERVER['PHP_SELF']); ?>" method="post">
    Email address: <br/>
    <input type="text" name="email" /> <br/>
    Password: <br/>
    <input type="password" name="password" /> <br/>
    First name: <br/>
    <input type="text" name="first_name" /> <br/>
    Last name: <br/>
    <input type="text" name="last_name" /> <br/>
    Permissions: <br/>
    <input type="checkbox" name="perms[view]" value="1" />View
    <input type="checkbox" name="perms[add]" value="1" />Add
    <input type="checkbox" name="perms[edit]" value="1" />Edit
    <input type="checkbox" name="perms[delete]" value="1" />Delete
    <input type="submit" name="submit" value="Create" />
  </form>
</body>
</html>
<?php endif; ?>

На рисунке 1 показана измененная форма создания учетной записи пользователя. При отправке формы отмеченные на ней полномочия назначаются создаваемой учетной записи с помощью ключа permissions.

Рисунок 1. Форма создания учетной записи с полномочиями
Форма создания учетной записи с полномочиями
Форма создания учетной записи с полномочиями

Проверка пользовательских полномочий

Назначение полномочий – это только половина задачи. Как правило, в различных точках приложения нужно проверять, имеет ли пользователь полномочия для выполнения конкретных задач, и запрещать доступ пользователям, не имеющим достаточных привилегий. Sentry 2 легко решает эту задачу с помощью методаhasAccess(), который позволяет определить, имеет ли пользователь необходимые полномочия. Листинг 3 – это модифицированный инструмент редактирования для пользователей, приведенный в первой части данной серии, который отображает ссылки add, edit и delete, только если вошедший в систему пользователь имеет соответствующие полномочия.

Листинг 3. Проверка пользовательских полномочий
<?php
// установка автозагрузчика
require ('vendor\autoload.php');

// настройка базы данных
$dsn = 'mysql:dbname=appdata;host=localhost';
$u = 'sentry';
$p = 'g39ejdl';
Cartalyst\Sentry\Facades\Native\Sentry::setupDatabaseResolver(new PDO($dsn, $u, $p));

try {
  $currentUser = Cartalyst\Sentry\Facades\Native\Sentry::getUser();  
  if (!$currentUser->hasAccess('view')) {
    throw new Exception ('You don\'t have permission to view this page.');
  }
  
  $users = Cartalyst\Sentry\Facades\Native\Sentry::findAllUsers();
?>
<html>
  <head></head>
  <body>
    <h1>Users</h1>
    <table border="1">
      <tr>
        <td>Email address</td>
        <td>First name</td>
        <td>Last name</td>
        <td>Last login</td>
      </tr>
    <?php foreach ($users as $u): ?>
    <?php $userArr = $u->toArray(); ?>
      <tr>
        <td><?php echo $userArr['email']; ?></td>
        <td><?php echo isset($userArr['first_name']) ? 
          $userArr['first_name'] : '-'; ?></td>
        <td><?php echo isset($userArr['last_name']) ? 
          $userArr['last_name'] : '-'; ?></td>
        <td><?php echo isset($userArr['last_login']) ? 
          $userArr['last_login'] : '-'; ?></td>
        <?php if ($currentUser->hasAccess('edit')): ?>
        <td><a href="edit.php?id=<?php echo $userArr['id']; ?>">
          Edit</a></td>
        <?php endif; ?>
        <?php if ($currentUser->hasAccess('delete')): ?>
        <td><a href="delete.php?id=<?php echo $userArr['id']; ?>">
          Delete</a></td>
        <?php endif; ?>
      </tr>
    <?php endforeach; ?>
    </table>
    <?php if ($currentUser->hasAccess('add')): ?>
    <a href="create.php">Add new user</a>
    <?php endif; ?>
  <body>
</html>
<?php
} catch (Exception $e) {
  echo $e->getMessage();
}
?>

В листинге 3 выполняется подключение к базе данных Sentry 2 и используется методgetUser() для извлечения объектаUser, соответствующего пользователю, вошедшему в систему. Потом в различных точках используется методhasAccess() этого объекта, чтобы определить, имеет ли пользователь полномочия на просмотр (view) списка учетных записей, и какие ссылки редактирования (edit, delete, add) нужно отображать рядом с каждой учетной записью в списке.

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

Листинг 4. Проверка пользовательских полномочий
<?php
if (isset($_GET['id'])) {
  // установка автозагрузчика
  require ('vendor\autoload.php');

  // настройка базы данных
  $dsn      = 'mysql:dbname=appdata;host=localhost';
  $u     = 'sentry';
  $p = 'g39ejdl';
  Cartalyst\Sentry\Facades\Native\Sentry::setupDatabaseResolver(new PDO($dsn, $u, $p));

  try {
    $currentUser = Cartalyst\Sentry\Facades\Native\Sentry::getUser();  
    if (!$currentUser->hasAccess('delete')) {
      throw new Exception ('You don\'t have permission to view this page.');
    }
  
    // поиск пользователя по идентификатору и удаление
    $id = strip_tags($_GET['id']);    
    $user = Cartalyst\Sentry\Facades\Native\Sentry::findUserById($id);
    $user->delete();
    echo 'User successfully deleted.';
  } catch (Exception $e) {
    echo $e->getMessage();
  }
}    
?>

Создание групп

Sentry 2 также позволяет объединять пользователей в группы. Это особенно полезно, если нужно разрешить разным группам пользователей выполнять разные функции. Группы создаются с помощью методаcreateGroup() (см. листинг 5).

Листинг 5. Создание групп
<?php
// установка автозагрузчика
require ('vendor\autoload.php');

// настройка базы данных
$dsn = 'mysql:dbname=appdata;host=localhost';
$u = 'sentry';
$p = 'g39ejdl';
Cartalyst\Sentry\Facades\Native\Sentry::setupDatabaseResolver(
  new PDO($dsn, $u, $p));

// создание записи о группе
try {

  $group1 = Cartalyst\Sentry\Facades\Native\Sentry::createGroup(array(
      'name'    => 'staff',
      'permissions' => array(
        'view' => 1,
        'add' => 0,
        'edit' => 1,
        'delete' => 0
      )
  ));

  $group2 = Cartalyst\Sentry\Facades\Native\Sentry::createGroup(array(
      'name'    => 'managers',
      'permissions' => array(
        'view' => 1,
        'add' => 1,
        'edit' => 1,
        'delete' => 1
      )
  ));
  
} catch (Exception $e) {
  echo $e->getMessage();
}
?>

Листинг 5 создает новые группы staff и managers. Группа staff имеет полномочия view и edit, а группа managers – edit и delete.

Записи о группах хранятся в таблице groups. Чтобы увидеть структуру этой таблицы, используйте командуDESC.

mysql> DESC groups;

На рисунке 2 показана структура этой таблицы.

Рисунок 2. Структура таблицы groups
Структура таблицы groups
Структура таблицы groups

В листинге 6 демонстрирует процесс добавления пользователя в группу.

Листинг 6. Добавление пользователя в группу
<?php
// установка автозагрузчика
require ('vendor\autoload.php');

// настройка базы данных
$dsn = 'mysql:dbname=appdata;host=localhost';
$u = 'sentry';
$p = 'g39ejdl';
Cartalyst\Sentry\Facades\Native\Sentry::setupDatabaseResolver(
  new PDO($dsn, $u, $p));

// создание записи о пользователе
try {

  $user = Cartalyst\Sentry\Facades\Native\Sentry::createUser(array(
      'email'    => 'test@example.com',
      'password' => 'guessme',
      'first_name' => 'Test',
      'last_name' => 'User',
      'activated' => true
  ));
  $group = Cartalyst\Sentry\Facades\Native\Sentry::findGroupByName('managers');
  $user->addGroup($group);    
  
} catch (Exception $e) {
  echo $e->getMessage();
}
?>

Добавление пользователя в группу состоит из двух этапов. Сначала с помщью методаfindGroupByName() извлекается объектGroup, а затем вызывается методaddGroup() объектаUser для прикрепления группы к учетной записи пользователя.

Объединение полномочий пользователей и групп

Группы дают удобный способ неявно назначать полномочия пользователям; просто добавьте пользователя в группу, и пользователь автоматически унаследует полномочия группы. В листинге 5 пользователи, принадлежащие к группе staff, автоматически наследуют полномочия view и edit, а пользователи, принадлежащие к группе managers, автоматически наследуют полномочия edit и delete.

Полномочия пользователей по умолчанию, обусловленные принадлежностью к группе, можно переопределить, явно указав значения при создании или изменении учетной записи пользователя. Рассмотрим листинг 7, который иллюстрирует эту возможность.

Листинг 7. Переопределение полномочий, унаследованных от группы
<?php
// установка автозагрузчика
require ('vendor\autoload.php');

// настройка базы данных
$dsn = 'mysql:dbname=appdata;host=localhost';
$u = 'sentry';
$p = 'g39ejdl';
Cartalyst\Sentry\Facades\Native\Sentry::setupDatabaseResolver(
  new PDO($dsn, $u, $p));

// создание записи о пользователе
try {

  $user = Cartalyst\Sentry\Facades\Native\Sentry::createUser(array(
      'email'    => 'john@example.com',
      'password' => 'guessme',
      'first_name' => 'John',
      'last_name' => 'User',
      'activated' => true,
      'permissions' => array('edit' => '-1')
  ));
  $group = Cartalyst\Sentry\Facades\Native\Sentry::findGroupByName('managers');
  $user->addGroup($group);    
  
} catch (Exception $e) {
  echo $e->getMessage();
}
?>

В листинге 7 пользователь john@example.com относится к группе managers и соответственно должен был бы по умолчанию иметь все полномочия. Однако при создании учетной записи для нее было явно запрещено полномочие edit. В результате этот пользователь имеет только полномочия view, add и delete.

Листинг 8 демонстрирует использование методаgetMergedPermissions() для извлечения объединенных прямых и косвенных полномочий.

Листинг 8. Объединенные пользовательские и групповые полномочия
<?php
// установка автозагрузчика
require ('vendor\autoload.php');

// настройка базы данных
$dsn = 'mysql:dbname=appdata;host=localhost';
$u = 'sentry';
$p = 'g39ejdl';
Cartalyst\Sentry\Facades\Native\Sentry::setupDatabaseResolver(new PDO($dsn, $u, $p));

// если форма отправлена
if (isset($_POST['submit'])) {
  try {
    // проверка входных данных
    $username = filter_var($_POST['username'], FILTER_SANITIZE_EMAIL);
    $password = strip_tags(trim($_POST['password']));
    
    // указание учетных данных
    $credentials = array(
      'email'    => $username,
      'password' => $password,
    );

    // аутентификация
    // сообщение об ошибке, если аутентификация не выполнена
    Cartalyst\Sentry\Facades\Native\Sentry::authenticate($credentials, false);    
  } catch (Exception $e) {
    $failMessage = $e->getMessage();
  } 
}

// проверка входа пользователя в систему
if (Cartalyst\Sentry\Facades\Native\Sentry::check()) {
  $currentUser = Cartalyst\Sentry\Facades\Native\Sentry::getUser();
}
?>    
<html>
<head></head>
<body> 
  <?php if (isset($currentUser)): ?>
  Logged in as <?php echo $currentUser->getLogin(); ?>. 
  <a href="logout.php">[Log out]</a> <br/>
  Permissions: <?php echo implode(', ', 
    array_keys($currentUser->getMergedPermissions(), '1')); ?>
  <?php else: ?>
  <h1>Log In</h1>
  <div><?php echo (isset($failMessage)) ? 
    $failMessage : null; ?></div> 
  <form method="post">
    Username: <input type="text" name="username" /> <br/>
    Password: <input type="password" name="password" /> <br/>
    <input type="submit" name="submit" value="Log In" />
  </form>
  <?php endif; ?>
</body>
</html>

На рисунке 3 показаны полномочия пользователя john@example.com после выполнения листинга 8.

Рисунок 3. Пользовательские полномочия
Пользовательские полномочия

В Sentry 2 также предусмотрены специальные полномочия superuser, работающие в качестве ярлыка, который позволяет назначить пользователю все возможные полномочия. В листинге 9 показано их использование.

Листинг 9. Назначение полномочий superuser
<?php
// установка автозагрузчика
require ('vendor\autoload.php');

// настройка базы данных
$dsn = 'mysql:dbname=appdata;host=localhost';
$u = 'sentry';
$p = 'g39ejdl';
Cartalyst\Sentry\Facades\Native\Sentry::setupDatabaseResolver(
  new PDO($dsn, $u, $p));

// создание записи о пользователе
try {

  $user = Cartalyst\Sentry\Facades\Native\Sentry::createUser(array(
      'email'    => 'root@example.com',
      'password' => 'guessme',
      'first_name' => 'Root',
      'last_name' => 'User',
      'activated' => true,
      'permissions' => array('superuser' => '1')
  ));
  
} catch (Exception $e) {
  echo $e->getMessage();
}
?>

Блокирование входа пользователя

В Sentry 2 имеется интересная функция повышения безопасности приложений: автоматическое временное блокирование входа при большом числе неудачных попыток входа. Для ее реализации предназначены следующие методы:

  • МетодsetAttemptLimit() устанавливает максимальное число попыток до блокирования входа.
  • МетодsetSuspensionTime() устанавливает продолжительность блокирования входа в минутах.
  • МетодgetLoginAttempts() возвращает число попыток входа.
  • МетодclearLoginAttempts() очищает счетчик неудачных попыток входа и разблокирует учетную запись.
  • Методcheck() возвращает текущее состояние входа: заблокирован или нет.
  • Методsuspend() блокирует учетную запись, а методunsuspend() возвращает ее в активное состояние.
  • Методban() запрещает учетную запись, а методunBan() восстанавливает ее в активное состояние.

Листинг 10 показывает, насколько легко можно добавить блокирование в процесс входа.

Листинг 10. Блокирование входа
<?php
// установка автозагрузчика
require ('vendor\autoload.php');

// настройка базы данных
$dsn = 'mysql:dbname=appdata;host=localhost';
$u = 'sentry';
$p = 'g39ejdl';
Cartalyst\Sentry\Facades\Native\Sentry::setupDatabaseResolver(new PDO($dsn, $u, $p));

// включение блокирования
$throttleProvider = Cartalyst\Sentry\Facades\Native\Sentry::getThrottleProvider();
$throttleProvider->enable();

// если форма отправлена
if (isset($_POST['submit'])) {
  try {
    // проверка входных данных
    $username = filter_var($_POST['username'], FILTER_SANITIZE_EMAIL);
    $password = strip_tags(trim($_POST['password']));
    
    // настройка блокирования
    $throttle = $throttleProvider->findByUserLogin($username);
    $throttle->setAttemptLimit(3);
    $throttle->setSuspensionTime(5);
    
    $credentials = array(
      'email'    => $username,
      'password' => $password,
    );
    Cartalyst\Sentry\Facades\Native\Sentry::authenticate($credentials, false); 
    
  } catch (Exception $e) {
    // все остальные ошибки аутентификации
    $failMessage = $e->getMessage();
  } 
  
}

// проверка входа пользователя в систему
if (Cartalyst\Sentry\Facades\Native\Sentry::check()) {
  $currentUser = Cartalyst\Sentry\Facades\Native\Sentry::getUser();
}
?>    
<html>
<head></head>
<body> 
  <?php if (isset($currentUser)): ?>
  Logged in as <?php echo $currentUser->getLogin(); ?>
  <a href="logout.php">[Log out]</a>
  <?php else: ?>
  <h1>Log In</h1>
  <div><?php echo (isset($failMessage)) ? 
    $failMessage : null; ?></div> 
  <form method="post">
    Username: <input type="text" name="username" /> <br/>
    Password: <input type="password" name="password" /> <br/>
    <input type="submit" name="submit" value="Log In" />
  </form>
  <?php endif; ?>
</body>
</html>

В листинге 10 используется стандартный процесс входа Sentry 2, который имеет два существенных отличия от предыдущих примеров:

  • МетодgetThrottleProvider() используется для получения экземпляра глобального провайдера блокирования. Этот объект является основной точкой управления блокированием входа. Вызов методаenable() этого объекта активирует встроенную в Sentry 2 функцию блокирования.
  • МетодfindByUserLogin() объекта блокирования используется для получения экземпляра объекта блокирования пользователя. МетодыsetAttemptLimit() иsetSuspensionTime() этого объекта используются для настройки основных параметров блокирования входа – количества попыток входа и времени блокирования.

Теперь при трех попытках войти в приложение с неверным паролем учетная запись будет автоматически заблокирована на пять минут. Это обеспечивает надежную защиту против роботов и автоматизированных сценариев, которые пытаются подобрать пароли по словарю.

Интеграция со сторонними сетями (Twitter)

Как вы видели, Sentry 2 предоставляет все необходимое для аутентификации пользователей, учетные записи которых хранятся в собственной базе данных приложения. Но сегодня, как правило, приложение должно поддерживать вход через сторонних поставщиков OAuth-авторизации, таких как Google, Facebook или Twitter.

Sentry Social 3 является дополнением к Sentry 2 для интеграции с социальными сетями. Однако это дополнение распространяется только по коммерческой лицензии, поэтому может быть не лучшим решением для вашего проекта. Альтернативным решением является использование OAuth-библиотек с открытым исходным кодом, которые, как правило, предоставляет каждый сторонний поставщик.

Их использование является не настолько сложным, как может показаться. Мы проиллюстрируем это на примерах процессов аутентификации и создания учетных записей путем интеграции Sentry 2 с двумя из наиболее популярных на сегодня социальных сетей – Twitter и Google+.

Если вы когда-нибудь пытались войти в приложение, используя свои учетные записи в Twitter, Google или Facebook, то, вероятно, заметили, что приложение может получить доступ к вашим учетным записям в данных сетях и использовать их информацию для предварительного заполнения собственной регистрационной формы. В листинге 11 демонстрируется этот процесс с помощью двух компонентов инфраструктуры Zend Framework 1.x (в частности, Zend_Oauth_Consumer и Zend_Json), предназначенных для подключения к API Twitter и извлечения имени авторизованного пользователя. Затем эта информация используется для заполнения формы создания учетной записи приложения, при отправке которой создается новая учетная запись пользователя в базе данных Sentry 2.

Листинг 11 предполагает, что в вашем пути подключения PHP указана работающая версия Zend Framework. Обратите внимание, что перед использованием кода необходимо зарегистрировать свое приложение в сети Twitter, получить имя (key) и пароль (secret) клиента (см. пример на рисунке 4) и вставить эту информацию в соответствующее место листинга 11. В разделе Ресурсы приведены ссылки на Zend Framework и консоль разработчика Twitter API для управления приложениями.

Рисунок 4. Настройки консоли приложения в Twitter
Настройки консоли приложения в Twitter
Настройки консоли приложения в Twitter
Листинг 11. Аутентификация пользователя и извлечения профиля с помощью Twitter
<?php
session_start();
if (isset($_POST['submit'])) {
  // если форма отправлена, 
  // использовать введенные данные для создания учетной записи пользователя
  require ('vendor\autoload.php');

  // настройка базы данных
  $dsn = 'mysql:dbname=appdata;host=localhost';
  $u = 'sentry';
  $p = 'g39ejdl';
  Cartalyst\Sentry\Facades\Native\Sentry::setupDatabaseResolver(new PDO($dsn, $u, $p));

  try {
    $email = filter_var($_POST['email'], FILTER_SANITIZE_EMAIL);
    $fname = strip_tags($_POST['name']);
    $password = strip_tags($_POST['password']);
    
    $user = Cartalyst\Sentry\Facades\Native\Sentry::createUser(array(
        'email'    => $email,
        'password' => $password,
        'first_name' => $fname,
    ));

    echo 'User successfully created.';    
  } catch (Exception $e) {
    echo 'User could not be created. ' . $e->getMessage();
  }

} else {
  // если форма не отправлена, 
  // подключение к Twitter с помощью OAuth
  // запрос авторизации пользователя и получение имени пользователя с помощью
  // предварительно заполненной формы создания учетных записей Twitter API
  require_once 'Zend/Loader.php';
  Zend_Loader::loadClass('Zend_Oauth_Consumer');
  Zend_Loader::loadClass('Zend_Json');

  $config = array(
    'callbackUrl' => 'http://yourhost/path/to/script.php',
    'siteUrl' => 'https://api.twitter.com/oauth',
    'consumerKey' => 'YOUR-CONSUMER-KEY',
    'consumerSecret' => 'YOUR-CONSUMER-SECRET'
  );
  $consumer = new Zend_Oauth_Consumer($config);
   
  // получение маркера доступа
  if (!empty($_GET) & isset($_SESSION['TWITTER_REQUEST_TOKEN'])) {
      $token = $consumer->getAccessToken(
                   $_GET,
                   unserialize($_SESSION['TWITTER_REQUEST_TOKEN'])
               );
      $_SESSION['TWITTER_ACCESS_TOKEN'] = serialize($token);
  } 

  // извлечение маркера запроса
  if (!isset($_SESSION['TWITTER_REQUEST_TOKEN'])) {
    $token = $consumer->getRequestToken();
    $_SESSION['TWITTER_REQUEST_TOKEN'] = serialize($token);
    $consumer->redirect();
  }

  // использование маркера доступа для подключения к Twitter API
  // декодирование ответа и извлечение имени пользователя
  // создание формы с извлеченным именем
  if (isset($_SESSION['TWITTER_ACCESS_TOKEN'])) {
    $token = unserialize($_SESSION['TWITTER_ACCESS_TOKEN']);
    $client = $token->getHttpClient($config);
    $client->setUri('https://api.twitter.com/1.1/account/verify_credentials.json');
    $client->setMethod(Zend_Http_Client::GET);
    $response = $client->request();     
    $data = Zend_Json::decode($response->getBody());
?>
  <html>
  <head></head>
  <body> 
    <form method="post">
    Name: <input type="text" name="name" 
      value="<?php echo $data['name']; ?>" /> <br/>
    Email address: <input type="text" name="email" value=" /> <br/>
    Password: <input type="password" name="password" value=" /> <br/>
    <input type="submit" name="submit" value="Create Account" />
    </form>
  </body>
  </html>
<?php
  }
}
?>

Листинг 11 начинается с запуска нового сеанса, который используется в качестве контейнера для хранения OAuth-маркеров доступа и запроса, полученных в процессе аутентификации. Затем автозагрузчик Zend Framework загружает необходимые классы и инициализирует новый объект Zend_Oauth_Consumer. После этого выполняется стандартный OAuth-процесс получения маркера запроса и перенаправления пользователя на Twitter для входа, аутентификации и получения маркера доступа.

Теперь, имея маркер доступа, код листинга 11 может выполнить аутентифицированный запрос к оконечной точке API Twitter/verify_credentials. Ответом на этот запрос является документ JSON, содержащий краткий профиль пользователя, в том числе имя пользователя. После этого можно декодировать JSON-данные, извлечь имя пользователя и использовать эту информацию для предварительного заполнения формы создания учетной записи. Затем пользователю просто нужно добавить свой адрес электронной почты и отправить форму, которая запустит Sentry 2 и создаст учетную запись с помощью методаcreateUser(), описанного ранее.

На рисунке 5 показан пример аутентификации в Twitter и итоговая форма создания учетной записи с именем пользователя, полученным из Twitter API.

Рисунок 5. Процесс аутентификации в Twitter
Процесс аутентификации в Twitter
Процесс аутентификации в Twitter

Причина того, что листинг 11 не заполняет поле адреса электронной почты пользователя в форме создания учетной записи, проста: Twitter не раскрывает адреса электронной почты через свой API. Поскольку в Sentry 2 адрес электронной почты является основным идентификатором учетной записи, для завершения процесса ее создания эту информацию необходимо запросить и ввести вручную. Это ограничение характерно для Twitter, однако в листинге 12 (для Google+) используется другой подход.

Интеграция со сторонними сетями (Google)

Социальная сеть Google+ становится все более популярной, а Google также предоставляет надежную реализацию клиента PHP OAuth. Этот клиент, который можно загрузить бесплатно, позволяет подключаться ко всем программным интерфейсам Google API, включая Google+ API. Однако перед использованием кода, приведенного в последующих листингах, нужно зарегистрировать приложение в Google, получить идентификатор (ID) и пароль (secret) клиента и вставить эту информацию в соответствующие места каждого листинга (см. рисунок 6 в качестве примера). В разделе Ресурсы приведены ссылки на консоль Google API и клиента Google PHP OAuth.

Рисунок 6. Настройки консоли приложения в Google
Настройки консоли приложения в Google
Настройки консоли приложения в Google

В листинге 12 приведен пример аутентификации посредством сервиса Google OAuth, извлечения из Google+ API имени и адреса электронной почты авторизованного пользователя, а затем автоматического создания новой учетной записи Sentry 2 с помощью этой информации.

Листинг 12. Аутентификация пользователя и создание учетной записи с помощью Google
<?php
// загрузка требуемых классов
require_once 'vendor/google-api-php-client/src/Google_Client.php';
require_once 'vendor/google-api-php-client/src/contrib/Google_Oauth2Service.php';
require_once 'vendor/google-api-php-client/src/contrib/Google_PlusService.php';
require_once 'vendor\autoload.php';

// настройка базы данных
$dsn = 'mysql:dbname=appdata;host=localhost';
$u = 'sentry';
$p = 'g39ejdl';
Cartalyst\Sentry\Facades\Native\Sentry::setupDatabaseResolver(new PDO($dsn, $u, $p));

// начало сеанса
session_start();

// инициализация клиента OAuth 2.0
// установка областей действия
$client = new Google_Client();
$client->setApplicationName('Project X');
$client->setClientId('YOUR-CLIENT-ID');
$client->setClientSecret('YOUR-CLIENT-SECRET');
$client->setRedirectUri('http://yourhost/path/to/script.php');
$client->setScopes(array(
  'https://www.googleapis.com/auth/userinfo.email', 
  'https://www.googleapis.com/auth/userinfo.profile',
  'https://www.googleapis.com/auth/plus.login'
));
$oauth2Service = new Google_Oauth2Service($client);
$plusService = new Google_PlusService($client);

// если код получен, аутентификация и сохранение маркера в сеансе
if (isset($_GET['code'])) {
  $client->authenticate();
  $_SESSION['token'] = $client->getAccessToken();
  $redirect = 'http://' . $_SERVER['HTTP_HOST'] . $_SERVER['PHP_SELF'];
  header('Location: ' . filter_var($redirect, FILTER_SANITIZE_URL));
  exit;
}

// если маркер доступен в сеансе, установка маркера в клиенте
if (isset($_SESSION['token'])) {
  $client->setAccessToken($_SESSION['token']);
}

// если маркер доступен в клиенте, обращение к API
// получение имени и адреса электронной почты пользователя
// создание случайного пароля
// создание пользователя в базе данных Sentry2 и уведомление
// пользователя по электронной почте
if ($client->getAccessToken()) {
  $userinfo = $oauth2Service->userinfo->get();
  $profile = $plusService->people->get('me');
  $email = filter_var($userinfo['email'], FILTER_SANITIZE_EMAIL);
  $fname = $profile['name']['givenName'];
  $lname = $profile['name']['familyName'];
  $password = substr(md5(rand()),0,7);
  
  try {    
    $user = Cartalyst\Sentry\Facades\Native\Sentry::createUser(array(
      'email'    => $email,
      'password' => $password,
      'first_name' => $fname,
      'last_name' => $lname,
      'activated' => true,
    ));
    
    $body = <EOM
Your account was successfully created. Please log in with the credentials below:
Email: $email
Password: $password
EOM;
    @mail($email, 'Account created successfully', $body);    
  } catch (Exception $e) {
    $failMessage = $e->getMessage();
  }
  
  $_SESSION['token'] = $client->getAccessToken();
  
} else {
  $authUrl = $client->createAuthUrl();
}
?>
<html>
<head></head>
<body> 
  <?php if (isset($authUrl)): ?>
    <a href='<?php echo $authUrl; ?>'>Register with Google+</a>
  <?php else: ?>
    <div><?php echo (isset($failMessage)) ? 
      $failMessage : "Account created for '$fname $lname'. 
      Credentials: $email / $password"; ?></div>  
  <?php endif; ?>
</body>
</html>

Листинг 12 начинается с загрузки библиотеки Google PHP OAuth, настройки подключения к базе данных Sentry 2 и запуска нового PHP-сеанса. Затем выполняется инициализация Google OAuth, устанавливаются идентификатор и пароль клиента, полученные из консоли Google API, и, что немаловажно, определяются области действия клиента. Наконец, инициализируются два объекта сервисов OAuth и Google+, а также создается URL аутентификации с помощью методаcreateAuthUrl() этого клиента.

В соответствии со стандартным процессом OAuth нажатие на URL аутентификации направляет пользователя на страницу аутентификации Google (см. рисунок 7). После аутентификации приложения пользователем и подтверждения данных, к которым оно имеет доступ, генерируется и сохраняется в сеансе маркер доступа. Маркер доступа предоставляет клиенту доступ к выбранным Google API.

Рисунок 7. Процесс аутентификации Google
Процесс аутентификации Google
Процесс аутентификации Google

С помощью маркера доступа объекты сервисов получают информацию о профиле для аутентификации пользователя (например, фамилия, имя и адрес электронной почты). Затем эта информация дополняется автоматически генерируемым случайным паролем для создания и активации новой учетной записи пользователя в базе данных Sentry 2. После этого информация об учетной записи, включая автоматически сгенерированный пароль, передается пользователю по электронной почте с помощью PHP-функцииmail().

Одним из вариантов такого подхода является создание учетной записи на основе информации сторонней сети и регистрация пользователя в приложении вручную. Листинг 13 представляет собой модифицированный вариант листинга 12, в котором реализован данный подход.

Листинг 13. Аутентификация пользователя и вход в систему с помощью Google
<?php
// загрузка требуемых классов
require_once 'vendor/google-api-php-client/src/Google_Client.php';
require_once 'vendor/google-api-php-client/src/contrib/Google_Oauth2Service.php';
require_once 'vendor/google-api-php-client/src/contrib/Google_PlusService.php';
require_once 'vendor\autoload.php';

// настройка базы данных
$dsn = 'mysql:dbname=appdata;host=localhost';
$u = 'sentry';
$p = 'g39ejdl';
Cartalyst\Sentry\Facades\Native\Sentry::setupDatabaseResolver(new PDO($dsn, $u, $p));

// начало сеанса
session_start();

// инициализация клиента OAuth 2.0
// установка областей действия
$client = new Google_Client();
$client->setApplicationName('Project X');
$client->setClientId('YOUR-CLIENT-ID');
$client->setClientSecret('YOUR-CLIENT-SECRET');
$client->setRedirectUri('http://yourhost/path/to/script.php');
$client->setScopes(array(
  'https://www.googleapis.com/auth/userinfo.email', 
  'https://www.googleapis.com/auth/userinfo.profile',
  'https://www.googleapis.com/auth/plus.login'
));
$oauth2Service = new Google_Oauth2Service($client);
$plusService = new Google_PlusService($client);

// если код получен, аутентификация и сохранение маркера в сеансе
if (isset($_GET['code'])) {
  $client->authenticate();
  $_SESSION['token'] = $client->getAccessToken();
  $redirect = 'http://' . $_SERVER['HTTP_HOST'] . $_SERVER['PHP_SELF'];
  header('Location: ' . filter_var($redirect, FILTER_SANITIZE_URL));
  exit;
}

// если маркер доступен в сеансе, установка маркера в клиенте
if (isset($_SESSION['token'])) {
  $client->setAccessToken($_SESSION['token']);
}

// если получен запрос на выход, уничтожение сеанса и аннулирование маркера
if (isset($_REQUEST['logout'])) {
  unset($_SESSION['token']);    
  $client->revokeToken();
  Cartalyst\Sentry\Facades\Native\Sentry::logout();
}

// если маркер доступен в клиенте, обращение к API
// получение имени и адреса электронной почты пользователя
// автоматический вход в систему при наличии пользователя в базе данных Sentry2
// иначе – создание новой учетной записи и вход пользователя в систему
if ($client->getAccessToken()) {
  $userinfo = $oauth2Service->userinfo->get();
  $profile = $plusService->people->get('me');
  $email = $userinfo['email'];
  $fname = $profile['name']['givenName'];
  $lname = $profile['name']['familyName'];
  
  try {
    try {    
      $user = Cartalyst\Sentry\Facades\Native\Sentry::findUserByLogin($email);
    } catch (Cartalyst\Sentry\Users\UserNotFoundException $e) {
      $password = substr(md5(rand()),0,7);
      $user = Cartalyst\Sentry\Facades\Native\Sentry::createUser(array(
          'email'    => $email,
          'password' => $password,
          'first_name' => $fname,
          'last_name' => $lname,
          'activated' => true,
      ));
      $createdMessage = "Account created for '$fname $lname'. 
        Credentials: $email / $password";
    }

    Cartalyst\Sentry\Facades\Native\Sentry::login($user, false); 
    
    if (Cartalyst\Sentry\Facades\Native\Sentry::check()) {
      $currentUser = Cartalyst\Sentry\Facades\Native\Sentry::getUser();
    }
  } catch (Exception $e) {
    $failMessage = $e->getMessage();
  }

  $_SESSION['token'] = $client->getAccessToken();
    
} else {
  $authUrl = $client->createAuthUrl();
}
?>
<html>
<head></head>
<body> 
  <?php if (isset($authUrl)): ?>
    <a href='<?php echo $authUrl; ?>'>Log in with Google+</a>
  <?php else: ?>
    <div><?php echo (isset($failMessage)) ? 
      $failMessage : null; ?></div>  
    <div><?php echo (isset($createdMessage)) ? 
      $createdMessage : null; ?></div>  
    Logged in as <?php echo $currentUser->getLogin(); ?>. 
      [<a href='?logout'>Log out</a>]
  <?php endif; ?>
</body>
</html>

Большая часть кода в листинге 13 аналогична коду в листинге 12. Основное отличие состоит в том, что после завершения процесса аутентификации в Google сценарий проверяет, существует ли в Sentry 2 учетная запись с тем же адресом электронной почты, и в случае отсутствия создает ее. После этого используется метод Sentry 2login() для ручной аутентификации и входа пользователя в приложение. Также реализован процесс выхода пользователя из приложения и аннулирования маркера доступа Google API.

Заключение

Как показывают приведенные примеры, Sentry 2 предоставляет полнофункциональную инфраструктуру для аутентификации и детального управления доступом в Web-приложении, написанном на PHP. Добавляя легкодоступные библиотеки с открытым исходным кодом, можно интегрировать ее со сторонними социальных сетями и системами аутентификации, решая широкий спектр типовых задач с минимальными затратами усилий на разработку. Попробуйте и оцените эту систему при следующей разработке Web-приложения.


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


Похожие темы


Комментарии

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

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Open source
ArticleID=999009
ArticleTitle=Sentry 2 и PHP: Часть 2. Аутентификация и управление доступом для PHP
publish-date=02272015