برنامه نویسی 348 بازدید

در این مقاله با روش راه‌اندازی احراز هویت کاربران در PHP با استفاده از کامپوننت امنیت سیمفونی آشنا خواهیم شد. سیمفونی یک فریمورک PHP برای توسعه وب با عملکرد بالا است. در این مقاله علاوه بر روش احراز هویت، با چگونگی مجوزدهی مبتنی بر نقش نیز آشنا می‌شویم که در عمل می‌توانید بر اساس نیازهای خود آن را بسط دهید.

کامپوننت امنیت سیمفونی

کامپوننت امنیت سیمفونی امکان راه‌اندازی برخی امکانات امنیتی مانند احراز هویت، مجوزدهی مبتنی بر نقش، توکن‌های CSRF و موارد دیگر را به آسانی فراهم می‌سازد. در واقع این کامپوننت، خود به چهار کامپوننت فرعی تقسیم می‌شود که می‌توانید بر حسب نیاز از هر کدام آن‌ها استفاده کنید.

کامپوننت امنیت چهار کامپوننت فرعی زیر را دارد:

  • symfony/security-core
  • symfony/security-http
  • symfony/security-csrf
  • symfony/security-ac

در این مقاله، قصد داریم به بررسی امکان احراز هویت ارائه شده از سوی کامپوننت symfony/security-core بپردازیم. به طور معمول کار خود را با دستورالعمل‌های نصب و پیکربندی آغاز می‌کنیم و سپس به بررسی نمونه‌های عملی از این کامپوننت، جهت نمایش مفاهیم کلیدی ادامه می‌دهیم.

کامپوننت امنیت سیمفونی

نصب و پیکربندی

در این بخش قصد داریم کامپوننت امنیت سیمفونی را نصب کنیم. ما تصور می‌کنیم که شما قبلاً Composer را روی سیستم خود نصب کرده‌اید، چون آن را برای نصب کامپوننت امنیت که در Packagist قرار دارد نیاز داریم.

بنابراین پیش‌تر می‌رویم و کامپوننت امنیت را با استفاده از دستور زیر نصب می‌کنیم:

$composer require symfony/security

ما قصد داریم کاربران را از پایگاه داده MySQL در مثال خود بارگذاری کنیم و از این رو باید یک لایه انتزاع برای پایگاه داده داشته باشیم. بنابراین در ادامه یکی از محبوب‌ترین لایه‌های انتزاع پایگاه داده یعنی Doctrine DBAL را نصب می‌کنیم:

$composer require doctrine/dbal

بدین ترتیب فایلی به نام composer.json ایجاد می‌شود که مانند زیر است:

{
    "require": {
        "symfony/security": "^4.1",
        "doctrine/dbal": "^2.7"
    }
}

در ادامه اقدام به تغییر دادن فایل composer.json می‌کنیم تا به حالت زیر دربیاید:

{
    "require": {
        "symfony/security": "^4.1",
        "doctrine/dbal": "^2.7"
    },
    "autoload": {
         "psr-4": {
             "Sfauth\\": "src"
         },
         "classmap": ["src"]
    }
}

از آنجا که اینک مدخل classmap را اضافه کرده‌ایم، می‌توانیم بارگذار خودکار کامپوزر را با اجرای دستور زیر به‌روزرسانی کنیم:

$composer dump -o

اینک می‌توانیم از فضای نام sfauth برای بارگذاری خودکار کلاس‌های موجود در دایرکتوری src استفاده کنیم.

بدین ترتیب بخش نصب به پایان می‌رسد. برای استفاده از این کامپوننت کافی است که فایل autoload.php ایجاد شده از سوی کامپوزر که در اپلیکیشن خود ایجاد کرده‌ایم را به صورتی که در قطعه کد زیر می‌بینید include کنیم:

<?php
require_once './vendor/autoload.php';
 
// application code
?>

مثالی از دنیای واقعی

