Безпека

27/04/2016 0 symfony, налаштування

Система безпеки у Symfony надзвичайно потужна, проте, деколи можуть виникати труднощі з її налаштуванням. У цьому розділі, ви навчитеся, як  встановити та налаштувати систему безпеки крок за кроком, від налаштування фаєрвола і завантаження користувачів до заборони доступу і вибірки об’єкта User. Залежно від того, що вам буде потрібно, початкове налаштування може бути складним процесом. Але коли ви все зробите, побачите наскільки ця система безпеки гнучка і легка у роботі.  

Так як у нас буде довга розмова, ми поділили цей розділ на декілька підрозділів:  

  1. Початкове налаштування security.yml (автентифікація);

  2. Забороняємо доступ до вашого додатку (авторизація);

  3. Вибірка поточного об’єкта User.

Після кожного підрозділу ви зможете прочитати пару маленьких, але цікавих секцій, деякі з них це “Вихід з системи” та “Кодування паролів користувачів”.

1) Початкове налаштування security.yml (автентифікація);

Система безпеки налаштовуватиметься у файлі app/config/security.yml. Налаштування за замовчуванням виглядає наступним чином:

YAML

# app/config/security.yml
security:
    providers:
        in_memory:
            memory: ~

    firewalls:
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false

        default:
            anonymous: ~

XML

<!-- app/config/security.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<srv:container xmlns="http://symfony.com/schema/dic/security"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:srv="http://symfony.com/schema/dic/services"
    xsi:schemaLocation="http://symfony.com/schema/dic/services
        http://symfony.com/schema/dic/services/services-1.0.xsd">

    <config>
        <provider name="in_memory">
            <memory />
        </provider>

        <firewall name="dev"
            pattern="^/(_(profiler|wdt)|css|images|js)/"
            security="false" />

        <firewall name="default">
            <anonymous />
        </firewall>
    </config>
</srv:container>

PHP

// app/config/security.php
$container->loadFromExtension('security', array(
    'providers' => array(
        'in_memory' => array(
            'memory' => null,
        ),
    ),
    'firewalls' => array(
        'dev' => array(
            'pattern'    => '^/(_(profiler|wdt)|css|images|js)/',
            'security'   => false,
        ),
        'default' => array(
            'anonymous'  => null,
        ),
    ),
));

Ключ фаєрвола (firewall) - це серце всього налаштування системи безпеки. Фаєрвол  dev в даному випадку не важливий, він просто перевіряє чи інструменти розробки Symfony - які знаходяться під URL-адресами /_profiler і  /_wdt - не блокуватимуться системою безпеки.  

Ви також може зробити запит щодо інших деталей запиту (напрклад, хостинг). Більше інформації читайте у довіднику.

Всі інші URL-адреси оброблятимуться фаєрволом за замовчуванням (якщо немає  ключа pattern - це означає, що вони збігаються з усіма URL-адресами). Ви можете вважати фаєрвол вашою системою безпеки, і майте один основний фаєрвол - зазвичай це важливо. Але це не означає що кожній URL-адресі потрібна автентифікація, адже про це попіклується ключ anonymous.  Фактично, якщо ви зараз зайдете на вашу домашню сторінку, ви матимете доступ, і будете “автентифіковані” під іменем anon.. Не звертайте уваги на напис "Yes"  біля “Authenticated”, тому що ви анонімний користувач:  

 

 

Як заборонити доступ до деяких URL-адрес, або контролерів ви дізнаєтесь раніше.  

Безпеку можна легко налаштувати прочитавши пояснення абсолютно до усіх опцій в документації.  

A) Налаштовуємо автентифікацію користувачів

Основне завдання фаєрвола - налаштувати автентифікацію користувачів. Чи будуть вони використовувати форму реєстрації? Базову автентифікацію HTTP? Знак API? А можливо все перераховане?  

Давайте розпочнемо з базової автентифікації HTTP. Для активації додайте після фаєрволу ключ http_basic:

YAML

# app/config/security.yml
security:
    # ...

    firewalls:
        # ...
        default:
            anonymous: ~
            http_basic: ~

XML

<!-- app/config/security.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<srv:container xmlns="http://symfony.com/schema/dic/security"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:srv="http://symfony.com/schema/dic/services"
    xsi:schemaLocation="http://symfony.com/schema/dic/services
        http://symfony.com/schema/dic/services/services-1.0.xsd">

    <config>
        <!-- ... -->

        <firewall name="default">
            <anonymous />
            <http-basic />
        </firewall>
    </config>
</srv:container>

PHP

// app/config/security.php
$container->loadFromExtension('security', array(
    // ...
    'firewalls' => array(
        // ...
        'default' => array(
            'anonymous'  => null,
            'http_basic' => null,
        ),
    ),
));

