2012-01-30 18 views
10

Pracuję nad aplikacją Symfony2 z interfejsem API dostępnym dla innych aplikacji. Chcę zabezpieczyć dostęp do interfejsu API. Do tej części nie mam problemu.Używanie niestandardowego dostawcy uwierzytelniania w Symfony2

Ale muszę udostępnić to połączenie nie za pomocą zwykłej pary login/hasło, ale po prostu za pomocą klucza API.

Poszedłem więc na oficjalną stronę i jej wspaniałą książkę kucharską na creating a custom authentication provider, dokładnie tego, czego potrzebuję, powiedziałem sobie.

Przykład nie był tym, czego potrzebowałem, ale postanowiłem dostosować go do moich potrzeb.

Niestety nie udało mi się.

Dam ci mój kod, a ja wyjaśnię później mój problem.

Oto moja fabryka tworzenia dostawcy uwierzytelniania i słuchacza:

<?php 

namespace Pmsipilot\UserBundle\DependencyInjection\Security\Factory; 

use Symfony\Component\DependencyInjection\ContainerBuilder; 
use Symfony\Component\DependencyInjection\Reference; 
use Symfony\Component\DependencyInjection\DefinitionDecorator; 
use Symfony\Component\Config\Definition\Builder\NodeDefinition; 
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\SecurityFactoryInterface; 

class ApiFactory implements SecurityFactoryInterface 
{ 
    /** 
    * @param \Symfony\Component\DependencyInjection\ContainerBuilder $container 
    * @param string $id 
    * @param aray $config 
    * @param string $userProvider 
    * @param string $defaultEntryPoint 
    * @return array 
    */ 
    public function create(ContainerBuilder $container, $id, $config, $userProvider, $defaultEntryPoint) 
    { 
    $providerId = 'security.authentification.provider.api.'.$id; 
    $container 
     ->setDefinition($providerId, new DefinitionDecorator('api.security.authentification.provider')) 
     ->replaceArgument(0, new Reference($userProvider)) 
    ; 

    $listenerId = 'security.authentification.listener.api.'.$id; 
    $listener = $container->setDefinition($listenerId, new DefinitionDecorator('api.security.authentification.listener')); 

    return array($providerId, $listenerId, $defaultEntryPoint); 
    } 

    /** 
    * @return string 
    */ 
    public function getPosition() 
    { 
    return 'http'; 
    } 

    /** 
    * @return string 
    */ 
    public function getKey() 
    { 
    return 'api'; 
    } 

    /** 
    * @param \Symfony\Component\Config\Definition\Builder\NodeDefinition $node 
    * @return void 
    */ 
    public function addConfiguration(NodeDefinition $node) 
    { 
    } 
} 

Następny mój kod słuchacz:

<?php 

namespace Pmsipilot\UserBundle\Security\Firewall; 

use Symfony\Component\HttpFoundation\Response; 
use Symfony\Component\HttpKernel\Event\GetResponseEvent; 
use Symfony\Component\Security\Http\Firewall\ListenerInterface; 
use Symfony\Component\Security\Core\Exception\AuthenticationException; 
use Symfony\Component\Security\Core\SecurityContextInterface; 
use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface; 
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; 
use Pmsipilot\UserBundle\Security\WsseUserToken; 
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; 
use Symfony\Component\Security\Core\Exception\BadCredentialsException; 

class ApiListener implements ListenerInterface 
{ 
    protected $securityContext; 
    protected $authenticationManager; 

    /** 
    * Constructor for listener. The parameters are defined in services.xml. 
    * 
    * @param \Symfony\Component\Security\Core\SecurityContextInterface $securityContext 
    * @param \Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface $authenticationManager 
    */ 
    public function __construct(SecurityContextInterface $securityContext, AuthenticationManagerInterface $authenticationManager) 
    { 
    $this->securityContext = $securityContext; 
    $this->authenticationManager = $authenticationManager; 
    } 

    /** 
    * Handles login request. 
    * 
    * @param \Symfony\Component\HttpKernel\Event\GetResponseEvent $event 
    * @return void 
    */ 
    public function handle(GetResponseEvent $event) 
    { 
    $request = $event->getRequest(); 

    $securityToken = $this->securityContext->getToken(); 

    if($securityToken instanceof AuthenticationToken) 
    { 
     try 
     { 
     $this->securityContext->setToken($this->authenticationManager->authenticate($securityToken)); 
     } 
     catch(\Exception $exception) 
     { 
     $this->securityContext->setToken(null); 
     } 
    } 
    } 
} 

Mój kod dostawcy uwierzytelniania:

<?php 