در ابتدا به بررسی گردش کار احراز هویت معمول ارائه شده از سوی کامپوننت امنیت سیمفونی می‌پردازیم.

  • نخستین کاری که باید انجام داد، بازنویسی اطلاعات امنیتی کاربر و ایجاد یک توکن غیر احراز شده است.
  • سپس یک توکن احراز نشده را به بخش مدیریت احراز هویت می‌فرستیم تا اعتبارسنجی شود.
  • مدیریت احراز هویت می‌تواند شامل ارائه‌دهندگان مختلف احراز هویت باشد و یکی از آن‌ها برای احراز هویت درخواست کاربر جاری استفاده می‌شود. منطق چگونگی احراز هویت کاربر در «ارائه‌دهنده احراز هویت» (Authnication Provider) تعریف شده است.
  • ارائه‌دهنده احراز هویت با ارائه‌دهنده کاربر تماس می‌گیرد و کاربر را بازیابی می‌کند. بارگذاری کاربران از بک‌اند مربوطه در حوزه وظایف بخش ارائه‌دهنده کاربر است.
  • بخش ارائه‌دهنده کاربر تلاش می‌کند که کاربر را با استفاده از اطلاعات ارائه شده از سوی ارائه‌دهنده احراز هویت بارگذاری کند. در اغلب موارد ارائه‌دهنده کاربر، شیء کاربر را که به پیاده‌سازی رابط UserInterface می‌پردازد بازگشت می‌دهد.
  • اگر کاربر پیدا شود، ارائه‌دهنده احراز هویت یک توکن احراز نشده بازگشت می‌دهد و می‌توانید این توکن را برای درخواست‌های بعدی ذخیره کنید.

در این مثال ما قصد داریم اطلاعات کاربر را با پایگاه داده MySQL مقایسه کنیم و از این رو باید یک پایگاه داده ارائه‌دهنده کاربر را ایجاد کنیم. همچنین باید پایگاه داده ارائه‌دهنده احراز هویت را نیز ایجاد کنیم تا بتواند منطق احراز هویت را مدیریت کند. در نهایت کلاس «کاربر» (User) را ایجاد می‌کنیم که به پیاده‌سازی رابط UserInterface می‌پردازد.

کلاس User

در این بخش، کلاس User را ایجاد می‌کنیم که نهاد کاربر را در فرایند احراز هویت نمایندگی می‌کند. بدین منظور باید در ادامه فایل src/User/User.php را با محتوای زیر ایجاد کنیم:

<?php
namespace Sfauth\User;
 
use Symfony\Component\Security\Core\User\UserInterface;
 
class User implements UserInterface
{
    private $username;
    private $password;
    private $roles;
 
    public function __construct(string $username, string $password, string $roles)
    {
        if (empty($username))
        {
            throw new \InvalidArgumentException('No username provided.');
        }
 
        $this->username = $username;
        $this->password = $password;
        $this->roles = $roles;
    }
 
    public function getUsername()
    {
        return $this->username;
    }
 
    public function getPassword()
    {
        return $this->password;
    }
 
    public function getRoles()
    {
        return explode(",", $this->roles);
    }
 
    public function getSalt()
    {
        return '';
    }
 
    public function eraseCredentials() {}
}

نکته مهم این است که کلاس User باید به پیاده‌سازی اینترفیس UserInterface در کامپوننت امنیت سیمفونی بپردازد. به جز آن هیچ چیز نامعمولی در این بخش وجود ندارد.

کلاس ارائه‌دهنده پایگاه داده

بارگذاری کاربران از بک‌اند در حیطه وظایف ارائه‌دهنده کاربر است. در این بخش یک پایگاه داده ارائه‌دهنده کاربر می‌سازیم که کاربر را از پایگاه داده MySQL بارگذاری می‌کند:

<?php
namespace Sfauth\User;
 
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use Doctrine\DBAL\Connection;
use Sfauth\User\User;
 
class DatabaseUserProvider implements UserProviderInterface
{
    private $connection;
 
    public function __construct(Connection $connection)
    {
        $this->connection = $connection;
    }
 
