Приветствую!
Расскажу про такую в общем-то несложную задачку, как настройка 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.