Pull to refresh

Symfony, OAuth, ВКонтакте

Приветствую!

Расскажу про такую в общем-то несложную задачку, как настройка OAuth2 авторизации на сайте, написанном на php, а точнее на фреймворке Symfony 7. Вдруг кому-то пригодится.

Понадобилось мне тут обновить свой старый сайт, написанный более 10 лет назад. А там была реализована авторизация через ВКонтакт, причем шла проверка и допускались только подписчики определенного паблика. Все это было написано на голом php и js. А теперь я переписал сайт на Symfony и стал думать, как сделать аналогичную авторизацию на нем. Я был уверен, что все нужное давно написано за нас, и сочинять велосипеды не хотелось. Забегая вперед скажу, что немного повелосипедить все же пришлось.

Первым делом я ухватился за рекомендованный в документации фреймворка пакет HWIOAuthBundle. Но заплутав в его описании и примерах, я это дело не осилил, а тратить кучу времени на метод проб и ошибок мне не хотелось. Тогда я нагуглил другой пакет для своих целей - knpuniversity/oauth2-client-bundle.

Этот оказался более дружелюбным и гибким. Однако для его работы понадобился еще пакет-провайдер именно под ВКонтакт. Указанный в списке провайдеров прямо на странице бандла - давно устарел. И вот его-то пришлось форкнуть и немного допилить.

  • Первым делом обновил composer.json, указав свежую версию php. Было: "php" : ">=5.6", стало: "php" : ">=8.1".

  • Далее в файле J4k\OAuth2\Client\Provider\Vkontakte.php изменил версию api на актуальную (на данный момент это $version = 5.199).

В целом если вам больше ничего не надо делать после авторизации, этого достаточно. Но мне-то надо было еще добавить проверку на участие в определенном паблике. Поэтому в тот же файл я добавил метод проверки, который вызывает соответствующий API метод ВКонтакта. Обработку ответов тоже пришлось актуализировать, тк там используется guzzle http client, он возвращает стрим, а старый код ждет массив и очень удивляется, что его ожидания не оправдались.

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

Итак, пакеты установлены, теперь надо заняться настройками. Настройки бандла не сложные и их можно брать прямо с документации на гитхабе. В конфиге должен появиться файл knpu_oauth2_client.yaml, в котором я написал:

knpu_oauth2_client:
    clients:
        vkontakte_client:
            type: vkontakte
            client_id: '%env(VK_API_ID)%'
            client_secret: '%env(VK_SECRET_KEY)%'
            redirect_route: connect_vkontakte_check
            redirect_params: { }

client_id и client_secret - это ИД приложения ВКонтакте и секретный ключ для работы с ним. Кто не знает что это и для чего надо - читайте тут. У меня данный сайт уже был зарегистрирован, ключ тоже был, так что я просто занес эти параметры в переменные окружения. redirect_route - это роут, который вам надо сделать для вывода страницы прверки (описано в документации бандла).

Я сделал контроллер, который отображал бы страницу авторизации через ВКонтакт.

<?php

namespace App\Controller;

use KnpU\OAuth2ClientBundle\Client\ClientRegistry;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;

#[Route('/vkontakte', name: 'connect_vkontakte_')]
class VKController extends AbstractController
{
    #[Route('/', name: 'start')]
    public function connectAction(ClientRegistry $clientRegistry)
    {
        return $clientRegistry
            // Имя клиента, указанное в config/packages/knpu_oauth2_client.yaml
            ->getClient('vkontakte_client')
            // scopes (см доку ВКонтакта, мне нужны эти)
            ->redirect(['public_profile', 'email', 'groups']);
    }

    #[Route('/check/', name: 'check')]
    public function connectCheckAction(Request $request, ClientRegistry $clientRegistry)
    {
        // Вся проверка будет в сервисе App\Security\VKAuth.php
    }
}

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

    public function authenticate(Request $request): Passport
    {
       /** @var KnpU\OAuth2ClientBundle\Client\Provider\VKontakteClient $client */
        $client = $this->clientRegistry->getClient('vkontakte_client');
        $accessToken = $this->fetchAccessToken($client);

        /** @var \J4k\OAuth2\Client\Provider\User $vkUser */
        $vkUser = $client->fetchUserFromToken($accessToken);

        /** @var J4k\OAuth2\Client\Provider\Vkontakte $vkProvider */
        $vkProvider = $client->getOAuth2Provider();

        // Проверка на участие юзера в группе
        $groups = $vkProvider->groupsGet($vkUser->getId(), $accessToken, ['group_id' => $this->bag->get('group_id')]);

        if (!isset($groups['groups'][0]) || ($groups['groups'][0]['is_member'] === 0 && $groups['groups'][0]['is_admin'] === 0)) {
            throw new CustomUserMessageAuthenticationException('Вам сюда нельзя.');
        }
...

Дальше все как в примере. Только в методе start поправил редирект так, чтоб он вел на мою страницу авторизации в контроллере (см. выше функцию connectAction).

Осталось еще добавить настройку в файл app/config/packages/security.yaml, чтобы закрыть сокровенное нашей авторизацией. Тут есть нюанс. Если у вас кроме этой авторизации будут еще какие-то, вам надо добавить настройку провайдера для фаерволла, котрый будет использовать oauth авторизацию. У меня другая авторизация есть, в админку люди ходят иным способом. Поэтому конфиг выглядит так:

security:
    password_hashers:
        Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
    providers:
        admin_provider:
            entity:
                class: App\Entity\Admin # это сущность юзера админа
                property: email
        cabinet_provider:
            entity:
                class: App\Entity\User # это сущность юзера который заходит через oauth
                property: vkId
    firewalls:
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false
        admin:
            lazy: true
            pattern: ^/admin
            provider: admin_provider
            form_login:
                login_path: app_admin_login
                check_path: app_admin_login
                enable_csrf: true
            logout:
                path: /logout/
                target: app_admin_login
        main:
            provider: cabinet_provider
            custom_authenticators:
                - App\Security\VKAuth # это наш сервис авторизации
    access_control:
        - { path: ^/admin/login, roles: PUBLIC_ACCESS }
        - { path: ^/admin, roles: ROLE_ADMIN }
        - { path: ^/cabinet, roles: ROLE_USER }

То есть я создал сущность юзера, отдельную от сущности админа.

Это только один из способов, как можно провернуть данную задачу. Я выбрал, как мне было проще. Ну, всяко проще, чем разбираться с HWIOAuthBundle.

Tags:
Hubs:
You can’t comment this publication because its author is not yet a full member of the community. You will be able to contact the author only after he or she has been invited by someone in the community. Until then, author’s username will be hidden by an alias.