namespace Pmsipilot\UserBundle\Security\Authentication\Provider; 

use Symfony\Component\Security\Core\Authentication\Provider\AuthenticationProviderInterface; 
use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface; 
use Symfony\Component\Security\Core\User\UserProviderInterface; 
use Symfony\Component\Security\Core\User\UserCheckerInterface; 
use Symfony\Component\Security\Core\User\UserInterface; 
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException; 
use Symfony\Component\Security\Core\Exception\AuthenticationServiceException; 
use Symfony\Component\Security\Core\Exception\BadCredentialsException; 
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; 
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; 

class ApiProvider implements AuthenticationProviderInterface 
{ 
    private $userProvider; 

    /** 
    * Constructor. 
    * 
    * @param \Symfony\Component\Security\Core\User\UserProviderInterface $userProvider An UserProviderInterface instance 
    */ 
    public function __construct(UserProviderInterface $userProvider) 
    { 
    $this->userProvider = $userProvider; 
    } 

    /** 
    * @param string $username 
    * @param \Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken $token 
    * @return mixed 
    * @throws \Symfony\Component\Security\Core\Exception\AuthenticationServiceException|\Symfony\Component\Security\Core\Exception\UsernameNotFoundException 
    */ 
    protected function retrieveUser($username, UsernamePasswordToken $token) 
    { 
    $user = $token->getUser(); 
    if($user instanceof UserInterface) 
    { 
     return $user; 
    } 

    try 
    { 
     $user = $this->userProvider->loadUserByApiKey($username, $token->getCredentials()); 

     if(!$user instanceof UserInterface) 
     { 
     throw new AuthenticationServiceException('The user provider must return a UserInterface object.'); 
     } 

     return $user; 
    } 
    catch (\Exception $exception) 
    { 
     throw new AuthenticationServiceException($exception->getMessage(), $token, 0, $exception); 
    } 
    } 

    /** 
    * @param TokenInterface $token 
    * @return null|\Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken 
    * @throws \Symfony\Component\Security\Core\Exception\AuthenticationServiceException|\Symfony\Component\Security\Core\Exception\BadCredentialsException|\Symfony\Component\Security\Core\Exception\UsernameNotFoundException 
    */ 
    function authenticate(TokenInterface $token) 
    { 
    $username = $token->getUsername(); 
    if(empty($username)) 
    { 
     throw new AuthenticationServiceException('No username given.'); 
    } 

    try 
    { 
     $user = $this->retrieveUser($username, $token); 

     if(!$user instanceof UserInterface) 
     { 
     throw new AuthenticationServiceException('retrieveUser() must return a UserInterface.'); 
     } 

     $authenticatedToken = new UsernamePasswordToken($user, null, 'api', $user->getRoles()); 
     $authenticatedToken->setAttributes($token->getAttributes()); 

     return $authenticatedToken; 
    } 
    catch(\Exception $exception) 
    { 
     throw $exception; 
    } 
    } 

    /** 
    * @param TokenInterface $token 
    * @return bool 
    */ 
    public function supports(TokenInterface $token) 
    { 
    return true; 
    } 
} 

Aby korzystać te dwa obiekty użyłem pliku yml do ich konfiguracji:

<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"> 

    <services> 
    <service id="pmsipilot.api.security.authentication.factory" class="Pmsipilot\UserBundle\DependencyInjection\Security\Factory\ApiFactory" public="false"> 
     <tag name="security.listener.factory" /> 
    </service> 
    </services> 
</container> 

Teraz kod dostawcy uwierzytelniania:

<?php 

namespace Pmsipilot\UserBundle\Security\Authentication\Provider; 

use Symfony\Component\Security\Core\Authentication\Provider\AuthenticationProviderInterface; 
use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface; 
use Symfony\Component\Security\Core\User\UserProviderInterface; 
use Symfony\Component\Security\Core\User\UserCheckerInterface; 
use Symfony\Component\Security\Core\User\UserInterface; 
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException; 
use Symfony\Component\Security\Core\Exception\AuthenticationServiceException; 
use Symfony\Component\Security\Core\Exception\BadCredentialsException; 
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; 
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; 

class ApiProvider implements AuthenticationProviderInterface 
{ 
    private $userProvider; 

    /** 
    * Constructor. 
    * 
    * @param \Symfony\Component\Security\Core\User\UserProviderInterface $userProvider An UserProviderInterface instance 
    */ 
    public function __construct(UserProviderInterface $userProvider) 
    { 
    $this->userProvider = $userProvider; 
    } 