Так просто! Щоб спробувати цей спосіб, вам потрібно надіслати запит користувачеві, про те, що він повинен бути зареєстрованим, аби побачити сторінку. Щоб зробти дану ситуацію цікавішою, створимо нову сторінку в /admin. Якщо ви використовуєте анотації, то напишіть подібний код:

// src/AppBundle/Controller/DefaultController.php
// ...

use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Component\HttpFoundation\Response;

class DefaultController extends Controller
{
    /**
     * @Route("/admin")
     */
    public function adminAction()
    {
        return new Response('<html><body>Admin page!</body></html>');
    }
}

Далі, додамо до файлу security.yml клас access_control, який покаже користувачеві, що для доступу до цієї URL-адреси, він повинен бути зареєстрованим:  

YAML

# app/config/security.yml
security:
    # ...
    firewalls:
        # ...
        default:
            # ...

    access_control:
        # require ROLE_ADMIN for /admin*
        - { path: ^/admin, roles: ROLE_ADMIN }

XML

<!-- app/config/security.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<srv:container xmlns="http://symfony.com/schema/dic/security"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:srv="http://symfony.com/schema/dic/services"
    xsi:schemaLocation="http://symfony.com/schema/dic/services
        http://symfony.com/schema/dic/services/services-1.0.xsd">

    <config>
        <!-- ... -->

        <firewall name="default">
            <!-- ... -->
        </firewall>

        <!-- require ROLE_ADMIN for /admin* -->
        <rule path="^/admin" role="ROLE_ADMIN" />
    </config>
</srv:container>

PHP

// app/config/security.php
$container->loadFromExtension('security', array(
    // ...
    'firewalls' => array(
        // ...
        'default' => array(
            // ...
        ),
    ),
   'access_control' => array(
       // require ROLE_ADMIN for /admin*
        array('path' => '^/admin', 'role' => 'ROLE_ADMIN'),
    ),
));

Більше інформації щодо ROLE_ADMIN, а також заборону доступу, ви прочитаєте у наступни підрозілах.

Чудово! Тепер коли ви перейдете до /admin, ви побачите рядок вводу базової автентифікації HTTP:

Але кого ви будете реєструвати? Звідки приходять користувачі?  

Хочете використати традиційну форму реєстрації або створити її іншими методами? Супер! У довіднику ви зможете дізнатися про те, які ще методи можна використати.  

Якщо ваш додаток логує користувачів через стороннє обслуговування таке як Google, Facebook, або Twitter, перевірте пакет HWIOAuthBundle.

B) Налаштовуємо процес завантаження користувачів

Коли ви вписуєте ваш логін, Symfony потрібно завантажити інформацію про користувача з програмного забезпечення. Це називається "постачальник користувачів" (“user provider”), і ви відповідальні за його налаштування. Symfony використовує вбудований шлях завантаження користувачів з бази даних, проте, ви можете створити власного постачальника користувачів. Найлегший (проте найбільш обмежений) шлях - налаштувати Symfony тким чином, щоб програма завантажувала користувачів напряму з файлу security.yml. Цей процес називається провайдер “у пам’яті”, але краще називати його провайдером “ у конфігурації”:

YAML

# app/config/security.yml
security:
    providers:
        in_memory:
            memory:
                users:
                    ryan:
                        password: ryanpass
                        roles: 'ROLE_USER'
                    admin:
                        password: kitten
                        roles: 'ROLE_ADMIN'
    # ... 

XML

<!-- app/config/security.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<srv:container xmlns="http://symfony.com/schema/dic/security"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:srv="http://symfony.com/schema/dic/services"
    xsi:schemaLocation="http://symfony.com/schema/dic/services
        http://symfony.com/schema/dic/services/services-1.0.xsd">

    <config>
        <provider name="in_memory">
            <memory>
                <user name="ryan" password="ryanpass" roles="ROLE_USER" />
                <user name="admin" password="kitten" roles="ROLE_ADMIN" />
            </memory>
        </provider>
        <!-- ... -->
    </config>
</srv:container>

PHP

// app/config/security.php
$container->loadFromExtension('security', array(
    'providers' => array(
        'in_memory' => array(
            'memory' => array(
                'users' => array(
                    'ryan' => array(
                        'password' => 'ryanpass',
                        'roles' => 'ROLE_USER',
                    ),
                    'admin' => array(
                        'password' => 'kitten',
                        'roles' => 'ROLE_ADMIN',
                    ),
                ),
            ),
        ),
    ),
    // ...
));

Так само як і з фаєрволами (firewalls), ви можете мати багато провайдерів (providers), проте потрібен вам лише один. Якщо у вас їх багато, налаштуйте котрий з них ви будете використовувати для фаєрволу під ключем провайдера: (наприклад, provider: in_memory).

Про деталі, налаштування і використання багатьох провайдерів, читайте у довіднику.  

Постарайтесь увійти використовуючи логін admin і пароль kitten. Ви побачите повідомлення про помилку.