    public function loadUserByUsername($username)
    {
        return $this->getUser($username);
    }
 
    private function getUser($username)
    {
        $sql = "SELECT * FROM sf_users WHERE username = :name";
        $stmt = $this->connection->prepare($sql);
        $stmt->bindValue("name", $username);
        $stmt->execute();
        $row = $stmt->fetch();
 
        if (!$row['username'])
        {
            $exception = new UsernameNotFoundException(sprintf('Username "%s" not found in the database.', $row['username']));
            $exception->setUsername($username);
            throw $exception;
        }
        else
        {
            return new User($row['username'], $row['password'], $row['roles']);
        }
    }
 
    public function refreshUser(UserInterface $user)
    {
        if (!$user instanceof User)
        {
            throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', get_class($user)));
        }
 
        return $this->getUser($user->getUsername());
    }
 
    public function supportsClass($class)
    {
        return 'Sfauth\User\User' === $class;
    }
}

ارائه‌دهنده کاربر باید اینترفیس UserProviderInterface را پیاده‌سازی کند. ما از دکترین DBAL برای اجرای عملیات مرتبط با پایگاه داده استفاده می‌کنیم. از آنجا که ما اینترفیس UserProviderInterface را پیاده‌سازی کرده‌ایم، باید متدهای loadUserByUsername، refreshUser و supportsClass را نیز پیاده‌سازی کنیم.

متد loadUserByUsername باید کاربر را بر اساس «نام کاربری» (username) وی بارگذاری کند و این کار در متد getUser صورت می‌گیرد. اگر کاربر پیدا شود، شیء Sfauth\User\User متناظر را بازگشت می‌دهیم که اینترفیس UserInterface را پیاده‌سازی می‌کند. از سوی دیگر، متد refreshUser اقدام به تازه‌سازی شیء User ارائه شده از طریق واکشی جدیدترین اطلاعات از پایگاه داده می‌کند.

در نهایت متد supportsClass بررسی می‌کند که آیا ارائه‌دهنده DatabaseUserProvider از کلاس کاربر ارائه شده پشتیبانی می‌کند یا نه.

کلاس پایگاه داده ارائه‌دهنده احراز هویت

در نهایت ما باید ارائه‌دهنده احراز هویت کاربر را که به تعریف منطق احراز هویت، یعنی چگونگی احراز شدن هویت کاربر می‌پردازد، پیاده‌سازی کنیم. در مورد مثال خودمان، باید اطلاعات کاربر را با پایگاه داده MySQL مطابقت دهیم و از این رو باید منطق احراز هویت را نیز بر همین مبنا تعریف کنیم.

بدین ترتیب در ادامه فایل src/User/DatabaseAuthenticationProvider.php را با محتوای زیر ایجاد می‌کنیم:

<?php
namespace Sfauth\User;
 
use Symfony\Component\Security\Core\Authentication\Provider\UserAuthenticationProvider;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Core\User\UserCheckerInterface;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\Exception\AuthenticationServiceException;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
 
class DatabaseAuthenticationProvider extends UserAuthenticationProvider
{
    private $userProvider;
 
    public function __construct(UserProviderInterface $userProvider, UserCheckerInterface $userChecker, string $providerKey, bool $hideUserNotFoundExceptions = true)
    {
        parent::__construct($userChecker, $providerKey, $hideUserNotFoundExceptions);
 
        $this->userProvider = $userProvider;
    }
 
    protected function retrieveUser($username, UsernamePasswordToken $token)
    {
        $user = $token->getUser();
 
        if ($user instanceof UserInterface)
        {
            return $user;
        }
 
        try {
            $user = $this->userProvider->loadUserByUsername($username);
 
            if (!$user instanceof UserInterface)
            {
                throw new AuthenticationServiceException('The user provider must return a UserInterface object.');
            }
 
            return $user;
        } catch (UsernameNotFoundException $e) {
            $e->setUsername($username);
 
            throw $e;
        } catch (\Exception $e) {
            $e = new AuthenticationServiceException($e->getMessage(), 0, $e);
            $e->setToken($token);
            throw $e;
        }
    }
 