    /** 
    * @param string $username 
    * @param \Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken $token 
    * @return mixed 
    * @throws \Symfony\Component\Security\Core\Exception\AuthenticationServiceException|\Symfony\Component\Security\Core\Exception\UsernameNotFoundException 
    */ 
    protected function retrieveUser($username, UsernamePasswordToken $token) 
    { 
    $user = $token->getUser(); 
    if($user instanceof UserInterface) 
    { 
     return $user; 
    } 

    try 
    { 
     $user = $this->userProvider->loadUserByApiKey($username, $token->getCredentials()); 

     if(!$user instanceof UserInterface) 
     { 
     throw new AuthenticationServiceException('The user provider must return a UserInterface object.'); 
     } 

     return $user; 
    } 
    catch (\Exception $exception) 
    { 
     throw new AuthenticationServiceException($exception->getMessage(), $token, 0, $exception); 
    } 
    } 

    /** 
    * @param TokenInterface $token 
    * @return null|\Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken 
    * @throws \Symfony\Component\Security\Core\Exception\AuthenticationServiceException|\Symfony\Component\Security\Core\Exception\BadCredentialsException|\Symfony\Component\Security\Core\Exception\UsernameNotFoundException 
    */ 
    function authenticate(TokenInterface $token) 
    { 
    $username = $token->getUsername(); 
    if(empty($username)) 
    { 
     throw new AuthenticationServiceException('No username given.'); 
    } 

    try 
    { 
     $user = $this->retrieveUser($username, $token); 

     if(!$user instanceof UserInterface) 
     { 
     throw new AuthenticationServiceException('retrieveUser() must return a UserInterface.'); 
     } 

     $authenticatedToken = new UsernamePasswordToken($user, null, 'api', $user->getRoles()); 
     $authenticatedToken->setAttributes($token->getAttributes()); 

     return $authenticatedToken; 
    } 
    catch(\Exception $exception) 
    { 
     throw $exception; 
    } 
    } 

    /** 
    * @param TokenInterface $token 
    * @return bool 
    */ 
    public function supports(TokenInterface $token) 
    { 
    return true; 
    } 
} 

Wystarczy FYI mój dostawca użytkownik:

<?php 

namespace Pmsipilot\UserBundle\Security\Provider; 

use Propel\PropelBundle\Security\User\ModelUserProvider; 
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException; 
use \Symfony\Component\Security\Core\Encoder\MessageDigestPasswordEncoder; 

class ApiProvider extends ModelUserProvider 
{ 
    /** 
    * Constructeur 
    */ 
    public function __construct() 
    { 
    parent::__construct('Pmsipilot\UserBundle\Model\User', 'Pmsipilot\UserBundle\Proxy\User', 'username'); 
    } 

    /** 
    * @param string $apikey 
    * @return mixed 
    * @throws \Symfony\Component\Security\Core\Exception\UsernameNotFoundException 
    */ 
    public function loadUserByApiKey($apikey) 
    { 
    $queryClass = $this->queryClass; 
    $query  = $queryClass::create(); 

    $user = $query 
     ->filterByApiKey($apikey) 
     ->findOne() 
    ; 

    if(null === $user) 
    { 
     throw new UsernameNotFoundException(sprintf('User with "%s" api key not found.', $apikey)); 
    } 
    $proxyClass = $this->proxyClass; 
    return new $proxyClass($user); 
    } 
} 

oraz za część konfiguracji mój security.yml:

security: 
    factories: 
    PmsipilotFactory: "%kernel.root_dir%/../src/Pmsipilot/UserBundle/Resources/config/security_factories.xml" 

    providers: 
    interface_provider: 
     id: pmsipilot.security.user.provider 
    api_provider: 
     id: api.security.user.provider 

    encoders: 
    Pmsipilot\UserBundle\Proxy\User: sha512 

    firewalls: 
    assets: 
     pattern:    ^/(_(profiler|wdt)|css|images|js|favicon.ico)/ 
     security:    false 

    api: 
     provider:    api_provider 
     access_denied_url:  /unauthorizedApi 
     pattern:    ^/api 
     api:     true 
     http_basic:    true 
     stateless:    true 

    interface: 
     provider:    interface_provider 
     access_denied_url:  /unauthorized 
     pattern:    ^/ 
     anonymous:    ~ 
     form_login: 
     login_path:   /login 
     check_path:   /login_check 
     use_forward:   true 
     default_target_path:/
     logout:     ~ 

    access_control: 
    - { path: ^/api, roles: IS_AUTHENTICATED_ANONYMOUSLY } 
    - { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY } 
    - { path: ^/, roles: SUPER_ADMIN } 

Wow to dużo kodu, mam nadzieję, że nie będzie to zbyt nudne.