“Для акаунта не було налаштовано кодер "Symfony\Component\Security\Core\User\User"

Щоб це виправити, додайте ключ encoder:

YAML
# app/config/security.yml
security:
    # ...

    encoders:
        Symfony\Component\Security\Core\User\User: plaintext
    # ...

XML

<!-- app/config/security.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<srv:container xmlns="http://symfony.com/schema/dic/security"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:srv="http://symfony.com/schema/dic/services"
    xsi:schemaLocation="http://symfony.com/schema/dic/services
        http://symfony.com/schema/dic/services/services-1.0.xsd">

    <config>
        <!-- ... -->

        <encoder class="Symfony\Component\Security\Core\User\User"
            algorithm="plaintext" />
        <!-- ... -->
    </config>
</srv:container>

PHP

// app/config/security.php
$container->loadFromExtension('security', array(
    // ...

    'encoders' => array(
        'Symfony\Component\Security\Core\User\User' => 'plaintext',
    ),
    // ...
));

Провайдери користувачів завантажують інформацію про них і поміщають її в об’єкт User. Якщо ви завантажуєте користувачів з бази даних або іншого ресурсу, ви будете використовувати власний користувацький клас User. Але коли ви будете користуватися провайдером “у пам’яті”, ви отримаєте об’єкт Symfony\Component\Security\Core\User\User.

Незважаючи на те, яким є ваш клас, вам потрібно вказати Symfony, який алгоритм використовувати для зчитування паролів. У даному випадку, паролі виступають простим текстом, а в іншому - вам потрібно змінити його на bcrypt.

Якщо тепер оновити сторінку, ви уввійдете в систему.

Тепер панель налагодження показує вам хто ви, і яка ваша роль:  

 

Коли ви входите під іменем ryan,  URL-адреса вимагає ROLE_ADMIN, і не дає вам доступу. У наступних підрозділах ми розповімо про це детальніше.   

Завантажуємо користувачів з бази даних

Якщо ви хочете завантажувати користувачів через Doctrine, будь ласка! Усі деталі ви зможете прочитати у довіднику.  

C) Шифрування пароля користувача

Незалежно від того, де зберігається інформація про користувачів, чи це security.yml, база даних, або щось інше, програма повинна шифрувати паролі. Тут найкращим алгоритмом буде bcrypt:

YAML

# app/config/security.yml
security:
    # ...

    encoders:
        Symfony\Component\Security\Core\User\User:
            algorithm: bcrypt
            cost: 12

XML

<!-- app/config/security.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<srv:container xmlns="http://symfony.com/schema/dic/security"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:srv="http://symfony.com/schema/dic/services"
    xsi:schemaLocation="http://symfony.com/schema/dic/services
        http://symfony.com/schema/dic/services/services-1.0.xsd">

    <config>
        <!-- ... -->

        <encoder class="Symfony\Component\Security\Core\User\User"
            algorithm="bcrypt"
            cost="12" />

        <!-- ... -->
    </config>
</srv:container>

PHP

// app/config/security.php
$container->loadFromExtension('security', array(
    // ...

    'encoders' => array(
        'Symfony\Component\Security\Core\User\User' => array(
            'algorithm' => 'bcrypt',
            'cost' => 12,
        )
    ),
    // ...
));

Тепер паролі користувачів повинні шифруватися за цим алгоритмом. Для жорстко закодованої інформації про користувачів, ви можете використати вбудовану команду:  

$ php bin/console security:encode-password

Ви отримаєте такий код:  

YAML

# app/config/security.yml
security:
    # ...

    providers:
        in_memory:
            memory:
                users:
                    ryan:
                        password: $2a$12$LCY0MefVIEc3TYPHV9SNnuzOfyr2p/AXIGoQJEDs4am4JwhNz/jli
                        roles: 'ROLE_USER'
                    admin:
                        password: $2a$12$cyTWeE9kpq1PjqKFiWUZFuCRPwVyAZwm4XzMZ1qPUFl7/flCM3V0G
                        roles: 'ROLE_ADMIN'

XML

<!-- app/config/security.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<srv:container xmlns="http://symfony.com/schema/dic/security"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:srv="http://symfony.com/schema/dic/services"
    xsi:schemaLocation="http://symfony.com/schema/dic/services
        http://symfony.com/schema/dic/services/services-1.0.xsd">

    <config>
        <!-- ... -->

        <provider name="in_memory">
            <memory>
                <user name="ryan" password="$2a$12$LCY0MefVIEc3TYPHV9SNnuzOfyr2p/AXIGoQJEDs4am4JwhNz/jli" roles="ROLE_USER" />
                <user name="admin" password="$2a$12$cyTWeE9kpq1PjqKFiWUZFuCRPwVyAZwm4XzMZ1qPUFl7/flCM3V0G" roles="ROLE_ADMIN" />
            </memory>
        </provider>
    </config>
</srv:container>

PHP