    protected function checkAuthentication(UserInterface $user, UsernamePasswordToken $token)
    {
        $currentUser = $token->getUser();
         
        if ($currentUser instanceof UserInterface)
        {
            if ($currentUser->getPassword() !== $user->getPassword())
            {
                throw new AuthenticationException('Credentials were changed from another session.');
            }
        }
        else
        {
            $password = $token->getCredentials();
 
            if (empty($password))
            {
                throw new AuthenticationException('Password can not be empty.');
            }
 
            if ($user->getPassword() != md5($password))
            {
                throw new AuthenticationException('Password is invalid.');
            }
        }
    }
}

ارائه‌دهنده احراز هویت با نام DatabaseAuthenticationProvider به بسط کلاس مجرد UserAuthenticationProvider می‌پردازد. از این رو باید متدهای مجرد retrieveUser و checkAuthentication را نیز پیاده‌سازی کنیم.

وظیفه متد retrieveUser بارگذاری کاربر از ارائه‌دهنده کاربر متناظر است. در مورد مثال ما، این متد از ارائه‌دهنده کاربر DatabaseUserProvider برای بارگذاری کاربر از پایگاه داده MySQL استفاده خواهد کرد.

از سوی دیگر، متد checkAuthentication بررسی‌های مورد نیاز را برای احراز هویت کاربر جاری اجرا می‌کند. دقت کنید که ما از متد MD5 برای رمزنگاری رمزعبور استفاده کرده‌ایم. البته شما باید از متدهای رمزنگاری امن‌تر برای ذخیره‌سازی رمزهای عبور کاربر استفاده کنید.

جمع‌بندی طرز کار کد

تا به اینجا ما همه عناصر مورد نیاز برای احراز هویت را ایجاد کرده‌ایم. در این بخش خواهیم دید که چگونه همه این عناصر در کنار هم قرار می‌گیرند تا کارکرد احراز هویت کاربر راه‌اندازی شود. در ادامه فایل db_auth.php را ایجاد کرده و آن را با محتوای زیر به‌روزرسانی کنید:

<?php
require_once './vendor/autoload.php';
 
use Sfauth\User\DatabaseUserProvider;
use Symfony\Component\Security\Core\User\UserChecker;
use Sfauth\User\DatabaseAuthenticationProvider;
use Symfony\Component\Security\Core\Authentication\AuthenticationProviderManager;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
 
// init doctrine db connection
$doctrineConnection = \Doctrine\DBAL\DriverManager::getConnection(
  array('url' => 'mysql://{USERNAME}:{PASSWORD}@{HOSTNAME}/{DATABASE_NAME}'), new \Doctrine\DBAL\Configuration()
);
 
// init our custom db user provider
$userProvider = new DatabaseUserProvider($doctrineConnection);
 
// we'll use default UserChecker, it's used to check additional checks like account lock/expired etc.
// you can implement your own by implementing UserCheckerInterface interface
$userChecker = new UserChecker();
 
// init our custom db authentication provider
$dbProvider = new DatabaseAuthenticationProvider(
    $userProvider,
    $userChecker,
    'frontend'
);
 
// init authentication provider manager
$authenticationManager = new AuthenticationProviderManager(array($dbProvider));
 
try {
    // init un/pw, usually you'll get these from the $_POST variable, submitted by the end user
    $username = 'admin';
    $password = 'admin';
 
    // get unauthenticated token
    $unauthenticatedToken = new UsernamePasswordToken(
        $username,
        $password,
        'frontend'
    );
 
    // authenticate user & get authenticated token
    $authenticatedToken = $authenticationManager->authenticate($unauthenticatedToken);
 
    // we have got the authenticated token (user is logged in now), it can be stored in a session for later use
    echo $authenticatedToken;
    echo "\n";
} catch (AuthenticationException $e) {
    echo $e->getMessage();
    echo "\n";
}