Moim problemem jest to, że mój zwyczaj dostawcą uwierzytelniania jest nazywany przez dwóch zapór api i interfejs zamiast tylko przez api jednym. I oczywiście nie zachowują się tak, jak chciałem.

Nie znalazłem nic na temat takiego problemu. Wiem, że popełniłem błąd, w przeciwnym razie zadziała, ale gdzie i dlaczego nie wiem.

Znalazłem również this tutorial, ale nie pomogło to znacznie więcej.

Oczywiście, nie wahaj się zasugerować mi, czy istnieje inne rozwiązanie do korzystania z innego dostawcy uwierzytelniania niż domyślny.

+0

Dlaczego potrzebujesz interfejsu API i dostawców? Ty ApiProvider nie ma żadnego kodu uwierzytelniającego (np. Porównywania klucza przekazanego przez użytkownika). –

+0

Ponieważ dla api części mojej aplikacji po prostu chcę, aby użytkownik uwierzytelniał się za pomocą klucza API, bez względu na jego hasło. Jeśli klucz API pasuje do użytkownika, jest automatycznie uwierzytelniany. Domyślny dostawca uwierzytelniania sprawdza hasło przy użyciu nazwy użytkownika i chcę, aby ta kontrola została wykonana. – Maxime

+1

Znalazłem inny sposób robienia tego, co chcę, ale to nie zadziałało. Utworzyłem usługę z "security.authentication.provider.dao.api" jako identyfikatorem iw tym dostawcy uwierzytelniania umieszczam ten sam kod, co powyżej. Ale mam ten sam problem, nadal jest używany nawet z zaporą interfejsu. Po prostu chcę przeciążać Symfony \ Component \ Security \ Core \ Authentication \ Provider \ DaoAuthenticationProvider, aby uniknąć weryfikacji hasła, nie powinno być tak skomplikowane, prawda? – Maxime

Odpowiedz

10

Więc odpowiem na własne pytanie, ponieważ znalazłem rozwiązanie mojego problemu, a powiem ci, jak to rozwiązałem.

W moim przykładzie wystąpił błąd i zrozumiałem, że szukają kodu Symfony.

Podobnie jak klucz zwracany przez metodę getKey klasy Factory. Zauważyłem, że utworzony przeze mnie plik API nie był dla mnie żadnym parametrem w moim pliku security.yml, ale zastąpił go plik http_basic. Dlatego mam problemy z korzystaniem z dwóch dostawców zamiast jednego, ponieważ mam dwa klucze (api i http_basic), z których oba korzystały z usług dostawcy. W rzeczywistości myślę, że to jest powód tego problemu.

Aby było to proste, wykonuję samouczek Symfony, z wyjątkiem klasy tokenów, ale zastąpiłem kod nowych klas kodem klas Symfony. W pewien sposób odtworzyłem podstawowe uwierzytelnianie w Symfony, aby umożliwić przeciążenie. I oto jestem, mógłbym zrobić, co chcę, skonfigurować inny typ uwierzytelniania http na podstawie Symfony, ale z kilkoma zmianami.

Ta historia pomogła mi, ponieważ wiem, że wiem, że najlepszym sposobem zrozumienia zasad Symfony jest zagłębienie się w kod i dbanie o niego.

+1

zaznacz jako odpowiedź, jeśli jest to odpowiedź –

0

Znalazłem znacznie prostsze rozwiązanie. W config.yml możesz wskazać niestandardowy auth. klasa dostawcy, tak:

security.authentication.provider.dao.class: App\Security\AuthenticationProvider\MyDaoAuthenticationProvider 

Oczywiście MyDaoAuthenticationProvider musiał przedłużyć Symfony \ Komponent \ Security \ Rdzeń \ Authentication \ Provider \ UserAuthenticationProvider

0

Przyszedłem na swoim problemie, a wydaje się, że dobrze zrobił twój kod. Problemem, który może być przyczyną problemów, jest zamawianie zapór w pliku security.xml.

Spróbuj wyobrazić sobie, że jeśli istnieje jakiś zdefiniowany Listener (wpis firewalla) przed twoją CustomListener, a ona zwraca trochę odpowiedzi, przerwie pętlę obsługi.
W końcu spowoduje to, że twój CustomListener jest zarejestrowany, ale metoda obsługi nigdy nie zostanie wywołana.

0

Może trochę za późno (właściwie 5 lat później), ale w swojej fabryce masz literówkę. Napisałeś: $providerId = 'security.authentification.provider.api.'.$id;

Gdzie „uwierzytelnianie” ma być autoryzacja

Powiązane problemy