// app/config/security.php
$container->loadFromExtension('security', array(
    // ...

    'providers' => array(
        'in_memory' => array(
            'memory' => array(
                'users' => array(
                    'ryan' => array(
                        'password' => '$2a$12$LCY0MefVIEc3TYPHV9SNnuzOfyr2p/AXIGoQJEDs4am4JwhNz/jli',
                        'roles' => 'ROLE_USER',
                    ),
                    'admin' => array(
                        'password' => '$2a$12$cyTWeE9kpq1PjqKFiWUZFuCRPwVyAZwm4XzMZ1qPUFl7/flCM3V0G',
                        'roles' => 'ROLE_ADMIN',
                    ),
                ),
            ),
        ),
    ),
    // ...
));

Все працюватиме точнісінько як і раніше. Проте, якщо у вас є активні користувачі (наприклад, з бази даних), як ви зможете програмно шифрувати паролі перед завантаженням інформації до бази даних? Щоб отримати відповідь на питання читайте підрозділ “Шифрування паролів активних користувачів”.  

Алгоритми для цього методу залежатимуть від того, якою версією PHP ви користуєтетеся, але і включатимуть алгоритми повернені функцією PHP hash_algos, а також іншими (наприклад, bcrypt). В секції безпеки ви можете знайти приклади ключа кодерів (encoders key).  

Можна використовувати різні хешовані алгоритми на основі “користувач за користувачем”. Щоб поглибити знання стосовно цієї теми, читайте довідник.  

D) З налаштуванням справились!  

Вітаємо! Тепер у вас є робоча система автентифікації, що використовує  автентифікацію основану на HTTP і завантажує інформацію про користувачів напряму з файлу  security.yml.

Наступні кроки будуть залежати від налаштування:  

  • налаштуйте інший шлях для входу в систему ваших користувачів, наприклад форму або щось повністю користувацьке;  

  • інформацію про користувачів завантажуйте зовсім з іншого ресурсу, типу бази даних або інших;  

  • навчіться забороняти доступ; завантажте об’єкт User, і візміться за “ролі” які ми опишемо в підрозділі “Авторизація”.

2) Як заборонити доступ, що таке ролі та інші властивості авторизації

Тепер користувачі можуть увійти у ваш додаток використовуючи http_basic, або інший метод. Чудово! Тепер вам пора навчитися забороняти доступ і працювати з об’єктом User. Все це авторизація, і її завдання - вирішити чи користувач може мати доступ до ресурсів (URL-адреси, об’єкт-моделі, запиту на метод…). 

Процес авторизації має дві різні сторони:  

  1. Користувач отримує особливий набір ролей при логуванні (наприклад, ROLE_ADMIN).

  2. Ви додаєте код, і ресурс вимагає певної “властивості” (зазвичай ролі ROLE_ADMIN) для отримання доступу.   

Окрім ролей (ROLE_ADMIN), ви можете захистити ресурс іншими властивостями/ланцюжками (наприклад, EDIT), і використати список контролю доступу Symfony, щоб задати ці значення. Це стане у пригоді, якщо ви захочете перевірити, чи користувач А може “редагувати” певний об’єкт В (наприклад, продукт з id 5). Детальніше у підрозділі “Списки контролю доступу (ACLs): Захист окремих об’єктів бази даних”.

Ролі

Коли користувач входить в систему, він отримує набір ролей (англ. roles), наприклад, ROLE_ADMIN. У прикладі, який ми наводили вище, це записано у файлі security.yml. Якщо ви завантажуєте інформацію про користувачів з бази даних, скоріше за все вона буде розташована у колонці вашої таблиці.

Усі ролі, які ви приписуєте користувачеві, повинні розпочинатися з префікса ROLE_. В іншому випадку система безпеки Symfony не буде нормально їх зчитувати (тобто, якщо ви робите щось нове, приписуючи користувачеві роль, наприклад FOO, а потім перевіряєте її так як описано у підрозділі “Додаємо код, щоб заборонити доступ” - це не спрацює).

Ролі посуті прості, це рядки, які ви можете створювати і використовувати при потребі. Наприклад, якщо вам потрібно обмежити доступ до адмінки вашого веб-сайту, ви можете захистити цю секцію, використавиши роль ROLE_BLOG_ADMIN. Цю роль не потрібно визначати,  просто починайте користуватися нею.  

Усі користувачі повинні мати хоча б одну роль, в іншому випадку, вони не будуть автентифіковані. Зазвичай кожен користувач має роль ROLE_USER.

Ви можете також вказати ієрархію ролей, де деякі ролі автоматично визначають що існують і інші ролі.  

Додаємо код, щоб заборонити доступ

Існує два шляхи, за допомогою яких можна заборонити доступ:  

  1. access_control in security.yml дозволяє захистити шаблони URL (наприклад,  /admin/*). Це просто, проте менш зручно.  

  2. у вашому коді через сервіс security.authorization_checker.

Безпека шаблонів URL (access_control)

Найпростіший спосіб тримати у безпеці частину вашої програми - це оберігати суцільний шаблон URL. Ви вже бачили це раніше,  де все, що збігається з регулярним виразом  ^/admin, вимагає роль ROLE_ADMIN:

YAML

# app/config/security.yml
security:
    # ...

    firewalls:
        # ...
        default:
            # ...

    access_control:
        # require ROLE_ADMIN for /admin*
        - { path: ^/admin, roles: ROLE_ADMIN }

XML

<!-- app/config/security.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<srv:container xmlns="http://symfony.com/schema/dic/security"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:srv="http://symfony.com/schema/dic/services"
    xsi:schemaLocation="http://symfony.com/schema/dic/services
        http://symfony.com/schema/dic/services/services-1.0.xsd">

    <config>
        <!-- ... -->

        <firewall name="default">
            <!-- ... -->
        </firewall>

        <!-- require ROLE_ADMIN for /admin* -->
        <rule path="^/admin" role="ROLE_ADMIN" />
    </config>
</srv:container>

PHP

// app/config/security.php
$container->loadFromExtension('security', array(
    // ...

    'firewalls' => array(
        // ...
        'default' => array(
            // ...
        ),
    ),
   'access_control' => array(
       // require ROLE_ADMIN for /admin*
        array('path' => '^/admin', 'role' => 'ROLE_ADMIN'),
    ),
));

Це хороший спосіб, для безпеки цілих секцій, але ви також захочете підтримувати безпеку окремих контролерів.  
Ви можете визначити стільки шаблонів URL, скільки вам потрібно - кожен має бути регулярним виразом. Проте, лише один буде збігатися. Symfony переглядатиме кожен з них, починаючи з самого верху і зупиниться тоді, коли знайде клас access_control, який збігатиметься з URL.

YAML

# app/config/security.yml
security:
    # ...

    access_control:
        - { path: ^/admin/users, roles: ROLE_SUPER_ADMIN }
        - { path: ^/admin, roles: ROLE_ADMIN }

XML

<!-- app/config/security.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<srv:container xmlns="http://symfony.com/schema/dic/security"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:srv="http://symfony.com/schema/dic/services"
    xsi:schemaLocation="http://symfony.com/schema/dic/services
        http://symfony.com/schema/dic/services/services-1.0.xsd">

    <config>
        <!-- ... -->

        <rule path="^/admin/users" role="ROLE_SUPER_ADMIN" />
        <rule path="^/admin" role="ROLE_ADMIN" />
    </config>
</srv:container>

PHP

// app/config/security.php
$container->loadFromExtension('security', array(
    // ...

    'access_control' => array(
        array('path' => '^/admin/users', 'role' => 'ROLE_SUPER_ADMIN'),
        array('path' => '^/admin', 'role' => 'ROLE_ADMIN'),
    ),
));

Використавши знак ^ в рядку означає, що підходитимуть лише ті URL-адреси, які починаються з шаблону. Наприклад, рядок  /admin (без ^) буде збігатися з /admin/foo і  URL на кшталт /foo/admin.

Як працює  how access_control

Секція access_control є дуже потужною, і в одночас небезпечною (адже вона містить захист) для тих, хто не знає, як нею користуватися. Крім URL-адреси, access_control може збігатися з IP-адресою, іменем хостингу і методами HTTP. Секція access_control також використовується для перенаправлення користувача до версії http шаблона  URL.  

Детальніше про це можна буде вивчити у довіднику.  

Підтримуємо безпеку контролерів та іншого коду

Ви легко можете заборонити доступ зсередини контролера:

// ...

public function helloAction($name)
{
    // The second parameter is used to specify on what object the role is tested.
    $this->denyAccessUnlessGranted('ROLE_ADMIN', null, 'Unable to access this page!');

    // Old way :
    // if (false === $this->get('security.authorization_checker')->isGranted('ROLE_ADMIN')) {
    //     throw $this->createAccessDeniedException('Unable to access this page!');
    // }

    // ...
}

В обох випадках ви побачите повідомлення AccessDeniedException, яке зумовлює відповідь HTTP 403 в Symfony.   

Ось так! Якщо користувач ще не увійшов в систему, вона попросить його увійти (наприклад, перенаправить на іншу сторінку). Якщо він вже увійшов, але не має ролі ROLE_ADMIN, він буде перенаправлений на сторінку заборони доступу 403 (яку ви можете налаштувати). Якщо він увійшов і має відповідні ролі, код виконуватиметься.

Разом з SensioFrameworkExtraBundle, ви можете надавати безпеку вашому контролері використовуючи анотації:

// ...
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;

/**
 * @Security("has_role('ROLE_ADMIN')")
 */
public function helloAction($name)
{
    // ...
}

Деталі ви можете дізнатися тут.

Контроль доступу в шаблонах

Якщо ви хочете перевірити чи поточний користувач має роль всередині шаблону , використовуйте вбудовану допоміжну функцію is_granted().  

TWIG

{% if is_granted('ROLE_ADMIN') %}
    <a href="...">Delete</a>
{% endif %}

PHP

<?php if ($view['security']->isGranted('ROLE_ADMIN')): ?>
    <a href="...">Delete</a>
<?php endif ?>

Безпека інших сервісів

У Symfony ви можете захистити все, роблячи щось на кшталт коду, який використовується, аби захистити контролер. Уявімо, у вас є сервіс (тобто PHP клас), і його завдання - надсилати електронні листи. Ви вправі обмежити використання даного класу - не важливо де він використовувася - лише певними користувачами.  

Детальніше читайте у довіднику.

Перевіряємо чи користувач зайшов в систему (IS_AUTHENTICATED_FULLY)

Ви вже перевірили доступ, що базується на ролях - користувачам призначаються рядки які розпочинаються з ROLE_. Але якщо ви хочете перевірити чи користувач увійшов в систему (неважливо, яка у нього роль), тоді використовуйте IS_AUTHENTICATED_FULLY:

// ...

public function helloAction($name)
{
    if (!$this->get('security.authorization_checker')->isGranted('IS_AUTHENTICATED_FULLY')) {
        throw $this->createAccessDeniedException();
    }

    // ...
}

Ви також можете використати це при роботі з access_control.

IS_AUTHENTICATED_FULLY не виступає роллю, але будь-який користувач, хто  успішно увійшов в систему, матиме таку властивість. Загалом, існує три спеціальні властивості такого типу:  

  • IS_AUTHENTICATED_REMEMBERED: Всі зареєстровані користувачі матимуть цю властивість, навіть у випадку, якщо вони увійшли в систему використавши файли куки “запам’ятати мене”. Якщо ви не використовуєте функцію “запам’ятати мене”, ви можете використати дану властивість, щоб перевірити чи користувач увійшов в систему.  

  • IS_AUTHENTICATED_FULLY: подібна до функції  IS_AUTHENTICATED_REMEMBERED, проте потужніша. Користувачі, які увійшли в систему лише завдяки файлам куки “запам’ятати мене”, матимуть властивість IS_AUTHENTICATED_REMEMBERED але не IS_AUTHENTICATED_FULLY.

  • IS_AUTHENTICATED_ANONYMOUSLY: цю властивість отримують усі користувачі (навіть анонімні) - це стає у пригоді, коли список URL-адрес гарантує доступ - дельніше читайте у довіднику Symfony.  

А ще ви можете використовувати вирази всередині ваших шаблонів:  

TWIG

{% if is_granted(expression(
    '"ROLE_ADMIN" in roles or (user and user.isSuperAdmin())'
)) %}
    <a href="...">Delete</a>
{% endif %}

PHP

<?php if ($view['security']->isGranted(new Expression(
    '"ROLE_ADMIN" in roles or (user and user.isSuperAdmin())'
))): ?>
    <a href="...">Delete</a>
<?php endif; ?>

Якщо ви бажаєте поглибити знання, вивчити більше про вирази та безпеку, читайте розділ “Безпека: складний контроль доступу, використовуючи вирази” у довіднику.  

Список контролю доступу: зберігаємо окремі об’єкти у базі даних

Уявіть, що ви створюєте блог, де користувачі можуть коменувати ваші пости. Тепер ви хочете щоб користувач міг редагувати власний коментар, але не міг редагувати коментарі інших користувачів. Також в якості адміністратора, ви хочете мати можливість редагувати коментарі усіх користувачів.  

Оберіть один з двох варіантів для виконання даного завдання:  

  • Voters дозволяють написати власний алгоритм (наприклад, користувач хоче редагувати власний пост, тому що публікував його власноруч) щоб визначити доступ. Вам пригодиться дана опція, адже вона достатньо гнучка, щоб справитися з нашим завданням.  

  • Список контролю доступу дозволяє створювати структуру бази даних, де ви зможете призначити довільного користувача, який матиме будь-який доступ (наприклад, редагування або перегляд) до усіх об’єктів вашої системи. Користуйтеся даним способом, коли вам потрібно, щоб адмін міг надавати налаштований доступ по всій системі через певний інтерфейс.  

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

Отримання об’єкта користувача

Після автентифікації, об’єкт User поточного користувача можна отримати через сервіс  security.token_storage. З середини контролера, це виглядатиме наступним чином:

public function indexAction()
{
    if (!$this->get('security.authorization_checker')->isGranted('IS_AUTHENTICATED_FULLY')) {
        throw $this->createAccessDeniedException();
    }

    $user = $this->getUser();

    // the above is a shortcut for this
    $user = $this->get('security.token_storage')->getToken()->getUser();
}

Користувач буде об’єктом, а клас цього об’єкта залежатиме від провайдера користувача.

Тепер ви можете використовувати будь-які методи з вашим об’єктом User. Наприклад, якщо об’єкт User містить метод getFirstName(), зробіть наступне:

use Symfony\Component\HttpFoundation\Response;
// ...

public function indexAction()
{
    // ...

    return new Response('Well hi there '.$user->getFirstName());
}

Завжди перевіряйте чи користувач увійшов в систему

Важливо спочатку перевірити чи користувач автентифікований. Якщо ні,  $user матиме значення null або рядок символів anon.. Якщо ви не увійшли в систему, користувач технічно міститиме рядок символів  anon., хоча скорочення контролера getUser() перетворюватиме його для зручності на null.  

Суть ось у чому: завжди перевіряйте чи користувач увійшов в систему перед використанням об’єкта User,  для цього застосовуйте метод isGranted (або access_control):

// yay! Use this to see if the user is logged in
if (!$this->get('security.authorization_checker')->isGranted('IS_AUTHENTICATED_FULLY')) {
    throw $this->createAccessDeniedException();
}

// boo :(. Never check for the User object to see if they're logged in
if ($this->getUser()) {

}

Отримуємо користувача в шаблоні

У шаблоні Twig цей об’єкт можна отримати через ключ app.user:

TWIG

{% if is_granted('IS_AUTHENTICATED_FULLY') %}
    <p>Username: {{ app.user.username }}</p>
{% endif %}

PHP

<?php if ($view['security']->isGranted('IS_AUTHENTICATED_FULLY')): ?>
    <p>Username: <?php echo $app->getUser()->getUsername() ?></p>
<?php endif; ?>

Вихід з системи

Зазвичай, вам буде потрібно, щоб користувачі могли вийти із системи. Нащастя коли ви активуєте параметр конфігурації logout, фаєрвол справиться з цим автоматично:

YAML

# app/config/security.yml
security:
    # ...

    firewalls:
        secured_area:
            # ...
            logout:
                path:   /logout
                target: /

XML

<!-- app/config/security.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<srv:container xmlns="http://symfony.com/schema/dic/security"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:srv="http://symfony.com/schema/dic/services"
    xsi:schemaLocation="http://symfony.com/schema/dic/services
        http://symfony.com/schema/dic/services/services-1.0.xsd">

    <config>
        <!-- ... -->

        <firewall name="secured_area">
            <!-- ... -->
            <logout path="/logout" target="/" />
        </firewall>
    </config>
</srv:container>

PHP

// app/config/security.php
$container->loadFromExtension('security', array(
    // ...

    'firewalls' => array(
        'secured_area' => array(
            // ...
            'logout' => array('path' => '/logout', 'target' => '/'),
        ),
    ),
));

Далі, створимо маршрут для URL-адреси (але не контролера):

YAML

# app/config/routing.yml
logout:
    path: /logout

XML

<!-- app/config/routing.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<routes xmlns="http://symfony.com/schema/routing"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://symfony.com/schema/routing
        http://symfony.com/schema/routing/routing-1.0.xsd">

    <route id="logout" path="/logout" />
</routes>

PHP

// app/config/routing.php
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\Route;

$collection = new RouteCollection();
$collection->add('logout', new Route('/logout'));

return $collection;

Ось і все! Відсилаючи користувача до /logout (або де інде, залежно від того як ви налаштували path), Symfony “розавтентифікує” поточного користувача. Після того як він вийшов з системи, користувач буде перенаправлений на те ім’я шляху, яке задано параметром target зверху (наприклад, homepage).

Після того як ви вийшли з системи, зробимо щось цікавіше, просто вкажемо помічника успішного виходу із системи, додавши ключ success_handler і впишемо це до id сервісу в класі, який виконує LogoutSuccessHandlerInterface. Детальніше у розділі “Налаштування безпеки”.  

Зверніть увагу, що при використанні автентифікованих, основаних на http, фаєрволів, немає ніякого способу виходу із системи: єдиний шлях - не дати браузерові більше надсилати ваш логін і пароль на кожен запит.  Зазвичай, проблему можна вирішити, почистивши кеш браузера, або перезапустивши його. Деякі інструменти веб-розробки також стануть в пригоді у нашому випадку.

Шифрування пароля

Якщо ви зберігаєте дані про користувачів у БД, вам потрібно буде шифрувати їхні паролі перш ніж їх вставляти. Незалажно від того, який алгоритм ви обрали для об’єкта User, хешований пароль завжди визначатиметься з контролера наступним чином:

// whatever *your* User object is
$user = new AppBundle\Entity\User();
$plainPassword = 'ryanpass';
$encoder = $this->container->get('security.password_encoder');
$encoded = $encoder->encodePassword($user, $plainPassword);

$user->setPassword($encoded);

Щоб це спрацювало, переконайтеся, що об’єкт-шифратор для класу user (наприклад, AppBundle\Entity\User) налаштований під ключем encoder у app/config/security.yml.

Об’єкт $encoder також має метод isPasswordValid, який приймає об’єкт User як перший аргумент, а простий пароль для перевірки, у якості другого аргумента.  

Коли ви дозволяєте користувачеві відправити пароль у формі простого тексту (наприклад, форма реєстрації, форма зміни пароля), у вас має бути підтвердження, що пароль складається з 4096 символів або менше. Більше деталей у розділі “Як створити просту форму реєстрації”.

Ієрархічні ролі

Замість того, щоб присвоювати користувачеві багато ролей, ви можете визначити правила наслідування ролей, створивши їхню їєрархію:

YAML

# app/config/security.yml
security:
    # ...

    role_hierarchy:
        ROLE_ADMIN:       ROLE_USER
        ROLE_SUPER_ADMIN: [ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH]

XML

<!-- app/config/security.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<srv:container xmlns="http://symfony.com/schema/dic/security"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:srv="http://symfony.com/schema/dic/services"
    xsi:schemaLocation="http://symfony.com/schema/dic/services
        http://symfony.com/schema/dic/services/services-1.0.xsd">

    <config>
        <!-- ... -->

        <role id="ROLE_ADMIN">ROLE_USER</role>
        <role id="ROLE_SUPER_ADMIN">ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH</role>
    </config>
</srv:container>

PHP

// app/config/security.php
$container->loadFromExtension('security', array(
    // ...

    'role_hierarchy' => array(
        'ROLE_ADMIN'       => 'ROLE_USER',
        'ROLE_SUPER_ADMIN' => array(
            'ROLE_ADMIN',
            'ROLE_ALLOWED_TO_SWITCH',
        ),
    ),
));

У нашому налаштуванні, користувачі з роллю ROLE_ADMIN також матимуть роль ROLE_USER. Роль ROLE_SUPER_ADMIN містить ролі ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH та ROLE_USER (успадковані від ROLE_ADMIN).

Автентифікація без збереження стану (stateless)

За замовчуванням, Symfony використовує куки (сесію) для збереження контексту безпеки користувача. Але якщо ви, для прикладу, користуєтеся сертифікатами або автентифікацією HTTP, збереження не потрібне, так як дані авторизації доступні для кожного запиту. В даному випадку, якщо ви нічого не хочете зберігати між запитами,  активуйте автентифікацію stateless (без збереження стану, так як Symfony не створюватиме куки):

YAML

# app/config/security.yml
security:
    # ...

    firewalls:
        main:
            http_basic: ~
            stateless:  true

XML

<!-- app/config/security.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<srv:container xmlns="http://symfony.com/schema/dic/security"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:srv="http://symfony.com/schema/dic/services"
    xsi:schemaLocation="http://symfony.com/schema/dic/services
        http://symfony.com/schema/dic/services/services-1.0.xsd">

    <config>
        <!-- ... -->

        <firewall name="main" stateless="true">
            <http-basic />
        </firewall>
    </config>
</srv:container>

PHP

// app/config/security.php
$container->loadFromExtension('security', array(
    // ...

    'firewalls' => array(
        'main' => array('http_basic' => null, 'stateless' => true),
    ),
));

Якщо ви використаєте форму реєстрації, Symfony створить куки, навть якщо ви поставите в налаштуваннях stateless або true.

Перевіряємо відомі вразливі місця системи безпеки у залежностях

Коли ви використовуєте багато залежностей у проектах Symfony, деякі з них можуть мати вразливі місця. Тому, Symfony включає команду під назвою security:check, яка перевірить файл composer.lock, чи немає небезпеки у встановлених залежностях:

$ php bin/console security:check

Добре буде якщо ви запускатимете цю команду на постійній основі, для швидкого оновлення, або заміни пошкоджених залежностей. Всередині, дана команда використовує публічну консультативну базу з питань безпеки, опубліковану організацією FriendsOfPHP.  

Команда security:check закінчується кодом помилки виходу, у випадку якщо будь-яка з ваших залежностей знаходиться під впливом вразливості безпеки. Тому, ви легко можете інтегрувати її у процесі побудови.  

Щоб активувати команду security:check встановіть пакет SensioDistributionBundle.

$ composer require 'sensio/distribution-bundle'

У якості висновку

Вау! Чудова робота! Тепер ви знаєте набагато більше ніж просто основи безпеки. Найважча частина - користувацькі вимоги, наприклад користувацька стратегія автентифікації (знаки API), складний алгоритм авторизації і багато інших деталей, адже безпека складна!.   

Але вам пощастило, тому що існує велика кількість статей на тему безпеки в довіднику, де описано багато подібних ситуацій. Також, ви можете почитати про безпеку у документації.  Багато опцій не мають конкретних деталей, але розглянути ймовірне дерево конфігурації повністю не завадить.

Вперед!

Дізнавайтесь більше у довіднику:

Поділитися