به خاطر داشته باشید که در گردش کار احراز هویت که در ابتدای این مقاله مورد بررسی قرار دادیم، به همین توالی کارهایی که در کد فوق می‌بینید، اشاره کرده بودیم. نخستین چیزی که باید بازیابی شود اطلاعات کاربر است و به وسیله آن یک توکن احراز نشده می‌سازیم.

$unauthenticatedToken = new UsernamePasswordToken(
    $username,
    $password,
    'frontend'
);

سپس توکن را به بخش مدیریت احراز هویت ارسال می‌کنیم تا مورد اعتبار سنجی قرار گیرد.

// authenticate user & get authenticated token
$authenticatedToken = $authenticationManager->authenticate($unauthenticatedToken);

زمانی که متد احراز هویت فراخوانی می‌شود، چیزهای زیادی در پس‌زمینه اجرا می‌شوند. در ابتدا بخش مدیریت احراز هویت اقدام به گزینش ارائه‌دهنده مناسب احراز هویت می‌کند. در مورد مثال ما، این همان ارائه‌دهنده احراز هویت با نام DatabaseAuthenticationProvider است که برای احراز هویت گزینش خواهد شد.

کامپوننت امنیت سیمفونی

سپس اقدام به بازیابی کاربر به وسیله نام کاربری از ارائه‌دهنده کاربر به نام DatabaseUserProvider می‌کنیم. در نهایت متد checkAuthentication بررسی‌های لازم برای احراز هویت درخواست کاربر جاری را انجام می‌دهد.

اگر می‌خواهید اسکریپت db_auth.php را تست کنید، باید یک جدول sf_users در پایگاه داده MySQL بسازید.

CREATE TABLE `sf_users` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `username` varchar(255) NOT NULL,
  `password` varchar(255) NOT NULL,
  `roles` enum('registered','moderator','admin') DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB;
 
INSERT INTO `sf_users` VALUES (1,'admin','21232f297a57a5a743894a0e4a801fc3','admin');

در ادامه اسکریپت db_auth.php را اجرا کنید تا طرز کار آن را ببینید. با تکمیل موفقیت‌آمیز آن شما می‌توانید یک توکن احراز هویت را چنان که در قطعه کد زیر می‌بینید دریافت کنید.

$php db_auth.php
UsernamePasswordToken(user="admin", authenticated=true, roles="admin")

زمانی که کاربر احراز هویت شد، می‌توانید توکن احراز هویت را در یک «سِشِن» (Session) ذخیره کنید تا برای درخواست‌های بعدی استفاده شود. به این ترتیب دموی احراز هویت ساده ما تکمیل می‌شود.

سخن پایانی

در این مقاله به بررسی کامپوننت امنیت سیمفونی پرداختیم که به ما امکان می‌دهد تا امکانات امنیتی را در اپلیکیشن‌های PHP خود داشته باشیم. به طور خاص ما به بررسی امکان احراز هویت پرداختیم که در کامپوننت فرعی symfony/security-core ارائه شده است و با ارائه نمونه‌ای از پیاده‌سازی آن در یک اپلیکیشن PHP مثالی از طرز کار آن ارائه کردیم.

اگر این نوشته برای شما مفید بوده است، آموزش‌های زیر نیز به شما پیشنهاد می‌شوند:

==

اگر بازخوردی درباره این مطلب دارید یا پرسشی دارید که بدون پاسخ مانده است، آن را از طریق بخش نظرات مطرح کنید.

«میثم لطفی» دانش‌آموخته ریاضیات و شیفته فناوری به خصوص در حوزه رایانه است. وی در حال حاضر علاوه بر پیگیری علاقه‌مندی‌هایش در رشته‌های برنامه‌نویسی، کپی‌رایتینگ و محتوای چندرسانه‌ای، در زمینه نگارش مقالاتی با محوریت نرم‌افزار نیز با مجله فرادرس همکاری دارد.

بر اساس رای 1 نفر

آیا این مطلب برای شما مفید بود؟

نظر شما چیست؟

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *