skobkin
@skobkin
Гентушник, разработчик на PHP и Symfony.

Как правильно оформить кастомную аутентификацию в Symfony2?

Сразу оговорюсь, что официальная документация с примером WSSE-аутентификации не очень прояснила мне суть вопроса.

Необходимо сделать аутентификацию через метод API сервиса, который внешне похож на OpenID, но им не является. Механика работы аутентификации:
1. Проверка аутентификации пользователя и, если он не аутентифицирован то выдать ему сообщение с предложением пройти на страницу аутентификации. Тут срабатывает мой контроллер, отображающий выбор региона, а потом переадресовывающий пользователя на нужный метод API.
2. На странице он должен будет выбрать один параметр: регион. При клике на кнопку нужного ему региона пользователь попадает вызывает определённый Action контроллера (либо же это правильнее сделать внутри механизма аутентификации, но тогда как и куда редиректить пользователя?), где происходит запрос к API внешнего сервиса нужного региона (регион запоминается) для получения уникальной ссылки аутентификации, после чего пользователь редиректится по этой ссылке, где он должен будет подтвердить передачу сайту данных: токена доступа к API этого самого внешнего сервиса, юзернейма во внешнем сервисе и айдишника там же. Тут непонятно, правильно ли будет делать это в контроллере, или надо переложить это тоже на кастомную аутентификацию? Если надо переложить, то как? После клика по кнопке региона от пользователя до возврата с данными на сайт больше ничего не потребуется.
3. После подтверждения пользователь редиректится обратно на сайт (в первом запросе внешнему API передаётся путь, куда редиректить пользователя), на страницу, которая принимает данные - токен, юзернейм, айдишник.
4. Система аутентификации в Listener'е создаёт токен, записывает в него пришедшие данные и отправляет токен на проверку:
protected function attemptAuthentication(Request $request)
    {
        if ($request->get($this->options['status_parameter'], null, true) == 'ok') {
            // Наполнение токена данными
            return $this->authenticationManager->authenticate($token);
        }
    }

Тут, к счастью, всё понятно. Разве что, хотелось бы уточнить, когда вызывается метод attemptAtuthentication() у Listener'а. Если только при переходе на адрес, который указан в файрволе как адрес проверки данный - значит я всё правильно понял.
5. В дело вступает кастомный провайдер аутентификации. В методе authenticate() происходит вызов другого метода, который делает запрос ко внешнему сервису для проверки валидности токена (ведь с редиректом можно вернуть фальшивый юзернейм) и, если токен валидный - происходит поиск пользователя с таким юзернеймом, который указан в токене через userProvider:
$user = $this->userProvider->loadUserByUsername($token->getUsername());

Если пользователь не существует - его нужно создать и задать ему юзернейм, и параметр айди на внешнем сервисе (не внутренний id, который primary key, а дополнительное поле, которое хранит внешний id).
if (!$user) {
    $user = new User();
    $user->setUsername($token->getUser());
    //...
    // Setting role
    //...
}

В итоге у нас получается либо новый пользователь, либо найденный в базе. Новому токену задаётся юзер и статус аутентификации:
public function authenticate(TokenInterface $token)
{
    // Тут вышеописанная рутина
    $authenticatedToken->setUser($user);
    $authenticatedToken->setAuthenticated(true);
    return $authenticatedToken;
}

Здесь непонятно то, как можно в методе authenticate() провайдера аутентификации инициировать создание пользователя, ведь это не контроллер и тут просто так не получить менеджер сущностей и отправив нового пользователя на сохранение
$em = $this->getDoctrine()->getManager();
$em->persist($user);
$em->flush();

Наверное, мне нужно каким-то образом передать в конструктор провайдера сервис Doctrine?
6. После этого, по идее, где-то нужно положить текущий токен в контекст безопасности, да и пользователя где-то нужно положить так, чтобы в дальнейшем он был прозрачно доступен по $this->getUser() в контроллерах. Тут непонятно, где конкретно это сделать будет правильно. Или система аутентификации позаботится об этом сама?
7. Ну и как-то после этого процесса нужно опять же выдать свой кастомный ответ в JSON о том, что аутентификация пройдена.

P.S. Сейчас имеется токен, который, кажется, реализован правильно. По крайней мере, с ним всё просто и понятно. Ещё имеется Listener унаследованный от AbstractAuthenticationListener. У абстрактного класса в параметрах есть 'login_path'. Я так понимаю, где-то там мне и нужно отображать выбор региона для пользователя, но так как это обрабатывает бандл Security, а не мои контроллеры - я пока не понимаю, как это реализовать. 'check_path' - это, как я понимаю, страница, куда пользователь должен приземлиться после редиректа со стороннего сервиса (в случае с обычной формой логина сюда должны прилетать данные формы, так?).
P.P.S. Официальную документацию читал. Там речь о WSSE, у которого другой принцип работы. И эту статью - тоже. Благодаря ним частично разобрался, но всё вышеописанное - пробелы после чтения.

Прошу прощения за такой объёмный "вопрос". Подозреваю, что может вскрыться что-то ещё, что я не понял, но если разберусь с вышеописанным механизмом - будет значительно проще разбираться дальше.
  • Вопрос задан
  • 2719 просмотров
Пригласить эксперта
Ответы на вопрос 1
@PaulJE
Собственно тебе нужен сделать внедрение через DI, сам провайдер ведь описан у тебя в сервисах? Ну так и инжектируй в него менеджер сущностей.
Получиться нечто такое:

private $_em;
public function setEntityManager(EntityManager $em) {
    $this->_em = $em;
}

public function authenticate(TokenInterface $token)
{
    // Тут вышеописанная рутина
    $this->_em->persist($user);
    $this->_em->flush();
Ответ написан
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Войти через центр авторизации
Похожие вопросы