Реализация oAuth 2.0 авторизации с использованием бандла HWIOAuthBundle
Принципы
Авторизация oAuth - процесс проверки существования пользователя на oAuth-сервере, в результате чего мы получаем только UserName (Логин или левую часть емэйл-адреса пользователя) и при желании др. информацию о пользователе.
Провайдер - это прослойка, т.е. нам понадобится написать свой класс (прослойку) через который будет выполняться Авторизация (не Аутентификация) пользователя (по UserName).
В книге рецептов Symfony2 есть пример реализации своего провайдера » Следуя этому примеру и будем писать провайдер, который для авторизации будет использовать только UserName, как я уже и писал выше.
token - некий рэндомно сгенерированный ключ, с помощью которого происходит доверие и общение oAuth-сервера (например Facebook) и oAuth-клиента (Вашего сайта).
Устанавливаем HWIOAuthBundle
Первым делом конечно устанавливаем бандл, как установить написано на странице https://github.com/hwi/HWIOAuthBundle читай параграф Installation
Теперь создадим свое приложение на сайте live.com
Заходите на страницу управления приложениями ( если нужно авторизуйтесь или зарегистрируйте себе учетную запись в live.com ).
Теперь создавайте свое приложение кликнув по ссылке "Create application". Обращаю внимание, что поле "Redirect domain:" нужно заполнить например так: http://yapro.ru/
В результате, у Вас будут 2 нужных Вам параметра:
client_id: 0000000045704B0E
client_secret: lofQ9aZXCZXCZXCZXC18gk-0dDAXp
И прежде чем приступить к рутинной работе, для самых опытных выкладываю diff.patch всего ниже перечисленного.
Конфигурируем бандл
В конце файла /app/config/config.yml пропишите следующее:
Пишем свой провайдер
Создайте файл /src/Acme/DemoBundle/Security/Provider.php со следующим содержимым:
<?php /** * Провайдер аутентификации (не Авторизации) - возвращает менеджеру ( Symfony AuthenticationManager ), а тот, в свою * очередь listener’y авторизованный (или не авторизванный) token. На данном этапе задействуется UserProvider, который * и работает с данными о пользователе. */ namespace Acme\DemoBundle\Security; use Symfony\Component\Security\Core\User\UserInterface; use HWI\Bundle\OAuthBundle\OAuth\Response\UserResponseInterface; use HWI\Bundle\OAuthBundle\Security\Core\User\OAuthUserProvider; use Symfony\Component\Security\Core\Exception\UsernameNotFoundException; use Symfony\Component\Security\Core\Exception\UnsupportedUserException; use Acme\DemoBundle\Entity\User; class Provider extends OAuthUserProvider { private$doctrine; publicfunction__construct($doctrine) { $this->doctrine=$doctrine; } /** * находит и возвращает экземпляр класса User или пустой массив (а по версии Symfony должен возвращать false). * @param string $username - ID пользователя * @return User|\Symfony\Component\Security\Core\User\UserInterface * @throws \Symfony\Component\Security\Core\Exception\UsernameNotFoundException */ publicfunction loadUserByUsername($username) { if(empty($username)){ return; } // получаем данные о пользователе /** @var $user \Acme\DemoBundle\Entity\User */ $user=$this->getUserByWindowsLive($username); if($user&&$user->getId()){ $user->setPassword(sha1($username)); return$user; } thrownew UsernameNotFoundException(sprintf('Username "%s" does not exist.',$username)); } /** * Предположительно: метод вызывается, когда процесс Аутентификации (не Авторизации) успешно выполнен * проверяет сущестование пользователя в Б.Д., заводит сессию и возвращает данные пользователя по $username * @param UserResponseInterface $response * @return User|\Symfony\Component\Security\Core\User\UserInterface * @throws \Symfony\Component\Security\Core\Exception\UsernameNotFoundException */ publicfunction loadUserByOAuthUserResponse(UserResponseInterface $response) { $email=$response->getEmail();// емэйл пользователя, например:totx@narod.ru $name=$response->getRealName();// имя пользователя на стороне oAuth-сервера, например:Nikolay Lebedenko $username=$response->getUsername();// уникальный ID пользователя на стороне oAuth-сервера, например:8d86a051742940e3 $response->getAccessToken();// токен (уникальный идентификатор) для авторизации, например:ZxC1/2+3 (более 255 символов) $response->getExpiresIn();// предположительно: через какое время токен становится недействительным, например:3600 (секунд) $response->getProfilePicture();// изображение профиля, может не быть, например:пусто $response->getRefreshToken();// например:пусто $response->getTokenSecret();// например:пусто if(empty($email)){ thrownew UsernameNotFoundException('Вы не идентифицированы т.к. не получен Email-адрес'); } $user=$this->getUserByWindowsLive($username);// находим пользователя /** @var $user \Acme\DemoBundle\Entity\User */ // если пользователя нет в базе данных - добавим его if(!$user||!$user->getId()){ $user=new User(); $user->setName($name); $user->setEmail($email); $user->setWindowsLive($username); $this->doctrine->getManager()->persist($user); $this->doctrine->getManager()->flush(); $user_id=$user->getId(); }else{ $user_id=$user->getId(); } if(!$user_id){ thrownew UsernameNotFoundException('Возникла проблема добавления или определения пользователя'); } return$this->loadUserByUsername($username); } publicfunction refreshUser(UserInterface $user) { if(!$userinstanceof User){ thrownew UnsupportedUserException(sprintf('Instances of "%s" are not supported.',get_class($user))); } return$this->loadUserByUsername($user->getUsername()); } /** * Метод проверки класса пользователя * предположение: нужен чтобы Symfony использовал правильный класс Пользователя для получения объекта пользователя * @param string $class * @return bool */ publicfunction supportsClass($class) { return$class==='Acme\\DemoBundle\\Entity\\User'; } privatefunction getUserByWindowsLive($username='') { return$user=$this->doctrine->getRepository('AcmeDemoBundle:User')->findOneBy(array('windows_live'=>$username)); } }
Сущность пользователя
Чтобы используя Doctrine2 добавить пользователя в базу данных или проверить есть ли такой пользователь, нам нужен класс сущности. Создадим файл /src/Acme/DemoBundle/Entity/User.php со следующим содержимым:
<?php /** * сущность пользователя с признаками OAuthUser */ namespace Intranet\AuthBundle\Entity; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Security\Core\User\UserInterface; /** * @ORM\Entity(repositoryClass="Intranet\AuthBundle\Repository\UserRepository") * @ORM\Table(name="users", uniqueConstraints={@ORM\UniqueConstraint(name="email",columns={"email"})}) */ class User implements UserInterface { /** * @ORM\Id * @ORM\Column(type="integer") * @ORM\GeneratedValue(strategy="AUTO") */ private$id=0; /** * @ORM\Column(type="string", length=255, options={"default":""}) */ private$email=''; /** * @ORM\Column(type="integer", options={"default":0}) */ private$time_created=0; /** * @ORM\Column(type="string", length=255, options={"default":""}) */ private$name=''; /** * @ORM\Column(type="string", length=16, options={"default":""}) */ private$windows_live=''; /** * Get id * * @return integer */ publicfunction getId() { return$this->id; } /** * Set email * * @param string $email * @return User */ publicfunction setEmail($email) { $this->email=(string)$email; return$this; } /** * Get email * * @return string */ publicfunction getEmail() { return$this->email; } /** * Set time_created * * @param integer $timeCreated * @return User */ publicfunction setTimeCreated($timeCreated) { $this->time_created=$timeCreated; return$this; } /** * Get time_created * * @return integer */ publicfunction getTimeCreated() { return$this->time_created; } /** * Set name * * @param string $name * @return User */ publicfunctionsetName($name) { $this->name=(string)$name; return$this; } /** * Get name * * @return string */ publicfunction getName() { return$this->name; } /** * Set windows_live * * @param string $windowsLive * @return User */ publicfunction setWindowsLive($windowsLive) { $this->windows_live=(string)$windowsLive; return$this; } /** * Get windows_live * * @return string */ publicfunction getWindowsLive() { return$this->windows_live; } /* ниже прописаны методы необходимые для работы oAuth-авторизации */ /** * создан, т.к. этого требует интерфейс UserInterface * @return string - обязан возвращать уникальный ID пользователя */ publicfunction getUsername() { return$this->windows_live; } /** * роли пользователеля, то благодаря чему Аутентифицированный пользователь приобретает статут Авторизованного */ publicfunction getRoles() { returnarray('ROLE_USER','ROLE_OAUTH_USER'); } /** * @var переменная и ниже прописанные гетер и сетер обязательны, т.к. этого требует интерфейс UserInterface */ private$password; publicfunction setPassword($password='') { $this->password=(string)$password; } publicfunction getPassword() { return$this->password; } /** * создан, т.к. этого требует интерфейс UserInterface * @return null|string - отдает пустую строку т.к. при oAuth не используется */ publicfunction getSalt() { return''; } /** * создан, т.к. этого требует интерфейс UserInterface */ publicfunction eraseCredentials() { } }
Класс репозитория
Ну, а раз мы упомянули в описании сущности класс репозитория пользователя, то создадим и его, все равно ведь потом пригодится. Поэтому создайте файл /src/Acme/DemoBundle/Repository/UserRepository.php со следующим содержимым:
<?php /** * Класс для редактирования пользователя */ namespace Acme\DemoBundle\Repository; use Acme\DemoBundle\Entity\User; use Doctrine\ORM\EntityRepository; class UserRepository extends EntityRepository { }
Подключение нашего провайдера (класса, который мы написали выше)
Опишу 2 примера, ведь точно не знаю, как у Вас устроено, на основе xml или yml файла.
Настройки на основе xml файла /src/Acme/DemoBundle/Resources/config/services.xml Добавьте в свой файл то, что выделено крассным.
<?xml version="1.0" ?><container xmlns="http://symfony.com/schema/dic/services"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd"><parameters><parameter key="ib_user.oauth_user_provider.class">Acme\DemoBundle\Security\Provider</parameter></parameters><services><service id="ib_user.oauth_user_provider" class="%ib_user.oauth_user_provider.class%">
<argument type="service" id="doctrine" /></service><service id="twig.extension.acme.demo" class="Acme\DemoBundle\Twig\Extension\DemoExtension" public="false"><tag name="twig.extension" /><argument type="service" id="twig.loader" /></service><service id="acme.demo.listener" class="Acme\DemoBundle\EventListener\ControllerListener"><tag name="kernel.event_listener" event="kernel.controller" method="onKernelController" /><argument type="service" id="twig.extension.acme.demo" /></service></services></container>
Настройки на основе yml файла /src/Acme/DemoBundle/Resources/config/services.yml
parameters:ib_user.oauth_user_provider.class: Acme\DemoBundle\Security\Providerservices:ib_user.oauth_user_provider:class: %ib_user.oauth_user_provider.class%arguments: [@doctrine]
Подключаем роуты
Пропишите следующее в файл /app/config/routing.yml
hwi_oauth_login:resource: "@HWIOAuthBundle/Resources/config/routing/login.xml"prefix: /loginhwi_oauth_redirect:resource: "@HWIOAuthBundle/Resources/config/routing/redirect.xml"prefix: /connectwindows_live_login:pattern: /login/check-windows_livelogout:path: /logout
Настройки безопасности
Выкладываю содержимое файла /app/config/security.yml и внем выделю жирным то, что добавил:
security: encoders: Symfony\Component\Security\Core\User\User: plaintext role_hierarchy: ROLE_ADMIN: ROLE_USER ROLE_SUPER_ADMIN: [ROLE_USER, ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH] providers: chain_provider: chain: providers: [in_memory, user_db] in_memory: memory: users: user: { password: userpass, roles: [ 'ROLE_USER' ] } admin: { password: adminpass, roles: [ 'ROLE_ADMIN' ] } # user_db - просто название провайдера (кроме как здесь, более нигде название не светится) user_db: # ИД провайдера (прописано в /src/Acme/DemoBundle/Resources/config/services.xml id: ib_user.oauth_user_provider firewalls: dev: pattern: ^/(_(profiler|wdt)|css|images|js)/ security: false login: pattern: ^/demo/secured/login$ security: false secured_area: pattern: ^/demo/secured/ form_login: check_path: _security_check login_path: _demo_login logout: path: _demo_logout target: _demo #anonymous: ~ #http_basic: # realm: "Secured Demo Area" # main - просто название фаервола (я его сам выдумал) main: # ВНИМАНИЕ: т.к. в этом фаерволе НЕ указано security: false - значит проверкой доступа займется # выше описанный security: encoders: Symfony\Component\Security\Core\User\User: plaintext pattern: ^/ # расскомментируем, чтобы начал работать oAuth-бандл HWIOAuthBundle anonymous: true logout: true logout: path: /logout target: / # укажем название ресурса авторизации, чтобы начал работать oAuth-бандл HWIOAuthBundle oauth: # укажим владельцев ресурса resource_owners: # owner_windows_live - название владельца указанное в /app/config/config.yml # значение данного владельца - урл по которому будут обращаться пользователи owner_windows_live: "/login/check-windows_live" # урл, где лежит форма авторизации ( если пользователь не авторизован - отправляем его на страницу с именем роутинга _demo_login - /demo/secured/login ) login_path: /login # наверное: урл страницы, куда попадает пользователь, если не авторизовался (ввел неправильно данные или еще что-либо) failure_path: /login # oAuth-бандлу HWIOAuthBundle нужен сервис, который позволяет загружать пользователей, основываясь на oauth-действиях пользователя # конечной точки. Если бы у нас была своя пользовательская служба, то она должена была бы реализовать интерфейс: # HWI\Bundle\OAuthBundle\Security\Core\User\OAuthAwareUserProviderInterface. oauth_user_provider: # HWIOAuthBundle поставляется с тремя реализациями по умолчанию: # OAuthUserProvider (сервисное название: hwi_oauth.user.provider) - данные не сохраняют пользователей # EntityUserProvider (сервисное название: hwi_oauth.user.provider.entity) - загружает пользователей из базы данных # FOSUserBundle интеграция (сервисное название: hwi_oauth.user.provider.fosub_bridge). service: ib_user.oauth_user_provider access_control: ## - { path: ^/demo/secured/hello/admin/, roles: ROLE_ADMIN } #- { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY, requires_channel: https } - { path: ^/login, role: IS_AUTHENTICATED_ANONYMOUSLY } - { path: ^/connect, role: IS_AUTHENTICATED_ANONYMOUSLY } - { path: ^/, role: ROLE_USER }
База данных
Я надеюсь Вы не забыли создать базу данных и прописать настройки подключения к ней в файле /app/config/parameters.yml
Что тут скажешь, наверное это все, ведь после всех проделанных Выше действий Вы можете зайти на свой сайт, он перекинет Вас на страницу /login где будет ссылка на авторизацию!
p.s. не забывайте про то, как разлогиниться https://login.live.com/oauth20_logout.srf?client_id=CLIENT_ID&redirect_uri=REDIRECT_URL
Мои проблемы
1. Нужно было в конфиге включать работу провайдера начиная от корня сайта.
2. Выпала ошибка: No oauth code in the request.
Решение: нужно в файле /app/config/config.yml указать вид информации о пользователе, запрашиваемой у сервиса windows_live для своего ресурса, например так:
scope: wl.signin
3. Если WindowsLive спрашивает у клиента (или у Вас при тестировании) пароль для подтверждения, например так:
значит, Вы в файле /app/config/config.yml указали строгий scope.
4. Вываливалась ошибка:
The controller must return a response (null given). Did you forget to add a return statement somewhere in your controller?
оказалось:
form_login:check_path: _security_checklogin_path: _demo_loginlogout:path: _demo_logouttarget: _demopattern: ^/
5. Что-то сделал, определенно невнятную ошибку - почисти кэш, возможно ошибка пропадет (короче rm -rf app/cache/* никто не отменял :).
Источники:
p.s. тем, кто въехал во все по полной, может подключить свой сайт в качестве oAuth-сервера, благо для этого под Симфони уже написали бандл »
p.s. не по теме - понравился пример двух фаерволов:
firewalls:
install:
pattern:^/install/.*
security:false
main:
pattern:^/(?!install/)